【问题标题】:Difference in initializing and zeroing an array in c/c++?在 C/C++ 中初始化和归零数组的区别?
【发布时间】:2010-10-01 23:48:31
【问题描述】:

在 c (或者可能是 c++) 中,有什么区别

char myarr[16]={0x00};


char myarr[16]; memset(myarr, '\0', sizeof(myarr));

??

编辑:我问这个是因为在 vc++ 2005 中结果是一样的..
编辑更多: 和

char myarr[16]={0x00,}; 
?
也许可以得到更全面的答案,而不是模棱两可,因为下面的一些答案是指这种代码,即。 将逗号放在大括号之前。 vc++ 2005 中的结果也是一样的。

【问题讨论】:

    标签: c++ c arrays


    【解决方案1】:

    重要的区别是第一个默认值以特定于元素的方式初始化数组:指针将接收一个 空指针值,它不需要为 0x00(如在所有位中) -zero),布尔值将是 false。如果元素类型是不是所谓的POD(普通旧数据类型)的类类型,那么你只能做第一个,因为第二个只适用于最简单的情况(你没有虚拟函数、用户定义的构造函数等等)。相比之下,使用 memset 的第二种方法将数组的所有元素设置为所有位为零。这并不总是你想要的。例如,如果您的数组具有指针,则它们不一定会设置为空指针。

    第一个将默认初始化数组的元素,但第一个除外,它显式设置为 0。如果数组是本地的并且在堆栈上(也就是说,不是静态的),编译器内部通常会执行一个 memset 来清除数组。如果数组是非本地的或静态的,第一个版本可能效率更高。编译器可以在编译时将初始化程序放入生成的汇编代码中,使其根本不需要运行时代码。或者,当程序以快速方式(即分页方式)启动时,可以将数组布置在自动归零的部分(也适用于指针,如果它们具有全位零表示)。

    第二个在整个数组上显式地执行 memset。优化编译器通常会将较小区域的 memset 替换为仅使用标签和分支循环的内联机器代码。

    这是为第一种情况生成的汇编代码。我的 gcc 的东西没有得到太多优化,所以我们真正调用了 memset(堆栈顶部的 16 个字节总是分配的,即使我们没有本地人。$n 是一个寄存器号):

    void f(void) {
        int a[16] = { 42 };
    }
    
    sub     $29, $29, 88 ; create stack-frame, 88 bytes
    stw     $31, $29, 84 ; save return address
    add     $4, $29, 16  ; 1st argument is destination, the array.
    add     $5, $0, 0    ; 2nd argument is value to fill
    add     $6, $0, 64   ; 3rd argument is size to fill: 4byte * 16
    jal     memset       ; call memset
    add     $2, $0, 42   ; set first element, a[0], to 42
    stw     $2, $29, 16  ;
    ldw     $31, $29, 84 ; restore return address
    add     $29, $29, 88 ; destroy stack-frame
    jr      $31          ; return to caller
    

    来自 C++ 标准的血腥细节。上面的第一种情况将默认初始化剩余的元素。

    8.5:

    对 T 类型的对象进行零初始化存储意味着:

    • 如果 T 是标量类型,则存储设置为值 0(零)转换为 T
    • 如果 T 是非联合类类型,则每个非静态数据成员和每个基类子对象的存储都是零初始化的;
    • 如果 T 是联合类型,则其第一个数据成员的存储为零初始化;
    • 如果 T 是数组类型,则每个元素的存储都是零初始化的;
    • 如果 T 是引用类型,则不执行初始化。

    默认初始化 T 类型的对象意味着:

    • 如果 T 是非 POD 类类型,则调用 T 的默认构造函数
    • 如果 T 是数组类型,则每个元素都是默认初始化的;
    • 否则,对象的存储将被初始化为零。

    8.5.1:

    如果列表中的初始值设定项少于聚合中的成员, 那么每个未显式初始化的成员都应默认初始化(8.5)。

    【讨论】:

    • 请不要使用代码块进行引用(因为它使用等宽字体,可能会导致不必要的水平滚动条;烦人:/)。
    • cic,我使用
       因为否则它会删除换行符并将所有内容放在一行中。我不知道如何解决这个问题。
    • 好的,修好了。自动进入社区模式。但我认为这是值得的,因为它现在看起来好多了,没有丑陋的水平滚动条 :)
    【解决方案2】:

    ISO/IEC 9899:TC3 6.7.8,第 21 段:

    如果大括号括起来的列表中的初始值设定项少于聚合的元素或成员,或者用于初始化已知大小数组的字符串文字中的字符少于数组中的元素,则剩余的聚合应隐式初始化,与具有静态存储持续时间的对象相同。

    具有静态存储持续时间的数组初始化为0,因此C99 规范保证未显式初始化的数组元素也设置为0


    在我对这篇文章的第一次编辑中,我说了一些关于在初始化后使用复合文字分配给数组的废话。那是行不通的。如果你真的想使用复合文字来设置数组的值,你必须这样做:

    #define count(ARRAY) (sizeof(ARRAY)/sizeof(*ARRAY))
    
    int foo[16];
    memcpy(foo, ((int [count(foo)]){ 1, 2, 3 }), sizeof(foo));
    

    借助一些宏魔法和非标准的__typeof__ 运算符,这可以大大缩短:

    #define set_array(ARRAY, ...) \
        memcpy(ARRAY, ((__typeof__(ARRAY)){ __VA_ARGS__ }), sizeof(ARRAY))
    
    int foo[16];
    set_array(foo, 1, 2, 3);
    

    【讨论】:

    • @litb:暂时删除了关于复合文字的内容——我会在确定发生了什么后重新发布一些内容;我很困惑,因为数组参数作为指针传递,所以我的测试代码编译得很好......
    • 我有点困惑,因为我没有看到 litb 的评论,也没有看到 SoapBox 谈到复合文字的地方,但你的答案肯定是错误的,你不能直接分配给原始数组。
    • 啊,好的。好吧,我已经删除了我的 cmets,因为它们不再有上下文了 :) 祝你好运,我会喜欢使用更好的语法来设置数组 :)
    • 记录 - 我推荐 char *c = (char[16]){0};和 char (*c)[16]; c = &(char[16]){0};作为替代品。第一个将完全失败,因为大小丢失了。第二个需要 *c 才能访问数组 - 也很丑。
    【解决方案3】:

    也许char myarr[16]={0x00}; 不是一个好的例子,因为显式和隐式成员初始化都使用零,这使得解释在这种情况下发生的事情变得更加困难。我认为一个具有非零值的真实示例可能更能说明问题:

    /**
     * Map of characters allowed in a URL
     *
     * !, \, (, ), *, -, ., 0-9, A-Z, _, a-z, ~
     *
     * Allowed characters are set to non-zero (themselves, for easier tracking)
     */
    static const char ALLOWED_IN_URL[256] = {
    /*          0      1      2      3      4      5      6      7      8      9*/
    /*   0 */   0,     0,     0,     0,     0,     0,     0,     0,     0,     0,
    /*  10 */   0,     0,     0,     0,     0,     0,     0,     0,     0,     0,
    /*  20 */   0,     0,     0,     0,     0,     0,     0,     0,     0,     0,
    /*  30 */   0,     0,     0,    '!',    0,     0,     0,     0,     0,   '\'',
    /*  40 */  '(',   ')',   '*',    0,     0,    '-',   '.',    0,    '0',   '1',
    /*  50 */  '2',   '3',   '4',   '5',   '6',   '7',   '8',   '9',    0,     0,
    /*  60 */   0,     0,     0,     0,     0,    'A',   'B',   'C',   'D',   'E',
    /*  70 */  'F',   'G',   'H',   'I',   'J',   'K',   'L',   'M',   'N',   'O',
    /*  80 */  'P',   'Q',   'R',   'S',   'T',   'U',   'V',   'W',   'X',   'Y',
    /*  90 */  'Z',    0,     0,     0,     0,    '_',    0,    'a',   'b',   'c',
    /* 100 */  'd',   'e',   'f',   'g' ,  'h',   'i',   'j',   'k',   'l',   'm',
    /* 110 */  'n',   'o',   'p',   'q',   'r',   's',   't',   'u',   'v',   'w',
    /* 120 */  'x',   'y',   'z',    0,     0,     0,    '~',
    };
    

    这是一个可在对字符串进行 URL 编码时使用的查找表。只有 URL 中允许的字符设置为非零值。零表示不允许该字符,需要进行 URL 编码 (%xx)。请注意,该表突然在波浪字符后以逗号结尾。波浪号后面的任何字符都是不允许的,因此应设置为零。但是,我们没有编写更多的零来填充表最多 256 个条目,而是让编译器隐式地将其余条目初始化为零。

    【讨论】:

    • 这似乎是一种非常脆弱的方式来编写 ALLOWED_IN_URL 数组。为什么不使用 C99 指定的初始值设定项? const char ALLOWED_IN_URL[256] = { ['A'] = 1, ['B'] = 1, /* ... / },或者如果您使用 gcc,{ ['A'... 'Z'] = 1, ['a'...'z'] = 1, / ... */}
    • @Hudson:天哪!我绝对不知道那种语法。像这样的查找表非常方便。感谢您的提示。
    【解决方案4】:

    鉴于= { 0 } 的可读性比memset(..., ..., ... sizeof ...) 的可读性要高得多,这是无可争辩的事实,那么以下内容将不鼓励明确使用memset

    在 Visual Studio 2005 中,为 Windows Mobile 编译,完全优化的发布版本:

    ; DWORD a[10] = { 0 };
    
    mov         r3, #0
    mov         r2, #0x24
    mov         r1, #0
    add         r0, sp, #4
    str         r3, [sp]
    bl          memset
    add         r4, sp, #0
    mov         r5, #0xA
    
    ; DWORD b[10];
    ; memset(b, 0, sizeof(b));
    
    mov         r2, #0x28
    mov         r1, #0
    add         r0, sp, #0x28
    bl          memset
    add         r4, sp, #0x28
    mov         r5, #0xA
    

    差不多。

    【讨论】:

    • -1 你明白在ANSI C(至少)数组[] = {0};构造仅在您定义数组时有效,并且当您稍后尝试执行此操作时会出现编译错误!
    • ... 所以在尝试将数组归零时,除了在 decleration 时,您仍然需要使用 memset。为了提高可读性,您无能为力
    【解决方案5】:

    在变量声明中定义初始值与使用 memset 不同。

    对于前一种情况,零在二进制中以某种形式定义为零初始化内存(或非零,取决于您初始化的内容),并且您希望加载程序尊重这一点,与 C 完全无关语言标准。后者,使用 memset 取决于您也可以使用的 C 库。我对图书馆更有信心。

    我做了很多嵌入式代码,你学会了避免将变量初始化为变量声明的一部分的坏习惯,而是在代码中进行。

    对于标准操作系统、Linux、Windows 等,变量声明期间的 init 很好,您将获得难以察觉的性能提升,但如果您正在运行一个操作系统,那么您在一个足够快的平台上看不到这一点区别。

    根据二进制类型,声明期间 init 的前一种情况可以使二进制更大。这非常容易测试。如上所述编译二进制文件,然后将数组大小从 [16] 更改为 [16000] 然后再次编译。然后在没有 = {0x00} 的情况下编译并比较三个二进制大小。

    对于大多数程序员将看到的大多数系统,没有功能差异。我推荐 memset 作为一种习惯。尽管标准说很多(如果不是大多数)C 编译器(大多数程序员在他们的职业生涯中永远不会看到)不会喜欢那个 init,因为元素的数量与大小不匹配。即使他们声称,大多数编译器也不符合标准。相反,要养成良好的习惯,避免使用捷径或几乎任何应该适用于标准 X 但与先前的标准 M 不同的东西。(避免任何 gee whiz 编译器或基于标准的技巧)。

    【讨论】:

      【解决方案6】:

      实际上它们是相同的。第一种形式保证将整个类型初始化为 0x00(例如,甚至结构元素之间的填充空间),这是从 C90 开始定义的。 不幸的是,gcc 使用 -Wmissing-field-initializers 选项对第一个表单发出警告。更多细节在这里:

      http://www.pixelbeat.org/programming/gcc/auto_init.html

      【讨论】:

        猜你喜欢
        • 1970-01-01
        • 2011-10-28
        • 1970-01-01
        • 1970-01-01
        • 2018-07-26
        • 1970-01-01
        • 1970-01-01
        • 2012-11-23
        • 2023-03-04
        相关资源
        最近更新 更多