24 June 2022

C++ 核心指南目录

F.43: Never (directly or indirectly) return a pointer or a reference to a local object

理由

悬空指针(dangling pointer)会导致程序崩溃,数据破坏。

例子

虽然我们可以把函数内部的局部变量的地址返回出去,但是在函数执行完后,局部变量的内存已经被覆盖或者清空了。

#include <gsl/gsl>
using namespace gsl;
int* f()
{
    int fx = 9;
    return &fx;  // BAD
}

void g(int* p)   // looks innocent enough
{
    int gx;
    cout << "*p == " << *p << '\n';
    *p = 999;
    cout << "gx == " << gx << '\n';
}

void h()
{
    int* p = f();
    cout << *p;
    int z = *p;  // read from abandoned stack frame (bad)
    g(p);        // pass pointer to abandoned stack frame to function (bad)
}
int main()
{
    h();
    return 0;
}

这里,我们可能会得到如下结果:

*p == 999
gx == 999

原因可能是g()重用了f()函数不再使用的堆栈空间。所有*p指向了 gx 的堆栈空间。

那么,我们再考虑下如下几种情况:

  • fxgx 不是同一类型
  • fxgx 是同一类型的变体
  • 这个悬空指针在一组函数之间传来传去
  • 这个悬空指针被黑客发现了,会用来做什么事情

幸运的是,大部分现代编译器都会对简单的情况进行警告

注意

返回值是引用的话,也会出现一样的问题

#include <gsl/gsl>
using namespace gsl;
int& f()
{
    int x = 7;
    // ...
    return x;  // Bad: returns reference to object that is about to be destroyed
}
int main()
{
    cout << f();
    return 0;
}
7

注意

静态局部变量的地址或引用返回不会出现此类问题。因为静态变量静态地分配了存储空间,指向他们的指针不会悬空。

#include <gsl/gsl>
using namespace gsl;
int& f()
{
    static int x = 7;
    return x;
}

int* g()
{
    static int x = 7;
    cout << &x << '\n';        
    return &x;
}

int main()
{
    cout << f() << '\n';
    cout << *g() << '\n';
    cout << g() << '\n';
    return 0;
}
7
0x100328004
7
0x100328004
0x100328004

例子

有些局部变量指针悬空的情况不太明显

#include <gsl/gsl>
using namespace gsl;
int* glob;       // global variables are bad in so many ways

template<class T>
void steal(T x)
{
    glob = x();  // BAD
}

void f()
{
    int i = 99;
    cout << "address of i    = " << &i << endl;
    steal([&] { return &i; });
}

int main()
{
    cout << "address of glob = " << glob << endl;
    f();
    cout << *glob << '\n';
    cout << "address of glob = " << glob << endl;
}
address of glob = 0x0
address of i    = 0x16ae6b2bc
99
address of glob = 0x16ae6b2bc

这里我们在函数f()中把局部变量的引用赋值给了 glob ,后续代码使用 glob 会产生一些不确定的结果。

注意

局部变量地址会通过返回、T&入出参数、返回对象的成员、返回数组的元素等方式泄漏出来。

注意

内层作用域的变量,也可能通过类似方式泄漏到外层作用域。

此问题的另一个变体是将指针放在一个容器内,此容器的生存周期却比指针指向的对象的生存周期长。

强化

  • 编译器捕捉返回值为指向局部对象的引用和指针。
  • 静态代码分析确认用指针表示对象位置的情况。