未初始化的变量和未定义的行为

未初始化的变量

与某些编程语言不同,C/C++ 不会自动将大多数变量初始化为给定值(例如零)。因此,当给变量一个用于存储数据的内存地址时,该变量的默认值是该内存地址中已经存在的任何(垃圾)值!尚未赋予已知值(通常通过初始化或赋值)的变量称为未初始化变量

初始化意味着在定义时为对象提供了初始值。未初始化意味着对象尚未被赋予已知值(通过任何方式,包括赋值)。因此,未初始化但随后赋值的对象不再是未初始化的(因为它已被赋予已知值)。

回顾一下:

  • 初始化=对象在定义时被赋予一个已知值。
  • 赋值=给对象赋予一个超出定义点的已知值。
  • 未初始化 = 该对象尚未被赋予已知值。

这种缺乏初始化的情况是从 C 语言继承下来的性能优化,当时计算机速度还很慢。想象一下您要从文件中读取 100,000 个值的情况。在这种情况下,您可以创建 100,000 个变量,然后用文件中的数据填充它们。如果 C++ 在创建时使用默认值初始化所有这些变量,这将导致 100,000 次初始化(这会很慢),并且没有什么好处(因为无论如何你都会覆盖这些值)。

目前,您应该始终初始化变量,因为这样做的成本与收益相比微乎其微。一旦您对该语言更加熟悉,在某些情况下您可能会出于优化目的而省略初始化。但这应该始终有选择地、有意识地进行。

使用未初始化变量的值可能会导致意外结果。考虑以下短程序:

#include <iostream>

int main()
{
    // 定义一个名为 x 的整数变量
    int x; // 这个变量未初始化,因为我们没有为它赋值

    // 将 x 的值打印到屏幕上
    std::cout << x << '\n'; // 不知道会得到什么值,因为 x 未初始化

    return 0;
}

在这种情况下,计算机会将一些未使用的内存分配给x。然后它将将该内存位置中的值发送到std::cout,后者将打印该值(解释为整数)。但它会打印什么值呢?答案是“谁知道呢!”,每次运行程序时答案可能(也可能不会)改变。

大多数现代编译器将尝试检测变量是否在未给定值的情况下被使用。如果他们能够检测到这一点,他们通常会发出编译时警告或错误。例如,在 Visual Studio 上编译上述程序会产生以下警告:

c:\VCprojects\test\test.cpp(11) : warning C4700: uninitialized local variable 'x' used

如果您的编译器不允许您编译并运行上述程序(例如,因为它将问题视为错误),那么这里有一个可能的解决方案来解决此问题:

#include <iostream>

void doNothing(int&) // 暂时不用担心 & 是什么意思,我们只是用它来欺骗编译器,让它认为变量 x 被使用了
{
}

int main()
{
    // 定义一个名为 x 的整数变量
    int x; // 这个变量未初始化

    doNothing(x); // 让编译器认为我们正在为这个变量分配一个值

    // 将 x 的值打印到屏幕上(不知道会得到什么值,因为 x 未初始化)
    std::cout << x << '\n';

    return 0;
}

使用未初始化的变量是新手程序员最常犯的错误之一,不幸的是,它也可能是调试最具挑战性的错误之一(因为如果未初始化的变量碰巧被分配到某个内存点,程序无论如何都可以正常运行其中有一个合理的值,例如 0)。

这是“始终初始化变量”最佳实践的主要原因。

未定义的行为

使用未初始化变量的值是我们未定义行为的第一个示例。未定义的行为Undefined behavior通常缩写为UB)是执行 C++ 语言未明确定义其行为的代码的结果。在这种情况下,C++ 语言没有任何规则来确定如果您使用尚未给定已知值的变量的值会发生什么情况。因此,如果您确实这样做,将会导致未定义的行为。

实现未定义行为的代码可能会出现以下任何症状:

  • 您的程序每次运行时都会产生不同的结果。
  • 您的程序始终产生相同的错误结果。
  • 您的程序行为不一致(有时会产生正确的结果,有时则不会)。
  • 您的程序似乎正在运行,但稍后会在程序中产生不正确的结果。
  • 您的程序立即或稍后崩溃。
  • 您的程序可以在某些编译器上运行,但不能在其他编译器上运行。
  • 您的程序可以正常工作,直到您更改其他一些看似不相关的代码。

或者,您的代码实际上可能会产生正确的行为。

实现定义的行为和未指定的行为

实现定义的行为意味着某些语法的行为由实现(编译器)来定义。此类行为必须一致并记录在案,但不同的编译器可能会产生不同的结果。

让我们看一个实现定义行为的简单示例:

#include <iostream>

int main()
{
	std::cout << sizeof(int); // 打印一个整数所占用的字节数

	return 0;
}

在大多数编译器上,这将产生4,但在其他编译器上可能会产生2

未指定的行为与实现定义的行为几乎相同,因为该行为由实现决定,但不需要实现来记录该行为。

通常情况下,我们希望避免实现定义和未指定的行为,因为这意味着如果在不同的编译器上编译我们的程序,它可能不会按预期工作(甚至在相同的编译器上,如果更改了影响实现行为的项目设置,也可能出现问题!)

原创文章,作者:jkhxw,如若转载,请注明出处:https://www.jkhxw.com/uninitialized-variables-and-undefined-behavior/

(0)
上一篇 2023年9月30日
下一篇 2023年9月30日

相关推荐

发表回复

您的电子邮箱地址不会被公开。 必填项已用*标注