难道“总是初始化变量”会导致重要的错误被隐藏吗?


35

C ++核心准则具有ES.20规则:始终初始化一个object

避免使用预先设置的错误及其相关的未定义行为。避免理解复杂的初始化问题。简化重构。

但是,此规则无助于发现错误,只会将它们隐藏起来。
假设程序具有执行路径,在该路径中使用未初始化的变量。这是一个错误。除了不确定的行为外,这还意味着出现了问题,程序可能无法满足其产品要求。当将其部署到生产中时,可能会造成金钱损失,甚至更糟。

我们如何筛选错误?我们编写测试。但是测试不能覆盖100%的执行路径,测试也不能覆盖100%的程序输入。不仅如此,即使测试覆盖了错误的执行路径-它仍然可以通过。毕竟,这是未定义的行为,一个未初始化的变量可以具有一个有效值。

但是除了测试外,我们还有一些编译器,它们可以将0xCDCDCDCD写入未初始化的变量。这会稍微提高测试的检测率。
更好的是-有诸如Address Sanitizer之类的工具,它将捕获所有未初始化内存字节的读取。

最后是静态分析器,它可以查看程序并告诉您该执行路径上有一个先读后设置的内容。

因此,我们有许多功能强大的工具,但是如果我们初始化变量清理器,将一无所获

int bytes_read = 0;
my_read(buffer, &bytes_read); // err_t my_read(buffer_t, int*);
// bytes_read is not changed on read error.
// It's a bug of "my_read", but detection is suppressed by initialization.
buffer.shrink(bytes_read); // Uninitialized bytes_read could be detected here.

// Another bug: use empty buffer after read error.
use(buffer);

还有另一个规则-如果程序执行遇到错误,则程序应尽快死亡。无需保持活动状态,只需崩溃,编写崩溃转储,然后将其交给工程师进行调查即可。
不必要地初始化变量则相反-程序保持活动状态,否则程序将已获得分段错误。


10
尽管我认为这是一个很好的问题,但我不理解您的示例。如果发生读取错误bytes_read且未更改(因此保持为零),为什么这应该是错误?该程序仍然可以以明智的方式继续运行,只要它不会隐式地期望bytes_read!=0此后。所以很好的消毒剂不要抱怨。另一方面,如果bytes_read未事先初始化,则程序将无法以合理的方式继续运行,因此未初始化bytes_read实际上会引入事先未存在的错误。
布朗

2
@Abyx:即使它是第三方,如果它不处理以\0它开头的缓冲区也是错误的。如果有文件证明不予处理,则您的调用代码有问题。如果您bytes_read==0在调用使用之前修复了调用代码以进行检查,那么您将回到开始的地方:如果不初始化bytes_read,则代码有错误,如果这样做,则是安全的。(通常,即使发生错误,函数也应填写其参数外:并非如此。通常,输出要么保持不变,要么未定义。)
2015年

1
出于某种原因,此代码将忽略err_t返回的my_read()?如果示例中的任何地方都有错误,就是这样。
Blrfl 2015年

1
这很简单:仅在有意义的情况下初始化变量。如果不是,那就不要。我可以同意,尽管使用“虚拟”数据来做是不好的,因为它隐藏了错误。
Pieter B

1
“还有另一条规则-如果程序执行遇到错误,程序应尽快死亡。无需保持运行状态,只需崩溃,编写崩溃转储,然后交给工程师进行调查即可。”:试飞控制软件。祝您好运,从飞机残骸中恢复了事故转储。
Giorgio

Answers:


44

您的推理在以下几个方面有误:

  1. 分段错误远非必然发生。使用未初始化的变量会导致未定义的行为。分段错误是这种行为可以表现出来的一种方式,但看起来很正常。
  2. 编译器永远不会使用定义的模式(例如0xCD)填充未初始化的内存。一些调试器会执行此操作,以帮助您查找使用未初始化变量的位置。如果在调试器之外运行此类程序,则该变量将包含完全随机的垃圾。它同样可能像一个计数器bytes_read的值是10因为它具有价值0xcdcdcdcd
  3. 即使您在将未初始化的内存设置为固定模式的调试器中运行,它们也只会在启动时这样做。这意味着该机制仅对静态(并且可能是堆分配)变量可靠地起作用。对于自动变量,这些变量在堆栈上分配或仅存在于寄存器中,因此很有可能将变量存储在以前使用过的位置,因此,讲述型存储模式已被覆盖。

