【问题标题】:When zeroing a struct such as sockaddr_in, sockaddr_in6 and addrinfo before use, which is correct: memset, an initializer or either?在使用前将 sockaddr_in、sockaddr_in6 和 addrinfo 等结构清零时,正确的是:memset、初始化程序还是其中之一?
【发布时间】:2010-10-28 00:22:18
【问题描述】:

每当我查看书籍、手册页和网站中的真实代码或示例套接字代码时,我几乎总是会看到以下内容:

struct sockaddr_in foo;
memset(&foo, 0, sizeof foo); 
/* or bzero(), which POSIX marks as LEGACY, and is not in standard C */
foo.sin_port = htons(42);

代替:

struct sockaddr_in foo = { 0 }; 
/* if at least one member is initialized, all others are set to
   zero (as though they had static storage duration) as per 
   ISO/IEC 9899:1999 6.7.8 Initialization */ 
foo.sin_port = htons(42);

或:

struct sockaddr_in foo = { .sin_port = htons(42) }; /* New in C99 */

或:

static struct sockaddr_in foo; 
/* static storage duration will also behave as if 
   all members are explicitly assigned 0 */
foo.sin_port = htons(42);

例如,在将 struct addrinfo 提示传递给 getaddrinfo 之前将其设置为零也是如此。

这是为什么?据我了解,不使用 memset 的示例可能与使用 memset 的示例相同,如果不是更好的话。我意识到存在差异:

  • memset 会将所有位设置为零,这不一定是将每个成员设置为 0 的正确位表示。
  • memset 还将填充位设置为零。

将这些结构设置为零时,这些差异是否相关或必需的行为,因此使用初始化程序是错误的?如果是这样,为什么,以及哪个标准或其他来源验证了这一点?

如果两者都是正确的,为什么 memset/bzero 倾向于出现而不是初始值设定项?只是风格问题吗?如果是这样,那很好,我认为我们不需要主观回答哪种风格更好。

通常的做法是使用初始化器而不是 memset,这正是因为通常不需要所有位为零,而是我们希望为类型正确表示零。这些与套接字相关的结构是否相反?

在我的研究中,我发现 POSIX 似乎只要求 sockaddr_in6(而不是 sockaddr_in)在 http://www.opengroup.org/onlinepubs/000095399/basedefs/netinet/in.h.html 处归零,但没有提到它应该如何归零(memset 或初始化程序?)。我意识到 BSD 套接字早于 POSIX 并且它不是唯一的标准,那么它们对遗留系统或现代非 POSIX 系统的兼容性考虑是什么?

