C/C++内存管理

作者:毛晋

版本:1.0

完成日期:2009年10月16日

C++不同于C#Java的一个地方是它可以动态管理内存,但鱼与熊掌两者不可兼得,灵活性的代价是程序员需要花费更多的精力保证代码不发生内存错误。

1.       C/C++内存模型

1.1.     内存模型

C/C++内存模型由BSS数据段代码段组成。


C/C++内存模型图

 BSS:存放未初始化的全局静态变量。BSS段属于静态内存分配

 数据段:存放已初始化的全局静态变量。数据段属于静态内存分配。

 代码段:存放程序执行代码。代码段在内存区域中属于只读。在代码段中,也包含一些只读的常量表 达式。

  注:程序所需的BSS段、数据段、代码段存储空间在程序运行之前就已经被分配了。(?)

 :存放动态分配的内存块。堆的大小不固定,可动态扩展和缩减。

 :存放程序临时创建的局部变量。(注:静态局部变量在数据段中存放???)一般来说,这些局部变量为函数的参数、在函数体(或数据块)中创建的临时非静态局部变量。

1.2.     关于栈和堆的进一步讨论

1.2.1.    内存模型中的栈和堆与数据结构中的栈和堆的比较

其实,内存模型中的栈和堆与数据结构中的栈和堆不是一回事,不要把两者混淆。其中,数据结构中的栈是一种FILO的线性表;数据结构中的堆是一种树结构,这种树结构的每一个结点有一个值,根结点的值最小。

1.2.2.    程序申请栈和堆后系统的响应

:只要栈的剩余空间大于所申请空间,系统将为程序提供内存,否则将报异常提示栈溢出。

:首先应该知道操作系统有一个记录空闲内存地址的链表,当系统收到程序的申请时,会遍历该链表,寻找第一个空间大于所申请空间的堆结点,然后将该结点从空闲结点链表中删除,并将该结点的空间分配给程序,另外,对于大多数系统,会在这块内存空间中的首地址处记录本次分配的大小,这样,代码中的 delete语句才能正确的释放本内存空间。另外,由于找到的堆结点的大小不一定正好等于申请的大小,系统会自动的将多余的那部分重新放入空闲链表中。
 
也就是说堆会在申请后还要做一些后续的工作这就会引出申请效率的问题。

1.2.3.    栈和堆申请效率的比较

:系统自动分配,速度较快。

:由程序员分配,且容易产生碎片。

1.2.4.    栈和堆申请大小的限制

:在Windows,栈是向低地址扩展的数据结构,是一块连续的内存的区域。这句话的意思是栈顶的地址和栈的最大容量是系统预先规定好的,在 WINDOWS下,栈的大小是2M

可以通过/STACK:reserve[,commit]链接器选项设置操作系统分配给应用程序的最大栈空间。

:堆是向高地址扩展的数据结构,是不连续的内存区域。这是由于系统是用链表来存储的空闲内存地址的,自然是不连续的,而链表的遍历方向是由低地址向高地址。堆的大小受限于计算机系统中有效的虚拟内存。

可以通过/HEAP:reserve[,commit] 链接器选项设置操作系统分配给应用程序的最大堆空间。

1.2.5.    栈和堆中的存储内容

:在函数调用时,第一个进栈的是主函数中函数调用后的下一条指令(函数调用语句的下一条可执行语句)的地址,然后是函数的各个参数,在大多数的C编译器中,参数是由右往左入栈的,然后是函数中的局部变量。注意静态变量是不入栈的。
当本次函数调用结束后,局部变量先出栈,然后是参数,最后栈顶指针指向最开始存的地址,也就是主函数中的下一条指令,程序由该点继续运行。

:一般是在堆的头部用一个字节存放堆的大小。堆中的具体内容由程序员安排。

2.       C/C++内存分配

C/C++内存分配方式包括静态空间分配栈空间分配堆空间分配

静态存储空间分配:存放全局静态变量、局部静态变量和常量。静态存储空间在程序运行期间一直存在。

栈空间分配:存放局部变量。当程序运行超出变量作用域时,栈空间被释放。