始终初始化变量的指导背后的想法是启用这两种情况

  1. 变量从其存在之初就包含有用的值。如果将其与仅在需要时声明变量的指导相结合,则可以避免将来的维护程序员陷入陷入在声明和首次赋值之间开始使用变量(该变量将存在但未初始化)的陷阱。

  2. 该变量包含一个定义的值,您可以稍后对其进行测试,以判断类似的函数my_read是否已更新该值。如果没有初始化,您将无法确定它bytes_read实际上是否具有有效值,因为您不知道它以什么值开头。


8
1)全部与概率有关,例如1%对99%。2和3)VC ++也会为局部变量生成此类初始化代码。3)静态(全局)变量始终使用0初始化
。– Abyx

5
@Abyx:1)以我的经验,概率是〜80%“没有立即明显的行为差异”,10%“做错事”,10%“ segfault”。至于(2)和(3):VC ++仅在调试版本中执行此操作。依靠它是一个非常糟糕的主意,因为它有选择地破坏发行版本,并且不会出现在很多测试中。
Christian Aichinger

8
我认为“指导思想”是此答案中最重要的部分。该指南绝对不会告诉您使用来跟随每个变量声明= 0;。建议的目的是在您将对其有用的值处声明该变量,然后立即分配该值。在紧随其后的规则ES21和ES22中明确指出了这一点。这三个都应该被理解为共同努力。不作为个别无关规则。
GrandOpener 2015年

1
@GrandOpener确实如此。如果在声明变量时没有有意义的值要分配,则变量的范围可能是错误的。
凯文·克鲁姆维德

5
“编译器永不满足”不是不总是这样吗?
CodesInChaos

25

您写了“此规则无助于发现错误,只是将它们隐藏了” –嗯,该规则的目标不是帮助发现错误,而是避免它们。并且,当避免了一个错误时,就不会隐藏任何东西。

让我们以您的示例来讨论这个问题:假设该my_read函数具有bytes_read在所有情况下都可以初始化的书面约定,但是在发生错误的情况下不进行初始化,因此至少在这种情况下是有错误的。您的意图是使用运行时环境通过不bytes_read首先初始化参数来显示该错误。只要您确定有适当的地址清除程序,那确实是检测此类错误的一种可能方法。要修复该错误,必须在my_read内部更改该功能。

但是存在另一种观点,至少同样有效:错误行为仅是由于未事先初始化随后调用(期望在此之后初始化)的组合而出现的。在这种情况下,经常会在现实世界的组件中发生这种情况,例如对于类似函数的书面说明不是100%清楚的,或者在出现错误的情况下甚至对行为有错误。但是,只要在调用之前将其初始化为零,该程序的行为就如同内部完成初始化一样,因此它的行为正确,在这种组合下程序中没有错误。bytes_readmy_readbytes_readmy_readbytes_readmy_read

因此,我的建议是:仅在以下情况下使用非初始化方法

  • 您要测试功能或代码块是否初始化特定参数
  • 您100%确定所涉功能具有合同,如果不给该参数赋值肯定是错误的
  • 您100%确信环境可以抓住这一点

这些是您通常可以在测试代码中为特定工具环境安排的条件。

但是,在生产代码中,最好总是事先初始化此类变量,这是一种更具防御性的方法,可以防止在合同不完整或错误,或者未激活地址清理器或类似安全措施的情况下出现错误。如程序编写正确,如果程序执行遇到错误,则应采用“早崩溃”规则。但是,如果事先初始化变量意味着没有问题,则无需停止进一步执行。


4
这正是我阅读时的想法。这不是在地毯下扫东西,而是将它们扫进垃圾箱!
corsiKa 2015年

