如何在c++中检测对作用域外堆栈变量的引用

How to detect references to out-of-scope stack variables in C++?

本文关键字:堆栈 变量 引用 作用域 c++ 检测      更新时间:2023-10-16

Valgrind用于检测对堆上已释放对象的残留引用。然而,对于堆栈上对范围外变量的持久引用,它似乎没有这个特性。例如:

#include <iostream>
struct CharHolder {
    const char ch;
    CharHolder(char _ch) : ch(_ch) {}
};
struct Printer {
    const CharHolder& ref;
    Printer(const CharHolder& _ref) : ref(_ref) {}
    void print() {
        std::cout << &ref << ": " << ref.ch << std::endl;
    }
};
int main() {
    // g++ -O0: prints 'x'
    // g++ -O3: prints undefined character
    Printer p1(CharHolder('x'));
    p1.print();
    // g++: prints undefined character
    CharHolder* h = new CharHolder('x');
    Printer p2(*h);
    delete h;
    p2.print();
}

第一个例子是p1,在这个例子中,打印机持有一个对作用域外堆栈变量的引用,因为一旦p1的构造完成,CharHolder('x')就会被销毁。

第二个例子,在p2中,打印机持有对堆变量的引用,在p2试图在print()中引用它之前,该变量被释放。

Valgrind抱怨第二个例子:

==82331== Invalid read of size 1
==82331==    at 0x400A8E: Printer::print()
==82331==    by 0x400967: main
==82331==  Address 0x5a1c040 is 0 bytes inside a block of size 1 free'd
==82331==    at 0x4C2C2BC: operator delete(void*)
==82331==    by 0x40095F: main

如何检测第一种错误,也许使用像Valgrind这样的工具?

没有一个静态分析工具是完美的。像valgrind这样的静态分析工具在捕捉常见的编程错误方面有着很好的记录。

但是他们不能100%的抓住它们。

我试图尽可能避免产生这类编程错误的方法是防御性编程原则,其目的是通过契约证明,这类编程错误在逻辑上是不可能的。这包括如下内容:

  1. 使用智能指针代替引用和指针。你可以通过契约证明,使用智能指针会导致对超出作用域的对象的引用在逻辑上变得不可能。

  2. 使用迭代器和标准库算法,而不是经典的for (size_t i=0; i<container.size(); ++i)方法。有了明确定义的开始和结束迭代器,在逻辑上就不可能在数组的末尾运行了。另外,作为额外的奖励,如果由于某种原因,容器的选择被切换,代码将需要更少的更改。

在您的情况下,仅运行时静态分析工具几乎不可能检测到这一点。最终编译的代码绝对不包含任何在运行时正式将临时标记为超出作用域的内容。生成的代码分配一个堆栈帧,足以容纳自动作用域变量和作为参数传递的临时变量。构造函数调用完成后,不会生成显式调用来将临时对象标记为销毁。我不明白valgrind或任何其他静态分析工具是如何知道这一点的。

如果临时的类有显式析构函数,理论上泛型静态分析工具可以通过调用类实例的析构函数来知道类实例现在被销毁了。

但这告诉你没有完美的答案。即使是我提到的编程实践也不能100%避免问题;而且它们有时会引入自己的复杂性,必须加以考虑(如使用智能指针时的循环引用)。