函数返回内置类型以及类对象的不同

本文详细探讨了函数返回内置类型及对象的过程,包括返回值、指针和引用的不同实现方式,以及这些方式如何影响栈空间的使用。同时,还介绍了X86寄存器的基础知识及其在函数调用中的应用。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

Return

希望对此文档的阅读,可以对栈空间使用、函数调用以及返回内置类型或对象的过程有所掌握。

〇、基础知识

0.1 X86 寄存器基础

(1)ESP:栈顶指针,X86中的栈是向下增长,所以入站push时 esp--,出栈pop时,esp++

(2)EBP:函数的参数和局部变量都是存储在程序栈中,所以当一个函数想要获取它自己的参数或者局部变量时,想到的第一个方案就是使用(ESP寄存器的值+栈偏移量)推算出参数和局部变量的地址。但是栈顶指针的值会随着程序入栈和出栈操作不断变化。所以为了计算方便,可以将该值保存到另一个寄存器---EBP(extended base pointer,扩展基址寄存器)。这样获取参数可以用:EBP+偏移量,获取局部变量就可以用  EBP-偏移量了。(下图中的数据是根据第一部分的代码得到的)

(3)EBX ,基址寄存器,在内存中寻址时使用。

(4)ESI/EDI,源/目的地址寄存器,暂时不清楚有什么用

(5)ECX,(extendedcounter )计数器寄存器,和rep和loop指令搭配使用。主要用来进行循环计数

0.2汇编语言

1)call 指令,格式 :call+目标地址,作用:将程序调转到目标地址处执行。call指令使用的是相对寻址,所谓的相对寻址就是:基址+偏移量 = 最终地址。在call指令中,基址就是call指令的下一条指令的起始地址。偏移量就是call指令中后4字节的内容。call指令返回地址会在指令执行过程中被压到程序栈中。等价指令:push EIP+5 ,jmp 目标地址

2)ret指令,作用:将栈顶保存的地址弹入EIP指令寄存器,这个过程ESP要增大(因为执行了一次出栈操作)

3)rep 指令,格式 rep+其他指令,作用:重复rep后面的其他指令,重复次数记录在ECX寄存器中,每次循环ECX寄存器执行减减操作。

4)stos指令,格式 stos+目的地址,将寄存器EAX中的内容保存到目的地址处。目的地址格式 ES:[EDI] ,ES保存了段选择符,EDI保存了段偏移量。如果设置了direction flag, 那么EDI会在该指令执行后减小, 如果没有设置direction flag, 那么EDI的值会增加, 为下一次的存储做准备

5)MOV指令的功能是传送数据,例如MOV AX,[1000H],作用是将1000H作为偏移地址,寻址找到内存单元,将该内存单元中的数据送至AX。

6)LEA指令的功能是取偏移地址,例如LEA AX,[1000H],作用是将源操作数[1000H]的偏移地址1000H送至AX;

7)mov与lea 对[]的区别(针对第二个参数):1对于mov指令来说:有没有[]对于变量是无所谓的,其结果都是取值;对于寄存器而言,有[]表示取地址,没[]表示取值。2. 对于lea指令来说:有没有[]对于变量是无所谓的,其结果都是取变量的地址,相当于指针(与mov相反);对于寄存器而言,有[]表示取值,没[]表示取地址

一、返回内置类型:

现给出结论:内置类型的返回,会使用到CPU的寄存器eax进行两个栈空间内的赋值。

测试使用的部分源码如下:

int TestBuiltInReturnValue(int a, int b, intc)  //返回值

{

   return a;

}

 

int* TestBuiltInReturnPointer()//返回指针

{

   int  a=10;

   return &a;

}

 

int& TestBuiltInReturnreference()//返回引用

{

   int d =1;

   returnd;

}

 

void Do_BuiltIn()//main函数会调用此函数

{

   int a =0; 

   a =TestBuiltInReturnValue(1, 2, 3);

   int b =TestBuiltInReturnValue(1, 2, 3);

 

int *p =TestBuiltInReturnPointer();

 

   int&d = TestBuiltInReturnreference();

   inte = TestBuiltInReturnReference();

}

 

1.1返回值

为了方便之后内容的展开,现对调用a=TestBuiltInReturnValue(1,2,3);以及以上的过程进行占内存的展示。

调用的函数为

int TestBuiltInReturnValue(int a, int b, intc)  //返回值

{

   return a;

}

 

void Do_BuiltIn()//main函数会调用此函数

{

   int a =0; 

   a =TestBuiltInReturnValue(1, 2, 3);

}

由此过程可知:在进行内置数据的返回时,会把待返回的值赋值给CPU的寄存器eax。之后通过eax把该值赋值给目标变量。

   代码intb = TestBuiltInReturnValue(1, 2, 3)的汇编语言如下所示:


经查看,此过程与a= TestBuiltInReturnValue(1,2,3)一样。因此上述的结论在此处一样适用。

1.2返回指针

调用的函数为:

int* TestBuiltInReturnPointer()//返回指针

{

   int  a=10;

   return &a;

}

int *p = TestBuiltInReturnPointer()

汇编代码。如下图



此时,使用到了汇编指令LEA。LEA指令的功能是取偏移地址,例如LEA AX,[1000H],作用是将源操作数[1000H]的偏移地址1000H送至AX。理解时,可直接将[ ]去掉,等同于MOV AX,1000H。再如:LEA BX,[AX],等同于MOV BX,AX。此处是把a的地址保存到寄存器EAX中。之后,调用int *p = TestBuiltInReturnPointer();时,把TestBuiltInReturnPointer内变量a的地址返回出去了。