22

始终初始化变量

您正在考虑的情况之间的差异是,没有初始化的情况导致未定义的行为,而花时间进行初始化的情况会创建一个定义明确的确定性错误。我不能强调这两种情况有多么大的不同。

考虑一个假设示例,该假设可能发生在假设模拟程序的假设雇员身上。假设该团队正在尝试进行确定性模拟,以证明他们假设销售的产品能够满足需求。

好吧,我将停止注射。我认为你说对了 ;-)

在此模拟中,有数百个未初始化的变量。一位开发人员在模拟中运行valgrind,并注意到存在多个“未初始化值分支”错误。“嗯,这可能会导致不确定性,使我们最需要的时候很难重复测试。” 开发人员去了管理层,但是管理层的时间表非常紧迫,无法节省资源来跟踪此问题。“我们最终在使用变量之前将所有变量初始化。我们有良好的编码习惯。”

在最终交付之前的几个月,当模拟处于完全搅动模式时,整个团队都在冲刺以完成管理层承诺的所有预算,就像每个资助的项目一样,预算太小。有人注意到他们无法测试基本功能,因为出于某种原因,确定性sim卡无法确定性地进行调试。

整个团队可能已经停下来,并花费了2个月的大部分时间来梳理整个仿真代码库,以解决未初始化的值错误,而不是实施和测试功能。不用说,员工跳过了“我告诉过你的事”,直接帮助其他开发人员了解什么是未初始化的值。奇怪的是,此事件发生后不久就更改了编码标准,这鼓励开发人员始终初始化其变量。

这是警告镜头。这是子弹般掠过你的鼻子。 实际问题远远超出您甚至想像的阴险。

使用未初始化的值是“未定义的行为”(除了一些特殊情况,例如char)。未定义的行为(或简称UB)是如此疯狂,对您完全有害,以至于您永远都不应该相信它比其他方法要好。有时,您可以确定自己的特定编译器定义了UB,然后安全地使用它,但是否则,未定义的行为是“编译器感觉到的任何行为”。它可能会执行您称之为“理智”的事情,例如具有未指定的值。它可能会发出无效的操作码,从而可能导致程序损坏。它可能在编译时触发警告,或者编译器甚至可能认为它完全是错误。

否则可能什么都不做

我在UB煤矿的金丝雀是我所读到的SQL引擎的案例。请原谅我未链接它,但未能再次找到该文章。当您将较大的缓冲区大小传递给函数时,但仅在特定版本的Debian上,SQL引擎中存在缓冲区溢出问题 该错误应有尽责地被记录下来,并进行了探索。有趣的部分是:检查了缓冲区溢出。有代码来处理缓冲区溢出。它看起来像这样:

// move the pointers properly to copy data into a ring buffer.
char* putIntoRingBuffer(char* begin, char* end, char* get, char*put, char* newData, unsigned int dataLength)
{
    // If dataLength is very large, we might overflow the pointer
    // arithmetic, and end up with some very small pointer number,
    // causing us to fail to realize we were trying to write past the
    // end.  Check this before we continue
    if (put + dataLength < put)
    {
        RaiseError("Buffer overflow risk detected");
        return 0;
    }
    ...
    // typical ring-buffer pointer manipulation followed...
}

我在演绎中添加了更多评论,但是想法是一样的。如果put + dataLength回绕,它将小于put指针(出于好奇,他们进行了编译时检查以确保unsigned int是指针的大小)。如果发生这种情况,我们知道标准的环形缓冲区算法可能会因这次溢出而感到困惑,因此我们返回0。 或者我们呢?

事实证明,在C ++中未定义指针溢出。因为大多数编译器都将指针视为整数,所以我们最终遇到典型的整数溢出行为,这恰好是我们想要的行为。但是,这未定义的行为,这意味着允许编译器执行所需的任何操作