堆空间分配(动态内存空间分配):堆内存的分配和释放均有程序员完成。

3.       C/C++内存管理常见错误

混淆静态存储空间和栈空间

代码示例:

Code:
  1. char* p = “Hello World1”;  
  2. char a[] = “Hello World2”;  
  3. p[2] = ‘A’; //Error  
  4. a[2] = ‘A’; //OK  
  5. char* p1 = “Hello World1;”  

     注意,在上面的代码中,数据 Hello World1”和数据“Hello World2”是存储于不同的区域的。

4行代码是正确的,因为数据“Hello World2”存在于数组中,所以,此数据存储于栈区,对它修改是没有任何问题的。

3行代码是错误的,因为指针变量p仅仅能够存储某个存储空间的地址,数据“Hello World1”为字符串常量,所以存储在静态存储区。虽然通过p[2]可以访问到静态存储区中的第三个数据单元,即字符‘l’所在的存储的单元。但是因为 数据“Hello World1”为字符串常量,不可以改变,所以在程序运行时,会报告内存错误。并且,如果此时对p和p1输出的时候会发现p和p1里面保存的地址是完全相同的。换句话说,在数据区只保留一份相同的数据(见图)。


内存泄漏

代码示例:

Code:
  1. void f()  
  2.   
  3. {  
  4.   
  5.    …  
  6.   
  7.    char * p;  
  8.   
  9.    p = (char*)new char[100];  
  10.   
  11.    …  
  12.   
  13. }  

这个程序做了一件很无意义并且会带来很大危害的事情。因为,虽然申请了堆内存,p保存了堆内存的首地址。但是,此变量是临时变量,当函数调用结束时p变量消失。也就是说,再也没有变量存储这块堆内存的首地址,我们将永远无法再使用那块堆内存了。

在实践中,使newdelete成对出现可以避免这样的问题。

函数返回指向局部变量的指针

代码示例:

Code:
  1. char* f1()  
  2.   
  3. {  
  4.   
  5.    char* p = NULL;  
  6.   
  7.    char a;  
  8.   
  9.    p = &a;  
  10.   
  11.    return p;  
  12.   
  13. }  
  14.   
  15. char* f2()  
  16.   
  17. {  
  18.   
  19.    char* p = NULL:  
  20.   
  21.    p =(char*) new char[4];  
  22.   
  23.    return p;  
  24.   
  25. }  

函数f1有问题。函数f1虽然返回的是一个存储空间,但是此空间为临时栈空间。该空间在函数f1调用结束时被释放。所以,当调用f1函数时,如果程序中有下面的语句:

Code:
  1. char* p ;  
  2.   
  3. p = f1();  
  4.   
  5. *p = ‘a’; //此时p为悬垂指针  

此时,编译并不会报告错误,但是在程序运行时,会发生异常错误。因为,你对不应该操作的内存(即,已经释放掉的存储空间)进行了操作。

函数f2不会有任何问题。因为,new这个命令是在堆中申请存储空间,一旦申请成功,除非你将其delete或者程序终结,这块内存将一直存在。

数组越界、读未初始化的内存、读/写已被释放的内存、释放已被释放的内存空间

代码示例:

Code:
  1. char* str1="four";  
  2. char* str2=new char[4]; //not enough space  
  3. char* str3=str2;  
  4. cout<<str2<<endl;  //UMR  
  5. strcpy(str2,str1); //ABW  
  6. cout<<str2<<endl;  //ABR  
  7. delete str2;  
  8. str2[0]+=2;  //FMR and FMW  
  9. delete str3; //FFM  

其中:

         UMR:Uninitialized Memery Read.        读未初始化内存

         ABR/W:Array Bound Write.                     数组越界/

         FMR/W:Freed Memery Read/Write.     /写已被释放的内存 

         FFM:Free Freed Memery.                       释放已被释放的内存

4.       C/C++内存字节对齐

4.1.1.    问题的由来

某些平台的CPU堆某些特定的数据只能从某些特定的地址开始读取,最常见的是如果不按照适合其平台的要求对数据进行存放,会造成存取效率上的损失。

