C专家编程精华第二篇----C对内存的使用,底层探索

这里以Linux中C编程为例,有些东西可能在不同的系统中处理是不一样的

/**************运行时:内存的布局****************/

以下面这段程序为例:

// A:未初始化的全局和静态变量
int array[40];
static int num1;
// B:初始化后的全局和静态变量
int num2 = 10;
static int num3 = 15;
int main()
{
// C:局部变量
int i = 3;
int j;
// D:可执行文件的指令
array[4] = i;
j = num1;
return 0;
}

(1)C源文件各对应可执行文件的部分

1、对于未初始化的全局和静态变量,它一般被存到可执行文件的BSS段,这BSS段是为了更有效的节省空间而开辟的,它只存放没有值的变量,除了运行时记录BSS段的大小外,它不占目标文件的任何空间

2、对于初始化后的全局和静态变量,它被放到了可执行文件的数据段,数据段被保存到目标文件中

3、对于局部变量,它并不会成为可执行文件的一部分,它会在运行时创建,所以这也解释局部变量的生存周期相对短的原因了

4、对于可执行文件指令,它被放入可执行文件的文本段,这个文本段最易受到编译器的优化处理

(2)查看各个段的大小

在Linux下,运行size即可得到可执行文件的各段大小,在windows下可以通过查看对应段的第一个和最后一个变量和所对应的地址来计算出各段的大小,当然,下面这个程序片段能够得到你的栈顶地址:

int main()
{
int i = 0;
printf("The Top Stack is %p\n",&i);
return 0;
}

(3)可执行文件对应内存地址段的部分

链接器会把指令部分直接拷贝到内存中,CPU会从首地址开始逐条语句开始执行

系统会为可执行文件开辟一段空间,成为虚拟地址空间,从高地址到底地址一次为:栈段、BSS段、数据段、文本段、未映射区域

1、可执行文件的BSS段、数据段、文本段一一对应这内存中的BSS段、数据段、文本段

2、栈段:保存程序的局部变量、临时数据、函数参数等

3、虚拟地址空间的最底端是未映射区域,它位于进程的地址空间,但是并未赋予它物理地址,它一般用于捕捉对空指针使用的情况

注意:BSS段和数据段有时又合称为数据段,在数据段中有一个区域是堆区域,相对于可自增长的栈段而言,堆区域也是一段可自增长的区域,它通过动态内存申请增长)

/****************栈的应用:函数调用***************/

在函数进行调用的时候,系统会产生一个过程活动记录,它包括:局部变量、参数、指向前一个活动记录的指针、返回地址

一般来说这个过程活动记录块是压到栈中的,不过,如果不利用递归程序的,栈段就并非必须的了,因为当一个简单的函数被调用的时候不会将它的过程活动记录入栈,这大大的增加了计算机的运算负担。

另外栈段还可以用于暂时的存储区,也就是前面提到的临时数据的保存,这个临时数据一般是通过alloca()函数申请的,这也是一个防止内存泄漏的办法,但是它可能被下一个函数覆盖,所以它并不是一个好的办法。

程序例:

int Recall(int i)
{
if(i == 1)
printf("i is the 1\n");
else
Recall(--i);return 0;
}
int main()
{
	int i = 3;
	Recall(i);
	return 0;
}


(1)递归调用的流程图


栈段是一般是由内存高地址向底地址生长,首先main的过程活动记录入栈,然后调用Recall函数,Recall的过程活动记录入栈,反复,知道printf函数执行了,再由printf函数返回,挨着返回,挨着退栈,知道main结束,这是整个递归函数的调用过程。

一般来说,对于非递归的简单函数,是不会让它入栈的,因为编译器会考虑到效率问题,从上面的递归函数调用原理来看,每一个Recall函数的参数、局部变量、前一个活动的指针和返回地址都要入栈,这大大拖慢了程序的速度,虽然经典的栈模式可以很好的解决递归问题,但是先入栈、最后等都执行完了再出栈,这种方法肯定慢了很多,效率不高,所以一般就不要用递归函数解决方法,即使它真的很利于大家理解。

(2)从函数中返回值的办法

从上面对递归函数调用原理的解说,我们可以更深刻的理解这么一个问题:不能从函数中返回一个指向该函数内部局部变量的指针

