未初始化变量的危险是什么?

7
在我正在编写的程序中,我的.h文件中有几个未初始化的变量,所有这些变量在运行时初始化。然而,在Visual Studio中,每当我这样做时,它会警告我“始终初始化成员变量”,尽管这似乎毫无意义。我很清楚,在未初始化变量时尝试使用它将导致未定义的行为,但据我所知,这可以通过不这样做来避免。我是否忽略了什么?
谢谢。

5
我通常认为安全措施就像飞行员的清单一样。是的,他们知道如何驾驶飞机。但是那个清单以及强制自己确实按照它进行检查已经拯救了生命。只需初始化变量,过一段时间就会有一个变量会被忽略导致出现问题。 - sweenish
4
我有没有忽略什么?如果您初始化,您一定不会。 - Peter - Reinstate Monica
1
更具体地说:我在交通运输领域工作,SIL-4级别,我们测试程序的每一行、每一个分支。这是一项巨大的工作。通过像“始终初始化”这样简单、廉价的措施来消除一个错误源是很有价值的。证明没有路径跳过了懒惰初始化是困难的。不过你是对的,并不是所有的程序都需要正确:我的电视机顶盒大约每一两周就会冻结一次。那没关系。它并不控制飞机。 - Peter - Reinstate Monica
1
除非它们是类中的成员变量,否则我不建议在我的.h文件中使用变量。这通常是导致多重定义错误的快速方式。 - user4581301
为什么还要问这个问题?既然你知道未初始化的变量是不好的,那就把它们初始化。很简单。 - Gabriel
显示剩余3条评论
4个回答

7
如果您不初始化这些变量,并在未初始化的状态下读取它们,那么这些变量可以包含任何值,这是未定义行为。(除非它们是零初始化)
如果您忘记初始化其中一个变量,并且由于未定义行为而意外地从中读取结果与您当前系统配置下期望的值相同,则在系统更新、在不同的系统上或在更改代码时,您的程序可能会表现出不可预测/意外的行为。
这种错误很难调试。因此,即使在运行时设置它们,建议将它们初始化为已知值,以便拥有具有可预测行为的受控环境。
有一些例外情况,例如,如果您在声明变量后立即设置变量,并且无法直接设置变量,例如使用流操作符设置其值。

1
如果您不初始化这些变量,它们可能包含任何值。但是这是错误的。未定义行为意味着任何事情都可能发生。程序可能会崩溃。 - bolov
2
即使是简单的未初始化整数,它们引起的未定义行为也会导致编译器在发布版本中出现奇怪的问题。@MasonSchmidgall - HolyBlackCat
1
@MasonSchmidgall 错了。"未初始化的变量包含某些值"是一个不正确的陈述,不幸的是这种说法被教授。访问未初始化变量的程序具有未定义行为,这意味着它可以具有任何行为。 - bolov
1
或许我们在错误的页面上。未初始化是指值未设置还是空间未分配。如果是后者,那么就是100%未定义的行为。然而,如果已经分配了空间,它始终具有一些值,即使程序员没有设置。 - spicy.dll
@MasonSchmidgall,请查看我的回答,看一个明确的例子说明你的推理是错误的。 - bolov
显示剩余3条评论

1
这是一项安全措施,禁止使用未初始化的变量,这是一件好事,但如果你确定自己在做什么,并确保在使用前始终初始化变量,可以关闭此选项。右键单击解决方案资源管理器中的项目 ->属性 -> C/C++ -> SDL检查,将其标记为NO。默认情况下为YES。
请注意,这些编译时检查不仅检查未初始化的变量,因此在关闭之前建议阅读https://learn.microsoft.com/en-us/cpp/build/reference/sdl-enable-additional-security-checks?view=vs-2019
您还可以使用warning pragma在代码中禁用特定的警告。
个人而言,我会保持这些开启,因为在安全/烦恼的权衡中,我更喜欢安全,但我认为其他人可能有不同的观点。

这将关闭更多的未初始化变量警告。如果某些人非常在意,最好只禁用特定的警告代码。 - Blastfurnace
@Blastfurnace 是的,但你可以保留警告。在同一菜单中,你可以设置警告级别为-Wall,并向上设置三个其他警告级别。我认为对于有经验的用户来说,这已经足够了。个人而言,我会保持这个设置,因为我喜欢安全,而且在声明时初始化变量也不是很麻烦。 - anastaciu
“/sdl”选项所做的远不止警告未初始化变量,而您的回答完全忽略了这一点。禁用特定的警告代码比直接关闭多个编译时和运行时功能要好得多。 - Blastfurnace
@Blastfurnace,没错,我会在我的答案中注明的。 - anastaciu

