堆、栈及静态数据区详解

堆、栈及静态数据区详解

五大内存分区

在C++中,内存分成5个区,他们分别是自由存储区全局/静态存储区常量存储区

,就是那些由编译器在需要的时候分配,在不需要的时候自动清除的变量的存储区。里面的变量通常是局部变量、函数参数等。

,就是那些由new分配的内存块,他们的释放编译器不去管,由我们的应用程序去控制,一般一个new就要对应一个delete。如果程序员没有释放掉,那么在程序结束后,操作系统会自动回收。

自由存储区,就是那些由malloc等分配的内存块,他和堆是十分相似的,不过它是用free来结束自己的生命的。

全局/静态存储区,全局变量和静态变量被分配到同一块内存中,在以前的C语言中,全局变量又分为初始化的和未初始化的,在C++里面没有这个区分了,他们共同占用同一块内存区。

常量存储区,这是一块比较特殊的存储区,他们里面存放的是常量,不允许修改(当然,你要通过非正当手段也可以修改,而且方法很多)

明确区分堆与栈

在BBS上,堆与栈的区分问题,似乎是一个永恒的话题,由此可见,初学者对此往往是混淆不清的,所以我决定拿他第一个开刀。

首先,我们举一个例子:

void f() { int* p=new int[5]; } 

这条短短的一句话就包含了堆与栈,看到new,我们首先就应该想到,我们分配了一块堆内存,那么指针p呢?他分配的是一块栈内存,所以这句话的意思就是:在栈内存中存放了一个指向一块堆内存的指针p。在程序会先确定在堆中分配内存的大小,然后调用operator new分配内存,然后返回这块内存的首地址,放入栈中,他在VC6下的汇编代码如下:

00401028   push        14h

0040102A   call       operator new (00401060)

0040102F   add        esp,4

00401032   mov        dword ptr [ebp-8],eax

00401035   mov        eax,dword ptr [ebp-8]

00401038   mov        dword ptr [ebp-4],eax

这里,我们为了简单并没有释放内存,那么该怎么去释放呢?是delete p么?错,应该是delete []p,这是为了告诉编译器:我删除的是一个数组,VC6就会根据相应的Cookie信息去进行释放内存的工作。

我们回到我们的主题:堆和栈究竟有什么区别? 

堆和栈主要的区别由以下几点:

1. 管理方式不同:对于栈来讲,是由编译器自动管理,无需我们手工控制;对于堆来说,释放工作由程序员控制,容易产生memory leak。

2. 空间大小不同:一般来讲在32位系统下,堆内存可以达到4G的空间,从这个角度来看堆内存几乎是没有什么限制的。但是对于栈来讲,一般都是有一定的空间大小的,例如,在VC6下面,默认的栈空间大小是1M(好像是,记不清楚了)。当然,我们可以修改:

打开工程,依次操作菜单如下:Project->Setting->Link,在Category中选中Output,然后在Reserve中设定堆栈的最大值和commit。

注意:reserve最小值为4Byte;commit是保留在虚拟内存的页文件里面,它设置的较大会使栈开辟较大的值,可能增加内存的开销和启动时间。

3. 产生碎片不同:对于堆来讲,频繁的new/delete势必会造成内存空间的不连续,从而造成大量的碎片,使程序效率降低。对于栈来讲,则不会存在这个问题,因为栈是先进后出的队列,他们是如此的一一对应,以至于永远都不可能有一个内存块从栈中间弹出,在他弹出之前,在他上面的后进的栈内容已经被弹出,详细的可以参考数据结构,这里我们就不再一一讨论了。

4. 生长方向不同:对于来讲,生长方向是向上的,也就是向着内存地址增加的方向;对于来讲,它的生长方向是向下的,是向着内存地址减小的方向增长。

5. 分配方式不同堆都是动态分配的,没有静态分配的堆。栈有2种分配方式:静态分配和动态分配。静态分配是编译器完成的,比如局部变量的分配。动态分配由alloca函数进行分配,但是栈的动态分配和堆是不同的,他的动态分配是由编译器进行释放,无需我们手工实现。

