未初始化的变量及未定义的行为
本节阅读量:未初始化的变量
与某些编程语言不同,C/C++不会自动将大多数变量初始化为给定的值(如零)。因此,当变量被分配的内存地址来存储数据时,该变量的默认值是该内存地址中已经存在的任何(垃圾)值!尚未给定值的变量称为「未初始化变量」。
使用未初始化变量的值可能会导致意外的结果。例如以下程序:
|
|
在这种情况下,计算机将把一些未使用的内存分配给x。然后,它将在该内存位置的值发送到std::cout,将该值解释为整数并打印。但它将打印什么值?答案是“不知道!”,并且每次运行程序时,答案可能会(也可能不会)更改。
大多数现代编译器都会尝试检测是否在未给定值的情况下使用变量。如果它们能够检测到这一点,则通常会发出编译时警告或错误。例如,在Visual Studio上编译上述程序时产生以下警告:
|
|
如果编译器不允许您编译和运行上述程序(例如,因为它将该问题视为错误),则有一种可能的解决方案可以解决此问题:
|
|
使用未初始化的变量是初学者最常见的错误之一,不幸的是,它也可能是调试问题程序时最具挑战性的错误之一(因为如果未初始化变量碰巧分配给内存中具有合理值的点,如0,则程序可能无论如何都会运行良好)。
这是“始终初始化变量”是最佳实践的主要原因。
注
许多读者希望术语“已初始化”和“未初始化”是严格相反的,但它们并不完全相同!初始化意味着在定义点为对象提供了初始值。未初始化表示对象尚未被赋予已知值(通过任何方式,包括赋值)。因此,未初始化但随后被赋值的对象不再未初始化(因为它已被赋予已知值)。
扼要重述:
- 初始化=在定义点为对象给定已知值。
- 赋值=对象被赋予超出定义点的已知值。
- 未初始化=尚未为对象给定已知值。
作为旁白…
这种缺乏初始化的情况是从C继承来的性能优化,当时计算机速度很慢。假设您要从文件中读取100000个值,可以创建100000个变量,用文件中的数据填充它们。
如果C++在创建时用默认值初始化所有变量,将导致100000次初始化(非常缓慢的),并且没有好处(因为您无论如何都要覆盖这些值)。
始终初始化变量,这样做的成本与好处相比微不足道。一旦更熟悉该语言,在某些情况下出于优化目的而省略初始化。这应该有意识地进行。
未定义的行为
使用未初始化变量的值是未定义行为的第一个例子。一些代码,C++语言并未规定执行的结果,执行后的结果称为未定义的行为(Undefined behavior,通常缩写为UB)。C++语言没有任何规则,来确定如果使用尚未给定已知值的变量的值会发生什么。因此,如果确实这样做,将导致未定义的行为。
实现未定义行为的代码会出现以下问题:
- 程序每次运行时都会产生不同的结果。
- 程序始终会产生相同的错误结果。
- 程序的行为不一致(有时产生正确的结果,有时不产生)。
- 程序似乎正在工作,但稍后在程序中产生错误的结果。
- 程序立即或稍后崩溃。
- 程序可以在某些编译器上工作,但不能在其他编译器上工作。
- 更改了一些其他看似无关的代码,程序却无法正常运行。
或者,代码实际上可能会产生正确的行为。
C++包含许多情况,如果不小心,可能会导致未定义的行为。我们将在以后的课程中指出这些,如果遇到它们。注意这些情况的位置,并避免它们。
注
未定义的行为就像一盒巧克力。永远不知道会得到什么!
规则
注意避免导致未定义行为的情况,例如使用未初始化的变量。
注
相关的常见的问题是,“你说我不能做X,但我还是做了,我的程序也工作了!为什么?”。
有两个常见答案。一是,程序实际上表现出了未定义的行为,这种未定义行为碰巧产生了想要的结果。明天(或在另一个编译器或机器上)它可能不会正常工作了。
另一是,编译器作者随意处理语言规范,当这些要求更严格时。例如,标准说,“您必须在Y之前执行X”,但编译器作者觉得这是不必要的,并使Y工作,即使您不首先执行X。这不影响正确编写的程序的操作,但可能会导致错误编写的程序仍然工作。因此,上述问题的另一个答案是,编译器根本没有遵循标准!
由实现定义的行为和未指定行为
由实现定义的行为(Implementation-defined behavior)意味着某些语法的行为由实现(编译器)定义。这样的行为必须一致并记录在案,但不同的编译器可能会产生不同的结果。
让我们看一个实现定义的行为的简单示例:
|
|
在大多数编译器上,这将输出4,但在一些编译器上,它输出2。
未指定的行为(unspecified behavior)几乎与实现定义的行为相同,行为由实现决定,但实现不需要记录该行为。
为了避免实现定义的和未指定的行为,意味着如果在不同的编译器上编译,程序可能无法按预期工作(甚至更改项目设置后,可能也无法在同一编译器上产出相同的结果!)
最佳实践
尽量避免实现定义的和未指定的行为,它们可能会导致程序在其他编译器或者平台上发生故障。
