c语言运行时内存不够,浅析C语言运行时内存管理
文章图片
8种机械键盘轴体对比
本人程序员,要买一个写代码的键盘,请问红轴和茶轴怎么选?
主要讨论C语言怎样组织正在运行的程序的数据结构的细节。
我们知道知道在UNIX操作系统中,一个C语言文件经过预处理(cpp),编译(cc1),汇编(as)和链接(ld)后可以得到可执行文件a.out。我们可以用size命令(或nm、dump)检查可执行文件,它会告诉你这个文件中的三个段(文本段.text,数据段.data,.bss段)的大小。文本段包含程序的指令。由于程序的文本内容和大小都不会变化,链接器把指令直接从文件拷贝到内存中,不再处理。数据段包含经过初始化的全局和静态变量以及它们的值。BSS段的大小从可执行文件中得到,然后链接器得到这个大小的内存块,紧跟在数据段之后。当这个内存区进入程序的地址空间后全部清零,通常是memset全部置0。通常把数据段和BSS段统一称为数据区。上面这些内容可以通过size命令很直观的查看与验证。除了上面提到的程序指令、全局和静态变量,我们还有局部变量等数据需要存储。这就引出了堆栈段(stack)和堆空间(heap)。下面将详细讨论数据区、堆栈段和堆空间。
1.数据区存放着全局变量与静态变量,包括初始化和未初始化。其中未初始化的在进入程序地址空间的时候统一清零。这些数据的生命周期都是主程序运行时间。所谓静态变量,是指由static修饰的变量,可以是全局变量,也可以是局部变量。当修饰子函数的局部变量的时候,如果有初始化的话只在第一次进入子函数的时候执行初始化语句,并且在子函数退出的时候依然有效。关于static的另一个作用便是对链接器不可见,这个在以后再说。1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17void ()
{
static int a = 0;
a ++;
printf("%dn", a);
}
int main()
{
foo();
foo();
foo();
return 0;
}
2.堆栈段有三个主要的用途:一是为函数内部声明的局部变量(也称“自动变量”)提供存储空间;二是在进行函数调用时,堆栈存储与此相关的一些维护性信息。这些信息被称为堆栈结构(stack frame),或者过程活动记录(precedure activation recored),其中包括函数调用地址、任何不适合装入寄存器的参数以及一些寄存器的保存;三是用作暂时存储区。有时程序需要一些临时存储,比如算术表达式计算。值得注意的是堆栈的入栈是由高地址到低地址,并且局部变量的生命周期在子函数退出的时候也释放了。下面看一个程序1
2
3
4
5
6
7
8
9
10
11
12
13
14
15#include
int (int i)
{
volatile int a = 1;
volatile int b[2] = {0};
return b[i];
}
int main()
{
int b = foo(2);
printf("%dn", b);
return 0;
}
由于C语言中对数组越界并不会作检查,所以上述程序是可以编译通过并执行的。
volatile是为了防止编译器对代码进行优化,我们来看下foo子函数的堆栈段的情况,由高到低,压入i,a,b[1],b[0],则i==2时,返回的实际上是a。
上面提出局部变量的生命周期,如果一个函数返回了一个局部变量会怎么样呢?1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22int foo1()
{
int a = 100;
return a;
}
【c语言运行时内存不够,浅析C语言运行时内存管理】int *foo2()
{
int a = 101;
return &a;
}
int main()
{
int *y = foo2();
int x = foo1();
printf("%d, %dn", x, *y);
return 0;
}
两个函数是不一样的,第一个函数返回int型变量,第二个函数返回给一个指针,指向int类型。第一个函数是没有错的,return的时候return的是局部变量a的一个拷贝。而第二个函数返回了局部变量的地址,这是很不安全的。由于函数foo()退出后,局部变量a的地址不受保护,可能会被用作他用,如上面的程序先调用foo2(),再调用foo1()时,把之前的y指向的地址的内容修改了,所以上述程序输出为100, 100。
是不是感觉不过瘾,那再来一个1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19void foo1()
{
int a = 100;
}
void foo2()
{
int a;
printf("%dn", a);
}
int main()
{
foo1();
foo2();
return 0;
}
如果根据第二个例子,我们可以很容易推出结果是:100。但是结果对吗?跑一下就知道了,gcc -O2 test test.c。这里加入了优化选项,从程序中可以看出foo()函数其实什么都没有做,经过编译器可能直接路过foo1(),执行foo2(),所以结果是个随机数。验证也很简单,在foo1()中对a进行一次相关操作就好了。好了,这个有点涉及程序优化了,以后再说。
3.堆(heap)空间用于动态内存的分配,用于malloc相关函数。上面提到堆栈段,注意堆栈段就是栈,是向下生长的,而堆是向上生长的,正好形成一个闭区间。当我们调用malloc等分配内存的时候,要进行检查,因为如果分配失败的话,返回NULL指针,访问的话就会出现段错误了。还有一点要注意是内存泄漏,不用的时候需要显示free()释放。有些语言是加入了垃圾回收机制的,如LISP。对了,还有一点需要提防的就是野指针。野指针在我们进行free之后,指针的指向还是没有变的,所以需要手动置为NULL。
上面主要对C语言运行时的几个段进行一些讨论,当然更深入的学习还是要靠大家自己去实验。
参考:《C专家编程》,《深入理解计算机系统》
推荐阅读
- c语言|数据结构3--深入了解单向链表的实现
- 一些小实例|复数的加减乘除C语言实现
- 编程语言|【学术分享】发顶会论文,怎么就那么难()
- 算法|二分查找与移除元素(JavaScript语言实现)
- 大数据|Scala语言入门
- Java18的新特性
- 经典程序|利用C语言创建数据结构中链表的遍历及其基本操作
- linux中用c语言实现ping函数|linux中用c语言实现ping函数,linux c语言实现ping
- 十万个为什么|口诀快速记住C语言中的运算符优先级
- C语言|C语言求水仙花数