发生此错误的情况下,Debian 恰好选择使用新版本的gcc,而其他主要Linux版本在其生产版本中均未将其更新。这个新版本的gcc具有更积极的死代码优化器。编译器看到了未定义的行为,并决定该if语句的结果将是“使代码最优化的一切”,这是UB的绝对合法翻译。因此,假设由于没有UB指针溢出ptr+dataLength永远不会低于ptr该值,因此该if语句将永远不会触发,并优化了缓冲区溢出检查。

实际上,“明智的” UB的使用实际上导致一种主要的SQL产品具有缓冲区溢出漏洞利用,该漏洞已经编写了避免代码!

永远不要依赖未定义的行为。曾经


要获得关于“未定义行为”的非常有趣的阅读,software.intel.com / zh-cn / blogs / 2013/01/06 /… 是一篇写得很好的文章,讲述了它可能会变得多么糟糕。但是,该特定文章是关于原子操作的,这对于大多数人来说非常令人困惑,因此我避免将其推荐为UB入门以及它可能会出错的地方。
Cort Ammon 2015年

1
我希望C具有一个内在函数,以将一个左值或它们的数组设置为未初始化的,非陷阱的不确定值或未指定的值,或将讨厌的左值变成不太讨厌的值(未陷阱的不确定或未指定),同时不保留定义的值。编译器可以使用这样的指令来帮助进行有用的优化,而程序员可以使用它们来避免编写稀疏矩阵技术之类的东西时避免编写不必要的代码,同时阻止破坏“优化”。
supercat 2015年

@supercat假设您要针对的平台是有效的解决方案,这将是一个不错的功能。已知问题的示例之一是能够创建不仅对存储类型无效的存储模式,而且无法通过普通方式实现的存储模式。 bool这是一个很好的示例,其中存在明显的问题,但是除非您以为您正在使用非常有用的平台(例如x86或ARM或MIPS)工作,否则所有这些问题都将在操作码时得到解决,因此这些问题会在其他地方显示。
Cort Ammon 2015年

考虑一种情况,在这种情况下,由于整数算术运算的大小,优化器可以证明用于a的值switch小于8,因此他们可以使用快速指令,这些指令假定不存在“大”值的风险。出现未指定的值(永远无法使用编译器的规则构造该值),这发生了意外情况,突然之间,您从跳转表的末尾跳出了一个巨大的跳转。此处允许未指定的结果意味着程序中的每个 switch语句都必须具有额外的陷阱以支持“永远不会发生”的情况。
Cort Ammon 2015年

如果内在函数是标准化的,则可能要求编译器执行尊重语义的所有必要工作;如果例如某些代码路径设置了变量而有些则没有,那么内在函数则说“如果未初始化或不确定,则转换为未指定的值;否则就别说了”,针对具有“非值”寄存器的平台的编译器必须插入代码要么在任何代码路径之前初始化变量,要么在任何代码路径上初始化变量,否则都会错过初始化,但是这样做所需的语义分析非常简单。
supercat 2015年

5

我主要使用函数式编程语言工作,不允许您重新分配变量。曾经 这完全消除了此类错误。起初这似乎是一个巨大的限制,但是它迫使您以与学习新数据的顺序一致的方式来构造代码,这往往会简化代码并使其易于维护。

这些习惯也可以结成命令式语言。几乎总是可以重构代码,以避免用虚拟值初始化变量。这些准则就是要告诉您的。他们希望您在其中添加有意义的内容,而不是仅会使自动化工具满意的内容。

您使用C风格API的示例比较棘手。在那些情况下,当我使用该函数时,我将初始化为零以防止编译器抱怨,但是在my_read单元测试中,有一次,我将初始化为其他内容以确保错误条件正常工作。您无需在每次使用时测试所有可能的错误情况。


5

不,它不会隐藏错误。相反,它以某种方式使行为具有确定性,这样,如果用户遇到错误,开发人员便可以重现该错误。


1
用-1初始化实际上是有意义的。在“ int bytes_read = 0”不好的情况下,因为您实际上可以读取0个字节,所以用-1初始化它很显然,没有读取字节的尝试已成功,您可以对此进行测试。
Pieter B

4

TL; DR:有两种方法可以使此程序正确无误,即初始化变量和祈祷。只有一个能始终如一地交付结果。