1
您没有包含源代码,所以我们必须猜测为什么会发生这种情况。我能看到可能的原因,并提供不同的解决方案(除了只将所有内容初始化为零):
  1. 您没有在构造函数开始处进行初始化,而是将成员初始化与调用未完全初始化对象的某些函数的其他代码相结合。这很混乱 - 您永远不知道何时某些函数将调用另一个使用某个未初始化成员的函数。如果您确实需要此功能,请不要发送整个对象 - 而只发送您需要的部分(可能需要进行更多的重构)。
  2. 您将初始化放在 Init 函数中。只需使用最近的 C++ 功能,在一个构造函数中调用另一个即可。
  3. 您没有在构造函数中初始化某些成员,但稍后进行了初始化。如果您真的不想初始化具有包含该数据的指针(或 std::unique_ptr)的对象,并在需要时创建它;或者不在对象中包含该成员。

即使您有代码,UB也使枚举所有可能性变得困难。 - user4581301
我认为这不是未定义行为,只是触发了编译器诊断的代码。显然,如果没有看到代码,我们无法确定 - 在某些情况下,即使有了代码,分析也会变得复杂。 - Hans Olsson

0

这个问题有两个部分:第一个是读取未初始化的变量是否危险,第二个是定义未初始化的变量是否危险,即使我确保从未访问过未初始化的变量。

访问未初始化变量的危险在哪里?

除了极少数例外,访问未初始化的变量会导致整个程序出现未定义行为。有一个常见的误解(不幸的是被教授),即未初始化的变量具有“垃圾值”,因此读取未初始化的变量将导致读取某些值。这是完全错误的。未定义行为意味着程序可以具有任何行为:它可能会崩溃,它可能会表现为变量具有某个值,它可能会假装变量甚至不存在或者出现各种奇怪的行为。

例如:

void foo();
void bar();

void test(bool cond)
{
    int a; // uninitialized

    if (cond)
    {
        a = 24;
    }

    if (a == 24)
    {
        foo();
    }
    else
    {
        bar();
    }
}

调用上述函数时,使用true的结果是什么?使用false呢?

test(true)将明确调用foo()

那么test(false)呢?如果你回答:“这取决于变量a中的垃圾值是什么,如果它是24,它将调用foo,否则它将调用bar”,那么你完全错了。

如果你调用test(false),程序将访问未初始化的变量,并具有未定义的行为,这是一条非法路径,因此编译器可以自由地假设cond永远不会是false(否则程序将是非法的)。而且,令人惊讶的是,启用优化的gcc和clang实际上都这样做,并生成此汇编函数:

test(bool):
        jmp     foo()

所以不要这样做!永远不要访问未初始化的变量!这是未定义的行为,比“变量具有一些垃圾值”更糟糕。此外,在您的系统上可能按预期工作,在其他系统或使用其他编译器标志时,它可能表现出意外的方式。

如果我确保在访问之前始终初始化它们,定义未初始化的变量有什么危险?

好吧,从这个角度来看,程序是正确的,但源代码容易出错。你必须时刻注意检查是否实际上初始化了变量。如果你忘记初始化一个变量,找到 bug 将很困难,因为你的代码中有很多定义未初始化的变量。

相反,如果你总是初始化你的变量,你和后来的程序员都会轻松得多。

这只是一种非常非常好的实践。


“这是完全错误的。”其实并不是。大部分编译器在关闭优化程序时会以此方式处理代码,这样做是有充分理由的。此外,初始化变量是良好的编程习惯,可以避免人为错误(出于这个原因,应该将其设置为默认选项),但并非出于优化目的。 - Acorn
@Acorn 访问未初始化的变量是未定义行为。是的,有时它会表现得像有一个垃圾值,但你不能说未初始化的变量会得到一个垃圾值。那只是其中一种可能的行为,这就是UB的含义:它可以有任何行为,包括崩溃或像我展示的奇怪行为。你误解了我的观点:我并没有说你应该初始化变量因为优化。我举了一个例子来展示“有一个垃圾值”之外的奇怪行为,以证明UB的含义。 - bolov
实际上,UB只是意味着编译器可以决定要做什么。当关闭优化传递时,编译器将生成代码以从堆栈指针读取垃圾值。我的观点是说,说它是“完全错误的”听起来像“那永远不会发生”,而事实上,这就是编译器在关闭优化时所做的。换句话说,UB并不意味着“程序可以具有任何行为”,而是行为由编译器决定——这是一个微妙但重要的观点。 - Acorn
关于初始化,我并没有说你写了那个 -- 我只是指出未初始化的变量在优化方面有其用途。 - Acorn

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接