在C/C++中初始化数组是好的编程实践吗?

10
我最近遇到一个案例,需要比较两个文件(黄金和预期)以验证测试结果,尽管写入两个文件的数据相同,但文件不匹配。
进一步调查发现,有一个结构包含一些整数和一个64字节的字符数组,在大多数情况下未使用字符数组的所有字节,并且数组中的未使用字段包含随机数据,这导致了不匹配。
这让我想问一个问题,是否在C / C ++中初始化数组也像Java一样是良好的实践?
8个回答

25

在使用变量之前对内存/变量进行初始化是一个好的实践 - 未初始化的变量是 bug 的主要来源,往往非常难以追踪。

当将数据写入文件格式时,初始化所有数据是一个非常好的想法:它使文件内容更加清晰易读,如果有人错误地尝试“使用”未初始化的数据(请记住,可能不仅仅是你自己的代码读取数据),那么就不容易出现问题,并且使文件更易于压缩。

唯一不初始化变量的好理由是在性能关键的情况下,其中初始化在技术上是“不必要”的并会产生显着的开销。但在大多数情况下,初始化变量不会造成显着的损害(特别是如果它们只是在使用之前立即声明),但它会通过消除常见的 bug 来节省您大量的开发时间。


9
只是一些指示,都是朝着同一个方向:尽可能使用最小的范围来定义变量。在声明后直接初始化。将变量仅用于一个目的。如果您打算使变量不改变值,请使用const。这些提示的主要原因是:它们提高了可读性,并减少了在代码更改期间出现微妙错误的机会。现在的代码可能没有错误,但是遵循这些提示可以更容易地保持每次代码更改时无错误。 - Tobias Langner
1
尽可能在一个语句中定义并初始化。 - user123456
1
不要在不合适的地方初始化变量。当循环变量在for语句中初始化时,for循环更易读。你的变量可能已经在循环之前声明了。 - Patrick Schlüter

