C++中函数调用的整个过程内存堆栈分配详解

72 篇文章 5 订阅
17 篇文章 6 订阅

一个函数执行过程中堆栈分配情况实例详解 

下面的例子可以完全展示不同的变量所占的内存区域:

//main.cpp
int a = 0; 全局初始化区
char *p1; 全局未初始化区
main()
{
int b; //栈中
char s[] = "abc"; //栈中
char *p2; //栈中
char *p3 = "123456"; //123456/0在常量区,p3在栈上
static int c =0; //全局(静态)初始化区
//以下分配得到的10和20字节的区域就在堆区
p1 = (char *)malloc(10);  //p1本身在全局区,它指向的空间是在堆中
p2 = new char[20];//(char *)malloc(20);     //p2本身是在栈中的,它指向的空间是在堆中
strcpy(p1, "123456"); //123456/0放在常量区,编译器可能会将它与p3所指向的"123456"优化成一个地方。
}

//实例2

下面两条定义或动态生成 Complex 对象的语句,都会导致该无参构造函数被调用,以对 Complex 对象进行初始化。

Complex c; //c用无参构造函数初始化
Complex *p = new Complex; //对象 *p 用无参构造函数初始化
如果为 Complex 类编写了构造闲数,如下所示:

Complex cl; //错,Complex 类没有无参构造函数(默认构造函数),cl在栈中分配,根据cl作用域系统自动回收。
Complex* pc = new Complex; //错,Complex 类没有默认构造函数, pc是一个指针变量本身在栈中(win32程序的话占4个字节),pc所指向的类在堆中分配,用完需手动释放内存。
Complex c2(2); //正确,相当于 Complex c2(2, 0), c2在栈中分配,根据c2作用域系统自动回收。
Complex c3(2, 4), c4(3, 5); //正确 c3在栈中分配,根据c3作用域系统自动回收。
Complex* pc2 = new Complex(3, 4); //正确  pc2是一个指针变量本身在栈中(win32程序的话占4个字节),pc2所指向的类在堆中分配,用完需手动释放内存。

三、栈(stack)和堆(heap)具体的区别
1、在申请方式上
栈(stack): 现在很多人都称之为堆栈,这个时候实际上还是指的栈。它由编译器自动管理,无需我们手工控制。 例如,声明函数中的一个局部变量 int b 系统自动在栈中为b开辟空间;在调用一个函数时,系统自动的给函数的形参变量在栈中开辟空间。
堆(heap): 申请和释放由程序员控制,并指明大小。容易产生memory leak。
在C中使用malloc函数。
如:p1 = (char *)malloc(10);
在C++中用new运算符。
如:p2 = new char[20];//(char *)malloc(10);
但是注意p1本身在全局区,而p2本身是在栈中的,只是它们指向的空间是在堆中。

内存分配方式有三种:   
   
   1.从静态存储区域分配。内存在程序编译时候就已经分配好,这块内存在程序整个运行期间都存在。例如全局变量,static变量。   
   
   2.在栈上创建。在执行函数时,函数内局部变量存储单元都可以在栈上创建,函数执行结束时这些存储单元自动被释放。栈内存分配运算内置于处理器指令集中,效率很高,但是分配内存容量有限。   
   
   3.从堆上分配,亦称动态内存分配。程序在运行时候用malloc或new申请任意多少内存,程序员自己负责在何时用free或delete释放内存。动态内存生存期由我们决定,使用非常灵活,但问题也最多。

如果C++中的一个类是定义在栈上的,就使用"."开访问它的成员。如果是定义在堆上的,就使用"->"指针来开访问。