当这个函数结束的时候,函数的过程活动记录退栈,内存被回收,其他的函数可能入栈覆盖掉了该函数,那么里面记录的所有值都是不确定的了,接着指针就成了野指针,这可是相当危险的事情。

但是我们仍然可以从函数中返回一个如我们希望的指针,看看最开始的C源文件在可执行文件的布局,数据段的生存周期是和程序一样的,它包括全局变量和静态变量,这也就是说我们把函数中的指针定义为静态变量就可以将这个指针轻松的返回了,因为它不会被回收,在程序结束之前。

/***********内存使用************/

(1)查看程序能被分配的最大内存

int main()
{
int MB = 0;
while(malloc(1<<20))++MB;
printf("The Max Memory %d MB Can Be Alloc.\n",MB);
return 0;
}

这个程序片段可以查看你的电脑能为你的程序最大分配多大的内存空间,1<<20表示将1乘以2的20次方,也就是1MB。

(2)内存申请:数据段中堆的增长

堆段在内存中的位置处于数据段里面的最高地址处,与栈段相接。

堆段用于动态分配的存储,也就是通过malloc函数获得的内存,利用指针进行访问,需要自己对它进行回收。一个malloc请求的数据大小一般都会被优化为圆整为2的次幂。

堆的回收不必像栈那么按照分配顺序回收,这也导致了动态内存像碎片一样四处分布,引起了内存回收的困难。

另外,堆的底部,即堆的最高地址,由一个break指针来标识,它有两个作用:1、保证你对内存的引用是正确的,如果你引用的内存地址超过了break指针指向的地址,那么它就会发出警报;2、通过系统调用brk和sbrk来移动break指针,这样就可以获得更多的内存

/*************内存泄漏*****************/

(1)避免内存泄漏

前面说了,堆增长(也就是动态内存的申请)会产生很对内存碎片,如果我们不对其进行回收,那么系统会识别到这些碎片是一直属于这个程序的,在程序结束之后也不会把这些碎片标记为可用,那么其他的程序就无法使用这些内存,相当于碎片会一直存在,除非你重启电脑,这就是内存泄漏,你的内存再大也可能被它整死机的。

还需要注意的是动态申请的一般会被圆整,像申请215B的内存,其实会被优化为申请256B的内存,多的41B是不会被引用的,除非你对它非法引用,所以,内存泄漏往往泄漏的比你忘记释放的数据要多得多的。

内存对齐:数据项只能存储在地址是数据项大小的整数倍的内存位置上,例如一个4字节的int型数据只能存放到地址是4的倍数的内存中。当然,如果你要检查内存是否对齐基本是不太可能的,因为编译器一般都会通过自动分配和填充数据来进行对齐,例如下面这个结构体定义:

struct student
{
	char name[21];
	int number;
	double id;
};
以我自己的电脑为例,系统会一次性提取8个字节的内存,那么上面那个结构体对它sizeof,name会占3*8=24字节,多余了3个字节,但是紧接着定义的int型不能刚好放到多余的3个字节里,所以有开辟8个字节放number,这里又多余了四个字节,显然id的8字节是不能放到四个字节里的,所以有开辟8个字节放id,总共sizeof之后就是5*8=40字节了。

如果将name大小改为25,有4*8=32字节容纳它,多了7个字节,能够容纳number,那么就相当于多余了7-4=3字节了,显然id不能放到3字节里,开辟8字节容纳id,这样sizeof之后仍未5*8=40字节

为了更好的节省内存空间,我们需要注意类似的变量定义的先后顺序,将number和name的位置交换结果又不一样,因为指令是逐条执行的。

(3)段错误

(还有一种总线错误,真心还没弄懂,求高手指点)

段错误是由于内存管理单元异常所致,导致段错误有以下几个原因:

1、解引用一个包含非法值的指针,也就是对一个已经free掉的内存继续使用

2、解引用一个空指针,一般就是初始化为NULL了还对它进行解引用,还可能是从函数中返回了NULL却没有进行检查就直接使用

3、对超过了申请内存边界的内存进行解引用,有点像数组的越界一样

4、把你的电脑可用动态内存用完了。。。。这个几乎很困难

  • 3
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值