在回答您的问题之前,我需要首先解释未定义行为的含义。实际上,我将让编译器作者完成大部分工作:

如果您不愿意阅读这些文章,则TL; DR为:

未定义行为是开发人员和编译人员之间的社会契约;编译器盲目地假设其用户将永远不会依赖未定义的行为。

不幸的是,“从鼻子飞来的恶魔”的原型完全未能传达这一事实的含义。虽然本来想证明任何事情都可能发生,但它简直令人难以置信,以至于大多都耸了耸肩。

然而,事实是,未定义行为会影响编译本身,甚至早于您甚至尝试使用程序(是否在调试器中插入或未插入)并可以完全更改其行为。

我在上面的第2部分中找到了引人注目的示例:

void contains_null_check(int *P) {
  int dead = *P;
  if (P == 0)
    return;
  *P = 4;
}

转换为:

void contains_null_check(int *P) {
  *P = 4;
}

因为很明显这P是不可能的,0因为在检查之前已将其取消引用。


这如何适用于您的示例?

int bytes_read = 0;
my_read(buffer, &bytes_read); // err_t my_read(buffer_t, int*);
// bytes_read is not changed on read error.
// It's a bug of "my_read", but detection is suppressed by initialization.
buffer.shrink(bytes_read); // Uninitialized bytes_read could be detected here.

好吧,您犯了一个普遍的错误,即假定未定义行为会导致运行时错误。可能不会。

让我们想象一下,的定义my_read是:

err_t my_read(buffer_t buffer, int* bytes_read) {
    err_t result = {};
    int blocks_read = 0;
    if (!(result = low_level_read(buffer, &blocks_read))) { return result; }
    *bytes_read = blocks_read * BLOCK_SIZE;
    return result;
}

并按预期进行内联的好的编译器:

int bytes_read; // UNINITIALIZED

// start inlining my_read

err_t result = {};
int blocks_read = 0;
if (!(result = low_level_read(buffer, &blocks_read))) {
    // nothing
} else {
    bytes_read = blocks_reads * BLOCK_SIZE;
}

// end of inlining my_read

buffer.shrink(bytes_read);

然后,正如一个好的编译器所期望的,我们优化了无用的分支:

  1. 请勿未初始化使用任何变量
  2. bytes_read将用于未初始化,如果result没有0
  3. 开发人员承诺result将永远不会0

所以result永远不会0

int bytes_read; // UNINITIALIZED
err_t result = {};
int blocks_read = 0;
result = low_level_read(buffer, &blocks_read);

bytes_read = blocks_reads * BLOCK_SIZE;
buffer.shrink(bytes_read);

哦,result从来没有用过:

int bytes_read; // UNINITIALIZED
int blocks_read = 0;
low_level_read(buffer, &blocks_read);

bytes_read = blocks_reads * BLOCK_SIZE;
buffer.shrink(bytes_read);

哦,我们可以推迟声明bytes_read

int blocks_read = 0;
low_level_read(buffer, &blocks_read);

int bytes_read = blocks_reads * BLOCK_SIZE;
buffer.shrink(bytes_read);

在这里,我们对原始文件进行了严格确认的转换,并且没有调试器会捕获未初始化的变量。

我一直走这条路,了解预期的行为和装配不匹配时的问题确实很无聊。


有时,我认为编译器在执行UB路径时应使程序删除源文件。然后程序员将学习什么UB手段,其最终用户....
mattnz

1

让我们仔细看看您的示例代码:

int bytes_read = 0;
my_read(buffer, &bytes_read); // err_t my_read(buffer_t, int*);
// bytes_read is not changed on read error.
// It's a bug of "my_read", but detection is suppressed by initialization.
buffer.shrink(bytes_read); // Uninitialized bytes_read could be detected here.

// Another bug: use empty buffer after read error.
use(buffer);

这是一个很好的例子。如果我们预料到这样的错误,我们可以插入行assert(bytes_read > 0);并在运行时捕获此错误,这对于未初始化的变量是不可能的。