6
在数组中使用未定义的值会导致未定义的行为。因此,程序可以产生不同的结果。这可能意味着您的文件最终略有不同,或者程序崩溃,或者程序格式化您的硬盘,或者程序导致恶魔从用户的鼻子里飞出来(http://catb.org/jargon/html/N/nasal-demons.html)。
这并不意味着创建数组时需要定义数组值,但必须确保在使用任何数组值之前初始化它。当然,确保这一点的最简单方法是在创建数组时这样做。
MyStruct array[10];
printf( "%f", array[2].v ); // POTENTIAL BANG!
array[3].v = 7.0;
...
printf( "%f", array[3].v ); // THIS IS OK.

不要忘记,对于大量的POD数组,有一种简便方法可以将所有成员初始化为零。
MyPODStruct bigArray[1000] = { 0 };

+1 对于速记法,我经常使用这种风格。但需要注意的是,没有具体证据,但我相信 = {0} 格式可以像 Win32 ZeroMemory (http://msdn.microsoft.com/en-us/library/aa366920%28VS.85%29.aspx) API 一样进行优化,取决于编译器的需求和优化设置。 - dirtybird
只有在编译器可以证明不会产生可观察的差异(即,所有值随后都被设置)时,才能将其优化。在这种情况下,由于数组被写入文件中,它肯定会产生可观察的差异。 - caf

4
我强烈反对给出的观点,认为这样做是“消除常见错误来源”或“不这样做会影响程序的正确性”。如果程序使用未初始化的值,则存在错误并且不正确。初始化值并不能消除此错误,因为它们在第一次使用时通常仍然没有预期的值。但是,当它们包含随机垃圾时,程序更有可能在每次尝试时以随机方式崩溃。始终具有相同的值可能会导致更具决定性的崩溃行为,并使调试更容易。
对于您的具体问题,覆盖未使用部分是良好的安全实践,因为它们可能包含来自先前使用的内容,您不希望将其写入文件中,例如密码。

我见过很多人只是认为初始化变量就可以解决所有问题。不,这只是在你把指针初始化为NULL时有所帮助,这样就总是会发生一些事情而不是只在某些地方出现问题,然后你都快被折磨疯了还要试图追踪它。还有一些情况,你在调试时会执行相同的代码块多次,如果你有逻辑缺陷,可能会走进意外的路径,因为变量/数组等恰好出现在相同的内存位置,使用先前的值。 - dirtybird

1

有关两种不同的编程风格,即在声明变量时初始化和在必要时初始化,可以写一篇大文章。我与一个始终在声明变量时初始化的人共同完成了一个大项目,而我现在明确更倾向于第二种类型。 始终初始化变量比不初始化带来了更多微妙的错误和问题,我将尝试解释为什么,并回忆我发现的情况。 第一个例子:

struct NODE Pop(STACK * Stack)
{
  struct NODE node = EMPTY_STACK;

  if(Stack && Stack->stackPointer)
    node = Stack->node[--Stack->stackPointer];

  return node;
}

这是另一个人编写的代码。这个函数是我们应用程序中最热门的函数(你可以想象在三叉树上对500,000,000个句子进行文本索引,FIFO堆栈用于处理递归,因为我们不想使用递归函数调用)。 这是他编程风格的典型特征,因为他系统地初始化变量。那段代码的问题在于初始化时隐藏了memcpy,以及结构体的另外两个副本(顺便说一下,它们并不是对memcpy的调用,gcc有时候很奇怪),所以在项目中最热门的函数中有3个副本+一个隐藏的函数调用。 重写它为

struct NODE Pop(STACK * Stack)
{
  if(Stack && Stack->stackPointer)
    return Stack->node[--Stack->stackPointer];
  return EMPTY_STACK;
}

只有一份副本(在SPARC上运行时具有补充优势,该函数是叶子函数,由于避免了对memcpy的调用,因此不需要构建新的寄存器窗口)。 因此,该函数速度提高了4倍。

我曾经发现另一个问题,但不记得确切的位置(所以没有代码示例,抱歉)。一个变量在声明时被初始化,但在循环中使用,在有限状态自动机中使用switch。问题是初始化值不是自动机的状态之一,在极其罕见的情况下,自动机无法正常工作。通过删除初始化程序,编译器发出的警告表明变量可能在正确初始化之前就被使用。然后很容易修复自动机。 道德:防御性地初始化变量可能会抑制编译器非常有用的警告。

结论:明智地初始化您的变量。系统化地做这件事只不过是在跟随一种货物崇拜(我的同事是最糟糕的货物崇拜者,他从不使用goto,总是初始化变量,使用大量静态声明(你知道它更快(实际上在SPARC 64位上非常慢),即使函数有500行也将所有函数inline(使用__attribute__((always_inline))当编译器不想要时)


1

如果您不初始化c++数组中的值,那么这些值可能是任何值,因此如果您想要可预测的结果,最好将它们清零。

但是,如果您像使用空终止字符串一样使用char数组,则应该能够使用适当的函数将其写入文件。

尽管在c++中,使用更多面向对象的解决方案可能会更好。例如:向量、字符串等。


不,如果您访问未初始化的内存,则程序中存在错误。系统地初始化变量往往会生成不必要的写访问。 - Patrick Schlüter

1

记住,保持数组未初始化可能具有性能优势。

只有从未初始化的位置读取数组才是不好的。如果它们一直存在而从未从未初始化的位置读取,则没有问题。

此外,如果您的程序存在错误,使其从数组中未初始化的位置读取,则通过防御性地将所有数组初始化为已知值来“掩盖”它并不是解决错误的方法,只会使其在以后浮出水面。


0
首先,如果不初始化数组、变量等,将会影响程序的正确性。
其次,在这种情况下,似乎不初始化数组并没有影响原始程序的正确性。相反,程序旨在比较文件,但它不知道足够多关于文件格式,以判断文件是否存在“有意义的差异”(“有意义”的定义由第一个程序定义)。
与其抱怨原始程序不如修复比较程序,让它更了解相关的文件格式。如果文件格式没有很好地记录文档,则您有理由抱怨。

0

我认为在C++中的良好实践是使用std::vector<>而不是数组。当然,这对于C语言无效。


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