/********************************

C语言中“指针”和“指针变量”的区别是什么

比较严格的说法是这样的: 
系统为每一个内存单元分配一个地址值,C/C++把这个地址值称为“指针”。如有int i=5;,存放变量i的内存单元的编号(地址)&i被称为指针。 
“指针变量”则是存放前述“地址值”的变量,也可以表述为,“指针变量”是存放变量所占内存空间“首地址”的变量(因为一个变量通常要占用连续的多个字节空间)。比如在int i=5;后有一句int *p=&i;,就把i的指针&i赋给了int *型指针变量p,也就是说p中存入着&i。所以说指针变量是存放指针的变量。 
有一个事实值得注意,那就是有不少资料和教科书并没有如上区分,而是认为“指针是指针变量的简称”,如对int *p=&i;的解释是:声明一个int *型指针p,并用变量i的地址初始化;而严格说应该是声明一个int *型指针变量p才对。所以有时看书要根据上下文理解实质,而不能过于拘泥于文字表述。

1.指针的概念
         想要清楚的理解指针概念我们必须先弄清楚数据在内存中是怎样储存的,又怎样读取的。在电脑的内存区里每一个字节都有一个编号,这个编号就是地址。我们在程序中定义一个变量,对程序进行编译时,系统会根据数据的类型分配给这个变量一定长度的空间,如int型分配四个字节。那这四个字节的编号就是这个变量的地址。反过来说这个变量就储存在这个地址内。你可以理解为酒店里的房间号。

        由于通过地址能找到这个变量,可以说地址指向这个变量。就像你住酒店时,你朋友可以通过房间号码找到你。这个地址我们形象化的把他称为指针。

        总的来说一个变量的地址就称为该变量的指针。

        理解了指针接下来我们来讨论一下我们常说的指针变量又是什么呢?

        通过上文我们可以知道指针就是地址,如地址1000是变量 i 的指针。那么这个指针(地址)该怎样储存呢?C语言里定义一个变量专门用来存放一个变量的地址,那么该变量称为指针变量。即指针变量的值是地址(我们在交流中讲的指针通常是指针变量)。

#include<stdio.h>
int main
{
    int a= 10;//在内存中开辟一块空间
    int *p = &a;//取出变量a的地址放入指针变量p中;
    return 0;
}
 
2.指针变量的大小
        在32位的计算机中,地址是32个0或者1组成二进制序列,那地址就得用4个字节的空间来存储,所以一个指针变量的大小就应该是4个字节。

        在64位的计算机中,如果有64个地址线,那一个指针变量的大小是8个字节,才能放下一个地址。

        总结,指针的大小在32位平台上是4个字节,在64位平台上是8个字节。
 

/*********************************

函数调用过程中内存分配情况

函数调用过程中:实参将值拷贝给函数的形参,而函数的形参相当于一个生存周期位于函数
内部的局部变量,函数内部的内存操作也只是将拷贝到形参的值进行操作,形参在函数结束
后会被栈自动回收释放(形参在栈中分配),这就是为什么值传递后在形参中改变不了实参的值
了,因为实参只是把值拷贝给了形参,而实参是在另一个堆栈地址中。
int swap(int x, int y)//实参x和y的值并没有交换
{
int temp;
temp = x; x = y; y = temp;
return temp;
}
所以只有引用传递,即相当于
把一个值类型的变量的地址传给形参,在形参里操作这个地址(即指针地址)才能改变实参对应的地址中的
值,如下就是引用传递,形参为指针类型。
int swap(int *x, int *y)//这里形参x和y是在栈中分配的一块内存并拷贝了实参的指针值,即x和y中的内容分别=实参的指针变量的值。
{
int temp;
temp = *x; *x = *y; *y = temp;
return temp;
}
main()
{
int a=8;
int b=9;
int* aa=&a;
int* bb=&b
swap(aa,bb)//这里形参是在栈中分配的一块内存并拷贝了实参aa,bb的指针值,即形参中的内容分别=实参的指针变量的值。
}

另外,如果在函数内部声明定义一个变量并且是在栈中分配内存的,那么这个变量是局部变量将会在函数运行结束自动被栈回收,如果是在堆中分配内存的那么这个变量一般都是指针类型即指针变量,这个指针变量本身也是在栈中分配的函数结束会自动被栈回收,但是它指向的内存地址实在堆中分配需要手动释放才行。

func()

{

int* a=new int(0);//a是局部变量是在栈中分配的一个存储指针地址的变量(32位程序的话就是4个字节大小,会在函数结束后被栈回收释放),而a所指向的地址(new int(0)是在堆中分配内存的一种方式,其他的还有 *p = (char *)malloc(num)等方式)是在堆中分配需要手动释放(函数结束后系统不会自动处理堆中的内存)。

}

一、内存布局

1、栈区(stack):由编译器自动分配释放,存放函数的参数值,局部变量值等,其操作方法类似数据结构中的栈。

2、堆区(heap):一般由程序员分配释放,与数据结构中的堆毫无关系,分配方式类似于链表。

3、全局/静态区(static):全局变量和静态变量的存储是放在一起的,在程序编译时分配。

4、文字常量区:存放常量字符串。

5、程序代码区:存放函数体(类的成员函数、全局函数)的二进制代码

二、栈与堆的比较

1、申请方式

stack:系统自动分配,如声明int a;系统自动在栈空间中为a开辟空间

heap:程序员申请,并指明大小,c中的malloc,如char*p=(char*)malloc(10);

    C++中的new运算符:如int*p2=new int(10);

    注意:p和p2本身是在栈中的,但他们指向的地址是堆空间

2、系统响应

栈:只要系统剩余空间大于申请空间就能申请,否则报错:栈溢出

堆:首先应该知道操作系统有一个记录空闲内存地址的链表,当系统收到程序的申请时,会遍历该链表,寻找第一个空间大于所申请空间的堆结点,然后将该结点从空闲结点链表中删除,并将该结点的空间分配给程序。另外,对于大多数系统,首地址处会记录这块内存空间中本次分配的大小,这样,代码中的delete语句才能正确的释放本内存空间。另外,由于找到的堆结点的大小不一定正好等于申请的大小,系统会自动的将多余的那部分重新放入空闲链表中。

3、申请大小的限制

栈:在Windows下,栈是向低地址扩展的数据结构,是一块连续的内存的区域。这句话的意思是栈顶的地址和栈的最大容量是系统预先规定好的,在WINDOWS下,栈的大小是2M(也有的说是1M,总之是一个编译时就确定的常数),如果申请的空间超过栈的剩余空间时,将提示overflow。因此,能从栈获得的空间较小。

堆:堆是向高地址扩展的数据结构,是不连续的内存区域。这是由于系统是用链表来存储的空闲内存地址的,自然是不连续的,而链表的遍历方向是由低地址向高地址。由此可见,堆获得的空间受限于计算机系统中有效的虚拟内存,比较灵活,也比较大。

4、申请效率

栈由系统自动分配速度较快,堆由new分配速度较慢,且容易产生内存碎片,但使用方便。

/****************************************************

一、一个经过编译的C/C++的程序占用的内存分成以下几个部分:
1、栈区(stack):

由编译器自动分配和释放 ,存放函数的参数值、局部变量的值等,甚至函数的调用过程都是用栈来完成。其操作方式类似于数据结构中的栈。


2、堆区(heap) :

一般由程序员手动申请以及释放, 若程序员不释放,程序结束时可能由OS回收 。注意它与数据结构中的堆是两回事,分配方式类似于链表。


3、全局区(静态区)(static):

全局变量和静态变量的存储是放在一块的,初始化的全局变量和静态变量在一块区域, 未初始化的全局变量和未初始化的静态变量在相邻的另一块区域。程序结束后由系统释放空间。


4、文字常量区:

常量字符串就是放在这里的。 程序结束后由系统释放空间。


5、程序代码区:

存放函数体的二进制代码。

二、内存段式管理:代码段、数据段、堆栈段

目前通用系统(Linux或者Window)多采用段页式内存管理方式,即段式管理与页式管理的组合。每个程序的内存空间分为若干段,进行内存的分配与释放管理;每个段又分为若干页,进行虚拟页与实际页面的映射。段式管理程序以段为单位分配内存,依赖地址映射机制完成段式虚拟地址与实际内存地址的转换。段式管理(程序分段思想)在无OS的嵌入式开发和微系统开发中具有广泛的应用。

程序内存一般分为代码段、数据段和堆栈段。

代码段(code/text segment):用来存放执行代码的一块内存区域。该区域的大小在程序运行前就已经确定,并且内存区域属于只读。其中可能包含一些只读的常数变量(如字符串常量)。

数据段(data segment):用来存放数据的一块内存区域,分成初始化为非零的数据区BSS(Heap)三个区域。初始化非零数据区域一般存放静态非零数据和全局的非零数据,属于静态内存分配;BSS(Block Started by Symbol)区域一般存放未初始化的全局数据和静态数据,属于静态内存分配;堆区域一般存放运行时动态分配的内存空间,其大小不固定,可动态扩张或缩减。当调用malloc等函数分配内存时,新分配的内存被动态添加到堆上;当调用free等函数释放内存时,被释放内存从堆中被剔除。

堆栈段(stack segment):亦称栈,用于存放程序临时创建的局部变量和函数参数,属于动态内存分配。

代码段和数据段之间有明确的分隔,但是数据段和堆栈段之间没有,而且栈是向下增长,堆是向上增长的,因此理论上堆和栈会“增长到一起”,操作系统的内存管理功能需要防止这样的错误发生。

下面的例子可以完全展示不同的变量所占的内存区域:

//main.cpp
int a = 0; 全局初始化区
char *p1; 全局未初始化区
main()
{
int b; //栈中
char s[] = "abc"; //栈中
char *p2; //栈中
char *p3 = "123456"; //123456/0在常量区,p3在栈上
static int c =0; //全局(静态)初始化区
//以下分配得到的10和20字节的区域就在堆区
p1 = (char *)malloc(10);  //p1本身在全局区,它指向的空间是在堆中
p2 = new char[20];//(char *)malloc(20);     //p2本身是在栈中的,它指向的空间是在堆中
strcpy(p1, "123456"); //123456/0放在常量区,编译器可能会将它与p3所指向的"123456"优化成一个地方。
}
三、栈(stack)和堆(heap)具体的区别
1、在申请方式上
栈(stack): 现在很多人都称之为堆栈,这个时候实际上还是指的栈。它由编译器自动管理,无需我们手工控制。 例如,声明函数中的一个局部变量 int b 系统自动在栈中为b开辟空间;在调用一个函数时,系统自动的给函数的形参变量在栈中开辟空间。
堆(heap): 申请和释放由程序员控制,并指明大小。容易产生memory leak。
在C中使用malloc函数。
如:p1 = (char *)malloc(10);
在C++中用new运算符。
如:p2 = new char[20];//(char *)malloc(10);
但是注意p1本身在全局区,而p2本身是在栈中的,只是它们指向的空间是在堆中。


2、申请后系统的响应上
栈(stack):只要栈的剩余空间大于所申请空间,系统将为程序提供内存,否则将报异常提示栈溢出。
堆(heap): 首先应该知道操作系统有一个记录空闲内存地址的链表,当系统收到程序的申请时,会遍历该链表,寻找第一个空间大于所申请空间的堆结点,然后将该结点从空闲结点链表中删除,并将该结点的空间分配给程序。另外,对于大多数系统,会在这块内存空间中的首地址处记录本次分配的大小,这样,代码中的delete或 free语句才能正确的释放本内存空间。另外,由于找到的堆结点的大小不一定正好等于申请的大小,系统会自动的将多余的那部分重新放入空闲链表中。

3、申请大小的限制
栈(stack):在Windows下,栈是向低地址扩展的数据结构,是一块连续的内存的区域。这句话的意思是栈顶的地址和栈的最大容量是系统预先规定好的,在WINDOWS下,栈的大小是2M(也有的说是1M,总之是一个编译时就确定的常数),如果申请的空间超过栈的剩余空间时,将提示 overflow。因此,能从栈获得的空间较小。例如,在VC6下面,默认的栈空间大小是1M(好像是,记不清楚了)。当然,我们可以修改:打开工程,依次操作菜单如下:Project->Setting->Link,在Category 中选中Output,然后在Reserve中设定堆栈的最大值和commit。
注意:reserve最小值为4Byte;commit是保留在虚拟内存的页文件里面,它设置的较大会使栈开辟较大的值,可能增加内存的开销和启动时间。

堆(heap): 堆是向高地址扩展的数据结构,是不连续的内存区域(空闲部分用链表串联起来)。正是由于系统是用链表来存储空闲内存,自然是不连续的,而链表的遍历方向是由低地址向高地址。一般来讲在32位系统下,堆内存可以达到4G的空间,从这个角度来看堆内存几乎是没有什么限制的。由此可见,堆获得的空间比较灵活,也比较大。

4、分配空间的效率上
栈(stack):栈是机器系统提供的数据结构,计算机会在底层对栈提供支持:分配专门的寄存器存放栈的地址,压栈出栈都有专门的指令执行,这就决定了栈的效率比较高。但程序员无法对其进行控制。
堆(heap):是C/C++函数库提供的,由new或malloc分配的内存,一般速度比较慢,而且容易产生内存碎片。它的机制是很复杂的,例如为了分配一块内存,库函数会按照一定的算法(具体的算法可以参考数据结构/操作系统)在堆内存中搜索可用的足够大小的空间,如果没有足够大小的空间(可能是由于内存碎片太多),就有可能调用系统功能去增加程序数据段的内存空间,这样就有机会分到足够大小的内存,然后进行返回。这样可能引发用户态和核心态的切换,内存的申请,代价变得更加昂贵。显然,堆的效率比栈要低得多。

5、堆和栈中的存储内容
栈(stack):在函数调用时,第一个进栈的是主函数中子函数调用后的下一条指令(子函数调用语句的下一条可执行语句)的地址,然后是子函数的各个形参。在大多数的C编译器中,参数是由右往左入栈的,然后是子函数中的局部变量。注意:静态变量是不入栈的。当本次函数调用结束后,局部变量先出栈,然后是参数,最后栈顶指针指向最开始存的地址,也就是主函数中子函数调用完成的下一条指令,程序由该点继续运行。
堆(heap):一般是在堆的头部用一个字节存放堆的大小,堆中的具体内容有程序员安排。

6、存取效率的比较
这个应该是显而易见的。拿栈上的数组和堆上的数组来说:

void main()
{
int arr[5]={1,2,3,4,5};
int *arr1;
arr1=new int[5];
for (int j=0;j<=4;j++)
{
arr1[j]=j+6;
}
int a=arr[1];
int b=arr1[1];
}
上面代码中,arr1(局部变量)是在栈中,但是指向的空间确在堆上,两者的存取效率,当然是arr高。因为arr[1]可以直接访问,但是访问arr1[1],首先要访问数组的起始地址arr1,然后才能访问到arr1[1]。

总而言之,言而总之:
堆和栈的区别可以用如下的比喻来看出:
使用栈就象我们去饭馆里吃饭,只管点菜(声明变量)、付钱、和吃(使用),吃饱了就走,不必理会切菜、洗菜等准备工作和洗碗、刷锅等扫尾工作,他的好处是快捷,但是自由度小。
使用堆就象是自己动手做喜欢吃的菜肴,比较麻烦,但是比较符合自己的口味,而且自由度大。

补充:

1.new、delete、malloc、free关系
delete会调用对象的析构函数,和new对应free只会释放内存,new调用构造函数。malloc与free是C++/C语言的标准库函数,new/delete是C++的运算符。它们都可用于申请动态内存和释放内存。对于非内部数据类型的对象而言,光用maloc/free无法满足动态对象的要求。对象在创建的同时要自动执行构造函数,对象在消亡之前要自动执行析构函数。由于malloc/free是库函数而不是运算符,不在编译器控制权限之内,不能够把执行构造函数和析构函数的任务强加于malloc/free。因此C++语言需要一个能完成动态内存分配和初始化工作的运算符new,以及一个能完成清理与释放内存工作的运算符delete。注意new/delete不是库函数。

2.delete与 delete []区别
delete只会调用一次析构函数,而delete[]会调用每一个成员的析构函数。在More Effective  C++中有更为详细的解释:“当delete操作符用于数组时,它为每个数组元素调用析构函数,然后调用operatordelete来释放内存。”delete与New配套,delete []与new []配套
MemTest*mTest1=newMemTest[10];
MemTest*mTest2=newMemTest;
int*pInt1=newint[10];
int*pInt2=newint;
delete[]pInt1;  //-1-
delete[]pInt2;  //-2-
delete[]mTest1;//-3-
delete[]mTest2;//-4-
在-4-处报错。


这就说明:对于内建简单数据类型,delete和delete[]功能是相同的。对于自定义的复杂数据类型,delete和delete[]不能互用。 delete[]删除一个数组,delete删除一个指针简单来说,用new分配的内存用delete删除用new[]分配的内存用delete[]删除 delete[]会调用数组元素的析构函数。内部数据类型没有析构函数,所以问题不大。如果你在用delete时没用括号,delete就会认为指向的是单个对象,否则,它就会认为指向的是一个数组。

-----------------------------------------------------------------------------------------------------------

常见的内存分配和使用错误:
1) 内存的申请和分配并没有成功,但程序员却使用了它。
判断指针的值是否为NULL可以有效地避免这种错误。

2) 内存的分配已经成功,但是却没有进行初始化就直接使用它了。
主观地认为自己申请的内存的缺省值为0,这样想是没有什么道理的,内存分配后的值是不确定的。

3) 申请了内存,使用完了却忘记了释放,导致内存泄露。它会慢慢地吞噬你的系统资源,直到你的程序彻底完蛋。

4) 你很小心地释放了内存,但是却又使用了它。由于程序很复杂或者调用顺序出错,这样可能导致出现上面的错误。

-----------------------------------------------------------------------------------------------------------

C#的值类型内存分配总是和上下文有关,而不总是在堆栈上

总是很常见到一些说法是值类型总是分配在堆栈上,引用类型总是分配在堆上(google搜索大约10万条记录- -)

最近仔细思考了一下发现有点问题....当然我个人水平有限, 有什么差错还请大家指正

个人总结的c#值类型和应用类型的分配应该是:

  应用类型肯定在托管堆上,值类型总是和上下文有关

1.类的实例成员, 类是引用类型,总是分配在堆上,那么a的内存就在ClassA的实例的内存里,也必然在堆上

class ClassA
    {
        int a;
    }

2.类的静态成员,静态成员a总是在Typeof(ClassA)的内存里,那么也必然在堆上

PS:每个类必然有一个定义类方法,类静态成员等的Type对象,一个类可能可能有无数个实例,当永远只有一个Type定义,这也就是为什么静态构造函数也叫类型构造函数,并且永远只执行一次的原因

class ClassA
    {
       static int a;
    }

3.结构体的静态和实例值类型成员和结构体所在的上下文有关

4.参数 Parameter,参数变量的内存总是位于执行堆栈上 

void Test(int i)
        {
            Console.WriteLine(i);
        }

5.本地变量(非闭包),参数变量的内存总是位于执行堆栈上

void Test()
        {
            int i = 100;
            Console.WriteLine(i);
        }

6.本地变量(闭包),dotnet会将lambda表达式和匿名委托编译为一个类,由于这个类中使用到了i1,本地变量i1将被编译到这个类里面,类的实例成员自然是在堆上啦

static void Main(string[] args)
        {
            int i1 = 1;
            Func<int> f = () => { return i1; };
        }

这里的堆栈:指的是执行堆栈

这里的堆:托管堆

/***************************************************

一、 函数参数传递机制的基本理论 
  函数参数传递机制问题在本质上是调用函数(过程)和被调用函数(过程)在调用发生时进行通信的方法问题。基本的参数传递机制有两种:值传递和引用传递。以下讨论称调用其他函数的函数为主调函数,被调用的函数为被调函数。
  值传递(passl-by-value)过程中,被调函数的形式参数作为被调函数的局部变量处理,即在堆栈中开辟了内存空间以存放由主调函数放进来的实参的值,从而成为了实参的一个副本。值传递的特点是被调函数对形式参数的任何操作都是作为局部变量进行,不会影响主调函数的实参变量的值。
  引用传递(pass-by-reference)过程中,被调函数的形式参数虽然也作为局部变量在堆栈中开辟了内存空间,但是这时存放的是由主调函数放进来的实参变量的地址。被调函数对形参的任何操作都被处理成间接寻址,即通过堆栈中存放的地址访问主调函数中的实参变量。正因为如此,被调函数对形参做的任何操作都影响了主调函数中的实参变量。

二、 C语言中的函数参数传递机制
  在C语言中,值传递是唯一可用的参数传递机制。但是据笔者所知,由于受指针变量作为函数参数的影响,有许多朋友还认为这种情况是引用传递。这是错误的。请看下面的代码:
int swap(int *x, int *y)
{
int temp;
temp = *x; *x = *y; *y = temp;
return temp;
}
void main()
{
int a = 1, b = 2;
int *p1 = &a;
int *p2 = &b;
swap(p1, p2)
}
  函数swap以两个指针变量作为参数,当main()调用swap时,是以值传递的方式将指针变量p1、p2的值(也就是变量a、b的地址)放在了swap在堆栈中为形式参数x、y开辟的内存单元中。这一点从以下的汇编代码可以看出(注释是笔者加的):
22: void main()
23: {
……
……
13: int a = 1, b = 2;
00401088 mov dword ptr [ebp-4],1
0040108F mov dword ptr [ebp-8],2
14: int *p1 = &a;
00401096 lea eax,[ebp-4]
00401099 mov dword ptr [ebp-0Ch],eax
15: int *p2 = &b;
0040109C lea ecx,[ebp-8]
0040109F mov dword ptr [ebp-10h],ecx
16: swap(p1, p2);
004010A2 mov edx,dword ptr [ebp-10h] ;参数p2的值进栈
004010A5 push edx
004010A6 mov eax,dword ptr [ebp-0Ch] ;参数p1的值进栈
004010A9 push eax
004010AA call @ILT+15(swap) (00401014) ;调用swap函数
004010AF add esp,8 ;清理堆栈中的参数
17: }
  阅读上述代码要注意,INTEL80x86系列的CPU对堆栈的处理是向下生成,即从高地址单元向低地址单元生成。从上面的汇编代码可知,main()在调用swap之前,先将实参的值按从右至左的顺序压栈,即先p2进栈,再p1进栈。调用结束之后,主调函数main()负责清理堆栈中的参数。Swap 将使用这些进入堆栈的变量值。下面是swap函数的汇编代码:
14: void swap(int *x, int *y)
15: {
00401030 push ebp
00401031 mov ebp,esp ;ebp指向栈顶
……
……
16: int temp;
17: temp = *x;
4: int temp;
5: temp = *x;
00401048 mov eax,dword ptr [ebp+8] ;操作已存放在堆栈中的p1,将p1置入eax
0040104B mov ecx,dword ptr [eax] ;通过寄存器间址将*p1置入ecx
0040104D mov dword ptr [ebp-4],ecx;经由ecx将*p1置入temp变量的内存单元。以下类似
6: *x = *y;
00401050 mov edx,dword ptr [ebp+8]
00401053 mov eax,dword ptr [ebp+0Ch]
00401056 mov ecx,dword ptr [eax]
00401058 mov dword ptr [edx],ecx
7: *y = temp;
0040105A mov edx,dword ptr [ebp+0Ch]
0040105D mov eax,dword ptr [ebp-4]
00401060 mov dword ptr [edx],eax
8: return temp;
00401062 mov eax,dword ptr [ebp-4]
9: }
由上述汇编代码基本上说明了C语言中值传递的原理,只不过传递的是指针的值而已。本文后面还要论述使用引用传递的swap函数。从这些汇编代码分析,这里我们可以得到以下几点:
  1. 进程的堆栈存储区是主调函数和被调函数进行通信的主要区域。
  2. C语言中参数是从右向左进栈的。
  3. 被调函数使用的堆栈区域结构为:
    局部变量(如temp)
    返回地址
    函数参数
    低地址 
    高地址
  4. 由主调函数在调用后清理堆栈。
  5. 函数的返回值一般是放在寄存器中的。
  这里尚需补充说明几点:一是参数进栈的方式。对于内部类型,由于编译器知道各类型变量使用的内存大小故直接使用push指令;对于自定义的类型(如structure),采用从源地址向目的(堆栈区)地址进行字节传送的方式入栈。二是函数返回值为什么一般放在寄存器中,这主要是为了支持中断;如果放在堆栈中有可能因为中断而被覆盖。三是函数的返回值如果很大,则从堆栈向存放返回值的地址单元(由主调函数在调用前将此地址压栈提供给被调函数)进行字节传送,以达到返回的目的。对于第二和第三点,《Thinking in C++》一书在第10章有比较好的阐述。四是一个显而易见的结论,如果在被调函数中返回局部变量的地址是毫无意义的;因为局部变量存于堆栈中,调用结束后堆栈将被清理,这些地址就变得无效了。

三、 C++语言中的函数参数传递机制
  C++既有C的值传递又有引用传递。在值传递上与C一致,这里着重说明引用传递。如本文前面所述,引用传递就是传递变量的地址到被调函数使用的堆栈中。在C++中声明引用传递要使用"&"符号,而调用时则不用。下面的代码是使用引用传递的swap2函数和main函数:
int& swap2(int& x, int& y) 
{
int temp;
temp = x;
x = y;
y = temp;
return x;
}

void main()
{
int a = 1, b = 2;
swap2(a, b);
}
  此时函数swap2将接受两个整型变量的地址,同时返回一个其中的一个。而从main函数中对swap2的调用swap2(a, b)则看不出是否使用引用传递,是否使用引用传递,是由swap2函数的定义决定的。以下是main函数的汇编代码:
11: void main()
12: {
……
……
13: int a = 1, b = 2;
00401088 mov dword ptr [ebp-4],1 ;变量a
0040108F mov dword ptr [ebp-8],2 ;变量b
14: swap2(a, b);
00401096 lea eax,[ebp-8] ;将b的偏移地址送入eax
00401099 push eax ;b的偏移地址压栈
0040109A lea ecx,[ebp-4] ;将a的偏移地址送入ecx

0040109D push ecx ;将a的偏移地址压栈
0040109E call @ILT+20(swap2) (00401019) ;调用swap函数
004010A3 add esp,8 ;清理堆栈中的参数
15: }
可以看出,main函数在调用swap2之前,按照从右至左的顺序将b和a的偏移地
址压栈,这就是在传递变量的地址。此时swap2函数的汇编代码是:
2: int& swap2(int& x, int& y)
3: {
00401030 push ebp
00401031 mov ebp,esp
……
……
4: int temp;
5: temp = x;
00401048 mov eax,dword ptr [ebp+8]
0040104B mov ecx,dword ptr [eax]
0040104D mov dword ptr [ebp-4],ecx
6: x = y;
00401050 mov edx,dword ptr [ebp+8]
00401053 mov eax,dword ptr [ebp+0Ch]
00401056 mov ecx,dword ptr [eax]
00401058 mov dword ptr [edx],ecx
7: y = temp;
0040105A mov edx,dword ptr [ebp+0Ch]
0040105D mov eax,dword ptr [ebp-4]
00401060 mov dword ptr [edx],eax
8: return x;
00401062 mov eax,dword ptr [ebp+8] ;返回x,由于x是外部变量的偏移地
;址,故返回是合法的
9: }
  可以看出,swap2与前面的swap函数的汇编代码是一样的。这是因为前面的swap函数接受指针变量,而指针变量的值正是地址。所以,对于这里的swap2和前面的swap来讲,堆栈中的函数参数存放的都是地址,在函数中操作的方式是一致的。但是,对swap2来说这个地址是主调函数通过将实参变量的偏移地址压栈而传递进来的--这是引用传递;而对swap来说,这个地址是主调函数通过将实参变量的值压栈而传递进来的--这是值传递,只不过由于这个实参变量是指针变量所以其值是地址而已。
  这里的关键点在于,同样是地址,一个是引用传递中的变量地址,一个是值传递中的指针变量的值。我想若能明确这一点,就不至于将C语言中的以指针变量作为函数参数的值传递情况混淆为引用传递了。
  虽然x是一个局部变量,但是由于其值是主调函数中的实参变量的地址,故在swap2中返回这个地址是合法的。
  c++ 中经常使用的是常量引用,如将swap2改为:
    Swap2(const int& x; const int& y)
  这时将不能在函数中修改引用地址所指向的内容,具体来说,x和y将不能出现在"="的左边。

四、 结束语
   本文论述了在 C 和 c++ 中函数调用的参数传递机制;同时附带说明了函数返回值的一些问题。本文示例使用的是VC++6.0。

可见值传递是传输了要传递的变量的一个副本,所以改变这个副本不会对调用函数造成影响,但是这个被调用函数一般有一个有用的返回值,也就是你用某个东西,在使用过程中,也许改变了它,但是时候后,你又保持原样给了人家。比如给你一个打好节的丝巾,你使用时换了另一种样式,照了像,还别人的时候,又按照人家的借你的样子还给人家,而这个照片就是需要得到的东西(类似返回值)。

而引用,就是将要传递的变量的地址传到了被调用函数中,如果在被调用函数中改变,那么就会在调用函数中改变。比如你借了人家布,如果你剪裁了不同的样式,那么还人家的样子就是你剪裁后的样子。一般c++可以使用值传递和引用传递,后者更多。因为这样不用另外在堆栈中开辟空间,而值传递就需要另外的开辟空间,对内存有一定的浪费。一般c中只使用值传递。

另外关于存储数据方面,一般是将局部变量,函数返回地址,函数参数放到堆栈中,而函数返回值一般放到寄存器中,为的是方便中断,如果有零时中断就可以直接从寄存器中处理,不用再进行压栈出栈操作。

/*******************************

指针做形参做局部变量以及内存分配

一级指针做形参:首先一定要明白形参和你传递参数的那个实参是两个不同的变量,即使同名也还依然不同。指针传递的是一个变量或者一个值的地址,但是它本身还是采用值传递的方式。即你不能使它指向另外一块地址,但是你可以改变它指向的空间里存的值。

二级指针做形参:二级指针也是传值,但是他指向的地址是个一维指针,所以可以改变二维指针指向的地址空间里的内容也就是要申请空间的一维指针,不能改变二维指针本身的值,即不能让他指向一个新的一维指针。所以二维指针传递的是一个一维指针。

具体看下面这个程序以及输出:

#include<stdio.h>
#include <malloc.h>
#include <string.h>

void GetMemory1(char *p)//
{
    //该函数的形参为字符指针,在函数内部修改形参并不能真正改变传入形参的实参值。
 //因此在主函数中执行完下面两句之后
 //char *str1 = NULL; 
 //GetMemory1(str1);
 //str1仍然为NULL,因此会报内存错误,没有输出结果。
    p = (char *)malloc(100);

    //要记得使用指针变量时,每次分配空间后要判断是否分配成功。而且在主函数中使用之后记得释放内存,并置空
    if (p == NULL)
    {
        printf("error memory");
    }
}
/*但是上面的函数参数变为*char *&p就可以在主函数中正常输出了。
指针做形参也是采用值传递的方式,也就是会把指针的值-地址传过去,所以可以改变这个地址里的内容,
但是你不能改变指针的值,也就是指向的地址。但是引用就是变量的别名,所以可以改变指针的值,
所以就可以在函数里申请空间*/

