C语言程序里全局变量、局部变量、堆、栈等概念
文章目录
- 一、 寄存器与固件库的差异
- 二、C语言程序中内存分配详解
- 三、关键字volatile
一、 寄存器与固件库的差异
固件库就是函数的集合。
当我们写代码的时候可以直接对寄存器进行操作,但是如果使用的寄存器较多我们就需要掌握每一个寄存器的操作方法,对于我们而言十分的不容易,因此便有了固件库的出现。它将每个寄存器的操作封装在一个函数中,在使用的时候我们直接调用这个函数就可以了,函数固件库函数的作用是向下负责与寄存器直接打交道。向上提供用户函数调用的接口(API)。我们就不用再细究这个寄存器具体是怎么操作的。
二、C语言程序中内存分配详解
C/C++编译程序的内存占用
(1)栈区(stack):由编译器自动分配释放,存放函数的参数值,局部变量的值等。
(2)堆区(heap):由程序员分配和释放,若程序员不释放,程序结束时可能由OS回收。
(3)全局区(静态区)(static):全局变量和静态变量的存储是放在一块的,初始化的全局变量和静态变量在一块区域, 未初始化的全局变量、未初始化的静态变量在相邻的另一块区域。
(4)文字常量区 :常量字符串就是放在这里的。
(5)程序代码区 :存放函数体的二进制代码
c\c++中内存分配4个分区:栈、堆、全局/静态存储区和常量存储区。下面我们分别简单地介绍一下各自的特点。
1 栈
通常是用于那些在编译期间就能确定存储大小的变量的存储区,用于在函数作用域内创建,在离开作用域后自动销毁的变量的存储区。通常是局部变量,函数参数等的存储区。他的存储空间是连续的,两个紧密挨着定义的局部变量,他们的存储空间也是紧挨着的。栈的大小是有限的,通常Visual C++编译器的默认栈的大小为1MB,所以不要定义int a[1000000]这样的超大数组。
2 堆
通常是用于那些在编译期间不能确定存储大小的变量的存储区,它的存储空间是不连续的,一般由malloc(或new)函数来分配内存块,并且需要用free(delete)函数释放内存。如果程序员没有释放掉,那么就会出现常说的内存泄漏问题。需要注意的是,两个紧挨着定义的指针变量,所指向的malloc出来的两块内存并不一定的是紧挨着的,所以会产生内存碎片。另外需要注意的一点是,堆的大小几乎不受限制,理论上每个程序最大可达4GB。
3 全局/静态存储区
和“栈”一样,通常是用于那些在编译期间就能确定存储大小的变量的存储区,但它用于的是在整个程序运行期间都可见的全局变量和静态变量。
4 常量存储区
和“全局/静态存储区”一样,通常是用于那些在编译期间就能确定存储大小的常量的存储区,并且在程序运行期间,存储区内的常量是全局可见的。这是一块比较特殊的存储去,他们里面存放的是常量,不允许被修改。
5 总结
根据上面的内容,分别将栈和堆、全局/静态存储区和常量存储区进行对比,结果如下。
栈区:
主要用来存放局部变量, 传递参数, 存放函数的返回地址。.esp 始终指向栈顶, 栈中的数据越多, esp的值越小。
堆区:
用于存放动态分配的对象, 当你使用 malloc和new 等进行分配时,所得到的空间就在堆中。动态分配得到的内存区域附带有分配信息, 所以你能够 free和delete它们。
数据区:
全局,静态和常量是分配在数据区中的,数据区包括bss(未初始化数据区)和初始化数据区。
注意:
堆向高内存地址生长;
栈向低内存地址生长;
堆和栈相向而生,堆和栈之间有个临界点,称为stkbrk。
函数的栈帧
函数调用时所建立的栈帧包含下面的信息:
1)函数的返回地址。返回地址是存放在主调函数的栈帧还是被调用函数的栈帧里,取决于不同系统的实现;
2)主调函数的栈帧信息, 即栈顶和栈底;
3)为函数的局部变量分配的栈空间;
4)为被调用函数的参数分配的空间取决于不同系统的实现。
注意:
BSS区(未初始化数据段):并不给该段的数据分配空间,仅仅是记录了数据所需空间的大小。 DATA(初始化的数据段):为数据分配空间,数据保存在目标文件中。
- 1
- 2
- 3
- 1
- 2
- 3
在Ubuntu中进行内存分配验证
#include <stdio.h>char global_arr[1024 * 1024]; //存放在.bss段int main(void){ return 0;}
1
2
3
4
5
6
1
2
3
4
5
6
编译后查看大小
显然,global_arr数组占据的1M空间并没有占据文件空间。将global_arr数组改放在.data段中:
char global_arr[1024 * 1024}={8}; //存放在.data段
- 1
- 1
编译后查看大小:
#include <stdio.h>char global_arr[1024 * 1024]={8}; //存放在.bss段int main(void){ return 0;}~
1
2
3
4
5
6
7
1
2
3
4
5
6
7
文件变成了1M多,显然.data段上的数据是占据文件空间的。
代码段(.txt)
.txt段存放代码(如函数)与部分整数常量,.txt段的数据可以被执行数据段(.data)
.data用于存放初始化过的全局变量。若全局变量值为0,为了优化编译器会将它放在.bss段中bss段(.bss)
.bss段被用来存放那些没有初始化或者初始化为0的全局变量。bss段只占运行时的内存空间而不占文件空间。在程序运行的整个周期内,.bss段的数据一直存在常量数据段(.rodata)
ro表read only,用于存放不可变修改的常量数据,一旦程序中对其修改将会出现段错误:
(1) 程序中的常量不一定就放在rodata中,有的立即数和指令编码放在.text中
(2) 对于字符串常量,若程序中存在重复的字符串,编译器会保证只存在一个
(3) rodata是在多个进程间共享的
(4) 有的嵌入式系统,rodata放在ROM(或者NOR FLASH)中,运行时直接读取无需加载 至RAM( 哈佛 和冯诺依曼,从STM32的const全局变量说起有所记录)
想要将数据放在.rodata只需要加上const属性修饰即可。栈
栈是用于存放临时变量和函数调用的。栈也是一种先进后出的数据结构,函数的递归调用正得益于栈的存在。需注意存在栈的数据只在当前函数和子函数中有效,一旦函数返回数据将会被自动释放。堆
堆的使用周期有使用者控制,程序中的内存泄漏多因程序员对堆的管理不当引起,需谨慎。.comment段
在上图中还看到.comment段,它存放的是编译器版本等信息。除了.comment,还有.note、.hash等其他段,了解即可。
验证
1 .内存栈区: 存放局部变量名;2. 内存堆区: 存放new或者malloc出来的对象;
#include <stdio.h>#include <string.h>#include <stdlib.h> void before(){ } char g_buf[16];char g_buf2[16];char g_buf3[16];char g_buf4[16];char g_i_buf[]='123';char g_i_buf2[]='123';char g_i_buf3[]='123'; void after(){ } int main(int argc, char **argv){ char l_buf[16]; char l_buf2[16]; char l_buf3[16]; static char s_buf[16]; static char s_buf2[16]; static char s_buf3[16]; char *p_buf; char *p_buf2; char *p_buf3; p_buf = (char *)malloc(sizeof(char) * 16); p_buf2 = (char *)malloc(sizeof(char) * 16); p_buf3 = (char *)malloc(sizeof(char) * 16); printf('g_buf: 0x%x\n', g_buf); printf('g_buf2: 0x%x\n', g_buf2); printf('g_buf3: 0x%x\n', g_buf3); printf('g_buf4: 0x%x\n', g_buf4); printf('g_i_buf: 0x%x\n', g_i_buf); printf('g_i_buf2: 0x%x\n', g_i_buf2); printf('g_i_buf3: 0x%x\n', g_i_buf3); printf('l_buf: 0x%x\n', l_buf); printf('l_buf2: 0x%x\n', l_buf2); printf('l_buf3: 0x%x\n', l_buf3); printf('s_buf: 0x%x\n', s_buf); printf('s_buf2: 0x%x\n', s_buf2); printf('s_buf3: 0x%x\n', s_buf3); printf('p_buf: 0x%x\n', p_buf); printf('p_buf2: 0x%x\n', p_buf2); printf('p_buf3: 0x%x\n', p_buf3); printf('before: 0x%x\n', before); printf('after: 0x%x\n', after); printf('main: 0x%x\n', main); if (argc > 1) { strcpy(l_buf, argv[1]); } return 0;
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
- 30
- 31
- 32
- 33
- 34
- 35
- 36
- 37
- 38
- 39
- 40
- 41
- 42
- 43
- 44
- 45
- 46
- 47
- 48
- 49
- 50
- 51
- 52
- 53
- 54
- 55
- 56
- 57
- 58
- 59
- 60
- 61
- 62
- 63
- 64
- 65
- 66
- 67
- 68
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
- 30
- 31
- 32
- 33
- 34
- 35
- 36
- 37
- 38
- 39
- 40
- 41
- 42
- 43
- 44
- 45
- 46
- 47
- 48
- 49
- 50
- 51
- 52
- 53
- 54
- 55
- 56
- 57
- 58
- 59
- 60
- 61
- 62
- 63
- 64
- 65
- 66
- 67
- 68
结果
1@ubuntu:~$ vim b.c1@ubuntu:~$ gcc -o b.o b.c1@ubuntu:~$ ./b.og_buf: 0x6010a0g_buf2: 0x6010d0g_buf3: 0x6010b0g_buf4: 0x6010c0g_i_buf: 0x601050g_i_buf2: 0x601054g_i_buf3: 0x601058l_buf: 0xb52af550l_buf2: 0xb52af560l_buf3: 0xb52af570s_buf: 0x601070s_buf2: 0x601080s_buf3: 0x601090p_buf: 0x14f4010p_buf2: 0x14f4030p_buf3: 0x14f4050before: 0x400626after: 0x40062dmain: 0x4006341@ubuntu:~$
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
三、关键字volatile
volatile 的英文解释是——“易失的,易改变的”。顾名思义,这个关键字的含义是向编译器指明变量的内容可能会由于编译器意想不到的情况的变化而发生变化。
Volatile这个关键字的必要性
在其他程序(例如内核程序或一个中断)修改了内存中该变量的值,此时寄存器R中的值并不会随之改变而更新,由于优化器的作用编译器仍然去利用之前存放在寄存器R中的值,而不去寻址内存中的值(但我们必须改变这个变量的值,不然此时的数据还是向先前的数据,是错误的)。故为了解决这种情况C语言就引入了volatile限定词,让代码在引用该变量时,再去内存中取出该变量的值,虽然会花费更多的时间,但保证了数据的真实性。
Volatile这个关键字的作用
当用volatile关键字修饰变量时,优化器在用到这个变量时必须每次都小心地去内存重新读取(关键之处)这个变量的值,而不是使用保存在寄存器R里的备份。
Volatile和register的对比
volatile 跟以前的 register 相反。 register 告诉编译器尽量将变量放到寄存器中使用, 而volatile 强制将更改后的值写回内存。如果不写回内存,对于一些全局共享的变量,可能导致不一致问题。