6. 分配效率不同:栈是机器系统提供的数据结构,计算机会在底层对栈提供支持:分配专门的寄存器存放栈的地址,压栈出栈都有专门的指令执行,这就决定了栈的效率比较高。堆则是C/C++函数库提供的,它的机制是很复杂的,例如为了分配一块内存,库函数会按照一定的算法(具体的算法可以参考数据结构/操作系统)在堆内存中搜索可用的足够大小的空间,如果没有足够大小的空间(可能是由于内存碎片太多),就有可能调用系统功能去增加程序数据段的内存空间,这样就有机会分到足够大小的内存,然后进行返回。显然,堆的效率比栈要低得多。

从这里我们可以看到,堆和栈相比,由于大量new/delete的使用,容易造成大量的内存碎片;由于没有专门的系统支持,效率很低;由于可能引发用户态和核心态的切换,内存的申请,代价变得更加昂贵。所以栈在程序中是应用最广泛的,就算是函数的调用也利用栈去完成,函数调用过程中的参数,返回地址,EBP和局部变量都采用栈的方式存放。所以,我们推荐大家尽量用栈,而不是用堆。

虽然栈有如此众多的好处,但是由于和堆相比不是那么灵活,有时候分配大量的内存空间,还是用堆好一些。

无论是堆还是栈,都要防止越界现象的发生(除非你是故意使其越界),因为越界的结果要么是程序崩溃,要么是摧毁程序的堆、栈结构,产生意想不到的结果,就算是在你的程序运行过程中,没有发生上面的问题,你还是要小心,说不定什么时候就崩掉,那时候debug可是相当困难的)

对了,还有一件事,如果有人把堆栈合起来说,那它的意思是栈,可不是堆!

static用来控制变量的存储方式和可见性

函数内部定义的变量,在程序执行到它的定义处时,编译器为它在栈上分配空间,函数在栈上分配的空间在此函数执行结束时会释放掉,这样就产生了一个问题:如果想将函数中此变量的值保存至下一次调用时,如何实现?最容易想到的方法是定义一个全局的变量,但定义为一个全局变量有许多缺点,最明显的缺点是破坏了此变量的访问范围(使得在此函数中定义的变量,不仅仅受此函数控制)。

需要一个数据对象为整个类而非某个对象服务,同时又力求不破坏类的封装性,即要求此成员隐藏在类的内部,对外不可见。

static的内部机制:

静态数据成员要在程序一开始运行时就必须存在。因为函数在程序运行中被调用,所以静态数据成员不能在任何函数内分配空间和初始化

这样,它的空间分配有三个可能的地方,一是作为类的外部接口的头文件,那里有类声明;二是类定义的内部实现,那里有类的成员函数定义;三是应用程序的main()函数前的全局数据声明和定义处。

静态数据成员要实际地分配空间,故不能在类的声明中定义(只能声明数据成员)。类声明只声明一个类的“尺寸和规格”,并不进行实际的内存分配,所以在类声明中写成定义是错误的。它也不能在头文件中类声明的外部定义,因为那会造成在多个使用该类的源文件中,对其重复定义

static被引入以告知编译器,将变量存储在程序的静态存储区而非栈上空间,静态数据成员按定义出现的先后顺序依次初始化,注意静态成员嵌套时,要保证所嵌套的成员已经初始化了。消除时的顺序是初始化的反顺序。

static的优势:可以节省内存,因为它是所有对象所公有的,因此,对多个对象来说,静态数据成员只存储一处,供所有对象共用。静态数据成员的值对每个对象都是一样,但它的值是可以更新的。只要对静态数据成员的值更新一次,保证所有对象存取更新后的相同的值,这样可以提高时间效率。

引用静态数据成员时,采用如下格式:

<类名>::<静态成员名>

如果静态数据成员的访问权限允许的话(即public的成员),可在程序中,按上述格式来引用静态数据成员。

PS:

(1)类的静态成员函数是属于整个类而非类的对象,所以它没有this指针,这就导致了它仅能访问类的静态数据和静态成员函数

(2)不能将静态成员函数定义为虚函数。

(3)由于静态成员声明于类中,操作于其外,所以对其取地址操作,就多少有些特殊,变量地址是指向其数据类型的指针,函数地址类型是一个“nonmember函数指针”。