char *GetMemory2(void)
{
    char p[] = "hello world";
    return p;    //p[]数组为函数内部局部变量,在函数返回后,内存已经被释放了,
 //所以在主函数中调用该函数str2 = GetMemory2();输出的可能为乱码
}

void GetMemory3(char **p,int num)
{
    *p = (char *)malloc(num);

    //要记得使用指针变量时,每次分配空间后要判断是否分配成功。而且在主函数中使用之后记得释放内存,并置空
    if (*p == NULL)
    {
        printf("error memory");
    }
}
void main()
{
    int n=0;
    char *str1 = NULL;    
    char *str2 = NULL;
    char *str3 = NULL;

    //GetMemory1(str1);
 //strcpy(str1,"Hello world");
 //printf("%s\n",str1);

    str2 = GetMemory2();
    printf("%s\n",str2);//输出乱码

    GetMemory3(&str3,100);
    strcpy(str3,"hello world");
    printf("%s\n",str3);//输出hello world

    char *str4 = (char *)malloc(100);
    if (str4 == NULL)
    {
        printf("error memory");
    }
    else
    {
        strcpy(str4,"Hello");
        free(str4);
        str4 == NULL;//free后要置空,否则str可能变成“野指针”
    }
    if (str4 != NULL)
    {
        strcpy(str4,"world");
        printf("%s\n",str4);//输出world
    }
    scanf("%d",&n);
}

