1、C+ 内存管理之一(变量) 本章主要介绍几个概念(1)变量和常量变量的基本类型:bool、char、int、short、long、float、double,注意个变量的所占的字节数。常量分类:宏常量、const 常量、字符串常量,注意宏常量和 cons 常量之间的区别。#define MAX 100 /宏常量 const int MAX = 100; / C+ 语言的 const 常量char *p=“hello world“; /字符串常量(2)全局变量和静态变量全局变量:静态存储方式,具有全局作用域。全局变量只需在一个源文件中定义,就可以作用于所有的源文件,要用 extern 关键字再次声
2、明这个全局变量。静态变量:静态存储方式,其由于作用域不同而分为静态全局变量和静态局部变量。静态全局变量作用于其定义它的源文件里,不能作用到其它源文件,即被 static 关键字修饰过的变量具有文件作用域。静态局部变量只对定义自己的函数体始终可见。即若全局变量加上 static,可改变其作用域,不改变其内 存存储位置,而局部变量加上 static,则改变其内存存储位置,不改变其作用域。之所以也引入静态变量是因为,在函数内部定义的变量,在程序执行到它的定义处时,编译器为它在栈上分配空间,而函数在栈上分配的空间在此函数执行结 束时会释放掉,这样就产生了一个问题: 如果想将函数中此变量的值保存至下一次
3、调用时,如何实现? 最容易想到的方法是定义一个全局的变量,但定义为一个全局变量有许多缺点,最明显的缺点是破坏了此变量的访问范围(使得在此函数中定义的变量,不仅仅受此 函数控制)。关于静态变量 static 的一个小小的测试:#include using namespace std; int Test(int a)static int b=5;b+=a;return b;int main() int a=Test(10);coutaendl;int b=Test(20);coutbendl;return 0; 1、C+编译的程序占用的内存分类(1)栈区(stack):程序运行时由编译器自动分配,
4、存放函数的参数值,局部变量的值等。其操作方式类似于数据结构中的栈。程序结束时由编译器自动释放。(2)堆区(heap) :在内存开辟另一块存储区域。一般由程序员分配释放, 若程序员不释放,程序结束时可能由 OS 回收。注意它与数据结构中的堆是两回事,分配方式倒是类似于链表。(3)全局静态区(static):编译器编译时即分配内存。全局变量和静态变量的存储是放在一块的,初始化的全局变量和静态变量在一块区域, 未初始化的全局变量 和未初始化的静态变量在相邻的另一块区域。程序结束后由系统释放。(4)文字常量区 :常量字符串就是放在这里的。 程序结束后由系统释放。(5)程序代码区:存放函数体(类成员函数
5、和全局函数)的二进制代码。2、内存分配方式有三种(1)从静态存储区域分配:内存在程序编译的时候就已经分配好,这块内存在程序的整个运行期间都存在。例如全局变量,static 变量。(2)在栈上创建:在执行函数时,函数内局部变量的存储单元都可以在栈上创建,函数执行结束时这些存储单元自动被释放。栈内存分配运算内置于处理器的指令集中,效率很高,但是分配的内存容量有限。(3)从堆上分配:亦称动态内存分配。程序在运行的时候用 malloc 或 new 申请任意多少的内存,程序员自己负责在何时用 free 或 delete 释放内存。动态内存的生存期由我们决定,使用非常灵活,但问题也最多。3、C+变量内存分
6、布通过程序加以说明(1)变量分布View Code 运行结果:View Code 分析结果(debug):(1)变量在内存地址的分布为:栈-堆-代码区-全局静态-文字常量区(2)同一区域的各变量按声明的顺序在内存的中依次由低到高分配空间,但栈中是由高到低的。(3)全局变量和静态变量如果不赋值,默认为 0。 栈中的变量如果不赋值,则是一个随机的数据。(4)编译器会认为全局变量和静态变量是等同的,已初始化的全局变量和静态变量分配在一起,未初始化的全局变量和静态变量分配在另一起。(2)函数分布View Code 运行结果View Code 结果分析:主函数中栈的地址都要高于子函数中参数及栈地址,证明
7、了栈的伸展方向是由高地址向低地址扩展的。主函数和子函数中静态数据的地址也是相邻的,说明程序会将已初始化的全局变量和表态变量分配在一起,未初始化的全局变量和表态变量分配在另一起。程序变量分区中栈和堆的区别(1)申请方式stack: 由系统自动分配。 例如,声明在函数中一个局部变量 int b; 系统自动在栈中为 b 开辟空间。heap: 需要程序员自己申请,并指明大小,在 C 中 malloc 函数,C+中是 new运算符。如 p1 = (char *)malloc(10); p1 = new char10;如 p2 = (char *)malloc(10); p2 = new char20;但
8、是注意 p1、p2 本身是在栈中的。(2)申请后系统的响应栈:只要栈的剩余空间大于所申请空间,系统将为程序提供内存,否则将报异常提示栈溢出。堆:首先应该知道操 作系统有一个记录空闲内存地址的链表,当系统收到程序的申请时,会遍历该链表,寻找第一个空间大于所申请空间的堆结点,然后将该结点从空闲结点链表中删 除,并将该结点的空间分配给程序。对于大多数系统,会在这块内存空间中的首地址处记录本次分配的大小,这样,代码中的delete 语句才能正确的释放本内 存空间。由于找到的堆结点的大小不一定正好等于申请的大小,系统会自动的将多余的那部分重新放入空闲链表中。(3)申请大小的限制栈:在 Windows 下
9、,栈是向低地址扩展的数据结构,是一块连续的内存的区域。这句话的意思是栈顶的地址和栈的最大 容量是系统预先规定好的,在 WINDOWS下,栈的大小是 2M(也有的说是 1M,总之是一个编译时就确定的常数),如果申请的空间超过栈的剩余空间时,将提示 overflow。因 此,能从栈获得的空间较小。堆:堆是向高地址扩展的数据结构,是不连续的内存区域。这是由于系统是用链表来存储的空闲内存地址的,自然是不连续的,而链表的遍历方向是由低地址向高地址。堆的大小受限于计算机系统中有效的虚拟内存。由此可见,堆获得的空间比较灵活,也比较大。(4)申请效率的比较栈由系统自动分配,速度较快。但程序员是无法控制的。堆是
10、由 new 分配的内存,一般速度比较慢,而且容易产生内存碎片,不过用起来最方便。(5)堆和栈中的存储内容栈:在函数调用时,第一个进栈的是主函数中后的下一条指令(函数调用语句的下一条可执行语句)的地址,然后是函数 的各个参数,在大多数的 C 编译器中,参数是由右往左入栈的,然后是函数中的局部变量。注意静态变量是不入栈的。当本次函数调用结束后,局部变量先出栈,然 后是参数,最后栈顶指针指向最开始存的地址,也就是主函数中的下一条指令,程序由该点继续运行。堆:一般是在堆的头部用一个字节存放堆的大小。堆中的具体内容有程序员安排。(6)存取效率的比较char s1 = “a“;char *s2 = “b“
11、;a 是在运行时刻赋值的;而 b 是在编译时就确定的;但是,在以后的存取中,在栈上的数组比指针所指向的字符串(例如堆)快。 比如:int main()char a = 1;char c = “1234567890“;char *p =“1234567890“;a = c1;a = p1;return 0;对应的汇编代码10: a = c1;00401067 8A 4D F1 mov cl,byte ptr ebp-0Fh0040106A 88 4D FC mov byte ptr ebp-4,cl11: a = p1;0040106D 8B 55 EC mov edx,dword ptr eb
12、p-14h00401070 8A 42 01 mov al,byte ptr edx+100401073 88 45 FC mov byte ptr ebp-4,al第一种在读取时直接就把字符串中的元素读到寄存器 cl 中,而第二种则要先把指针值读到 edx 中,再根据 edx 读取字符,显然慢了。(7)小结堆和栈的主要区别由以下几点:1、管理方式不同;2、空间大小不同;3、能否产生碎片不同;4、生长方向不同;5、分配方式不同;6、分配效率不同;管理方式:对于栈来讲,是由编译器自动管理,无需我们手工控制;对于堆来说,释放工作由程序员控制,容易产生 memory leak。空间大小:一般来讲在
13、32 位系统下,堆内存可以达到 4G 的空间,从这个角度来看堆内存几乎是没有什么限制的。但是对于栈来讲,一般都是有一定的空间大小的,例如,在 VC6 下面,默认的栈空间大小是 1M。当然,这个值可以修改。碎片问题:对于堆来讲,频繁的 new/delete 势必会造成内存空间的不连续,从而造成大量的碎片,使程序效率降低。对于栈来讲,则不会存在这个问 题,因为栈是先进后出的队列,他们是如此的一一对应,以至于永远都不可能有一个内存块从栈中间弹出,在他弹出之前,在他上面的后进的栈内容已经被弹出,详 细的可以参考数据结构。生长方向:对于堆来讲,生长方向是向上的,也就是向着内存地址增加的方向;对于栈来讲,
14、它的生长方向是向下的,是向着内存地址减小的方向增长。分配方式:堆都是动态分配的,没有静态分配的堆。栈有 2 种分配方式:静态分配和动态分配。静态分配是编译器完成的,比如局部变量的分配。动态分配由 malloca 函数进行分配,但是栈的动态分配和堆是不同的,他的动态分配是由编译器进行释放,无需我们手工实现。分配效率:栈是机器系统提供的数据结构,计算机会在底层对栈提供支持:分配专门的寄存器存放栈的地址,压栈出栈都有专门的指令执行,这就决定了栈的效率 比较高。堆则是 C/C+函数库提供的,它的机制是很复杂的,例如为了分配一块内存,库函数会按照一定的算法(具体的算法可以参考数据结构/操作系统)在 堆内
15、存中搜索可用的足够大小的空间,如果没有足够大小的空间(可能是由于内存碎片太多),就有可能调用系统功能去增加程序数据段的内存空间,这样就有机会 分 到足够大小的内存,然后进行返回。显然,堆的效率比栈要低得多。从这里我们可以看到,堆和栈相比,由于大量 new/delete 的使用, 容易造成大量的内存碎片;由于没有专门的系统支持,效率很低;由于可能引发用户态和核心态的切换,内存的申请,代价变得更加昂贵。所以栈在程序中是应用最 广泛的,就算是函数的调用也利用栈去完成,函数调用过程中的参数,返回地址, EBP 和局部变量都采用栈的方式存放。所以,我们推荐大家尽量用栈,而不是用堆。虽然栈有如此众多的好处
16、,但是由于和堆相比不是那么灵活,有时候分配大量 的内存空间,还是用堆好一些。无论是堆还是栈,都要防止越界现象的发生(除非你是故意使其越界),因为越界的结果要么是程序崩溃,要么是摧毁程序的堆、栈 结构,产生以想不到的结果。C+ 内存管理之四(new/delete vs malloc/free) new/delete vs malloc/freemalloc 与 free 是 C+/C 语言的标准库函数,new/delete 是 C+的运算符。它们都可用于申请动态内存和释放内存。对于非内部数据类型的对象而言,光用 maloc/free 无法满足动态对象的要求。对象在创建的同时要自动执行构造 函数,
17、对象在消亡之前要自动执行析构函数。由于 malloc/free 是库函数而不是运算符,不在编译器控制权限之内,不能够把执行构造函数和析构函数的 任务强加于 malloc/free。因此 C+语言需要一个能完成动态内存分配和初始化工作的运算符 new,以及一个能完成清理与释放内存工作的运算符 delete。注意new/delete 不是库函数。既然 new/delete 的功能完全覆盖了 malloc/free,为什么 C+不把malloc/free 淘汰出局呢?这是因为 C+程序经常要调用 C 函数,而 C 程序只能用 malloc/free 管理动态内存C+ 内存管理之五(数组 vs 指针)
18、 C+/C 程序中,指针和数组在不少地方可以相互替换着用,让人产生一种错觉,以为两者是等价的。但二者有着本质的区别:数组:要么在静态存储区被创建(如全局数组),要么在栈上被创建。数组名对应着(而不是指向)一块内存,其地址与容量在生命期内保持不变,只有数组的内容可以改变。指针:可以随时指向任意类型的内存块,它的特征是“可变”,所以我们常用指针来操作动态内存。指针远比数组灵活,但也更危险。下面以字符串为例比较指针与数组的特性:(1)修改内容实例 1 代码中,字符数组 a 的容量是 6 个字符,其内容为 hello。a 的内容可以改变,如 a0= X。指针 p 指向常量字符串“world”(位于静态
19、存储区,内容为 world),常量字符串的内容是不可以被修改的。从语法上看,编译器并不觉得语句 p0= X有什么不妥,但是该语句企图修改常量字符串的内容而导致运行错误。实例 1 修改数组和指针内容char a = “hello”;a0 = X;cout a endl;char *p = “world”; / 注意 p 指向常量字符串p0 = X; / 编译器不能发现该错误cout p endl;(2)内容复制与比较不能对数组名进行直接复制与比较。示例 2 中,若想把数组 a 的内容复制给数组 b,不能用语句 b = a ,否则将产生编译错误。应该用标准库函数strcpy 进行复制。同理,比较
20、b 和 a 的内容是否相同,不能用 if(b=a) 来判断,应该用标准库函数 strcmp 进行比较。指针应用中,语句 p = a 并不能把 a 的内容复制指针 p,而是把 a 的地址赋给了 p。要想复制 a 的内容,可以先用库函数 malloc 为 p 申请一块容量为strlen(a)+1 个字符的 内存,再用 strcpy 进行字符串复制。同理,语句if(p=a) 比较的不是内容而是地址,应该用库函数 strcmp 来比较。实例 2 数组和指针的内容复制与比较/ 数组char a = “hello“;char b10;strcpy(b, a); / 不能用 b = a;if(strcmp(
21、b, a) = 0) / 不能用 if (b = a)/ 指针int len = strlen(a);char *p = (char *)malloc(sizeof(char)*(len+1);strcpy(p,a); / 不要用 p = a;if(strcmp(p, a) = 0) / 不要用 if (p = a)(3) 计算内存容量用运算符 sizeof 可以计算出数组的容量(字节数)。示例 3(a)中,sizeof(a)的值是 12(注意别忘 了)。指针 p 指向 a,但是 sizeof(p)的值却是 4。这是因为 sizeof(p)得到的是一个指针变量的字节数,相当于 sizeof(c
22、har*),而不是 p 所指的内存容量。C+/C 语言没有办法知道指针所指的内存容量,除非在申请内存时记住它。注意当数组作为函数的参 数进行传递时,该数组自动退化为同类型的指针。示例 7-3-3(b)中,不论数组 a 的容量是多少,sizeof(a)始终等于 sizeof(char *)。实例 3(a) 计算数组和指针的内存容量char a = “hello world“;char *p = a;cout sizeof(a) endl; / 12 字节cout sizeof(p) endl; / 4 字节实例 3(b) 数组退化为指针void Func(char a100)cout sizeo
23、f(a) endl; / 4 字节而不是 100 字节(4)指针参数是如何传递内存的?如果函数的参数是一个指针,不要指望用该指针去申请动态内存。示例4(a)中,Test 函数的语句 GetMemory(str, 200)并没有使 str 获得期望的内存,str 依旧是 NULL,为什么?实例 4(a) 试图用指针参数申请动态内存void GetMemory(char *p, int num)p = (char *)malloc(sizeof(char) * num);void Test(void)char *str = NULL;GetMemory(str, 100); / str 仍然为 N
24、ULL strcpy(str, “hello“); / 运行错误毛病出在函数 GetMemory 中。编译器总是要为函数的每个参数制作临时副本,指针参数 p 的副本是 _p,编译器使 _p = p。如果函数体内的程序修改了_p 的内容,就导致参数 p 的内容作相应的修改。这就是指针可以用作输出参数的原因。在本例中,_p 申请了新的内存,只是把 _p 所指的内存地址改变了,但是 p 丝毫未变。所以函数 GetMemory 并不能输出任何东西。事实上,每执行一次 GetMemory 就会泄露一块内存,因 为没有用 free 释放内存。如果非得要用指针参数去申请内存,那么应该改用“指向指针的指针”,
25、见示例 4(b)实例 4(b) 用指向指针的指针申请动态内存void GetMemory2(char *p, int num)*p = (char *)malloc(sizeof(char) * num);void Test2(void)char *str = NULL;GetMemory2( / 注意参数是 cout str endl;free(str); 由于“指向指针的指针”这个概念不容易理解,我们可以用函数返回值来传递动态内存。这种方法更加简单,见示例 4(c)实例 4(c) 用函数返回值来传递动态内存char *GetMemory3(int num)char *p = (char *
26、)malloc(sizeof(char) * num); /返回堆中的地址return p;void Test3(void)char *str = NULL;str = GetMemory3(100); strcpy(str, “hello“);cout str endl;free(str); 用函数返回值来传递动态内存这种方法虽然好用,但是常常有人把 return语句用错了。这里强调不要用 return 语句返回指向“栈内存”的指针,因为该内存在函数结束时自动消亡,见示例 4(d)实例 4(d) return 语句返回指向“栈内存”的指针char *GetString(void)char p
27、 = “hello world“; /返回栈中的地址return p; / 编译器将提出警告void Test4(void)char *str = NULL;str = GetString(); / str 的内容是垃圾cout str endl;用调试器逐步跟踪 Test4,发现执行 str = GetString 语句后 str 不再是NULL 指针,但是 str 的内容不是“hello world”而是垃圾。如果把示例 4(d)改写成示例 4(e),会怎么样?实例 4(e) return 语句返回常量字符串char *GetString2(void)char *p = “hello world“;return p;void Test5(void)char *str = NULL;str = GetString2();cout str endl;函数 Test5 运行虽然不会出错,但是函数 GetString2 的设计概念却是错误的。因为 GetString2 内的“hello world”是常量字符串,位于静态存储区,它在程序生命期内恒定不变。无论什么时候调用 GetString2,它返回的始终是同一个“只读”的内存块。