了解 C++ 中未定义行为的影响
C++ 中的未定义行为经常影响未定义行为发生后执行的代码,并可能导致不可预测的程序执行。然而,根据某些情况,未定义的行为可能会“回到过去”,从而影响有问题的行之前执行的代码。本文研究了此类行为的实际非虚构实例,展示了生产级编译器中的未定义行为如何导致意外结果。
我们将探讨代码在遇到未定义行为之前表现出异常行为的某些场景,从而对这种影响仅适用于以后的代码的观点产生怀疑。这些插图将集中于明显的后果,包括不准确或缺失的输出,让您一睹 C++ 中未定义行为的复杂性。
命令 | 描述 |
---|---|
std::exit(0) | 立即结束程序,退出状态为 0。 |
volatile | 表明该变量没有被编译器优化掉,并且可以随时更新。 |
(volatile int*)0 | 生成一个指向 volatile int 的空指针,然后用它来通过导致崩溃来进行说明。 |
a = y % z | 进行取模运算;如果 z 为零,这可能会导致无法定义的行为。 |
std::cout << | 用于将输出打印到标准的输出流。 |
#include <iostream> | 由C++标准输入输出流库组成。 |
foo3(unsigned y, unsigned z) | 函数定义中使用了两个无符号整数参数。 |
int main() | 启动程序执行的主要函数。 |
深入研究 C++ 的未定义行为
通过划分函数 foo3(unsigned y, unsigned z) 在第一个脚本中使用零,我们想说明未定义的行为。 bar() 由函数调用,该函数在立即结束程序之前打印“Bar called” std::exit(0)。下一行, a = y % z,旨在执行模数运算,如果 z 为零,会产生未定义的行为。为了模仿未定义行为的情况 foo3 影响似乎在未定义行为发生之前运行的代码的执行, std::exit(0) 被称为内 bar()。此方法显示了如果程序在到达有问题的行之前突然结束,可能会出现异常情况。
第二个脚本采用了稍微不同的策略,模拟了内部未定义的行为 bar() 方法通过使用空指针取消引用。为了触发崩溃,我们添加了这一行 (volatile int*)0 = 0 这里。这说明了为什么使用它至关重要 volatile 阻止编译器通过优化消除关键操作。再次使用 bar() 后,该函数 foo3(unsigned y, unsigned z) 尝试取模运算 a = y % z。通过致电 foo3(10, 0),主函数故意导致无法定义的行为。此示例提供了由未定义行为引起的“时间旅行”的具体示例,演示了它如何干扰程序的计划执行流程并导致程序终止或出现意外行为。
分析 C++ 中的未定义行为:实际情况
使用 Clang 编译器和 C++
#include <iostream>
void bar() {
std::cout << "Bar called" << std::endl;
std::exit(0); // This can cause undefined behaviour if not handled properly
}
int a;
void foo3(unsigned y, unsigned z) {
bar();
a = y % z; // Potential division by zero causing undefined behaviour
std::cout << "Foo3 called" << std::endl;
}
int main() {
foo3(10, 0); // Triggering the undefined behaviour
return 0;
}
C++ 中未定义行为的实际说明
在 C++ 中使用 Godbolt 编译器资源管理器
#include <iostream>
int a;
void bar() {
std::cout << "In bar()" << std::endl;
// Simulate undefined behaviour
*(volatile int*)0 = 0;
}
void foo3(unsigned y, unsigned z) {
bar();
a = y % z; // Potentially causes undefined behaviour
std::cout << "In foo3()" << std::endl;
}
int main() {
foo3(10, 0); // Triggering undefined behaviour
return 0;
}
检查未定义的行为和编译器优化
在谈论 C++ 中的未定义行为时,必须考虑编译器优化。 GCC 和 Clang 等编译器使用积极的优化技术来提高生成代码的有效性和性能。尽管这些优化是有利的,但它们可能会产生意想不到的结果,特别是当涉及未定义的行为时。例如,编译器可能会重新排列、删除或组合指令,因为它们不会以未定义的方式运行。这可能会导致奇怪的、没有意义的程序执行模式。此类优化可能会产生意想不到的后果,即导致“时间旅行”效应,其中未定义的行为似乎会影响在未定义的操作之前执行的代码。
各种编译器及其版本处理未定义行为的方式是一项令人着迷的功能。编译器的优化策略随着它们变得更加先进而改变,这导致未定义行为的出现方式有所不同。例如,对于相同的未定义操作,特定版本的 Clang 可能会以与早期或更高版本不同的方式优化一段代码,从而导致不同的可观察行为。它仔细检查编译器的内部工作原理以及使用优化的特定情况,以充分掌握这些微妙之处。因此,研究未定义的行为有助于开发更安全、更可预测的代码,也有助于理解编译器设计和优化技术的基本原理。
有关 C++ 未定义行为的常见问题
- 在C++中,什么是未定义行为?
- C++ 标准未定义的代码构造被称为“未定义行为”,这使得编译器可以自由地以他们认为合适的方式处理它们。
- 不可定义的行为可能会对程序的运行方式产生什么影响?
- 未定义的行为通常是编译器优化的结果,可能会导致崩溃、结果不准确或意外的程序行为。
- 为什么在显示不可定义的行为时打印到控制台很重要?
- 一个可见的、有形的结果可以用来说明未定义的行为如何影响程序输出,即打印到标准输出。
- 在未定义操作之前执行的代码是否会受到未定义行为的影响?
- 事实上,由于编译器优化,未定义的行为可能会导致在问题行之前运行的代码出现异常。
- 编译器所做的优化在未定义行为中起什么作用?
- 代码可以通过编译器优化来重新排列或删除,如果存在不可定义的行为,这可能会产生不可预见的影响。
- 各种编译器版本对未定义行为的处理是什么?
- 对于相同的未定义代码,不同的编译器版本可能会使用不同的优化技术,从而导致不同的行为。
- 编程错误总是会导致未定义的行为吗?
- 未定义的行为也可能是由编译器优化和代码之间复杂的交互造成的,尽管错误通常是其原因。
- 开发人员可以采取哪些步骤来减少出现不可定义行为的可能性?
- 为了减少不可定义的行为,开发人员应该遵循最佳实践,使用静态分析器等工具,并严格测试他们的代码。
- 为什么理解不明确的行为至关重要?
- 编写可靠、可预测的代码并对编译器的使用和优化做出明智的判断需要了解未定义的行为。
结束不确定行为的检查
分析 C++ 中的未定义行为说明了编译器优化会如何导致意外且令人吃惊的程序结果。这些插图显示了未定义的行为(甚至在错误的代码行之前)如何对代码的执行方式产生不可预见的影响。为了编写可靠的代码并有效利用编译器优化,理解这些微妙之处至关重要。当编译器发生变化时跟踪这些行为使开发人员能够避免麻烦并生成更可靠和一致的软件。