C++返回局部变量引用的具体细节

返回局部变量引用的情况

书上都说不能返回局部变量的引用或局部指针,说这种行为危险,但又没讲具体原因,那么今天就来看看这种行为的具体细节

PS:下面含有AT&T汇编内容,未学过汇编的朋友可以跳过直接看结论

先放一个实验用函数,即返回 int& 类型的局部变量的函数

int& RetInt(){
    int i=42;
    int& RefI=i;
    return RefI;
}

测试函数

稍后会用到的几个测试函数,用来说明不同情况下使用局部的结果

void nop(){
    int i=0,i2=12;
}
void Snop(){
    int i=3;
    int&ri=i;
}
void Skip(){
    const double&d=4.2;
}

第一次测试[正常]

主函数如下(待会使用控制变量法)
调用 nop 函数

/**
 * @brief FileName:main.cpp
 * environment : g++&Ubuntu18.04
*/
int main(){
    std::cout<<RetInt()<<std::endl;
    int& i=RetInt();
    nop();
    // Skip();
    // Snop();
    std::cout<<i<<std::endl;
    return 0;
}
$ g++ main.cpp -omain;./main #编译指令

输出结果为:
42
42

即两次使用返回的局部变量引用都表现正常

前瞻/需知

这时候回顾前瞻知识:

  1. 局部变量存放在栈区
  2. 局部变量使用后会被销毁(栈顶指针变化)
  3. 引用为变量别名,实际编译中仍会取变量地址(可见下面源程序和汇编相应部分)
int  i=42;
int& iref=i;
int* ip=&i;
movl    $42, -36(%rbp)      ;int i=42;
leaq    -36(%rbp), %rax     ;leaq 为汇编取地址操作
movq    %rax, -32(%rbp)     ;int& iref=i;//可见引用占的大小和 类型(int)大小是相同的
leaq    -36(%rbp), %rax
movq    %rax, -24(%rbp)     ;int* ip=&i;//指针为 8 个 字节

解释

  1. 引用存放地址不与变量存放地址相同
  2. 销毁对象并不会改变相应地址的值,只改变栈顶指针,所以值能很容易被重写,但目前没有数据去重写这块地址的值
  3. 连续声明两个变量的地址在之后(即没有覆盖)

汇编细节

下面看一下汇编的具体细节

main函数

C++返回局部变量引用的具体细节_第1张图片

RetInt

C++返回局部变量引用的具体细节_第2张图片
黄框部分为C++源文件中函数语句直接对应部分

nop

C++返回局部变量引用的具体细节_第3张图片
可以看到之前函数所用内存:-20(%rbp) -16(%rbp) 是没有改变的

第二次测试[接近真相]

调用 Snop 函数
C++返回局部变量引用的具体细节_第4张图片
编译指令不变
输出结果:

42
3
C++返回局部变量引用的具体细节_第5张图片

通过黄框可以明显看到调用 Snop 函数的时候定义变量和引用都是用的之前[上一个调用时]使用的内存地址,即发生了覆盖读写,main函数中的 i 的值发生变化

这时似乎可以怀疑是 int i=RetInt();时,这时的 i 已经是3了
那么修改了一下main函数
C++返回局部变量引用的具体细节_第6张图片
结果不变,和上面是一样啊
到这里就基本验证了之前的解释
只声明变量的函数未覆盖之前销毁的对象地址
而如果使用了引用则覆盖地址

第三次测试[危险结果]

调用 Skip 函数
C++返回局部变量引用的具体细节_第7张图片
编译指令不变
输出结果:

42
1074842828

Skip 函数中常量引用 d 的值改为 0.0 后,输出第二行为 0

如果第二次实验只是改变了数值,还勉强能看到值的由来的话,那这个 4.2 带来的值的变化,是着实看不懂了
其实的话,是用double类型浮点数的形式存储 4.2 然后用int类型的形式去读取相应内存单元的结果

汇编

Skip

C++返回局部变量引用的具体细节_第8张图片

RetInt

C++返回局部变量引用的具体细节_第9张图片
可以看出显示的是Skip函数定义的常量引用的高位地址的值
也许会有人质疑double类型的会影响是因为double 八位,相当于 int 与 int& 的总和
于是对 Skip 函数稍加更改
New_Skip
输出结果:

42
4

RetInt 函数的汇编没有变化

Skip

C++返回局部变量引用的具体细节_第10张图片
可以看到依然发生了对地址的覆盖改写

conclude

结论

  1. 实验结果是对书所说结果的验证。
  2. 销毁对象的意思是可以改写那一块的值。
  3. 局部变量引用的使用是有很大风险的
    1. 风险在于所使用的引用的值,可能发生意味的改变 使用者不知道或未注意到的
    2. 除非是当临时值使用 如同 std:cout< 不然没有语法检测的报错和警告,是很容易找不到问题所在的。
  4. 其实看着汇编代码来说的话 原来那个位置的值还是可以算出来的 能通过看代码最后知道那个值
    1. 不过 正经人谁做这个
  5. 简单点说就是 理解这种情况就相当于
    int retRef(){
    	int i;
    	return i;
    }
    int main(){
    	std::cout<<retRef()<<std::endl;
    }
    
  6. 补充一点就是 返回指向局部变量的指针 跟这个是差不多的

你可能感兴趣的:(c++,指针)