1-6 未初始化的变量与未定义行为
未初始化的变量
与某些编程语言不同,C/C++不会自动将大多数变量初始化为特定值(如零)。当未初始化的变量被分配内存地址用于存储数据时,该变量的默认值就是该内存地址中原本存在的任意(垃圾)值!未通过初始化或赋值获得已知值的变量称为未初始化变量uninitialized variable。
术语说明
许多读者认为“初始化”与“未初始化”是严格的对立概念,但事实并非如此!在通用语境中,“初始化”指对象在定义时被赋予了初始值;“未初始化”则表示对象尚未获得已知值(通过任何方式,包括赋值操作)。因此,未初始化但随后被赋值的对象不再属于未初始化状态(因其已获得已知值)。
总结如下:
- 初始化 = 对象在定义点被赋予已知值
- 赋值 = 对象在定义点之后被赋予已知值
- 未初始化 = 对象尚未获得已知值
相关概念:请看以下变量定义:int x;在第1.4课——变量赋值与初始化中,我们提到当未提供初始化表达式时,变量将进行默认初始化。在大多数情况下(如本例),默认初始化实际上并未执行任何初始化操作。因此我们说x是未初始化的。我们关注的是结果(对象未被赋予已知值),而非过程本身。
顺带一提……:
这种不初始化的做法是继承自C语言的性能优化策略,源于计算机运行缓慢的年代。试想当你需要从文件中读取10万个值时,若先创建10万个变量再填入数据,将导致10万次初始化操作(效率低下),且实际收益甚微(毕竟这些值最终都会被覆盖)。若C++在创建时为所有变量赋默认值,将导致10万次初始化操作(效率低下),且收益甚微(毕竟这些值最终会被覆盖)。
目前你应当始终初始化变量,因为其开销相较收益微乎其微。待你更熟悉语言特性后,某些场景下可为优化目的省略初始化操作,但此类操作必须经过审慎选择且出于明确目的。
使用未初始化变量的值可能会导致意外结果。请看以下简短程序:
#include <iostream>
int main()
{
// define an integer variable named x
int x; // this variable is uninitialized because we haven't given it a value
// print the value of x to the screen
std::cout << x << '\n'; // who knows what we'll get, because x is uninitialized
return 0;
}
在这种情况下,计算机将为变量x分配一段未使用的内存。随后,它会将该内存位置中的值发送给std::cout,后者将打印该值(被解释为整数)。但它会输出什么值?答案是“谁知道呢!”,且每次运行程序时结果可能(也可能不会)改变。作者在Visual Studio中运行此程序时,std::cout一次输出7177728,下次又输出5277592。欢迎自行编译运行程序(你的电脑不会爆炸)。
警告:
当您使用调试构建配置时,某些编译器(例如 Visual Studio)会将内存内容初始化为某个预设值。使用发布构建配置时不会发生这种情况。因此,如果您想自己运行上述程序,请确保您使用的是发布构建配置(请参阅课程 0.9 - 配置编译器:构建配置以提醒如何执行此操作)。例如,如果在 Visual Studio 调试配置中运行上述程序,它将始终打印 -858993460,因为这是 Visual Studio 在调试配置中初始化内存的值(解释为整数)。
这里按照0.9的配置,将报错等级提升之后,过于严格不能通过编译,所以我自己的编辑器vim搭配异步插件使用
clang++ main.cpp -o main暂时降低等级来构建,运行,演示其效果。
大多数现代编译器会尝试检测变量是否在未赋值的情况下被使用。若能检测到此类情况,通常会在编译时发出警告或报错。例如,在Visual Studio中编译上述程序时,会产生以下警告:
c:\VCprojects\test\test.cpp(11) : warning C4700: uninitialized local variable 'x' used
如果您的编译器无法编译并运行上述程序(例如因将其视为错误),以下提供一种可能的解决方法:
#include <iostream>
void doNothing(int&) // Don't worry about what & is for now, we're just using it to trick the compiler into thinking variable x is used
{
}
int main()
{
// define an integer variable named x
int x; // this variable is uninitialized
doNothing(x); // make the compiler think we're assigning a value to this variable
// print the value of x to the screen (who knows what we'll get, because x is uninitialized)
std::cout << x << '\n';
return 0;
}