指针做局部变量:如果你申请了空间(用new等,赋值不算)又没有delete,那么这个空间在你程序运行结束之前不会释放,只要你知道这个空间的地址,就可以访问。这里的赋值不算是指,比如,你先定义一个数组,然后把数组名赋值指针。但是char *d = "ZET";这种形式相当于new了4个空间。

下面是中兴通讯2012校招笔试题目,问输出什么?

当时答错(狂汗),现在搞明白,在函数里写了注释:

#include <stdio.h>

//此函数中d也是个局部变量,函数执行完自动销毁,但是指针分配的空间不会被自动回收,除非程序员delete掉。
//所以这个可以正常输出。
char *a()
{
    char *d = "ZET";//这个初始化的一种形式,相当于分配了四个空间
    return d;
}

//但是第二个数组空间是系统维护的,函数执行完自动销毁 
char *b()
{
    char p[10] = {"3G平台"};
    return p;
}


//参数是值传递方式,改变形参的地址,传递的实参的地址确不会因此改变 
void c(int n,char *pName)
{
    char *a[4] = {"aaa","bbb","ccc","ddd"};
    pName = a[n];
}

void main()
{
    int n=0;
    char *pName = "DB";
    printf("%s\n",a());//输出ZET
    printf("%s\n",b());//随机输出乱码

    c(2,pName);
    printf("%s\n",pName);    //输出DB,因为char *pName = "DB";已经使得pName指向了DB,但c(2,pName);并不能改变pName指向的地址。
 //形象点说就是:我有一个箱子给你用,你可以在里面装东西,但是你不能把我的箱子换成另外一个给我。
 //在这里指的是不能函数调用不能使pName变成函数中的二维数组a。
    
    scanf("%d",&n);
}

