为什么 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,所以变量没有作为参数传递。
- 方法本身不以任何方式使用
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.
据我了解,当代码包含未初始化变量的使用时,Valgrind 应该报告错误。在下面的这个玩具示例中,printer
未初始化,但程序 "happily" 无论如何都会打印消息。
#include <iostream>
class Printer {
public:
void print() {
std::cout<<"I PRINT"<<std::endl;
}
};
int main() {
Printer* printer;
printer->print();
};
当我用 Valgrind 测试这个程序时,它没有报告任何错误。
这是预期的行为吗?如果是,为什么会这样?
该变量实际上从未使用过。
- 方法调用是内联的1,所以变量没有作为参数传递。
- 方法本身不以任何方式使用
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.