使用未初始化的变量是新手程序员最常犯的错误之一,不幸的是,这也可能是最难调试的错误之一(因为如果未初始化的变量恰好被分配到内存中已有合理值的位置,比如0,程序仍可能正常运行)。
这正是“始终初始化变量”这一最佳实践的主要原因。
未定义行为
使用未初始化变量的值是我们遇到的首个未定义行为示例。未定义行为Undefined behavior(常简写为UB)是指执行C++语言未明确定义其行为的代码所导致的结果。在此情况下,C++语言并未规定当使用未赋予已知值的变量时会发生什么。因此,若实际执行此操作,将导致未定义行为。
实现未定义行为的代码可能表现出以下任一症状:
- 程序每次运行结果不一致
- 程序始终产生相同的错误结果
- 程序行为不稳定(时而正确时而错误)
- 程序看似正常运行,但在后续阶段产生错误结果。
- 程序立即或延迟崩溃。
- 程序在某些编译器上运行正常,在其他编译器上却失败。
- 程序运行正常直至修改其他看似无关的代码。
或者,您的代码可能始终产生正确行为。
作者注:
未定义行为就像一盒巧克力。你永远不知道会得到什么!
C++中存在许多情况,若处理不当可能导致未定义行为。在后续课程中遇到这些情况时,我们将逐一指出。请务必留意这些情况的具体位置,并确保避免它们的发生。
规则
务必避免所有导致未定义行为的情况,例如使用未初始化的变量。
作者注
读者最常提出的质疑之一是:“你说不能做X操作,但我照做了程序却正常运行!为什么?”对此有两种常见解释。最普遍的解释是:你的程序实际上正表现出未定义行为,只是这种行为恰好产生了你期望的结果——至少目前如此。明天(或在其他编译器/机器上)情况可能截然不同。
另一种情况是,当语言规范要求过于严格时,编译器作者有时会自行放宽限制。例如标准规定“必须先执行X再执行Y”,但编译器作者可能认为此要求多余,从而允许在未执行X的情况下仍能运行Y。这通常不影响正确编写的程序,但可能导致错误程序意外运行。因此上述问题的另一种解释是:你的编译器可能根本未遵循标准规范!这种情况确实存在。通过确保关闭编译器扩展功能(详见第0.10课 配置编译器:编译器扩展),可避免多数此类问题。
实现定义的行为与未指定的行为
特定编译器及其配套的标准库被称为实现implementation(因为它们实际实现了C++语言)。在某些情况下,C++语言标准允许实现决定语言某些方面的行为方式,以便编译器能为特定平台选择高效的行为方案。由实现定义的行为称为实现定义的行为implementation-defined behavior。实现定义的行为必须有明确文档记录,且在特定实现中保持一致。
下面通过一个简单示例说明实现定义的行为:
#include <iostream>
int main()
{
std::cout << sizeof(int) << '\n'; // print how many bytes of memory an int value takes
return 0;
}

在大多数平台上,这将产生4,但在其他平台上可能产生2。
相关内容:
我们在第4.3课——对象大小与sizeof运算符中讨论了sizeof()。
未指定行为与实现定义行为几乎相同,即行为由实现自行定义,但实现方无需记录该行为。
我们通常应避免出现实现定义行为和未指定行为,因为这意味着程序在不同编译器上编译时可能无法按预期运行(即使在同一编译器上,若更改影响实现行为的项目设置也可能导致问题!)
最佳实践
尽可能避免实现定义行为和未指定行为,它们可能导致程序在其他实现环境中运行异常。
相关内容
我们在第6.1课——运算符优先级与结合性中展示了未指定行为的示例。
测验时间
问题 #1
什么是未初始化的变量?为什么应该避免使用它们?
显示解答
未初始化的变量是指程序尚未为其赋值的变量(通常通过初始化或赋值操作实现)。使用未初始化变量中存储的值将导致未定义行为。
问题 #2
什么是未定义行为?如果执行了导致未定义行为的操作,可能发生什么情况?
显示解答
未定义行为是指执行语言未明确定义其行为的代码所导致的结果。该结果可能呈现出几乎任何状态,包括行为正确的状态。



浙公网安备 33010602011771号