理解、正确运用计算机内存
引言
在最近学习数据结构和与同学交流问题过程中,发现了一个极具普遍性的问题,那就是为什么把一些代码块独立出来做成一个函数就会出现问题,而不做成函数反而没问题?难道这是要我们反对使用函数吗?不! 不是这样的,在阅读完相关资料后,我发现其实是同学们对内存空间的理解不够,说通俗点就是不知道内存空间长啥样。那接下来我将给大家讲解一下几种对内存的错误操作以及几种合理地解决方案,从而让大家对内存空间有更好的理解。当然以下的理解或多或少叙述上有失严谨的地方,这些都是基于我自己对与内存空间的理解,希望读者谅解并指出。
内存空间
一般笔记本的内存(RAM)有4G或8G。它分为5大区域,分别是代码区、全局数据空间、堆空间、栈空间、内核空间。它们的图例如下(以4G内存为例):
内存分布图.png栈与堆
我们从最常听到的栈空间与堆空间开始。堆空间和栈空间统称为动态储存空间,它们分担着储存程序运行时产生的变量的任务。就栈空间来说,可以这样理解:函数先运行main函数,则先把main函数放进栈空间,同时把main函数里面声明的变量全部包含在其中,当main函数结束时(当然也是程序结束时),则把main函数拿出栈(此时显然除了main函数其他函数都已结束),并同时把主函数里面的变量“带走”。所谓“带走”就是说在后续操作中就再也不能使用这些变量了,就像下面的例子,在调用完Max函数后printf("%d",max);是错误的:
#include <stdio.h>
int Max(int a,int b){
int max;
max=a>b?a:b;
return max;
}
int main(void){
int a,b,mmax;
scanf("%d%d",&a,&b);
mmax=Max(a,b);
printf("%d",mmax);
return 0;
}
当程序运行时,必然先运行main函数,于是先把main函数放进栈空间,同时把main函数里面声明的变量全部包含在其中。当程序进行到调用Max函数时,又把Max函数压入栈空间,同时把Max函数里面声明的变量全部包含在其中。以次类推,如果是在调用函数A时,在A函数里面又调用B函数,则栈空间的堆放顺序则是main函数在栈底,接着是A函数,再者是B函数。如果是在调用完A函数后,main函数再调用B函数,则栈空间的操作方式是main函数先压入栈底,再A函数压入栈,而后A函数拿出栈,最后B函数再压入栈。于是乎,我们就可以理解一类函数的错误所在了。如下为链式线性表的插入元素操作函数:
#include <stdio.h>
#include<stdlib.h>
#include<time.h>
typedef struct NODE {
struct NODE *next;
int value;
} Node;
typedef enum { ERROR = 0, OK = 1 } Status;
Status Insert1(Node **ppLink,int newValue){
Node new, *current;
new.value = newValue;
while(current=*ppLink,current!=NULL&¤t->value<newValue){
ppLink = ¤t->next;
}
*ppLink = new;
new.next = current;
return OK;
}
Insert函数.png
初看起来我们好像成功地将新元素插入链表之中。但是!结果恰恰相反。我们使得链表断截了,因为我们Insert函数里面的new是局部变量!!!所以在函数结束时new所占用的栈内存将被销毁,于是乎链表就被断了!!以下是错误运用此函数导致的错误输出结果(错误的程序):
94 21 11 99 37 68 19 62 61 10//乱序链表
10 11 19 21 37 61 62 68 94 99//排序好的链表
10 11 19 10 0//错误插入函数(试图插入新元素20)导致的结果
20 24 32 5 44 17 72 26 99 32//乱序链表
5 17 20 24 26 32 32 44 72 99//排序好的链表
5 17 32 0//错误插入函数(试图插入新元素20)导致的结果
46 96 22 19 63 26 82 24 3 36//乱序链表
3 19 22 24 26 36 46 63 82 96//排序好的链表
3 19 36 0//错误插入函数(试图插入新元素20)导致的结果
从以上几个输出结果可以看出错误插入函数导致的结果还不仅仅是将链表断开那么简单,其结果是很复杂的、莫名奇妙的。那么如何解决这个问题呢??接着我们就将介绍堆空间地用法:
查看网上对栈与堆的区别与联系请戳这里。总而言之,结合前面的内存分布图,我们可以这样理解堆空间:可以简单地理解为malloc区,也即保存malloc函数占用的内存,用free释放。若程序员不释放的话,程序结束时可能由OS(操作系统)回收。
所以,malloc出来的内存不会因为函数结束而消失。因而只要将上Insert函数稍加改变就能够使程序得出理想的结果(只要将Node new改成Node *new然后后面再malloc一块内存就行了):
//malloc版本
Status Insert1(Node **ppLink,int newValue){//可重复调用
Node *new, *current;
new = (Node *)malloc(sizeof(Node));
if(new==NULL){
return ERROR;
}
new->value = newValue;
while(current=*ppLink,current!=NULL&¤t->value<newValue){
ppLink = ¤t->next;
}
*ppLink = new;
new->next = current;
return OK;
}
以下是程序运行结果:
66 14 19 69 33 51 51 95 24 4//乱序链表
4 14 19 24 33 51 51 66 69 95//排序好的链表
4 14 19 20 24 33 51 51 66 69 95//成功将新元素20插入
到这里,或许还有同学对此嗤之以鼻:你说的我都会呀!难道就这样就能完全理解并运用内存了吗?
问的好,所以说对于上述Insert函数地修改方案绝对不止堆空间方案一种。我们还可以运用静态空间。
bss区与data区
其实这两个区储存的变量类型完全相同,都是储存全局变量和静态变量的内存,所以我们大可把两者看成同一块区域。但是从很浅的方面讲,它们有一个很大的不同之处,那就是bss区只储存未初始化或初始化为0的全局变量和静态变量,而data区则储存 已初始化的全局变量和静态变量但是除了const型的已初始化的全局变量。讲到这里,大家或许迫不及待地想运用静态变量解决上述Insert函数的问题了。那么如何运用呢?因为静态空间也不属于栈区,所以当调用完一个函数时,函数里面申明的静态变量是不会随之销毁的。于是乎我们得出将一个新元素插入链表的函数:
//static版本
Status Insert1(Node **ppLink,int newValue){//只能调用一次
static Node new;
Node *current;
new.value = newValue;
while(current=*ppLink,current!=NULL&¤t->value<newValue){
ppLink = ¤t->next;
}
*ppLink = new;
new.next = current;
return OK;
}
以下是程序运行结果:
29 70 61 85 68 67 72 87 59 95//乱序链表
29 59 61 67 68 70 72 85 87 95//排序好的链表
20 29 59 61 67 68 70 72 85 87 95//成功将新元素20插入
但是,别高兴地太早。细心的同学可能注意到了malloc版本有“可重复调用”的字样而static版本却是“只能调用一次”。那么为什么会这样呢?这里就必须讲清楚动态空间与静态空间的区别了。一般说来,静态空间的变量内存在程序一开始就分配好了,在程序运行时不能够再声明同一类静态变量多次。也就是说,当程序第二次调用static版本的函数时,再次遇到
static Node new;
时,并不会再申请一块静态空间的内存,而是对同一个new变量进行操作。这也就为什么我们可以用static变量记录调用函数的次数。
讲到这里,我们又不得不提一下全局变量。关于这点最近有了一些感悟。有编程经验以及参加过ACM程序设计比赛的同学们都知道,有时候我们需要申请一块很大的内存空间(如数组)来使用。但是在main函数里面申明一块很大的内存空间显然不明智。因为main函数里面的变量内存都是在栈空间里面的,所以申请大内存最好是全局变量。以下是网上对此的解释:
全局变量在静态存储区分配内存,局部变量是在栈上分配内存空间的,这么大的数组放到栈上不溢出吗?VC堆栈默认是1M,int a[1000000]的大小是4*1000000,将近4M,远远大于1M,编译连接的时候不会有问题,但运行是堆栈溢出,程序异常终止。如果你真的需要在堆栈上使用这么大的数组,那么可以在工程选项链接属性里设置合适的堆栈大小。
但是作为一名程序员,我们往往会听到我们老师们或学长学姐们或书上说写程序时最好不要用全局变量。这就很有意思了,因为确实全局变量很容易出问题。关于全局变量为什么危险在这里我就不在赘述,读者自行上网查找,这里有一篇我上网查的比较容易理解的文章。那么,如果我一定要使用一块大内存空间但是又不能用全局变量,我该怎么办?仔细想想,不觉得想起了我的编程启蒙老师张学,他对学生很严格,甚至可以说有些“吝啬”,但是如果上课真正地认真做笔记,学习他的程序设计方法,你会发现你受益无穷,而并不只是期末C语言能拿个高分而已。好了,接下来就是方法的讲解:
我们的确可以在main函数里面申请,但是,我们要声明的不是如下的样子:
···
int main(void){
int a[1000000];
···
}
我们应该在main函数里面声明一个int型的指针,然后再使用malloc函数。对,相信有的读者已经理解其中的精妙之处,修改后的代码长这样:
···
int main(void){
int *a;
a=(int*)malloc(1000000*sizeof(int));
if(a==NULL){
printf("申请内存失败!");
return 1;
}
···
}
如果已经看懂了文章前面关于堆空间的描述,那么就不会有怀疑了。在修改后的版本中,a是一个指针,在main函数里面声明一个指针变量显然是完全够用的。接着我们在堆空间里面malloc一块1000000*sizeof(int)个字节内存,在让a指向这块内存。如此,我们就可以像操作数组那样进行操作了。还有一个好处就是我们也不怎么用担心访问“数组”不够用了,因为如果malloc出来的区域用完了,我们还可以用realloc函数为“数组”加长。
text区
text区又称ROTEXT区,其中RO的意思是Read Only(只读)。那么这块内存是什么?又有什么用处?text区分为只读数据段和代码段。其中前者为保存程序文件中字符串内容的空间以及const型的全局变量。而代码段也就是整个程序文件的代码。在以下的代码中可以更好的理解:
内存分布图.png
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
const int g_A = 10; //text区
int g_B = 20; //data区
static int g_C = 30; //data区
static int g_D; //BSS区
int g_E; //BSS区
char *p1; //BSS区
const char *ch[]={"Mon","Tus","Wed","Thu","Fri","Sat","Sun"};
//ch和Mon、Tus、Wed、Thu、Fri、Sat、Sun都在text区
int main( )
{
int local_A; //栈
int local_B; //栈
static int local_C = 0; //data区
static int local_D; //data区
char *p3 = "123456"; //123456在text区,p3在栈上
p1 = (char *)malloc( 10 ); //堆,分配得来得10字节的区域在堆区
strcpy( p1, "123456" ); //123456{post.content}放在常量区,编译器可能会将它与p3所指向 的"123456"优化成一块
printf("hight address\n");
printf("-------------栈--------------\n");
printf( "栈, 局部变量, local_A, addr:0x%08x\n", &local_A );
printf( "栈, 局部变量,(后进栈地址相对local_A低) local_B, addr:0x%08x\n", &local_B );
printf("-------------堆--------------\n");
printf( "堆, malloc分配内存, p1, addr:0x%08x\n", p1 );
printf("------------BSS区------------\n");
printf( "BSS区, 全局变量, 未初始化 g_E, addr:0x%08x\n", &g_E, g_E );
printf( "BSS区, 静态全局变量, 未初始化, g_D, addr:0x%08x\n", &g_D );
printf( "BSS区, 静态局部变量, 初始化, local_C, addr:0x%08x\n", &local_C);
printf( "BSS区, 静态局部变量, 未初始化, local_D, addr:0x%08x\n", &local_D);
printf("-----------data区------------\n");
printf( "data区,全局变量, 初始化 g_B, addr:0x%08x\n", &g_B);
printf( "data区,静态全局变量, 初始化, g_C, addr:0x%08x\n", &g_C);
printf("-----------text区------------\n");
printf( "text区,全局初始化变量, 只读const, g_A, addr:0x%08x\n\n", &g_A);
printf("low address\n");
return 0;
}
以下是输出结果:
hight address
-------------栈--------------
栈, 局部变量, local_A, addr:0x0062fe44
栈, 局部变量,(后进栈地址相对local_A低) local_B, addr:0x0062fe40
-------------堆--------------
堆, malloc分配内存, p1, addr:0x001e13e0
------------BSS区------------
BSS区, 全局变量, 未初始化 g_E, addr:0x00407030
BSS区, 静态全局变量, 未初始化, g_D, addr:0x00407040
BSS区, 静态局部变量, 初始化, local_C, addr:0x00407044
BSS区, 静态局部变量, 未初始化, local_D, addr:0x00407048
-----------data区------------
data区,全局变量, 初始化 g_B, addr:0x00403020
data区,静态全局变量, 初始化, g_C, addr:0x00403024
-----------text区------------
text区,全局初始化变量, 只读const, g_A, addr:0x00404348
low address
结语
emmmm,敲文字敲得老阔疼。不管怎样我觉得多写总结(不是那种简单的笔记,而更像是给别人讲解)可以让我更好地深入理解知识,这让我受益匪浅。不管有没有很多人看我的文章,我的学习目的已经达到了,这是最让我开心的。当然,能够帮助对相关知识有问题的同学那就更好了。最后祭上我的新晋女神:
石原里美
网友评论