为什么 Valgrind 不检测未初始化变量的使用?

Why doesn't Valgrind detect usage of uninitialized variable?

据我了解,当代码包含未初始化变量的使用时,Valgrind 应该报告错误。在下面的这个玩具示例中,printer 未初始化,但程序 "happily" 无论如何都会打印消息。

#include <iostream>

class Printer {
    public:
        void print() {
            std::cout<<"I PRINT"<<std::endl;
        }
};


int main() {
    Printer* printer;
    printer->print();
};

当我用 Valgrind 测试这个程序时,它没有报告任何错误。

这是预期的行为吗?如果是,为什么会这样?

该变量实际上从未使用过。

  1. 方法调用是内联的1,所以变量没有作为参数传递。
  2. 方法本身不以任何方式使用 this,因此根本不使用变量。

以上与打开或关闭优化无关。

事实上,在优化代码中,变量根本不存在——即使是内存分配也不存在。

关于类似案例的问题:Extern variable only in header unexpectedly working,为什么? .


1 class 主体中定义的所有方法默认内联。

这是未定义的行为吗?

是的。调用该方法需要this指向一个实际的、初始化的对象实例,以便格式正确。正如 Nir ​​Friedman 指出的那样,编译器可以自由假设并在此基础上进行优化(IIRC 这种优化甚至可以在 -O0 下发生!)。

我个人希望所讨论的特定代码能够在任何实际条件下工作(因为指针值真的无关紧要),但我永远不会依赖它。 您应该立即修复您的代码。

检测

要检测 Clang/GCC 中未初始化变量的使用情况,请使用选项 -Wuninitialized(或简单地使用 -Wall,其中包括此标志)。

-Wuninitialized 应该主要涵盖堆栈分配内存的使用,尽管我猜堆栈分配数组的一些使用可能仍然会丢失。一些编译器可能支持使用 -fsanitize=... 选项对未初始化的读取进行额外的运行时检查,例如 -fsanitize=memory in Clang (thx, chtz)。这些检查应涵盖边缘情况以及堆分配内存的使用。

main() 函数具有未定义的行为,因为 printer 未初始化并且语句 printer->print() 都访问 printer 的值并通过 [=14= 取消引用它]和成员函数的调用。

然而,实际上,允许编译器通过简单地假设它不存在来处理未定义的行为。然后,如果编译器愿意,它可以遵循逻辑链;

  • 当它看到像 printer->print() 这样的语句时,这意味着它可以推断 printer 有一个可以访问和取消引用的值,而不会引入未定义的行为。
  • 基于此推理,可以假设 printer 必须已初始化(通过某种方式对编译器不可见)以指向有效对象。
  • 基于这个假设,可以推断语句 printer->print() 将导致 Printer::print().
  • 的调用
  • 因为编译器可以看到Printer::print()的定义,所以可以简单的内联它,然后执行语句std::cout<<"I PRINT"<<std::endl.
  • 由于它根本不需要访问 printer 来生成该输出,因此它可以优化掉对 main().[=42 中名为 printer 的变量的任何引用=]

如果编译器遵循上述逻辑顺序,程序将简单地打印 I PRINT 并退出,而不会以可能触发 Valgrind 报告的方式访问任何内存。

如果您认为以上内容听起来很牵强,那您就错了。 LLVM/Clang 是一个理论上遵循与我所描述的非常相似的逻辑链的编译器。有关详细信息,请查看 LLVM 项目博客 link to first article, second article,以及 third article.