c语言运行时内存不够,浅析C语言运行时内存管理

本文详细探讨了C语言程序运行时的内存结构,包括文本段、数据段、BSS段、堆栈段和堆空间。讨论了全局变量、静态变量、局部变量、堆栈和堆的特性和使用注意事项,特别强调了局部变量的生命周期和返回局部变量指针的风险。同时,提到了编译器优化可能导致的意外行为,并提醒了内存管理和防止野指针的重要性。
摘要由CSDN通过智能技术生成

66b52468c121889b900d4956032f1009.png

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 <stdio.h>

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;

}

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专家编程》,《深入理解计算机系统》

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值