(4)由于静态成员函数没有this指针,所以就差不多等同于nonmember函数,结果就产生了一个意想不到的好处:成为一个callback函数,使得我们得以将C++和C-based X Window系统结合,同时也成功的应用于线程函数身上。

(5)static并没有增加程序的时空开销,相反它还缩短了子类对父类静态成员的访问时间,节省了子类的内存空间。

(6)静态数据成员在<定义或说明>时前面加关键字static。

(7)静态数据成员是静态存储的,所以必须对它进行初始化。

(8)静态成员初始化与一般数据成员初始化不同:

初始化在类体外进行,而前面不加static,以免与一般静态变量或对象相混淆;

初始化时不加该成员的访问权限控制符private,public等;

初始化时使用作用域运算符来标明它所属类;

所以我们得出静态数据成员初始化的格式:

<数据类型><类名>::<静态数据成员名>=<>

(9)为了防止父类的影响,可以在子类定义一个与父类相同的静态变量,以屏蔽父类的影响。这里有一点需要注意:我们说静态成员为父类和子类共享,但我们又重复定义了静态成员,这会不会引起错误呢?不会,我们的编译器采用了一种绝妙的手法:name-mangling用以生成唯一的标志。

C语言变量的存储类别

内存中供用户使用的存储空间分为代码区数据区两个部分。变量存储在数据区,数据区又可分为静态存储区动态存储区

静态存储是指在程序运行期间给变量分配固定存储空间的方式。如全局变量存放在静态存储区中,程序运行时分配空间,程序运行完释放。

动态存储是指在程序运行时根据实际需要动态分配存储空间的方式。如形式参数存放在动态存储区中,在函数调用时分配空间,调用完成释放。

对于静态存储方式的变量可在编译时初始化,默认初值为0或空字符。对动态存储方式的变量如不赋初值,则它的值是一个不确定的值。

在C语言中,具体的存储类别有自动(auto)寄存器(register)静态(static)外部(extern)四种。静态存储类别与外部存储类别变量存放在静态存储区,自动存储类别变量存放在动态存储区,寄存器存储类别直接送寄存器。

变量存储类别定义方法:

存储类别类型变量表;

例如:

(1)a,b,c为整型自动存储类别变量:

auto int abc

(2)x,y,z为双精度型静态存储类别变量:

static double xyz

1. 变量有哪些存储类型?

变量的存储类型由“存储类型指明符”来说明。存储类型指明符可以是下列类键字之一:

auto

register

extern

static

下面是详细的解释:

auto存储类指明符:用于说明具有局部作用域的变量,它表示变量具有局部(自动)生成期,但由于它是所有局部作用域变量说明的缺省存储类指明符,所以使用得很少。要注意的是,所有在函数内部定义的变量都是局部变量,函数内部定义的变量其作用域只在函数内部。它的生存期为该函数运行期间,一旦离开这个函数或这个函数终止,局部变量也随之消失。

register存储类指明符:当声明了这个指明符后,编译程序将尽可能地为该变量分配CPU内部的寄存器作为变量的存储单元,以加快运行速度。注意,寄存器与存储器是不同的。寄存器一般在CPU内部,而存储器一般指外部的(比如内存条),CPU内部的寄存器其运算速度是很高的。当寄存器已分配完毕,就自动地分配一个外部的内存。它的作用等价于auto,也只能用于局部变量和函数的参量说明。

static存储类指明符:表示变量具有静态生成期。static变量的的特点是它离开了其作用域后,其值不会消失。当回到该作用域之后又可以继续使用这个static变量的值。

例:利用static变量统计调用函数的次数:

int two(); /*函数原型说明*/

void main()

{

int a=0;

a=two(); /*a的值等于1*/

a=two() /*a的值等于2*/

a=two(); /*a的值等于3*/

}

 

int two()

{

static int b=0;/*定义了一个局部的static变量*/

b ;

return b;

}

如果不是一个static变量就不会有这个效果了 

int two(); /*函数原型说明*/

void main()

{

int a=0;

a=two(); /*a的值等于1*/

a=two() /*a的值等于1*/

a=two(); /*a的值等于1*/

}

int two()

{

int b=0;

b ;

return b;

}

