{0}和calloc之间的区别是什么?

3
正如另一个问题的答案所涵盖的那样,使用聚合初始化。
struct foo {
    size_t a;
    size_t b;
};

struct foo bar = {0};

结果是内置类型被初始化为零。

使用上述方法和使用

struct foo * bar2 = calloc(1, sizeof(struct foo));

暂且不考虑一个变量是指针的事实。
通过调试器,我们可以看到上述两个示例中,ab都被设置为零。

上述两个示例有什么区别?是否存在任何陷阱或隐藏问题?


现在我很好奇 - 如果我们用alloca替换calloc,然后接着用memset清零,会有什么显著的不同吗? - MooseBoys
alloca 是非标准的。此外,alloca 在栈上分配内存,而不是在堆上,并且它不报告错误(分配比可用内存更多的内存具有未定义行为,并且很可能会导致程序在检测到错误之前崩溃)。 - Keith Thompson
“它不报告错误” - 调用函数也可能会导致堆栈溢出,同样不会报告错误。标准中的一个技术缺陷是它没有提供关于堆栈深度的任何保证,因此几乎没有C程序是严格符合标准的。 - Jim Balter
1
如果你只是在寻找初始化类型的答案,我建议你将问题更改为“{0}与memset之间的区别”,然后执行 struct foo bar2; memset(&bar2, 0, sizeof bar2); - M.M
1
那么会有任何显著的差异吗?-- 两者都将所有位设置为零,这在理论上与将它们设置为零值是不同的(请参见 Deduplicator 和 Keith Thompson 的答案)。 - Jim Balter
5个回答

4

是的,除了您的struct foo对象的存储类之外,还有一个关键的区别:

struct foo bar = {0};
struct foo * bar2 = calloc(1, sizeof *bar2);

每个 bar 的成员都进行了零初始化(对于没有初始化器的子对象,或者如果 barstaticthread_local 存储类,则填充也会被清零), 而所有的 *bar2 都被清零,这可能会产生完全不同的结果:
空指针 (T*)0 和具有值 0 的浮点数不能保证是全零比特。事实上,只有在 C99 后的某个时候,对于 charunsigned charsigned char(以及来自 <stdint.h> 的一些可选的精确大小类型),才保证所有比特为零与值 0 匹配。稍后的技术勘误为所有整数类型保证了这一点。
浮点格式可能不是 IEEE754。
(在大多数现代系统上,您可以忽略这种可能性。)
引用自 c-faq(感谢 Jim Balter 提供链接):

Prime 50 系列至少对于 PL/I 使用 段 07777,偏移量为 0 的空指针


1
对于 struct foo bar = {0};,我不认为有任何保证填充是零。它可能会是零,因为这样实现起来更容易 - 并且在由两个 size_t 成员组成的结构中几乎肯定不会有任何填充。 - Keith Thompson
1
在大多数现代系统上,你可以忽略那种可能性。尽管这样的机器曾经存在过,而且可能仍然存在:http://c-faq.com/null/machexamp.html - Jim Balter
1
@Billy:我最初的帖子已经说过空指针和浮点0不必全部为全0位。但是,我已经完全重新表述了这个问题,所以我怀疑他实际上在引用我的第一版本 ;-). - Deduplicator
2
所以,为了确认我是否正确理解您的意思,{0}将结构成员设置为零值的适当位表示,而calloc则将所有位设置为零,对吗? - Etheryte
感谢您的回答,非常感激。 - Etheryte
显示剩余7条评论

3
struct foo bar = {0};

这段内容描述了定义一个名为barstruct foo类型对象,并将其初始化为零。
“零”的定义是递归的。所有整数子对象都被初始化为0,所有浮点数子对象都被初始化为0.0,所有指针都被初始化为NULL
struct foo * bar2 = calloc(1, sizeof(struct foo));

我认为这可以更好地(但等效地)写成:

struct foo *bar2 = calloc(1, sizeof *bar2);

通过不重复类型名称,我们避免了在以后修改代码时出现不匹配的风险。

这会动态分配一个struct foo类型对象(在堆上),将该对象初始化为全零位,并将bar2初始化为指向它的指针。

calloc可能无法分配内存。如果出现这种情况,它将返回空指针。你应该始终检查它。(bar的声明也分配了内存,但如果失败,将导致栈溢出,没有好的处理方法。)

全零位并不保证与“零”相同。对于整数类型(包括size_t),几乎可以保证是一样的。对于浮点数和指针类型,0.0NULL完全可以有一些内部表示不同于全零位。你不太可能遇到这个问题,而且由于结构体的所有成员都是整数,所以你可能不需要担心它。