但是假设我们没有,我们在函数内部发现一个错误use(buffer)。我们将程序加载到调试器中,检查回溯,并从该代码中发现它已被调用。因此,我们在此代码段的顶部放置了一个断点,再次运行,并重现了该错误。我们一步一步地试图抓住它。

如果尚未初始化bytes_read,则其中包含垃圾。它不一定每次都包含相同的垃圾。我们越过界限my_read(buffer, &bytes_read);。现在,如果它的值与以前不同,那么我们可能根本无法重现我们的错误!完全出于偶然,下次在相同的输入下可能会起作用。如果始终为零,我们将获得一致的行为。

我们检查该值,甚至在同一运行中也可以回溯。如果为零,我们可以看到有问题。bytes_read成功不应为零。(或者如果可能的话,我们可能希望将其初始化为-1。)我们可能会在此处捕获该错误。bytes_read但是,如果有一个合理的值,那恰恰是错误的,我们是否一眼就能发现它?

对于指针尤其如此:NULL指针在调试器中总是很明显的,可以很容易地进行测试,并且如果我们尝试取消引用,则应该在现代硬件上出现段错误。垃圾指针可能会在以后导致无法再现的内存损坏错误,而这些错误几乎无法调试。


1

OP不依赖未定义的行为,或者至少不完全依赖。确实,依靠未定义的行为是不好的。同时,程序在意外情况下的行为也是未定义的,但是是另一种未定义的行为。如果将变量设置为零,但是您不打算使用该初始零作为执行路径,那么当您遇到错误时,程序的行为是否合理?有这样的路径?你现在在杂草丛中;您不打算使用该值,但无论如何您都在使用它。也许它将是无害的,或者可能导致程序崩溃,或者可能导致程序无提示地破坏数据。你不知道

OP的意思是,如果允许的话,有些工具可以帮助您发现此错误。如果您不初始化值,但无论如何都会使用它,那么静态和动态分析器会告诉您您有一个错误。静态分析器会在您甚至开始测试程序之前告诉您。另一方面,如果您盲目地初始化该值,则分析器无法告诉您您不打算使用该初始值,因此不会发现您的错误。如果幸运的话,它是无害的,或者只是使程序崩溃了;如果您不走运,它会无提示地破坏数据。

我唯一不同意OP的地方是在最后,他说:“否则,它将早已出现细分错误。” 实际上,未初始化的变量将不会可靠地产生分段错误。相反,我想说的是您应该使用静态分析工具,即使您试图执行该程序,也无法让您明白这一点。


0

您的问题的答案需要分解为程序内出现的不同类型的变量:


局部变量

通常,声明应该在变量首次获得其值的位置正确。不要像旧样式C那样预先声明变量:

//Bad: predeclared variables
int foo = 0;
double bar = 0.0;
long* baz = NULL;

bar = getBar();
foo = (int)bar;
baz = malloc(foo);


//Correct: declaration and initialization at the same place
double bar = getBar();
int foo = (int)bar;
long* baz = malloc(foo);

这样就消除了99%的初始化需要,变量从一开始就具有最终值。少数例外情况是初始化取决于某些条件的地方:

Base* ptr;
if(foo()) {
    ptr = new Derived1();
} else {
    ptr = new Derived2();
}

我相信写这样的案例是个好主意:

Base* ptr = nullptr;
if(foo()) {
    ptr = new Derived1();
} else {
    ptr = new Derived2();
}
assert(ptr);

即 明确声明已对变量执行了一些明智的初始化。


成员变量

在这里,我同意其他答复者所说的:这些应始终由构造函数/初始化程序列表初始化。否则,您很难确保成员之间的一致性。并且,如果您有一组似乎在所有情况下都不需要初始化的成员,请重构您的类,将这些成员添加到派生类中始终需要它们的成员。


缓冲液

这是我不同意其他答案的地方。当人们热衷于初始化变量时,他们常常最终会像这样初始化缓冲区:

char buffer[30];
memset(buffer, 0, sizeof(buffer));

char* buffer2 = calloc(30);