变量a的值总是1,原因是在函数two()中,变量b不是一个static变量,其值随着离开two函数就消失了,当回到two函数时又被重新赋值0。

extern存储类指明符:一般用在工程文件中。在一个工程文件中因为有多个程序文件,当某一个变量在一个程序文件中定义了之后,如果在另一个程序文件中予以定义,就会出现重复定义变量的错误。使用extern存储类型指明符就可以指出在该文件外部已经定义了这个变量。extern变量的作用域是整个程序。

2. 变量存储在内存的什么地方?

1)变量可以存储在内存的不同地方,这依赖于它们的生成期。在函数上部定义的变量(全局变量或static外部变量)和在函数内部定义的static变量,其生存期就是程序运行的全过程。这些变量被存储在数据段(Data Segment)中。数据段是在内存中为这些变量留出的一段大小固定的空间,它分为二部分,一部分用来初始化变量,另一部分用来存放未初始化的变量。

2)在函数内部定义的auto变量(没有用关键字static定义的变量)的生成期从程序开始执行其所在的程序块代码时开始,到程序离开该程序块时为止。作为函数参数的变量只在调用该函数期间存在。这些变量被存储在栈(stack)中。栈是内存中的一段空间,开始很小,以后逐渐自动变大,直到达到某个预定义的界限。

3)当用malloc等函数给指针分配一个地址空间的时候,这个分配的内存块位于一段名为“堆(heap)”的内存空间中。堆开始时很小,但调用malloc或alloc等内存分配函数时它就会增大。堆可以和数据段或栈共用一个内存段,也可以有它自己的内存段,这完全取决于编译选项和操作系统。与栈相似,堆也有一个增长界限,并且决定这个界限的规则与栈相同。

C语言变量的作用域和存储类型
一、作用域和生存期

C程序的标识符作用域有三种:局部、全局、文件。标识符的作用域决定了程序中的哪些语句可以使用它,换句话说,就是标识符在程序其他部分的可见性。通常,标识符的作用域都是通过它在程序中的位置隐式说明的。

1. 局部作用域

前面各个例子中的变量都是局部作用域,他们都是声明在函数内部,无法被其他函数的代码所访问。函数的形式参数的作用域也是局部的,它们的作用范围仅限于函数内部所用的语句块。

void add(int);

main()

{

   int num=5;

   add(num);

  printf("%d\n",num);      /*输出5*/

}

 

void add(int num)

{

   num++;

  printf("%d\n",num);      /*输出6*/

}

上面例子里的两个num变量都是局部变量,只在本身函数里可见。前面我们说了,在两个函数出现同名的变量不会互相干扰,就是这个道理。所以上面的两个输出,在主函数里仍然是5,在add()函数里输出是6。

2. 全局作用域

对于具有全局作用域的变量,我们可以在程序的任何位置访问它们。当一个变量是在所有函数的外部声明,也就是在程序的开头声明,那么这个变量就是全局变量。

void add(int);

int num;

main()

{

   int n=5;

   add(n);

  printf("%d\n",num);     /*输出6*/

}

void add(num)          /*形式参数没有指定类型*/

{

   num++;

  printf("%d\n",num);     /*输出6*/

}

上面的main()和add()里面,并没有声明num,但是在最后输出的时候却要求输出num,这是由于在程序的开始声明了num是全局变量,也就是在所有函数里都可以使用这个变量。这时候一个函数里改变了变量的值,其他函数里的值也会出现影响。上面的例子输出都是6,因为在add()函数里改变了num的值,由于num是全局变量,就好像它们两个函数共用一个变量,所以在main()函数里的num也随之改变了。

3. 文件作用域

