我最近遇到了一个案例,我需要比较两个文件(黄金和预期)以验证测试结果,即使写入两个文件的数据相同,文件也不匹配。
在进一步调查中,我发现有一个结构包含一些整数和一个 64 字节的 char 数组,并且在大多数情况下并非 char 数组的所有字节都被使用,并且数组中未使用的字段包含随机数据,并且导致不匹配。
这让我问了一个问题,在 C/C++ 中初始化数组是否也是一种好习惯,就像在 Java 中所做的那样?
我最近遇到了一个案例,我需要比较两个文件(黄金和预期)以验证测试结果,即使写入两个文件的数据相同,文件也不匹配。
在进一步调查中,我发现有一个结构包含一些整数和一个 64 字节的 char 数组,并且在大多数情况下并非 char 数组的所有字节都被使用,并且数组中未使用的字段包含随机数据,并且导致不匹配。
这让我问了一个问题,在 C/C++ 中初始化数组是否也是一种好习惯,就像在 Java 中所做的那样?
在使用它们之前初始化内存/变量是一种很好的做法 - 未初始化的变量是通常很难追踪的错误的重要来源。
将所有数据写入文件格式时,初始化所有数据是一个非常好的主意:它使文件内容更清晰,因此更易于使用,如果有人错误地尝试“使用”未初始化的数据,则不太容易出现问题(记住它可能不仅仅是您自己的代码,将来会读取数据),并使文件更具可压缩性。
在使用变量之前不初始化变量的唯一充分理由是在性能关键的情况下,在这种情况下,初始化在技术上是“不必要的”并且会产生大量开销。但在大多数情况下,初始化变量不会造成重大损害(特别是如果它们仅在使用前立即声明),但会通过消除常见的错误来源为您节省大量开发时间。
在数组中使用未定义的值会导致未定义的行为。因此,该程序可以自由产生不同的结果。这可能意味着您的文件最终会略有不同,或者程序崩溃,或者程序格式化您的硬盘驱动器,或者程序导致恶魔飞出用户的鼻子(http://catb.org/jargon/html/N/鼻-恶魔.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 };
我强烈不同意这样的观点,即这样做是“消除常见的错误来源”或“不这样做会破坏程序的正确性”。如果程序使用未初始化的值,那么它有一个错误并且不正确。初始化这些值并不能消除这个错误,因为它们在第一次使用时通常仍然没有预期值。但是,当它们包含随机垃圾时,程序更有可能在每次尝试时以随机方式崩溃。始终具有相同的值可能会在崩溃时提供更具确定性的行为,并使调试更容易。
对于您的特定问题,在将未使用的部分写入文件之前覆盖它们也是一种很好的安全做法,因为它们可能包含您不希望写入的以前使用的内容,例如密码。
人们可以写一篇大文章,讨论人们可能遇到的两种风格之间的区别,即在声明变量时总是初始化变量的人,以及在必要时初始化变量的人。我与属于第一类的人共享一个大项目,而我现在肯定更多属于第二类。总是初始化变量带来了比没有更微妙的错误和问题,我将尝试解释原因,记住我发现的案例。第一个例子:
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
具有有限状态自动机。初始化值的问题不是自动机的状态之一,在一些极其罕见的情况下,自动机无法正常工作。通过删除初始化程序,编译器发出的警告清楚地表明该变量可以在正确初始化之前使用。那时修理自动机很容易。
道德:防御性地初始化变量可能会抑制编译器的非常有用的警告。
结论:明智地初始化你的变量。系统地做这件事无非是追随一个cargo-cult(我的工作伙伴是可以想象的最糟糕的cargo-culter,他从不使用goto,总是初始化一个变量,使用很多静态声明(你知道它更快(它是事实上,在 SPARC 64 位上什至真的很慢),inline
即使它们有 500 行(__attribute__((always_inline))
当编译器不想要时使用),也会生成所有函数
如果您不初始化 c++ 数组中的值,那么这些值可以是任何值,因此如果您想要可预测的结果,最好将它们归零。
但是,如果您像使用空终止字符串一样使用 char 数组,那么您应该能够将其写入具有适当功能的文件。
尽管在 c++ 中使用更多的 OOP 解决方案可能会更好。IE 向量、字符串等
请记住,保持数组未初始化可能具有性能等优势。
从未初始化的数组中读取只是不好的。让它们在没有从未初始化的地方读取的情况下就可以了。
此外,如果您的程序存在从数组中未初始化的位置读取的错误,那么通过防御性地将所有数组初始化为已知值来“掩盖它”并不是解决错误的方法,只能在以后让它浮出水面。
首先,您应该初始化数组、变量等。如果不这样做会影响程序的正确性。
其次,在这种特殊情况下,不初始化数组似乎不会影响原始程序的正确性。相反,用于比较文件的程序对用于判断文件是否以有意义的方式(第一个程序定义的“有意义的”)不同的文件格式知之甚少。
我不会抱怨原始程序,而是修复比较程序以了解有关文件格式的更多信息。如果文件格式没有很好的记录,那么你就有充分的理由抱怨。
我想说 C++ 中的良好做法是使用 std::vector<> 而不是数组。当然,这对 C 无效。