指针做形参,指针做局部变量,数组做形参,数组做局部变量之类的容易迷糊,得不断学习...

 /***************************************************

如果函数的参数是一个指针,不要指望用该指针去申请动态内存。(详见《高质量C++编程7.4节》)
    
在C编译器原理上:编译器总是要为函数的每个参数制作临时副本,指针参数str的副本是 _str,编译器使 _str = str。如果函数体内的程序修改了_str的内容,就导致参数str的内容作相应的修改。这就是指针可以用作输出参数的原因。
即上面的函数代码经过编译后成为:
    char * fun(char *str)
    {
        char *_str;
        _str = str;
        _str = (char *) malloc(100);
    }

系统分配内存给_str指针,_str指针指向了系统分配的新地址,函数体内修改的只是_str的内容,对原str所指的地址的内容没有任何影响。因此,函数的参数是一个指针时,不要在函数体内部改变指针所指的地址,那样毫无作用,需要修改的只能是指针所指向的内容。即应当把指针当作常量。

如果非要使用函数指针来申请内存空间,那么需要使用指向指针的指针
    char * fun(char **str)
    {
        *str = (char *) malloc(100);
    }
还有另外的方案,通过函数返回值传递动态内存:
   char * fun()
    {
        char *str;
        str= (char *) malloc(100);

return str;
    }
这个倒还说得过去,因为函数返回的是一个地址的值,该地址就是申请的内存块首地址。

    此时,7.4节结束前还有一句话:不要用return语句返回指向“栈内存”的指针,因为该内存在函数结束时自动消亡。

这里区分一下静态内存,栈内存和动态分配的内存(堆内存)的区别:
(1) 从静态存储区域分配。内存在程序编译的时候就已经分配好,这块内存在程序的整个运行期间都存在。例如全局变量,static变量。
(2) 在栈上创建。在执行函数时,函数内局部变量的存储单元都可以在栈上创建,函数执行结束时这些存储单元自动被释放。栈内存分配运算内置于处理器的指令集中,效率很高,但是分配的内存容量有限。
(3) 从堆上分配,亦称动态内存分配。程序在运行的时候用malloc或new申请任意多少的内存,程序员自己负责在何时用free或delete释放内存。动态内存的生存期由我们决定,使用非常灵活,但问题也最多。

在此,辨析一下  char p[] = "abcdefg";

char *p = "abcdefg";

用来初始化字符数组的字符串常量"abcdefg",编译器会在栈中为字符数组分配空间,然后把字符串中的所有字符复制到数组中;

而用来初始化字符指针的字符串常量"abcdefg"会被编译器安排到只读数据存储区,但也是按字符数组的形式来存储的,我们可以通过一个字符指针读取字符串常量但不能修改它,否则会发生运行时错误。
 

  • 1
    点赞
  • 11
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
堆和栈是两个不同的概念 堆和栈的区别 一、预备知识—程序的内存分配 一个由c/C++编译的程序占用的内存分为以下几个部分 1、栈区(stack)— 由编译器自动分配释放 ,存放函数的参数值,局部变量的值等。其操作方式类似于数据结构的栈。 2、堆区(heap) — 一般由程序员分配释放, 若程序员不释放,程序结束时可能由OS回收 。注意它与数据结构的堆是两回事,分配方式倒是类似于链表,呵呵。 3、全局区(静态区)(static)—,全局变量和静态变量的存储是放在一块的,初始化的全局变量和静态变量在一块区域, 未初始化的全局变量和未初始化的静态变量在相邻的另一块区域。 - 程序结束后有系统释放 4、文字常量区 —常量字符串就是放在这里的。 程序结束后由系统释放 5、程序代码区—存放函数体的二进制代码。 二、例子程序 这是一个前辈写的,非常详细 //main.cpp 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); 分配得来得10和20字节的区域就在堆区。 strcpy(p1, "123456"); 123456\0放在常量区,编译器可能会将它与p3所指向的"123456"优化成一个地方。 } 二、堆和栈的理论知识 2.1申请方式 stack: 由系统自动分配。 例如,声明在函数一个局部变量 int b; 系统自动在栈为b开辟空间 heap: 需要程序员自己申请,并指明大小,在cmalloc函数 如p1 = (char *)malloc(10); 在C++用new运算符 如p2 = (char *)malloc(10); 但是注意p1、p2本身是在栈的。 2.2 申请后系统的响应 栈:只要栈的剩余空间大于所申请空间,系统将为程序提供内存,否则将报异常提示栈溢出。 堆:首先应该知道操作系统有一个记录空闲内存地址的链表,当系统收到程序的申请时, 会遍历该链表,寻找第一个空间大于所申请空间的堆结点,然后将该结点从空闲结点链表删除,并将该结点的空间分配给程序,另外,对于大多数系统,会在这块内存空间的首地址处记录本次分配的大小,这样,代码的delete语句才能正确的释放本内存空间。另外,由于找到的堆结点的大小不一定正好等于申请的大小,系统会自动的将多余的那部分重新放入空闲链表。 2.3申请大小的限制 栈:在Windows下,栈是向低地址扩展的数据结构,是一块连续的内存的区域。这句话的意思是栈顶的地址和栈的最大容量是系统预先规定好的,在 WINDOWS下,栈的大小是2M(也有的说是1M,总之是一个编译时就确定的常数),如果申请的空间超过栈的剩余空间时,将提示overflow。因此,能从栈获得的空间较小。 堆:堆是向高地址扩展的数据结构,是不连续的内存区域。这是由于系统是用链表来存储的空闲内存地址的,自然是不连续的,而链表的遍历方向是由低地址向高地址。堆的大小受限于计算机系统有效的虚拟内存。由此可见,堆获得的空间比较灵活,也比较大。 2.4申请效率的比较: 栈由系统自动分配,速度较快。但程序员是无法控制的。 堆是由new分配内存,一般速度比较慢,而且容易产生内存碎片,不过用起来最方便. 另外,在WINDOWS下,最好的方式是用VirtualAlloc分配内存,他不是在堆,也不是在栈是直接在进程的地址空间保留一快内存,虽然用起来最不方便。但是速度快,也最灵活 2.5堆和栈的存储内容 栈: 在函数调用时,第一个进栈的是主函数后的下一条指令(函数调用语句的下一条可执行语句)的地址,然后是函数的各个参数,在大多数的C编译器,参数是由右往左入栈的,然后是函数的局部变量。注意静态变量是不入栈的。 当本次函数调用结束后,局部变量先出栈,然后是参数,最后栈顶指针指向最开始存的地址,也就是主函数的下一条指令,程序由该点继续运行。 堆:一般是在堆的头部用一个字节存放堆的大小。堆的具体内容有程序员安排。 2.6存取效率的比较 char s1[] = "aaaaaaaaaaaaaaa"; char *s2 = "bbbbbbbbbbbbbbbbb"; aaaaaaaaaaa是在运行时刻赋值的; 而bbbbbbbbbbb是在编译时就确定的; 但是,在以后的存取,在栈上的数组比指针所指向的字符串(例如堆)快。 比如: #include void main() { char a = 1; char c[] = "1234567890"; char *p ="1234567890"; a = c[1]; a = p[1]; return; } 对应的汇编代码 10: a = c[1]; 00401067 8A 4D F1 mov cl,byte ptr [ebp-0Fh] 0040106A 88 4D FC mov byte ptr [ebp-4],cl 11: a = p[1]; 0040106D 8B 55 EC mov edx,dword ptr [ebp-14h] 00401070 8A 42 01 mov al,byte ptr [edx+1] 00401073 88 45 FC mov byte ptr [ebp-4],al 第一种在读取时直接就把字符串的元素读到寄存器cl,而第二种则要先把指针值读到edx,在根据edx读取字符,显然慢了。 ? 2.7小结: 堆和栈的区别可以用如下的比喻来看出: 使用栈就象我们去饭馆里吃饭,只管点菜(发出申请)、付钱、和吃(使用),吃饱了就走,不必理会切菜、洗菜等准备工作和洗碗、刷锅等扫尾工作,他的好处是快捷,但是自由度小。 使用堆就象是自己动手做喜欢吃的菜肴,比较麻烦,但是比较符合自己的口味,而且自由度大。 堆和栈的区别主要分: 操作系统方面的堆和栈,如上面说的那些,不多说了。 还有就是数据结构方面的堆和栈,这些都是不同的概念。这里的堆实际上指的就是(满足堆性质的)优先队列的一种数据结构,第1个元素有最高的优先权;栈实际上就是满足先进后出的性质的数学或数据结构。 虽然堆栈堆栈的说法是连起来叫,但是他们还是有很大区别的,连着叫只是由于历史的原因。

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值