例如,某些平台每次从内存中读/写数据都是从偶地址开始。如果一个int型(假设为32位),如果存放在偶地址开始的地方,那么CPU一个读周期就可以读出;而如果存放在奇地址开始的地方,那么CPU需要两个读周期将数据读出,并对两次读出的结果的高低字节进行拼凑才能得到该int型的数据。显然效率下降了一半。这是空间对时间的博弈。

4.1.2.    内存字节对齐的实现

通常,在写程序的时候,程序员不必考虑字节对齐的问题。编译器会自动选择合适的目标平台的对齐策略。当然,程序员可以通过C/C++的预编译指令#prama而改变对指定数据的对齐方法。

然而,若我们不了解这个问题,最常见的就是结构体的sizeof结果,出乎意料。因此,需要对C/C++编译器的字节对齐算法有所了解。

C/C++编译器对结构体的字节对齐算法VS2008平台):

四个概念值:

1)        数据类型自身的对齐值:就是上面交代的基本数据类型的自身对齐值。

2)        指定对齐值:#pragma pack (value)时的指定对齐值value

3)        结构体或者类的自身对齐值:其成员中自身对齐值最大的那个值。

4)        数据成员、结构体和类的有效对齐值:自身对齐值和指定对齐值中较小的那个值。

l  有效对齐值N是最终用来决定数据存放地址方式的值。有效对齐N,就是表示“对齐在N上”,也就是说该数据的"存放起始地址%N=0.

l  数据结构中的数据变量都是按定义的先后顺序来排放的。第一个数据变量的起始地址就是数据结构的起始地址。

l  结构体的成员变量要对齐排放,结构体本身也要根据自身的有效对齐值圆整,即结构体成员变量占用总长度是对结构体有效对齐值的整数倍。

代码示例

Code:
  1. struct B {  
  2.   
  3.     char b;  
  4.   
  5.     int a;  
  6.   
  7.     short c;  
  8.   
  9. };  

假设B从地址空间0x0000开始排放。该例子中没有定义指定对齐值,在VS2008环境下,该值默认为4。第一个成员变量b的自身对齐值是1,比指定或者默认指定对齐值4小,所以其有效对齐值为1,所以其存放地址0x0000符合0x0000%1=0.第二个成员变量a,其自身对齐值为4,所以有效对齐值也为 4,所以只能存放在起始地址为0x00040x0007这四个连续的字节空间中,复核0x0004%4=0,且紧靠第一个变量。第三个变量c,自身对齐 值为2,所以有效对齐值也是2,可以存放在0x00080x0009这两个字节空间中,符合0x0008%2=0。所以从0x00000x0009 放的都是B内容。再看数据结构B的自身对齐值为其变量中最大对齐值(这里是b)所以就是4,所以结构体的有效对齐值也是4。根据结构体圆整的要求, 0x00090x0000=10字节,(102)%40。所以0x0000A0x000B也为结构体B所占用。故B0x00000x000B 共有12个字节,sizeof(struct B)=12;

Code:
  1. #pragma pack (2) /*指定按2字节对齐*/  
  2.   
  3. struct C {  
  4.   
  5.     char b;  
  6.   
  7.     int a;  
  8.   
  9.     short c;  
  10.   
  11. };  
  12.   
  13. #pragma pack () /*取消指定对齐,恢复缺省对齐*/  

第一个变量b的自身对齐值为1,指定对齐值为2,所以,其有效对齐值为1,假设C0x0000开始,那么b存放在0x0000,符合0x0000%1= 0;第二个变量,自身对齐值为4,指定对齐值为2,所以有效对齐值为2,所以顺序存放在0x00020x00030x00040x0005四个连续 字节中,符合0x0002%2=0。第三个变量c的自身对齐值为2,所以有效对齐值为2,顺序存放在0x00060x0007中,符合0x0006%2=0。所以从0x00000x00007共八字节存放的是C的变量。又C的自身对齐值为4,所以 C的有效对齐值为28%2=0,C只占用0x00000x0007的八个字节。所以sizeof(struct C)=8.

 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值