重新编写内容,并附带了相关链接~
一、内存分配方式
一个由C/C++编译的程序占用的内存分为以下几个部分:
- 栈区(stack):由编译器自动分配释放 ,存放函数的参数值,局部变量的值等。其操作方式类似于数据结构中的栈。
- 堆区(heap) : 一般由程序员分配释放, 若程序员不释放,程序结束时可能由OS回收 。注意它与数据结构中的堆是两回事,分配方式倒是类似于链表,呵呵。
- 全局区(静态区)(static):全局变量和静态变量的存储是放在一块的,初始化的全局变量和静态变量在一块区域, 未初始化的全局变量和未初始化的静态变量在相邻的另一块区域。 - 程序结束后有系统释放
- 文字(字符)常量区 :常量字符串就是放在这里的。 程序结束后由系统释放
- 程序代码区 :存放函数体的二进制代码。
二、例子程序
1 | int a = 0; //全局初始化区 |
三、堆和栈的理论知识
3.1 申请方式
stack:由系统自动分配。
例如,声明在函数中一个局部变量 int b; 系统自动在栈中为b开辟空间
heap:需要程序员自己申请,并指明大小(在c中为,malloc函数)
例如,p1 = (char *)malloc(10);
Ps: 但是要注意p1
本身是在栈中的。
3.2 申请后系统的响应
栈:只要栈的剩余空间大于所申请空间,系统将为程序提供内存,否则将报异常提示栈溢出。
堆:首先应该知道操作系统有一个记录空闲内存地址的链表,当系统收到程序的申请时, 会遍历该链表,寻找第一个空间大于所申请空间的堆结点,然后将该结点从空闲结点链表中删除,并将该结点的空间分配给程序,另外,对于大多数系统,会在这块内存空间中的首地址处记录本次分配的大小,这样,代码中的delete语句才能正确的释放本内存空间。另外,由于找到的堆结点的大小不一定正好等于申请的大小,系统会自动的将多余的那部分重新放入空闲链表中。
3.3 申请大小的限制
栈:在Windows下,栈是向低地址扩展的数据结构,是一块连续的内存的区域。这句话的意思是栈顶的地址和栈的最大容量是系统预先规定好的,在 WINDOWS下,栈的大小是2M(也有的说是1M,总之是一个编译时就确定的常数),如果申请的空间超过栈的剩余空间时,将提示overflow。因此,能从栈获得的空间较小。
堆:堆是向高地址扩展的数据结构,是不连续的内存区域。这是由于系统是用链表来存储的空闲内存地址的,自然是不连续的,而链表的遍历方向是由低地址向高地址。堆的大小受限于计算机系统中有效的虚拟内存。由此可见,堆获得的空间比较灵活,也比较大。
3.4 申请效率的比较
栈由系统自动分配,速度较快。但程序员是无法控制的。
堆是由new分配的内存,一般速度比较慢,而且容易产生内存碎片,不过用起来最方便.
另外,在WINDOWS下,最好的方式是用VirtualAlloc分配内存,他不是在堆,也不是在栈是直接在进程的地址空间中保留一块内存,虽然用起来最不方便。但是速度快,也最灵活。
3.5 堆和栈的存储内容
栈: 在函数调用时,第一个进栈的是主函数中后的下一条指令(函数调用语句的下一条可执行语句)的地址,然后是函数的各个参数,在大多数的C编译器中,参数是由右往左入栈的,然后是函数中的局部变量。注意静态变量是不入栈的。
当本次函数调用结束后,局部变量先出栈,然后是参数,最后栈顶指针指向最开始存的地址,也就是主函数中的下一条指令,程序由该点继续运行。
堆:一般是在堆的头部用一个字节存放堆的大小。堆中的具体内容有程序员安排。
3.6 存取效率的比较
1 | char s1[] = "aaaaaaaaaaaaaaa"; |
aaaaaaaaaaa是在运行时刻赋值的;而bbbbbbbbbbb是在编译时就确定的;
但是,在以后的存取中,在栈上的数组比指针所指向的字符串(例如堆)快。比如:
1 |
|
对应的汇编代码:
1 | 10: a = c[1]; |
第一种在读取时直接就把字符串中的元素读到寄存器cl中,而第二种则要先把指针值读到edx中,再根据edx读取字符,显然慢了。
3.7 小结
堆和栈的区别可以用如下的比喻来看出:
使用栈就象我们去饭馆里吃饭,只管点菜(发出申请)、付钱、和吃(使用),吃饱了就走,不必理会切菜、洗菜等准备工作和洗碗、刷锅等扫尾工作,他的好处是快捷,但是自由度小。
使用堆就象是自己动手做喜欢吃的菜肴,比较麻烦,但是比较符合自己的口味,而且自由度大。
- 内存分配方面:
堆:一般由程序员分配释放, 若程序员不释放,程序结束时可能由OS回收 。注意它与数据结构中的堆是两回事,分配方式是类似于链表。可能用到的关键字如下:new、malloc、delete、free等等。
栈:由编译器(Compiler)自动分配释放,存放函数的参数值,局部变量的值等。其操作方式类似于数据结构中的栈。
- 申请方式方面:
堆:需要程序员自己申请,并指明大小。在c中malloc函数如p1 = (char *)malloc(10);在C++中用new运算符,但是注意p1、p2本身是在栈中的。因为他们还是可以认为是局部变量。
栈:由系统自动分配。 例如,声明在函数中一个局部变量 int b;系统自动在栈中为b开辟空间。
- 系统响应方面:
堆:操作系统有一个记录空闲内存地址的链表,当系统收到程序的申请时,会遍历该链表,寻找第一个空间大于所申请空间的堆结点,然后将该结点从空闲结点链表中删除,并将该结点的空间分配给程序,另外,对于大多数系统,会在这块内存空间中的首地址处记录本次分配的大小,这样代码中的delete语句才能正确的释放本内存空间。另外由于找到的堆结点的大小不一定正好等于申请的大小,系统会自动的将多余的那部分重新放入空闲链表中。
栈:只要栈的剩余空间大于所申请空间,系统将为程序提供内存,否则将报异常提示栈溢出。
- 大小限制方面:
堆:是向高地址扩展的数据结构,是不连续的内存区域。这是由于系统是用链表来存储的空闲内存地址的,自然是不连续的,而链表的遍历方向是由低地址向高地址。堆的大小受限于计算机系统中有效的虚拟内存。由此可见,堆获得的空间比较灵活,也比较大。
栈:在Windows下, 栈是向低地址扩展的数据结构,是一块连续的内存的区域。这句话的意思是栈顶的地址和栈的最大容量是系统预先规定好的,在WINDOWS下,栈的大小是固定的(是一个编译时就确定的常数),如果申请的空间超过栈的剩余空间时,将提示overflow。因此,能从栈获得的空间较小。
- 效率方面:
堆:是由new分配的内存,一般速度比较慢,而且容易产生内存碎片,不过用起来最方便,另外,在WINDOWS下,最好的方式是用VirtualAlloc分配内存,他不是在堆,也不是在栈是直接在进程的地址空间中保留一快内存,虽然用起来最不方便。但是速度快,也最灵活。
栈:由系统自动分配,速度较快。但程序员是无法控制的。
- 存放内容方面:
堆:一般是在堆的头部用一个字节存放堆的大小。堆中的具体内容有程序员安排。
栈:在函数调用时第一个进栈的是主函数中后的下一条指令(函数调用语句的下一条可执行语句)的地址然后是函数的各个参数,在大多数的C编译器中,参数是由右往左入栈,然后是函数中的局部变量。 注意: 静态变量是不入栈的。当本次函数调用结束后,局部变量先出栈,然后是参数,最后栈顶指针指向最开始存的地址,也就是主函数中的下一条指令,程序由该点继续运行。
- 存取效率方面:
堆:是在编译时就确定的;
栈:是在运行时赋值的;
char *s1 = "Hello Word";
char s1[] = "Hello Word";
用数组比用指针速度要快一些,因为指针在底层汇编中需要用edx
寄存器中转一下,而数组在栈上直接读取。
四、C++的内存分配方式
在C++中,内存分成5个区,他们分别是堆、栈、自由存储区、全局/静态存储区和常量存储区。
栈,就是那些由编译器在需要的时候分配,在不需要的时候自动清楚的变量的存储区。里面的变量通常是局部变量、函数参数等。
堆,就是那些由new分配的内存块,他们的释放编译器不去管,由我们的应用程序去控制,一般一个new就要对应一个delete。如果程序员没有释放掉,那么在程序结束后,操作系统会自动回收。
自由存储区,就是那些由malloc等分配的内存块,他和堆是十分相似的,不过它是用free来结束自己的生命的。
全局/静态存储区,全局变量和静态变量被分配到同一块内存中,在以前的C语言中,全局变量又分为初始化的和未初始化的,在C++里面没有这个区分了,他们共同占用同一块内存区。
常量存储区,这是一块比较特殊的存储区,他们里面存放的是常量,不允许修改(当然,你要通过非正当手段也可以修改,而且方法很多)
五、内存分配方式引发的概念区别(C)
5.1 只读变量和常量
下面的例子用一个const变量来初始化数组,ANSI C的编译器会报告一个错误呢?
1 | const int n = 5; |
答案与分析:
1)这个问题讨论的是“常量”与“只读变量”的区别。常量肯定是只读的,例如 5, “abc”,等,肯定是只读的,因为程序中根本没有地方存放它的值,当然也就不能够去修改它。而“只读变量”则是在内存中开辟一个地方来存放它的值,只不过这个值由编译器限定不允许被修改。C语言关键字const就是用来限定一个变量不允许被改变的修饰符(Qualifier)。上述代码中变量n被修饰为只读变量,可惜再怎么修饰也不是常量。而ANSI C规定数组定义时维度必须是“常量”,“只读变量”也是不可以的。
2)注意:在ANSI C中,这种写法是错误的,因为数组的大小应该是个常量,而const int n,n只是一个变量(常量 != 不可变的变量,但在标准C++中,这样定义的是一个常量,这种写法是对的),实际上,根据编译过程及内存分配来看,这种用法本来就应该是合理的,只是 ANSI C对数组的规定限制了它。
3)那么,在 ANSI C 语言中用什么来定义常量呢?答案是enum类型和#define宏,这两个都可以用来定义常量。
5.2 指针 和 字符串常量
请问下面的代码有什么问题?
1 | char *p = "i'm hungry!"; |
答案与分析:
上面的代码会造成内存的非法写操作。分析如下, “i’m hungry”实质上是字符串常量,而字符串常量被编译器放在只读的字符常量区内,不可写。指针p初始化,指向这个只读的内存区,是不能修改其中元素值(即存放值);而p[0] = 'I';
则企图修改内存存放值,编译器当然不会答应。
1 | char *p = "i'm hungry!"; |
虽说字符串常量(字符常量区)内容不可修改,但指针是变量,可以修改指针p指向的内存位置;即实际上又找了一个新的字符串常量(新申请在字符常量区)。
5.3 字符串数组 和 字符串常量
1 | char a[15] = "i'm hungry!"; |
答案与分析:
相比较与5.2,由于字符串数组是存放在 栈 或 堆 ,该区是可以修改的,因此对字符串(数组,这里并非常量)的元素可以修改。
5.4 指针 & 内存
往往别人在教指针知识的时候,往往强调指针存放着地址,并举出类似下面的例子:
1 | int *p; |
程序结果如下:
1 | 12ff7c |
的确是能够打印出来,0x12ff7c
是在前面随便打印一个int
类型变量的地址获取的。但是如果给这个内存进行赋值操作的话,就会出现 段错误。
1 | int *p; |
地址和内存是两回事!!!每段内存都有自己的地址,地址映射可以通过指针操作;但是,你无权更改内存存放的值,除非这段内存是 栈 或 堆 分配给你的。需要注意:有地址,没空间(字符常量区也是这样)。
Ps: 如果有地址就能合法操作对应内存空间,那还要什么(手动)内存分配。
六、内存知识的重新总结
一个程序分为:
- 栈区(stack) –编译器自动分配释放,主要存放函数的参数值,局部变量值等;
- 堆区(heap) –由程序员分配释放;
- 全局(静态)区 –存放全局变量和静态变量;程序结束时由系统释放,分为全局初始化区(.data)和全局未初始化区(.bss);
- 字符常量区(.rodata) –常量字符串放与此,程序结束时由系统释放;
- 程序代码区(.text)
栈 :(后进先出)栈在程序中用于维护函数的调用上下文;栈保存了一个函数调用所需要的维护信息。
1 | 1)函数参数,函数返回地址 |
堆:(堆内存需要主动申请)为什么有了栈还需要堆?
1 | 1)栈上的数据在函数返回后就会被释放掉,无法传递到函数外部,如局部变量(关键原因) |
(全局)静态存储区:
1 | 1)程序的静态存储区随着程序的运行而分配空间,直到程序运行结束 |
七、 非法内存操作分析
7.1 指针没有初始化进行内存操作
1 |
|
编译器1,程序执行的结果如下:
1 | 7->0x7ffeaaf62970 |
编译器2,程序执行的结果如下:
1 | 11 Segmentation fault |
这里理所应当地出现两种情况:
- 出现段错误,则是编译器自动将指针置
NULL
- 编译通过的,则是 野指针
7.2 没有给指针分配足够的内存
1 |
|
编译器1,程序执行的结果如下:
1 | 7->0x1b3d010 |
编译器2,程序执行的结果如下:
1 | 7->0x1602010 |
这里会出现free()
操作 非法(没有分配)的内存;还有个编译器神奇通过(野指针的恐怖之处)。
7.3 内存分配成功但是没有初始化
1 |
|
编译器程序执行的结果如下:
1 | A |
需要注意的是,上面虽然编译通过但是是有问题的。犯这个错误往往是由于没有初始化的概念或者是以为内存分配好之后其缺省初值自然为0。未初始化指针变量也许看起来不那么严重,但是它确确实实是个非常严重的问题,而且往往出现这种错误很难找到原因。(尤其在字库驱动)
内存的缺省初值究竟是什么并没有统一的标准,尽管有些时候为零值,但这点在不同的编译器上会有不同的实现。所以好的做法,是手动给数组赋上初值。
当你只分配内存给字符(串)型指针,并没有缺省初值,严格意义上来讲是不算初始化;有些编译器自然里面都是'\0'
,printf
打印时,打印出来的数据自然会被其中字符的'\0'
截胡。不然可以把上述例子的注释去掉再测试一遍。
也许这种严重的问题并不多见,但是也绝不能掉以轻心。所以在定义一个变量时,第一件事就是初始化。你可以把它初始化为一个有效的值。
7.4 内存(数组)越界
数组有两个特性,影响作用在数组上的函数:
- 不能复制数组;
- 使用数组名时, 数组名会自动指向其第一个元素的指针。
- 因为不能复制,所以无法编写使用数组类型的形参,数组会自动转化为指针。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15void f(int a[10])//这里面的[10]仅表示我们希望数组是多大;实际没有用处,传入是指针
{
int i=0;
for(i=0;i<10;i++)
{
a[i]=i;
printf("%d\n",a[i]);
}
}
int main()
{
int a[5];
f(a);
return 0;
}
编译器程序执行的结果如下:
1 | 0 |
数组作为形参时的一个陷阱:在数组当形参的函数中,使用sizeof来计算传入的实参数组的大小,这种方法是错误的!!! 因为当数组作为形参的时候,其退化为一个指针,如果sizeof其数组名将计算的是一个指针的大小!
7.5 内存泄漏
1 |
|
解决方法:当函数申请了内存:采用单入口,单出口程序
1 | void f(unsigned int size) |
7.6 多次释放指针
1 |
|
多次释放内存的后果,就是强退出
Ps:分配多次完全可以,既然是变量那就是可变的!但是多次释放就是自杀!
7.7 使用已经释放的内存
1 | //这个和前面的例子有点类似 |
八、C语言有关内存的规则
8.1用malloc申请了内存之后,应该立即检查指针值是否为NULL,防止使用值为NULL的指针
1 | int *p=(int *)malloc(5*sizeof(int)); |
8.2 牢记数组长度,防止数组越界操作,考虑使用柔性数组
1 | typedef struct _soft_array |
8.3 动态申请操作必须和释放操作匹配,防止内存泄漏和多次释放
Ps:可以用if…else…来确定是否释放
1 | void f() |
8.4 free指针之后必须赋值为NULL
1 | int *p=(int*)(malloc(sizeof(int)); |
九、嵌入式的内存分配
malloc( )属于标准C语言函数,当然可以在单片机上使用。
但是在嵌入式(裸机)中最好不要这么做!一般单片机的内存都比较小,而且没有MMU(内存管理管理单元),多次的malloc
与free
的使用容易造成内存碎片。当后面因为空间不足而分配失败,从而导致系统崩溃,因此应该慎用,或者自己实现内存管理。除了UCOS或FREERTOS等嵌入式操作系统有自带的MMU处理外,裸机长时间连续工作产生的内存碎片为系统工作稳定埋下隐患。
关于更多嵌入式内存分配的知识,可以看这篇博文:Stm32的内存管理(Code,RO-data,RW-data,ZI-data)
Ps: 通常应用程序可以调用ANSI C编译器的malloc()和free()函数来动态的分配和释放内存,但多次这样的操作会把原来很大的一块连续存储区域逐渐地分割成许多非常小并且彼此不相邻的存储区域,这就是存储碎片。