在很多C语言书上,都没有说明文件作用域,或者只是略微的提到,其实文件作用域在较大程序中很有作用(在多文件系统中)。文件作用域是指外部标识符仅在声明它的同一个转换单元内的函数汇总可见。所谓转换单元是指定义这些变量和函数的源代码文件(包括任何通过#include指令包含的源代码文件)。static存储类型修饰符指定了变量具有文件作用域。

static int num;

static void add(int);

main()

{

   scanf("%d",&num);

   add(num)

   printf("%d\n",num);

}

void add(num)

{

   num++;

}

上面的程序中变量num和函数add()在声明是采用了static存储类型修饰符,这使得它们具有文件作用域,仅爱定义它们的文件内可见。

由于我们提到的大多数程序都只有一个编译文件组成,所以这种写法没有实际意义。但是实际工程上的文件有很多,它们不是由一个人写成的,由很多人共同完成,这些文件都是各自编译的,这难免使得某些人使用了一样的全局变量名,那么为了以后程序中各自的变量和函数不互相干扰,就可以使用static修饰符,这样在连接到同一个程序的其他代码文件而言就是不可见的。

二、变量存储类型

前面我们说了,声明变量时用如下类似的形式:

int num;

float total;

它们都没有存储类型修饰符,我们在声明时也可以通过存储类型修饰符来告诉编译器将要处理什么类型的变量。存储类型有以下四种:自动(auto)静态(static)外部(extern)寄存器(regiser)

1. 自动存储类型

自动存储类型修饰符指定了一个局部变量为自动的,这意味着,每次执行到定义该变量的语句块时,都将会为该变量在内存中产生一个新的拷贝,并对其进行初始化。实际上,如果不特别指明,局部变量的存储类型就默认为自动的,因此,加不加auto都可以。

main()

{

   auto int num=5;

   printf("%d\n",num);

}

在这个例子中,不论变量num的声明是否包含关键字auto,代码的执行效果都是一样的。函数的形式参数存储类型默认也是自动的。

2. 静态存储变量

前面已经使用了static关键字,但是对于局部变量,静态存储类型的意义是不一样的,这时,它是和自动存储类型相对而言的。静态局部变量的作用域仍然近局限于声明它的语句块中,但是在语句块执行期间,变量将始终保持它的值。而且,初始化值只在语句块第一次执行是起作用。在随后的运行过程中,变量将保持语句块上一次执行时的值。

看下面两个对应的程序:

/*1.C*/                               /*2.C*/

int add();                            int add();

main()                                main()

{                                     {

   int result;                            int result;

   result=add()                           result=add();

   printf("%d",result);                 printf("%d ",result);

   result=add();                          result=add();

   printf("%d",result);                 printf("%d ",result);

   result=add();                          result=add();

  printf("%d",result);                  printf("%d",result);

}                                      }

int add()                             int add()

{                                     {

   int num=50;                            static int num=50;

   num++;                                 num++;

   return num;                            return num;

}                                     }

上面两个源文件,只有函数add()里的变量声明有所不同,一个是自动存储类型,一个是静态存储类型。

对于1.C文件,输出结果为51 51 51;这很好理解,每次初始值都是50,然后加1上来。

对于2.C文件,输出结果为51 52 53;这是由于变量是静态的,只在第一次初始化了50,以后都是使用上次的结果值。当第一次调用add()时,初始化为50,然后加1,输出为51;当第二次调用时,就不初始化了,这时num的值为上次的51,然后加1,输出52;当第三次调用时,num为52,加1就是53了。

比较就会发现它们的不同之处了。静态变量在下一节要说的递归函数中经常使用到。

当第一次不指明静态变量的初始值时,默认为0

下面举一个例子,把我们说到的静态变量理解一下。

求1+2+……+100的值的代码如下:

void add();

int result;

 

main()

{

   int i;

   result=0;

   for(i=0;i<100;i++) add();

   printf("%d\n",result);

}

void add()

{

   static int num=0;

   num++;

   result+=num;

}

add()函数被调用了100次,num的值从1一直变到100,这样就可以求出它们的和了。如果写成int num=0;那就是求1+1+……+1这100个1的值了。

实际上类似的这类问题我们可以通过递归函数来解决,什么是递归,我们下一节介绍。

3. 外部存储类型

外部存储类型声明了程序将要用到的、但尚未定义的外部变量。通常,外部存储类型都是用于声明在另一个转换单元中定义的变量。下面举一个例子,这个例子包括两个文件。

/*1.C*/

void a();

main()

{

   extern int num;

   a();

   printf("%d\n",num);

}

/*2.C*/

int num;

void a()

{

   num=5;

}

这两个程序是分别编译的,然后连接成一个执行文件。具体如何操作,可以查看一些手册,这儿我简单说了一下。把上面两个文件都编译好后,再制作一个.prj文件,里面的内容是:

1.c

2.c

只有这两行,这可在编辑状态下写成,存盘,取名为1.prj。

然后选择project选项,选择project name,填入1.prj文件名,按F9后,即可生成1.exe文件。

main()函数中变量num是在另一个文件中定义的。因此,当编译器编译1.c时,无法确定该变量的地址。这时,外部存储类型声明告诉编译器,把所有对num的引用当作暂且无法确定的引用,等到所有编译好的目标代码连接成一个可执行程序模块时,再来处理对变量num的引用。

外部变量的声明既可以在引用它的函数的内部,也可以在外部。如果变量声明在函数外部,那么同一转换单元内的所有函数都可以使用这个外部变量。反之,如果在函数内部,那么只有这一个函数可以使用该变量。

前面说了文件作用域的问题,如果在声明全局变量时,加上static修饰符,那么该变量只在当前文件内可见,而extern又可以引用其它文件里的变量。所以在一个大型程序中,每个程序员只是完成其中的一小块,为了让自己的变量不让其他程序员使用,保持一定的独立性,经常在全局变量前加static。我们可以这样来说明一下:

还是上面的两个文件,现在再增加一个文件3.c,内容为:

static int num;

void a()

{

   num=6;

}

把1.prj文件后面加上3.c 这样,我们生成的1.exe文件,执行时输出是5,而不是6。因为3.c文件的num变量增加了文件作用域,在其他文件中是无法使用它的。

4. 寄存器存储类型

被声明为寄存器存储类型的变量,除了程序无法得到其地址外,其余都和自动变量一样。至于什么是变量地址,以后说指针时会详细介绍。

main()

{

   egister int num;

   num=100;

   printf("%d",num);

}

使用寄存器存储类型的目的是让程序员指定某个局部变量存放在计算机的某个硬件寄存器里而不是内存中,以提高程序的运行速度。不过,这只是反映了程序员的主观意愿,编译器可以忽略寄存器存储类型修饰符。

寄存器变量的地址是无法取得的,因为绝大多数计算机的硬件寄存器都不占用内存地址。而且,即使编译器忽略寄存器类型修饰符把变量放在可设定地址的内存中,我们也无法取地址的限制仍然存在。

要想有效的利用寄存器存储类型,必须像汇编语言程序员那样了解处理器的内部构造,知道可用于存放变量的寄存器的数量和种类,以及他们是如何工作的。但是,不同计算机在这些细节上未必是一样的,因此对于一个可移植的程序来说,寄存器存储类型的作用不大。特别是现在很多编译器都能提供很好的优化效果,远比程序员来选择有效的多。不过,寄存器存储类型还是可以为优化器提供重要的参考。

C的作用域还有一种,静态块。比如:

/* 静态块作用域 */

{

...;

...;

}

 

/* 函数作用域 */

main()

{

...;

}

栈:由编译器自动分配释放管理。局部变量及每次函数调用时返回地址、以及调用者的环境信息(例如某些机器寄存器)都存放在栈中。新被调用的函数在栈上为其自动和临时变量分配存储空间。通过以这种方式使用栈,C函数可以递归调用。

堆:需要由程序员分配释放管理,若程序员不释放,程序结束时可能由OS回收。通常在堆中进行动态存储分配。

非初始化数据段:通常将此段称为BSS段,这一名称来源于早期汇编程序的一个操作符,意思是“block started bysymbol(由符号开始的块)”,未初始化的全局变量和静态变量存放在这里。在程序开始执行之前,内核将此段初始化为0。函数外的说明:long sum[1000] ; 使此变量存放在非初始化数据段中。

初始化的数据:通常将此段称为数据段,它包含了程序中需赋初值的变量。初始化的全局变量和静态变量存放在这里。例如,C程序中任何函数之外的说明:int maxcount = 99;使此变量以初值存放在初始化数据段中。

正文段:CPU执行的机器指令部分。通常,正文段是可共享的,所以即使是经常环境指针环境表环境字符串执行的程序(如文本编辑程序、C编译程序、shell等)在存储器中也只需有一个副本,另外,正文段常常是只读的,以防止程序由于意外事故而修改其自身的指令。

对于x86处理器上的Linux,正文段从0x08048000单元开始,栈底在0xC0000000之下开始(栈由高地址向低地址方向增长)。堆顶和栈底之间未用的虚拟空间很大。

Shell的size命令可以看到一个程序的正文段(text)、数据段(data)、非初始化数据段(bss)及文件长度。

[foxman@17:01:49 ]$size mydesign

   text    data    bss     dec     hex  filename

   79210  1380   404    80994  13c62  mydesign

内存分为代码区、全局数据区、堆区和栈区。堆一般存放动态数据,栈里一般存放局部成员。

 

关于堆栈和堆的概念

[问题]C++中创建本地(或者说局域)变量是在堆栈(stack)中分配内存地址,而创建全局变量则是在堆(heap)中分配内存地址。我想知道什么是堆(heap)?为什么全局变量和本地变量在分配内存地址时要分别对待,堆和堆栈哪一个更有效率?

[回答]在Window中执行的每一个应用程序都有其自己的内存地址。一部分内存空间用于存放程序代码,一部分内存空间用于存放程序执行期间创建的变量。创将变量的方法有两种,一种是在堆中,另外一种则在堆栈里。

理解堆的最好的方法是将它看成一个程序随时可以使用的内存块。为了创建堆变量,程序要使用“new”(在C++里)操作符或者“malloc”(在C中)例程,它们返回指向变量的指针(堆变量总是通过指针来处理和操作)。最后程序用“delete”(在C++里)操作符或者“free”(在C中)例程来删除或者释放内存空间。

而堆栈则不同,它是某个函数被调用后随时可以创建的一小块内存,被用于在函数范围内保存变量(也称为自动变量)。在函数中,任何包含在{}内的代码都有其自己的堆栈。当这个函数或者{}退出时,堆栈以及它包含的所有内容都被摧毁。因此下面的代码是不运行的:

void Myfunction()

{

Int I = 5;

{ int j = 6; }

Int k = i + j;

}

所以本文问题的答案是:使用堆栈(stack)分配本地或者局域变量的地址空间,而用堆(heap)分配大块内存地址或者动态创建对象的情形。

例如:

void MyFunction()

{

int i = 5; // 堆栈中的本地(自动)变量

int *iArray; // 堆栈中的本地(自动)变量

iArray = new int[10000]; // 这一行代码将在堆中创建10000个元素的数组,由iArray指向其地址

for (int k = 0; k < 10000 ; k++)

{  iArray[k] = k + i;

   cout << iArray[k] << "\n";

}

delete iArray; // 从堆中删除或释放数组占用的地址空间,否则将会有内存泄漏

} //函数结束注意:iArray 不是一个堆变量。它是一个局部指针变量,指向堆中未命名的一个数组。

一般认为在C中分为这几个存储区:

1、栈:由编译器自动分配释放

2、堆:一般由程序员分配释放,若程序员不释放,程序结束时可能由OS回收

3、全局区(静态区):全局变量和静态变量的存储是放在一块的,初始化的全局变量和静态变量在一块区域,未初始化的全局变量和未初始化的静态变量在相邻的另一块区域。

4、另外还有一个专门放常量的地方,程序结束释放。

在函数体中定义的变量通常是在栈上,用malloc,calloc,realloc等分配内存的函数分配得到的就是在堆上。在所有函数体外定义的是全局量,加了static修饰符后不管在哪里都存放在全局区(静态区),在所有函数体外定义的static变量表示在该文件中有效,不能extern到别的文件用,在函数体内定义的static表示只在该函数体内有效。另外,函数中的“adgfdf”这样的字符串存放在常量区。

比如代码:

int  a  =  0;    //全局初始化区

char  *p1;     //全局未初始化区

main()

{

   int   b;             //

   char  s[]  =  "abc";    //

   char  *p2;           //

   char  *p3  =  "123456";    //123456\0在常量区,p3在栈上

   static  int  c  =  0  //全局(静态)初始化区

   p1  =  (char  *)malloc(10);

   p2  =  (char  *)malloc(20);

     //分配得来得1020字节的区域就在堆区

strcpy(p1,  "123456");

//123456\0放在常量区,编译器可能会将它与p3所指向的"123456"优化成一块

}

还有就是函数调用时会在栈上有一系列的保留现场及传递参数的操作。

栈的空间大小有限定,VC的缺省是2M栈不够用的情况一般是程序中分配了大量数组和递归函数层次太深。有一点必须知道,当一个函数调用完返回后它会释放该函数中所有的栈空间。栈是由编译器自动管理的,不用你操心。

堆是动态分配内存的,并且你可以分配使用很大的内存。但是用不好会产生内存泄漏。并且频繁地malloc和free会产生内存碎片(有点类似磁盘碎片),因为C分配动态内存时是寻找匹配的内存的。而用栈则不会产生碎片。

在栈上存取数据比通过指针在堆上存取数据快些。

一般大家说的堆栈和栈是一样的,就是栈(stack),而说堆时才是堆heap。

栈是先入后出的,一般是由高地址向低地址生长。

 

堆(heap)和栈(stack)是C/C++编程不可避免会碰到的两个基本概念。首先,这两个概念都可以在讲数据结构的书中找到,他们都是基本的数据结构,虽然栈更为简单一些。

在具体的C/C++编程框架中,这两个概念并不是并行的。对底层机器代码的研究可以揭示,栈是机器系统提供的数据结构,而堆则是C/C++函数库提供的

具体地说,现代计算机(串行执行机制),都直接在代码底层支持栈的数据结构。这体现在,有专门的寄存器指向栈所在的地址,有专门的机器指令完成数据入栈出栈的操作。

这种机制的特点是效率高,支持的数据有限,一般是整数,指针,浮点数等系统直接支持的数据类型,并不直接支持其他的数据结构。因为栈的这种特点,对栈的使用在程序中是非常频繁的。对子程序的调用就是直接利用栈完成的。机器的call指令里隐含了把返回地址推入栈,然后跳转至子程序地址的操作,而子程序中的ret指令则隐含从堆栈中弹出返回地址并跳转之的操作。C/C++中的自动变量是直接利用栈的例子,这也就是为什么当函数返回时,该函数的自动变量自动失效的原因。

和栈不同,堆的数据结构并不是由系统(无论是机器系统还是操作系统)支持的,而是由函数库提供的。基本的malloc/realloc/free函数维护了一套内部的堆数据结构。当程序使用这些函数去获得新的内存空间时,这套函数首先试图从内部堆中寻找可用的内存空间,如果没有可以使用的内存空间,则试图利用系统调用来动态增加程序数据段的内存大小,新分配得到的空间首先被组织进内部堆中去,然后再以适当的形式返回给调用者。当程序释放分配的内存空间时,这片内存空间被返回内部堆结构中,可能会被适当的处理(比如和其他空闲空间合并成更大的空闲空间),以更适合下一次内存分配申请。这套复杂的分配机制实际上相当于一个内存分配的缓冲池(Cache),使用这套机制有如下若干原因:

1、系统调用可能不支持任意大小的内存分配。有些系统的系统调用只支持固定大小及其倍数的内存请求(按页分配);这样的话对于大量的小内存分类来说会造成浪费。

2、系统调用申请内存可能是代价昂贵的。系统调用可能涉及用户态和核心态的转换。

3、没有管理的内存分配在大量复杂内存的分配释放操作下很容易造成内存碎片。

堆和栈的对比:

从以上知识可知,栈是系统提供的功能,特点是快速高效,缺点是有限制,数据不灵活;而堆是函数库提供的功能,特点是灵活方便,数据适应面广泛,但是效率有一定降低。栈是系统数据结构,对于进程/线程是唯一的;堆是函数库内部数据结构,不一定唯一。不同堆分配的内存无法互相操作。栈空间分静态分配和动态分配两种。静态分配是编译器完成的,比如自动变量(auto)的分配。动态分配由alloca函数完成。栈的动态分配无需释放(是自动的),也就没有释放函数。为可移植的程序起见,栈的动态分配操作是不被鼓励的!堆空间的分配总是动态的,虽然程序结束时所有的数据空间都会被释放回系统,但是精确的申请内存/释放内存匹配是良好程序的基本要素。

 

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值