【问题标题】:C array initializing - some basicsC 数组初始化 - 一些基础知识
【发布时间】:2017-05-25 08:05:55
【问题描述】:

我在一本书中读到,当你有一个像"blabla"这样的字符串时,这意味着有一个隐藏的char数组,这个表达式将地址返回到第一个元素,并且它就像一个 const 数组。

这让我对两种情况感到困惑:

  1. char a[7] = "blabla" ,是不可能的,因为 "blabla" 返回一个 address 到数组的第一个元素,那么你将如何放置一个地址进入a 而不是实际元素?

  2. 它说当你看到“blabla”时,它的意思就像一个 const 字符数组,这意味着我根本无法更改 a(这不是真的)。​​

我想这里有些基本的东西我不清楚。

【问题讨论】:

    标签: c arrays string initialization string-literals


    【解决方案1】:

    根据 C 标准(6.3.2.1 左值、数组和函数指示符)

    3 除非是 sizeof 运算符的操作数或 一元 & 运算符,或者是一个字符串文字用于初始化一个 数组,具有“类型数组”类型的表达式被转换为 类型为“pointer to type”的表达式,指向初始 数组对象的元素并且不是左值。如果数组对象 有注册存储类,行为未定义。

    所以在这个声明中

    char a[7] = "blabla";
    

    由于包含终止零作为字符串文字的元素而具有字符数组char[7]类型的字符串文字的元素用于初始化字符数组a的元素

    其实这个声明等价于声明

    char a[7] = { 'b', 'l', 'a', 'b', 'l', 'a', '\0' };
    

    考虑到在 C 字符串文字中具有非常量字符数组的类型。然而,它们本身可能无法修改。

    来自 C 标准(6.4.5 字符串文字)

    7 不确定这些数组是否不同,前提是它们的 元素具有适当的值。如果程序试图 修改这样的数组,行为未定义。

    所以你可以写例如

    char *s = "blabla";
    

    在这种情况下,根据 C 标准的第一个引号,字符串文字被转换为指向其第一个元素的指针,并将指针的值分配给变量 s

    即在静态内存中创建了一个未命名的字符数组,并将数组的第一个元素的地址分配给指针s。例如,您可能不会使用指针来更改您可能不会写的文字

    char *s = "blabla";
    s[0] = 'B';
    

    在 C++ 中,字符串文字确实具有常量字符数组的类型。所以你必须用 C++ 程序编写

    const char *s = "blabla";
    

    你也可以用 C 来写

    char a[6] = "blabla";
         ^^^^
    

    在这种情况下,字符串文字的终止零将不会用于初始化字符数组 a 。所以数组不会包含字符串。

    在 C++ 中这样的声明是无效的。

    【讨论】:

      【解决方案2】:

      第一种情况,

      char a[7] = "blabla",不可能[...]

      是的,有可能,这是一个初始化

      引用C11,第 §6.7.9/P14 章,初始化

      字符类型的数组可以由字符串字面量或UTF−8字符串初始化 字面量,可选地用大括号括起来。字符串文字的连续字节(包括 如果有空间或数组大小未知,则终止空字符)初始化 数组的元素。

      第二种情况,

      它说当你看到“blabla”时,它的意思就像一个 const char array ,这意味着我根本无法更改 a(这不是真的)。​​

      [从直接尝试修改字符串文字的角度来看]

      可以,但必须不能。

      来自第 §6.4.5 章

      [...] 如果程序试图修改这样的数组,行为是 未定义。

      也就是说,在你的情况中,a 不是指向字符串文字的指针,它是一个数组,其中的元素使用字符串中的内容初始化文字。你完全可以修改a数组的内容。

      【讨论】:

      • 这个我知道谢谢,但这不是我问题的答案..这是关于“blabla”是一个地址这一事实与“a”不应该得到一个事实之间的差距地址。
      • @Curnelious 那你的问题是什么?
      【解决方案3】:

      “blabla”是书上所说的,一个 7 个字节的字符数组,最后一个是 '\0',放置在只读数据空间中(如果可能)。

      (1) 写作时:

       char a[7] = "blabla";
      

      您告诉编译器在堆栈上创建一个包含 7 个字符的可变数组,并在其中复制只读数组。请注意,你也可以写:

       char a[] = "blabla";
      

      ...这样更安全,因为编译器会为您计算字符数。

      (2) 鉴于 a[] 是“blabla”的副本,您可以毫无问题地写入它。如果你想保留只读属性,你可以写:

      const char *a = "blabla";
      

      这一次 a 将是一个指向常量字符串的 const 指针,它的 内容 将是不可变的。无论如何,您都可以重新分配指针:

      const char *a = "blabla";
      a = "blublu";
      

      【讨论】:

      • 这是一个很好的解释,所以 a[7]="blabla" 的意思是 "a" 是可变的,它有一个 "blabla" 的副本,但不可变的是 "blabla" (无论如何我都无法直接访问,除非我这样做: char *a="blabla" )?
      • @Curnelious 然后使用字符串文字来初始化数组,其元素用作初始化器。没有任何改变。
      【解决方案4】:

      为时已晚,但我仍然提供我的答案。

      所以让我们来区分一下

      main()
      {
        char *a="blabla";
        a[3]='x';
      }
      

      还有这个,你的。

      main() 
      {
        char a[7] = "blabla"
        a[3]='x';
      }
      

      所以他们之间有很大的不同。

      在第一种情况下,对象a 是一个指针,其值指向blabla 字符串的开头。

      转储汇编代码,我们看到:

        4004aa:       48 c7 45 f8 54 05 40    movq   $0x400554,-0x8(%rbp)
        4004b1:       00 
        4004b2:       48 8b 45 f8             mov    -0x8(%rbp),%rax
        4004b6:       48 83 c0 03             add    $0x3,%rax
        4004ba:       c6 00 78                movb   $0x78,(%rax)
      

      因此,它尝试将指针设置为指向地址0x400554

      Objdumpo 报告此地址在 .rodata 段中。

      段.rodata的反汇编:

      0000000000400550 <_IO_stdin_used>:
        400550:       01 00                   add    %eax,(%rax)
        400552:       02 00                   add    (%rax),%al
        400554:       62                      (bad)  
        400555:       6c                      insb   (%dx),%es:(%rdi)
        400556:       61                      (bad)  
        400557:       62                      .byte 0x62
        400558:       6c                      insb   (%dx),%es:(%rdi)
        400559:       61                      (bad)  
      

      因此,编译器在该地址的 .rodata 中安装了字符串 blabla,然后尝试修改 .rodata 段,以分段错误结束。

      readelf 报告在 .rodata 上没有 W 访问权限:

      [13] .rodata           PROGBITS         0000000000400550  00000550
           000000000000000b  0000000000000000   A       0     0     4
      

      另一方面,您尝试执行的操作(第二个程序)被编译为:

      00000000004004a6 <main>:
        4004a6:       55                      push   %rbp
        4004a7:       48 89 e5                mov    %rsp,%rbp
        4004aa:       c7 45 f0 62 6c 61 62    movl   $0x62616c62,-0x10(%rbp)
        4004b1:       66 c7 45 f4 6c 61       movw   $0x616c,-0xc(%rbp)
        4004b7:       c6 45 f6 00             movb   $0x0,-0xa(%rbp)
        4004bb:       c6 45 f3 78             movb   $0x78,-0xd(%rbp)
      

      在这种情况下,数组对象a在堆栈帧上分配了7个字节,从偏移量%RBP-0xA开始直到%RBP-0x10

      当它尝试执行 a[3]='x' 时,它将修改位于 %RBP-0xD 的堆栈。栈有write权限,一切正常。

      更多信息我建议你阅读https://en.wikipedia.org/wiki/Identity_and_change

      【讨论】: