C语言、内存管理、堆、栈、动态分配

昨晚整理了一晚上居然没了?没保存还是没登录我也忘了,贼心累

我捋了捋,还是得从操作系统,进程和内存开始理解。

进程

    从操作系统的角度简单介绍一下进程。进程是占有资源的最小单位,这个资源当然包括内存。在现代操作系统中,每个进程所能访问的内存是互相独立的(一些交换区除外)。而进程中的线程所以共享进程所分配的内存空间

    在操作系统的角度来看,进程=程序+数据+PCB(进程控制块)

内存单位和编址

  • 位 :( bit ) 是电子计算机中最小的数据单位。每一位的状态只能是0或1。
  • 字节:1 Byte = 8 bit ,是内存基本的计量单位,
  • KB :1KB = 1024 Byte。也就是1024个字节。MB : 1MB = 1024 KB。类似的还有GB、TB。
  • 内存编址计算机中的内存按字节编址,每个地址的存储单元可以存放一个字节(8个bit)的数据,CPU通过内存地址获取指令和数据,并不关心这个地址所代表的空间具体在什么位置、怎么分布,因为硬件的设计保证一个地址对应着一个固定的空间,所以说:内存地址和地址指向的空间共同构成了一个内存单元。
  • 内存地址内存地址通常用十六进制的数据表示,C、C++规定,16进制数必须以 0x 开头。使用十六进制表示一个内存地址是因为:1,二进制、十进制、十六进制之间相互转换比较方便;2,一位十六进制数可以表示4个二进制位数,更大的数使用十六进制数表示更加精短。3,计算机硬件设计需要。

为什么32位机器最大只能用到4GB内存

        在使用计算机时,其最大支持的内存是由  操作系统 和 硬件 两方面决定的。

  硬件方面在计算机中 CPU的地址总线数目 决定了CPU 的 寻址 范围,这种由地址总线对应的地址称作为物理地址。假如CPU有32根地址总线(一般情况下32位的CPU的地址总线是32位,也有部分32位的CPU地址总线是36位的,比如用做服务器的CPU),那么提供的可寻址物理地址范围 为 2^32=4GB(在这里要注意一点,我们平常所说的32位CPU和64位CPU指的是CPU一次能够处理的数据宽度,即位宽,不是地址总线的数目)。自从64位CPU出现之后,一次便能够处理64位的数据了,其地址总线一般采用的是36位或者40位(即CPU能够寻址的物理地址空间为64GB或者1T)。CPU访问任何存储单元必须知道其物理地址。

  用户在使用计算机时能够访问的最大内存不单是由CPU地址总线的位数决定的,还需要考虑操作系统的实现。实际上用户在使用计算机时,进程所访问到的地址是逻辑地址,并不是真实的物理地址,这个逻辑地址是操作系统提供的,CPU在执行指令时需要先将指令的逻辑地址变换为物理地址才能对相应的存储单元进行数据的读取或者写入(注意逻辑地址和物理地址是一一对应的)。

  对于32位的windows操作系统,其逻辑地址编码采用的地址位数是32位的,那么操作系统所提供的逻辑地址寻址范围是4GB,而在intel x86架构下,采用的是内存映射技术(Memory-Mapped I/O, MMIO),也就说将4GB逻辑地址中一部分要划分出来与BIOS ROM、CPU寄存器、I/O设备这些部件的物理地址进行映射,那么逻辑地址中能够与内存条的物理地址进行映射的空间肯定没有4GB了.基于以上的理论可以知道32位的系统,其最多只能管理的运存只有:

                                                 2^32B=2^(2+10+10+10)B=2^2*(2^10*2^10*2^10)B=4GB。

       所以当我们装了32位的windows操作系统,即使我们买了4GB的内存条,实际上能被操作系统访问到的肯定小于4GB,一般情况是3.2GB左右。假如说地址总线位数没有32位,比如说是20位,那么CPU能够寻址到1MB的物理地址空间,此时操作系统即使能支持4GB的逻辑地址空间并且假设内存条是4GB的,能够被用户访问到的空间不会大于1MB(当然此处不考虑虚拟内存技术),所以用户能够访问到的最大内存空间是由硬件和操作系统两者共同决定的,两者都有制约关系。

引用自 :https://www.cnblogs.com/dolphin0520/archive/2013/05/31/3110555.html

C程序内存分配

对于一个由C语言编写的程序而言,内存主要可以分为以下5个部分组成:

其中需要注意的是:代码段、数据段、BSS段在程序编译期间由编译器分配空间,在程序启动时加载,由于未初始化的全局变量存放在BSS段,已初始化的全局变量存放在数据段,所以程序中应该尽量少的使用全局变量以节省程序编译和启动时间;栈和堆在程序运行中由系统分配空间。

  • 代码区(text):用来存放CPU执行的机器指令(machine instructions),也有可能包含一些只读的常数变量,例如字符串常量等。通常,代码区是可共享的(即另外的执行程序可以调用它),因为对于频繁被执行的程序,只需要在内存中有一份代码即可。这部分区域的大小在程序运行之前就已经确定,通常是只读的,使其只读的原因是防止程序意外地修改了它的指令。另外,代码区还规划了局部变量的相关信息。

全局数据区(静态区)(static):全局变量和静态变量的存储是放在一块的,初始化的全局变量和静态变量在一块区域(数据区),未初始化的全局变量和静态变量在相邻的另一块区域(BSS区)。另外文字常量区,常量字符串就是放在这里,程序结束后由系统释放。

  • 数据区(全局初始化数据区 data):该区包含了在程序中明确被初始化的全局变量、静态变量(包括全局静态变量和局部静态变量)和常量数据(如字符串常量)
  • BSS区(未初始化数据区。):存入的是全局未初始化变量。BSS这个叫法是根据一个早期的汇编运算符而来,这个汇编运算符标志着一个块的开始。BSS区的数据在程序开始执行之前被内核初始化为0或者空指针(NULL)。llinux环境下可以用size命令 查看C程序的存储空间布局,可以看出,此可执行程序在存储时(没有调入到内存)分为代码区(text)、数据区(data)和未初始化数据区(bss)3个部分。
phoenix@Phoenix:~/桌面$ file struct
struct: ELF 64-bit LSB executable, x86-64, version 1 (SYSV), dynamically linked (uses shared libs), for GNU/Linux 2.6.24, BuildID[sha1]=0xb522d8002f06f33d9ce3d3b68a94ebc1910f4502, not stripped
phoenix@Phoenix:~/桌面$ size struct
   text	   data	    bss	    dec	    hex	filename
   1366	    688	     16	   2070	    816	struct

 一个正在运行着的C编译程序占用的内存分为代码区、初始化数据区、未初始化数据区、堆区和栈区5个部分。

  • 栈存储区:

(1)由编译器自动分配释放,通常存放程序临时创建的局部变量(但不包括static声明的变量,static意味着在数据段中存放变量),即函数括大括号 “{ }” 中定义的变量,其中还包括函数调用时其形参,调用后的返回值等。

(2)调用原理:每当一个函数被调用,该函数返回地址和一些关于调用的信息(比如某些寄存器的内容),被存储到栈区。然后这个被调用的函数再为它的自动变量和临时变量在栈区上分配空间,这就是C实现函数递归调用的方法。每执行一次递归函数调用,一个新的栈框架就会被使用,这样这个新实例栈里的变量就不会和该函数的另一个实例栈里面的变量混淆

(3)栈是由高地址向低地址扩展的数据结构,有先进后出的特点,即依次定义两个局部变量,首先定义的变量的地址是高地址,其次变量的地址是低地址。函数参数进栈的顺序是从右向左(主要是为了支持可变长参数形式)。

4)最后栈还具有“小内存、自动化、可能会溢出”的特点。栈顶的地址和栈的最大容量一般是系统预先规定好的,通常不会太大。由于栈中主要存放的是局部变量,而局部变量的占用的内存空间是其所在的代码段或函数段结束时由系统回收重新利用,所以栈的空间是循环利用自动管理的,一般不需要人为操作。如果某次局部变量申请的空间超过栈的剩余空间时就有可能出现 “栈的溢出”,进而导致意想不到的后果。所以一般不宜在栈中申请过大的空间,比如长度很大的数组、递归调用重复次数很多的函数等等。

  • 堆存储区:

(1)通常存放程序运行中动态分配的存储空间。它的大小,并不固定,可动态扩张或缩放。当进程调用malloc/free等函数分配内存时,新分配的内存就被动态添加到堆上(堆被扩张)/释放的内存从堆中被提出(堆被缩减)。