谢谢你的回答,还有感谢你关于分配语法的提示。你能否详细说明在什么情况下你最后一段所提到的会成为一个问题?问题中展示的结构体只是一个随机的例子。 - Etheryte
4
在实践中,你使用的任何系统都不太可能出现问题。我认为我从未见过除了使用全零位作为空指针或浮点数0.0以外的其他C语言实现。另一方面,作为一种风格,我喜欢编写尽可能具有可移植性的代码(但不是更多)。通常情况下,更具可移植性的代码更简单,并且可以减少你的担忧。 - Keith Thompson
没有现有的实现会执行那种奇怪的操作。除了语言律师之外,可能没有其他人需要担心它。 (另一方面,指针和浮点运算符...) - Keith Thompson
区别在于栈上分配和堆上分配。其余的都是无关紧要的废话。 - littleadv
1
@calloc与@memset一样,将初始化所有位为0,包括任何填充。但是,“当一个值被存储在结构体或联合类型的对象中,包括成员对象时,与任何填充字节对应的对象表示的字节采用未指定的值。”-- C11 6.2.6.1第6段。通常填充字节并不重要,因此语言不会特别保证它们的稳定性。把它们看作是脆弱的。 - Keith Thompson
显示剩余3条评论

3

calloc函数可以为你在堆上动态分配一个零初始化的内存区域(给你的bar2)。但是自动变量(比如bar,假设它的声明在函数内部)是分配在调用栈上的。另请参见calloc(3)

在C语言中,需要显式地free堆分配的内存区域。但是栈分配的数据在其函数返回时会被弹出。

请参阅C动态内存分配垃圾回收的维基页面。引用计数是C和C++中广泛使用的技术,可以看作是一种GC形式。考虑循环引用,它们很难处理。 Boehm保守GC可用于C程序。
请注意,内存区域的活跃性是一个全局的程序属性。通常您不能声明某个区域属于特定函数(或库)。但是您可以采用约定。
当您编写返回堆分配指针(即指向动态存储的指针)的函数时,应记录该事实并决定谁负责释放它。
关于初始化:使用 calloc 分配的指针会被清零(当 calloc 成功时)。使用{0}初始化的自动变量也会被清零。在实践中,一些实现可能会以不同方式 calloc 大对象(例如通过向内核请求整个清零页面,例如使用mmap(2))和小对象(通过重用先前的 free 区域并将其清零,如果可用)。清零区域是使用memset(3)的快速等效方法。 PS. 我忽略了奇怪的机器,所有零位内存区域不是C标准的清除数据,即像{0}。我在实践中不知道这样的机器,即使我知道它们原则上是可能的(在理论上, NULL 指针可能不是全零字) 顺便说一句,编译器可能会优化全零局部结构(并且可能根本不在堆栈上分配它,因为它适合寄存器)。

1
如果您正在查看具有动态存储期的标准动态分配对象,则“在调用堆栈上”等同于具有自动存储期的对象。 - Billy ONeal
感谢您的回答,这个问题不是关于内存管理的一般性问题,而是关于两种初始化方法的功能差异。 - Etheryte

1

(这个回答主要涉及结构体中只包含整数类型时初始化的差异)

这两种形式都将ab设置为0。这是因为标准定义整数类型的全零位必须表示0的值。

如果存在结构填充,那么calloc版本会设置它,但零初始化可能不会。例如:

struct foo a = { 0 }, b = { 0 };
struct foo c, d; memset(&c, 0, sizeof c); memset(&d, 0, sizeof d);

if ( memcmp(&a, &b, sizeof a) )
    printf("This line may appear.\n");

if ( memcmp(&c, &d, sizeof c) )
    printf("This line must not appear.\n");

有时您会看到一种技术(特别是在设计用于存储空间较小的系统的代码中),即使用memcmp来比较两个结构体是否相等。当结构体成员之间存在填充时,这种方法是不可靠的,因为即使结构体成员相同,填充也可能不同。
程序员不想逐个比较结构体成员,因为这会增加代码量,所以他会使用memcpy复制结构体,使用memset初始化结构体,以保留使用memcmp检查相等性的能力。
在现代编程中,我强烈建议不要这样做;并始终使用{ 0 }的初始化形式。后者的另一个好处是没有机会出错,因为大小参数不会被错误地设置为太多或太少的内存。

0

有一个严重的区别:自动变量的分配是在编译时完成的,并且是免费的(当堆栈帧被保留时,空间就在那里)。相反,动态分配是在运行时完成的,具有不可预测和不可忽略的成本。

关于初始化,编译器可以通过自动变量进行优化(例如,如果不必要,则不清除);这在调用calloc时是不可能的。

如果您喜欢calloc样式,还可以选择对自动变量执行memset。

memset(&bar, 0, sizeof bar);

更新:自动变量的分配在编译时几乎完成。


1
-1 自动变量的分配和初始化都不会在编译时发生...怎么可能呢?“使用calloc调用是不可能的”——是的,因为calloc是一个标准函数,其行为由标准规定,因此编译器可以依赖它。 - Jim Balter
我的负评是真诚和恰当的,我对它的解释也是有效的。你不欢迎它是无关紧要的,除了违背 SO 的精神之外。"如果严格来说编译器不分配内存空间"——事实上编译器不分配内存空间,它只生成分配该空间的代码。初始化也是如此。称其为"严格意义上"是在暗示说任何事情发生在运行时都是"严格意义上"的,因为编译器在编译时生成了代码。 - Jim Balter

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