函数引用返回底层分析 —— 栈帧创建与销毁

引用:可以理解为给一个变量起别名,或者和该变量共享一块空间

应用:引用传参、引用返回


目录

1、函数栈帧创建与销毁

(1) 准备工作

(2) 函数栈帧创建

(3) 函数栈帧销毁

(4) 小结

2、引用返回

(1) 测试一:

(2) 测试二:

3、引用返回和传值返回的效率比较

4、引用返回的适用范围


这里着重讲引用返回,在说引用返回之前,先说说函数栈帧的创建与销毁

函数栈帧创建时,会忽略寄存器ebx、esi的压栈

1、函数栈帧创建与销毁

(1) 准备工作

我们先准备一段简单的代码

#include <stdio.h>

int Add(int x,int y)
{
    int z = x + y;
    return z;
}

int main(){
    int a = 10;
    int b = 20;
    int ret = Add(a,b);
    return 0;
}

(2) 函数栈帧创建

=============== 调用Add函数之前的准备工作 ===============

程序开始运行的时候,OS为在栈上为 main函数开辟一块空间,然后在栈上存放局部变量

到了开始执行Add函数的时候,从右向左读取实参,将实参的拷贝放入寄存器 push 到栈顶

下面要跳转到Add函数,需要先记下 下一条指令的地址0CC2145,也就是 '' return 0;'' 这句代码的地址。即便离家,也得记住家的地址,下次回来知道要回哪

至此,进入Add函数的准备工作已经完成

=============== 执行Add函数 ===============

接下来OS会在栈上为Add函数开辟一块空间,并压到栈顶(栈的大小取决于函数中有多少变量)

到了Add函数中,int z = x+y,声明了一个变量z,所以要在Add的栈上开辟空间存放 z

由于要用到参数x 、y,程序会到ebp+8的位置,即 ecx 寄存器,去取值

放入ebx寄存器中,然后去ebp+12的位置,即eax寄存器,加到ebx中====> ebx += eax

最关键的部分来了!!下一步是区别 引用返回和传值返回 的关键

(3) 函数栈帧销毁

现在 z 有了,那要如何把 z 返回呢? mov 是汇编指令,代表赋值,其实就是把 z 的值赋值给进寄存器 eax,然后开始销毁空间返还给内存,这里的销毁并不是真正的销毁,只是移动 esp 指针,让这块区域不属于当前进程,下一次其他函数调用的时候,直接覆盖这块区域创建函数栈帧。

但是数据是否会被清除,取决于编译器,VS编译器不会清除数据,栈上存放的只是地址,数据放在常量区

注意观察 esp 的指向位置

根据记录下的地址,汇编指令 ret 让程序回到了Add函数的下一个位置,即return 0 语句所在位置

此时还需要继续弹栈,弹栈完毕以后,在main的栈上为ret开辟一块空间,把前面 eax 寄存器中存放的结果赋值给ret

(4) 小结

总的来说就是,Add函数将结果存放在 eax 寄存器中,回来的时候再赋值给ret

 但是并非一直如此,

如果传递的值比较小,仅占4个字节的话,是先存入寄存器的,然后再赋值给ret

如果传递的值比较大,大于4个字节时,是存入一个临时变量,这个临时变量在调用者的栈上开辟,也就是说main函数调用Add函数,Add函数要返回一个值给main,这个临时变量就在main函数上被创建

2、引用返回

(1) 测试一:

#include <stdio.h>

int& Add(int x, int y)
{
    int z = x + y;
    return z;
}

int main() {
    int a = 10;
    int b = 20;
    int& ret = Add(a, b);
    printf("ret的值:%d\n", ret);
    return 0;
}

测试结果如下:

Add函数返回的是z的引用,但是在出栈以后,z就被销毁了,这里的销毁只是说 刚才Add函数创建的栈区不属于当前进程了,但是空间还是一直在那的

(2) 测试二:

#include <stdio.h>

int& Add(int x, int y)
{
    int z = x + y;
    return z;
}

int main() {
    int a = 10;
    int b = 20;
    int& ret = Add(a, b);
    printf("ret的值:%d\n", ret);
    Add(100, 200);
    printf("ret的值:%d\n", ret);
    return 0;
}

我们再执行一次Add,但是这次不赋值给 ret

 我们会发现 ret 被暗改了:

  • 第一次执行Add函数,返回了 z 的引用
  • 第二次执行Add函数,依然返回 z 的引用,但由于 ret 是z的别名,与 z 共享一块空间,第二次执行时,改变z的同时,也改变了 ret

所以,要尽量避免返回局部变量的引用!!

3、引用返回和传值返回的效率比较

#include <stdio.h>
#include <time.h>
#include <iostream>
using std::cout;
using std::endl;

struct A { int a[10000]; };
A a;

A Test1()
{
    return a;
}

A& Test2()
{
    return a;
}

int main() {

    //传值返回
    size_t begin1 = clock(); 
    for (size_t i = 0; i < 100000; ++i) 
        Test1();
    size_t end1 = clock();

    //传引用返回
    size_t begin2 = clock();
    for (size_t i = 0; i < 100000; ++i)
        Test2();
    size_t end2 = clock();

    cout << "传值返回耗费时间: " << end1 - begin1 << endl;
    cout << "传引用返回耗费时间: " << end2 - begin2 << endl;
    return 0;
}

==》数据所占字节较小的时候,传值和传引用无太大差异

==》数据所占字节较大的时候,传引用更佳

引用可以减少拷贝,无需把值拷贝给寄存器或者临时变量,一定程度上提升了运行速度

4、引用返回的适用范围

函数返回时,出了函数作用域,如果返回对象还在,没有还给系统(静态变量、全局变量),可以使用引用返回

函数返回时,出了函数作用域,返回对象已经还给系统了(局部变量),这个时候必须使用传值返回

===》尽量避免 返回局部变量的引用

猜你喜欢

转载自blog.csdn.net/challenglistic/article/details/123537888#comments_28086206