(2)堆与数据结构中的堆是两回事,分配方式倒是类似于链表。

(3)堆是低地址向高地址扩展的数据结构,是一块不连续的内存区域。在标准C语言上,使用malloc等内存分配函数是从堆中分配内存的,在Objective-C中,使用new创建的对象也是从堆中分配内存的。

(4)堆具有“大内存、手工分配管理、申请大小随意、可能会泄露”的特点,堆内存是操作系统划分给堆管理器来管理的,管理器向使用者(用户进程)提供API(malloc和free等)来使用堆内存。需要程序员手动分配释放,如果程序员在使用完申请后的堆内存却没有及时把它释放掉,那么这块内存就丢失了(进程自身认为该内存没被使用,但是在堆内存记录中该内存仍然属于这个进程,所以当需要分配空间时又会重新去申请新的内存而不是重复利用这块内存),就是我们常说的-内存泄漏,所以内存泄漏指的是堆内存被泄露了。

之所以分成这么多个区域,主要基于以下考虑:

  • 一个进程在运行过程中,代码是根据流程依次执行的,只需要访问一次,当然跳转和递归有可能使代码执行多次,而数据一般都需要访问多次,因此单独开辟空间以方便访问和节约空间。
  • 临时数据及需要再次使用的代码在运行时放入栈区中,生命周期短。
  • 全局数据和静态数据有可能在整个程序执行过程中都需要访问,因此单独存储管理。
  • 堆区由用户自由分配,以便管理。
#include <stdio.h>
int a = 0;                  // 全局初始化区
char p1;                    // 全局未初始化区
int main(int argc, const char * argv[]) {
      static int c = 0;//全局(静态)初始化区
      int b ;                 // 栈
      char s[] = "abc";       //"abc\0"在常量区,s在栈区
      char p2 ;               // 栈
      char *p3 = "123456";     // "123456\0"在常量区,p3在栈上。
      char *p4 = "123456";//"123456\0"在常量区,p4在栈区
//p3和p4是一样的,都指向同一个位置,"123456\0"所在位置
      p1 = (char )malloc(10); // 分配的10字节的区域就在堆区
      p2 = (char )malloc(20); // 分配的20字节的区域就在堆区
      printf("%p\n",p1);      // 0xffffffb0
      printf("%p\n",p2);      // 0xffffffc0
      return 0;                
//p1 变量的地址 0xffffffb0 比 p2 变量的地址 0xffffffc0 要小
}

栈和堆对比:

C语言函数返回值实现机制

我们知道,在子函数中返回局部变量的值是不会出什么问题的,但是,返回一个局部变量的指针或者引用时,在后续解引用这个指针时就得不到理想的结果,原因在于:子函数中的自动变量(栈内存中的变量)会在子函数返回后被释放掉,但是返回值会被保存在cpu的寄存器中,因此,在返回子函数后,返回值能从寄存器中将返回值赋值给调用函数中的变量,如果返回值是一个指针,那么该指针所指的内存地址会被保存在寄存器中,但是,指针指向的内存却被释放掉了(即值未定义)。因此,在编写代码时一般不会返回指向局部变量的指针,除非一下三种情况:

1)子函数中定义了静态局部变量,函数可以返回指向该静态局部变量的指针。因为该变量分布在内存的静态区(在函数编译时就将被初始化),所以在子函数返回时该变量仍然存在。

char * func1()
{
    static char name[]="Jack";
    return nema;
}

这里“Jack”存储在只读存储区,不能更改,将其赋值给name数组,即复制到静态存储区,因此name中保存的字符串不是常量字符串,可以通过数组下表进行更改。

2)子函数返回一个常量的指针,比如字符串常量,整形常量等。因为常量是定义在只读存储区,所以该常量也不会在子函数返回时被释放。

char * func2()
{
    char *name="Jack";
    return nema;
}

3)子函数返回一个指向动态分配内存的指针。动态内存是在堆内存中分配的,需要程序员手动释放,否则造成内存泄漏,所以在手动释放该指针之前都可以返回该指针。

char * func3()
{
    char *name = (char *)malloc(20);
    return nema;
}

猜你喜欢

转载自blog.csdn.net/weixin_39371711/article/details/81783780