就个人而言,我更喜欢从风格(也许是良好实践)的角度使用初始化程序并完全避免 memset,但我不愿意,因为:

  • 其他源代码和半规范文本(如 UNIX Network Programming)使用 bzero(例如,第 2 版的第 101 页和第 3 版的第 124 页。(我拥有两者)。
  • 我很清楚它们并不相同,原因如上所述。

【问题讨论】:

    标签: c sockets memset


    【解决方案1】:

    部分初始化方法(即“{ 0 }”)的一个问题是 GCC 会警告您初始化程序不完整(如果警告级别足够高;我通常使用“-Wall”并且经常使用“ -Wextra')。使用指定的初始化方法,不应该给出警告,但 C99 仍然没有被广泛使用 - 尽管这些部分相当广泛,除了微软的世界。

    倾向于曾经倾向于一种方法:

    static const struct sockaddr_in zero_sockaddr_in;
    

    接着是:

    struct sockaddr_in foo = zero_sockaddr_in;
    

    静态常量中初始化器的省略意味着一切都为零 - 但编译器不会机智(不应该机智)。赋值使用编译器的固有内存副本,除非编译器严重不足,否则不会比函数调用慢。


    GCC 随着时间的推移发生了变化

    GCC 版本 4.4.2 到 4.6.0 生成与 GCC 4.7.1 不同的警告。具体来说,GCC 4.7.1 将 = { 0 } 初始化程序识别为“特殊情况”并且不会抱怨,而 GCC 4.6.0 等确实会抱怨。

    考虑文件init.c

    struct xyz
    {
        int x;
        int y;
        int z;
    };
    
    struct xyz xyz0;                // No explicit initializer; no warning
    struct xyz xyz1 = { 0 };        // Shorthand, recognized by 4.7.1 but not 4.6.0
    struct xyz xyz2 = { 0, 0 };     // Missing an initializer; always a warning
    struct xyz xyz3 = { 0, 0, 0 };  // Fully initialized; no warning
    

    使用 GCC 4.4.2(在 Mac OS X 上)编译时,警告如下:

    $ /usr/gcc/v4.4.2/bin/gcc -O3 -g -std=c99 -Wall -Wextra -c init.c
    init.c:9: warning: missing initializer
    init.c:9: warning: (near initialization for ‘xyz1.y’)
    init.c:10: warning: missing initializer
    init.c:10: warning: (near initialization for ‘xyz2.z’)
    $
    

    使用 GCC 4.5.1 编译时,警告如下:

    $ /usr/gcc/v4.5.1/bin/gcc -O3 -g -std=c99 -Wall -Wextra -c init.c
    init.c:9:8: warning: missing initializer
    init.c:9:8: warning: (near initialization for ‘xyz1.y’)
    init.c:10:8: warning: missing initializer
    init.c:10:8: warning: (near initialization for ‘xyz2.z’)
    $
    

    使用 GCC 4.6.0 编译时,警告如下:

    $ /usr/gcc/v4.6.0/bin/gcc -O3 -g -std=c99 -Wall -Wextra -c init.c
    init.c:9:8: warning: missing initializer [-Wmissing-field-initializers]
    init.c:9:8: warning: (near initialization for ‘xyz1.y’) [-Wmissing-field-initializers]
    init.c:10:8: warning: missing initializer [-Wmissing-field-initializers]
    init.c:10:8: warning: (near initialization for ‘xyz2.z’) [-Wmissing-field-initializers]
    $
    

    使用 GCC 4.7.1 编译时,警告如下:

    $ /usr/gcc/v4.7.1/bin/gcc -O3 -g -std=c99 -Wall -Wextra  -c init.c
    init.c:10:8: warning: missing initializer [-Wmissing-field-initializers]
    init.c:10:8: warning: (near initialization for ‘xyz2.z’) [-Wmissing-field-initializers]
    $
    

    上面的编译器是我自己编译的。 Apple 提供的编译器名义上是 GCC 4.2.1 和 Clang:

    $ /usr/bin/clang -O3 -g -std=c99 -Wall -Wextra -c init.c
    init.c:9:23: warning: missing field 'y' initializer [-Wmissing-field-initializers]
    struct xyz xyz1 = { 0 };
                          ^
    init.c:10:26: warning: missing field 'z' initializer [-Wmissing-field-initializers]
    struct xyz xyz2 = { 0, 0 };
                             ^
    2 warnings generated.
    $ clang --version
    Apple clang version 4.1 (tags/Apple/clang-421.11.65) (based on LLVM 3.1svn)
    Target: x86_64-apple-darwin11.4.2
    Thread model: posix
    $ /usr/bin/gcc -O3 -g -std=c99 -Wall -Wextra -c init.c
    init.c:9: warning: missing initializer
    init.c:9: warning: (near initialization for ‘xyz1.y’)
    init.c:10: warning: missing initializer
    init.c:10: warning: (near initialization for ‘xyz2.z’)
    $ /usr/bin/gcc --version
    i686-apple-darwin11-llvm-gcc-4.2 (GCC) 4.2.1 (Based on Apple Inc. build 5658) (LLVM build 2336.11.00)
    Copyright (C) 2007 Free Software Foundation, Inc.
    This is free software; see the source for copying conditions.  There is NO
    warranty; not even for MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
    
    $
    

    正如SecurityMatt 在下面的评论中所指出的,memset() 相对于从内存中复制结构的优势在于从内存中复制的成本更高,需要访问两个内存位置(源和目标)而不仅仅是一。相比之下,将值设置为零不必访问内存以获取源代码,并且在现代系统上,内存是一个瓶颈。因此,memset() 编码应该比简单初始化器的复制更快(其中相同的值,通常都是零字节,被放置在目标内存中)。如果初始值设定项是值的复杂组合(并非全为零字节),那么平衡可能会更改为有利于初始值设定项,以保证符号的紧凑性和可靠性。

    没有一个简单的答案......可能从来没有,现在也没有。我仍然倾向于使用初始化器,但memset() 通常是一个有效的替代方案。

    【讨论】:

    • 谢谢,我可以用哪些标志来重现这个 gcc 警告?鉴于使用单个 { 0 } 将聚合完全初始化为零是常见的做法,为什么它会发出警告?
    • 啊,我明白了 -Werror 完成了这项工作。您确实提供了一个很好的案例来反对 { 0 }。这可能是史蒂文斯等人不使用它的原因吗?
    • 我认为 Stevens 等人不太可能担心这种级别的警告。无论如何,我认为它是 GCC 的最新成员。我有几年没有更改的代码,现在收到警告,如果它是“当时”的警告,我已经修复了。
    • 这样做的缺点是它会将变量的 memset 变成零到 bss 部分的 memcpy,从而产生内存开销(将 zero_sockaddr_in 存储在 bss 中)当额外的内存读取以复制内存时。更好的方法是执行 memset(&var, 0, sizeof(var))。
    • @peter:混乱还是古老?当代码设计为使用标准 C(C89/90、C99、C11)时,不需要强制转换,因为 memset() 的参数是 void * 并且 &f 将自动转换为 void *。如果代码足够老,可以在 C89/90 之前编写,那么 memset() 采用 char * 参数而不是 void * 参数 - 那时还没有 void *。像lint 这样的程序(现在很少使用,因为如果代码编写得当,现代编译器几乎可以诊断出所有问题)会抱怨没有演员表。
    【解决方案2】:

    我想说这两种方法都不正确,因为您永远不应该自己创建 sockaddr_anything 类型的对象。而是始终使用getaddrinfo(或有时getsocknamegetpeername)来获取地址。

    【讨论】:

    • 同意并赞成。不幸的是,一些旧系统没有 getaddrinfo,您必须自己创建结构。
    • 好吧,与其用直接创建结构的东西来污染你的主代码库,不如直接替换掉缺少的getaddrinfo 功能......
    • 不幸的是,一些库(在这种情况下是 libuv)提供的 API 需要指向 sockaddr_storage (sockaddr_storage*) 的指针来存储它们的结果。(在 libuv 的情况下,例如 uv_tcp_getpeername 需要这个)。所以,对于这个用例,我不想替换缺少的功能,我想使用一个需要我提供空 sockaddr_* 的 API。
    【解决方案3】:

    正如许多人指出的那样,任何一个都是正确的。此外,您可以使用已返回零内存块的calloc 分配这些结构。

    【讨论】:

    • 这不能回答我问题的任何部分,但是是的,我知道 calloc 也会这样做。无论如何,我怀疑它的使用频率很高。
    【解决方案4】:

    这两种方法都不应该有问题——填充字节的值应该无关紧要。我怀疑 memset() 的使用源于伯克利主义 bzero() 的早期使用,它可能早于结构初始化器的引入或更有效。

    【讨论】:

    • 我怀疑你是对的 - bzero() 的这种用法太古老了,以至于许多程序员只是将其视为惯用语。
    【解决方案5】:

    "struct sockaddr_in foo = { 0 };"仅在第一次有效,而 "memset(&foo, 0, sizeof foo);"每次运行函数时都会清除它。

    【讨论】:

    • 如果 foo 是局部变量,则每次都有效。如果 foo 是全局的或静态的,那么你是正确的。
    • 一个具有自动存储持续时间的对象加上一个初始化器将在每次声明时被初始化。这对于静态结构是正确的,但这些结构通常不声明为静态的。
    • 只有当结构被声明为静态时才会出现问题,这不应该没有充分的理由。
    猜你喜欢
    • 2011-01-01
    • 2016-12-19
    • 1970-01-01
    • 1970-01-01
    • 2015-12-30
    • 2022-01-23
    • 2016-07-18
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多