我认为这几乎总是有害的:这些初始化的唯一作用是使工具变得valgrind无能为力。任何从初始化的缓冲区读取的代码超出应有的值都可能是错误。但是通过初始化,该错误无法通过公开valgrind。因此,除非真正依赖于内存中充满零,否则不要使用它们(在这种情况下,请添加注释以说明需要零的含义)。

我也强烈建议您向构建系统中运行整个测试套件的目标添加一个目标,valgrind或者使用一个类似的工具来暴露初始化前使用的错误和内存泄漏。这比所有变量的预初始化更有价值。该valgrind目标应定期执行,最重要的是在任何代码公开之前执行。


全局变量

您不能具有未初始化的全局变量(至少在C / C ++等中),因此请确保该初始化是您想要的。


请注意,您可以使用三元运算符编写条件初始化,例如 Base& b = foo() ? new Derived1 : new Derived2;
Davislor 2015年

@Lorehead这可能适用于简单的情况,但不适用于更复杂的情况:如果您具有三个或更多情况,并且构造函数采用三个或更多参数,只是为了提高可读性,您就不想这样做原因。而且,这甚至没有考虑可能需要完成的任何计算,例如在循环中为初始化的一个分支搜索参数。
cmaster

对于更复杂的情况,您可以将初始化代码包装在工厂函数中:Base &b = base_factory(which);。如果您需要多次调用该代码,或者使结果保持恒定,则此功能非常有用。
戴维斯洛

@Lorehead是的,如果所需的逻辑并不简单,当然可以走。但是,我确实相信,在其中通过?:PITA 进行初始化之间存在很小的灰色区域,而工厂功能仍然过高。这些情况很少见,但确实存在。
cmaster

-2

设置了正确的编译器选项的正确的C,C ++或Objective-C编译器会在编译时告诉您是否在设置变量值之前使用了变量。由于在这些语言中,使用未初始化变量的值是未定义的行为,因此“使用前设置值”不是提示,指导或良好实践,而是100%的要求;否则,您的程序将完全损坏。在其他语言中,例如Java和Swift,编译器将永远不允许您在初始化变量之前使用它。

“初始化”和“设置值”之间存在逻辑差异。如果要查找美元和欧元之间的转换率,并输入“ double rate = 0.0;”,那么该变量会设置一个值,但尚未初始化。此处存储的0.0与正确的结果无关。在这种情况下,如果由于错误而无法存储正确的转换率,则编译器将没有机会告诉您。如果您只写了“双倍率”;并且从未存储有意义的转换率,编译器会告诉您。

因此:不要仅仅因为编译器告诉您使用变量而没有初始化就初始化变量。那是一个错误。真正的问题是您正在使用不应该使用的变量,或者在一个代码路径上没有设置值。解决问题,不要隐藏它。

不要仅仅因为编译器可能告诉您未初始化就使用变量而初始化变量。同样,您隐藏了问题。

声明变量以供使用。这样可以增加在声明时使用有意义的值对其进行初始化的机会。

避免重用变量。当您重用变量时,将其用于第二个目的时,很有可能将其初始化为无用的值。

有人评论说,有些编译器具有假阴性,并且检查初始化等同于停止问题。实际上两者都不相关。如果引用的编译器在报告此错误十年后仍找不到未初始化的变量,那么该寻找替代编译器了。Java实现了两次。一次在编译器中,一次在验证器中,没有任何问题。解决暂停问题的简单方法不是要求在使用变量之前先初始化变量,而是在使用之前先通过简单,快速的算法检查变量的方式对其进行初始化。


从表面上看,这听起来不错,但过分依赖未初始化值警告的准确性。使它们完全正确等同于停止问题,并且生产编译器可能并且确实遭受错误的否定(即,当他们应该拥有时,他们不会诊断未初始化的变量)。例如,请参阅GCC错误18501,该错误现已修复十多年了。
zwol

您所说的关于gcc的内容就是刚刚说的。其余无关紧要。
gnasher729

对于gcc感到很难过,但是如果您不了解其余的原因,那么您需要进行自我教育。
zwol
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.