1.3 返回引用

         返回引用时,相关代码的汇编代码如下:


         由图六红框的内容可知,TestBuiltInReturnReference()函数返回的是函数内部变量d的地址。之后把该地址返还给调用函数的外部变量d。引用的本质是指针常量 * const

       如果不用引用取接收返回的引用数据呢?如int e= TestBuiltInReturnReference(),该语句的汇编代码如下:

 

         可知,TestBuiltInReturnReference()函数返回地址后,会通过mov eax,dword ptr[eax]从该地址读数据,并把该数据赋值给int e。

虽然对于内置类型,返回引用时由于返回的时地址(被调用函数的栈空间)。

在此时返回出的地址是可以使用的,但随着程序的执行。被调用函数的栈空间被释放(该函数的ebp被覆盖,再也无法找到)。当调用其他的函数时,该地址的值是会发生变化的。

二、返回类对象:

测试源码如下:

class Person

{

public:

   intm_nAge;

   intm_nId;

public:

 

   Person(intage,int id)

   {

      m_nAge= age;

      m_nId= id;

      cout<< "构造函数" << endl;

   }

   Person(constPerson& person)

   {

      m_nAge= person.m_nAge;

      m_nId= person.m_nId;

      cout<< "拷贝构造函数" << endl;

   }

   ~Person()

   {

      cout<< "析构函数" << endl;

   }

};

 

Person ReturnObjectValue1()

{

   Personp(10,1);

   returnp;

}

 

Person ReturnObjectValue2()

{

   returnPerson(10, 1);

}

 

Person* ReturnObjectPointer()

{

   Personp(20, 2);

   return&p;

}

 

Person& ReturnObjectReference()

{

   Personp(30, 3);

   returnp;

}

 

 

void Do_Object()

{

   //值传递

   Personvalue1(20, 2);

   value1 =ReturnObjectValue1();

   cout<< "*******************************************" << endl;

   Personvalue2 = ReturnObjectValue1();

   value1 =ReturnObjectValue2(); Person value3 =ReturnObjectValue2();

  

   //指针传递

   Person*p = ReturnObjectPointer();

 

   //引用传递 

   Person&referencre = ReturnObjectReference();

}

 

 

 

 

 

 

 

 

 

 

 

 

 

2.1 返回对象

         先给出结论:

情景如下:A函数调用函数B,并且B中返回对象(假设并非调用new 或malloc在堆中创建一个对象,即返回的对象是在栈中)。

执行到return时,会调用构造函数。生成一个匿名对象,但是此匿名对象所在的地址并非在B的栈中,而是在A函数中。匿名对象的生命周期看他之后是如何被处理的。

当赋值给一个已经存在的对象时,赋值完成则该匿名对象被析构掉;当初始化一个对象a时,则该对象a就是此匿名对象。

之后是详解:为了对这个过程有一个比较透彻的了解,现在对上述部分测试源码进行栈空间的绘制。

(绘制的栈空间所对应的源码为

Person ReturnObjectValue1()

{

   Personp(10,1);

   returnp;

}

 

void Do_Object()

{

   Personvalue1(20, 2);

   value1 =ReturnObjectValue1();//赋值给一个已经存在的对象

}

 

 

当返回的值初始化一个对象时,汇编代码如下:

调用的函数语句为Personvalue2=ReturnObjectValue1()

对比上述可知:此时并非传入一个匿名对象的地址([ebp-138h])而是直接把新对象的地址&value2传入被调用函数,因此被调用函数执行完后,并未上一个例子的两个对象的赋值过程。

2.2 返回对象指针

调用的函数为

Person* ReturnObjectPointer()

{

   Personp(20, 2);

   return&p;

}

Person *p = ReturnObjectPointer();

汇编代码如下:


此过程与返回内置类型的过程类似,是通过寄存器eax完成对象地址的赋值。

2.3 返回对象的引用

调用的函数如下

Person& ReturnObjectReference()

{

   Personp(30, 3);

   returnp;

}

 

Person& referencre = ReturnObjectReference();

对应的汇编代码为:


即:把被调用函数内部对象p的地址赋值给referencre,,但是此时p已经析构掉。因此通过分析可知:函数返回一个对象的引用的时候,最好该对象不是局部变量或临时变量。

做个小结:

1.我们都知道函数返回时,会销毁局部变量。那么到底是怎么销毁的局部变量?从汇编中稍微分析下就发现原来只是简单的用调用函数所在栈的EBP值覆盖掉被调用函数所在的栈空间的EBP值,函数定位局部变量就是用EBP寄存器作为基址,没了EBP,那么也就找不到局部变量了,换句话说也就是局部变量被销毁了。

2.函数的调用环节可以分为:(1)传参(2)保存上下文(3)向返回值空间写值(4)恢复上下文(5)从临时空间拷贝数据

其中,传参和返回数据的方式有两种:通过寄存器(由于寄存器大小和数量的限制,所以只能小型参数)和通过栈;返回值的方式也有两种寄存器和栈。

3.函数返回一个对象的引用的时候,最好该对象不是局部变量或临时变量。

4. 通常来讲,除非是迫不得已,否则最好不要采用传值的方式传递和返回对象,这是因为采用传值的方式传递和返回对象的过程中需要经历对象间的拷贝操作,这样会在一定程度上降低程序运行的效率,从而使得待处理数据量增大,增加内存的使用

 

 

 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值