美文网首页
C语言中的字符指针和字符数组

C语言中的字符指针和字符数组

作者: JamFF | 来源:发表于2021-03-05 17:24 被阅读0次

    我们先使用字符指针,声明字符串,并修改其中元素:

    #include <stdio.h>
    
    int main() {
        char *name = "Sam";
        name[0] = 'J';
        printf("%s\n", name);
        return 0;
    }
    

    使用 gcc 编译执行后,报错误:段错误 (核心已转储),英文的话是 Segmentation fault (core dumped)

    gcc main.c -o main
    ./main
    段错误 (核心已转储)
    

    再使用字符数组,声明字符串,并修改其中元素:

    #include <stdio.h>
    
    int main() {
        char name[] = "Sam";
        name[0] = 'J';
        printf("%s\n", name);
        return 0;
    }
    

    用 gcc 编译执行后一切正常:

    gcc main.c -o main
    ./main
    Jam
    

    那么字符指针和字符数组究竟有什么区别?为什么第一段代码会出段错误?

    字符指针

    我们把第一段代码反汇编来看一下,反汇编的命令是 objdump -d exec_file_name

    objdump -d main
    

    只关注 main 函数部分:

    0000000000400526 <main>:
      400526:   55                      push   %rbp
      400527:   48 89 e5                mov    %rsp,%rbp
      40052a:   48 83 ec 10             sub    $0x10,%rsp
      40052e:   48 c7 45 f8 d4 05 40    movq   $0x4005d4,-0x8(%rbp)
      400535:   00 
      400536:   48 8b 45 f8             mov    -0x8(%rbp),%rax
      40053a:   c6 00 4a                movb   $0x4a,(%rax)
      40053d:   48 8b 45 f8             mov    -0x8(%rbp),%rax
      400541:   48 89 c7                mov    %rax,%rdi
      400544:   e8 b7 fe ff ff          callq  400400 <puts@plt>
      400549:   b8 00 00 00 00          mov    $0x0,%eax
      40054e:   c9                      leaveq 
      40054f:   c3                      retq   
    

    main 函数所在地址是 0000000000400526,可以看出我这边是 64 位操作系统,地址占用 8 个字节。下文的地址为了简洁,都省略地址前面的 0,简化为 0x400526

    我们先看字符指针的赋值 char *name = "Sam"

      40052e:   48 c7 45 f8 d4 05 40    movq   $0x4005d4,-0x8(%rbp)
    

    这句话的意思是,查找 0x4005d4 地址上的值,并转移到 rbp 寄存器中。

    那么这个地址上的存储的值是什么呢?我们可以使用 objdump -s exec_file_name 命令,查看可执行文件中的所有段:

    objdump -s main
    main:     文件格式 elf64-x86-64
    ...省略...
    Contents of section .rodata:
     4005d0 01000200 53616d00                    ....Sam.        
    ...省略...
    

    可以看到 "Sam" 被编译器放到了 .rodata 段,ro 代表 read only,这个段的属性是只读的,0x4005d0 表示这一行的地址,字符 'S' 对应的 ASCII 是 0x53,前面的 01000200 是4个字节,所以刚好在 0x4005d0 + 4 = 0x4005d4 的位置。

    对于 char *name = "Sam",就是将字符串 "Sam" 的首地址 0x4005d4 赋值给 name 这个字符指针,而 name 存放在 main 函数栈的 -0x8(%rbp) 这个位置上。

    那段错误是如何产生的呢?

    这就要看修改元素的代码 name[0] = 'J'

      400536:   48 8b 45 f8             mov    -0x8(%rbp),%rax
      40053a:   c6 00 4a                movb   $0x4a,(%rax)
    

    -0x8(%rbp) 存储的是 name,也就是上面说的地址 0x4005d4,先把这个地址放在 rax 寄存器中,然后寻址 rax 寄存器的内容为地址的地址空间,也就是 0x4005d4 对应的地址空间,也就是字符 'S' 的位置,程序尝试向这个位置写入 0x4a,也就是 'J' 字符。

    上面我们已经说了, 0x4005d4 这个地址属于只读数据段,必然不允许写入,所以这个写入操作就是一个非法的访问,直接触发了段错误。

    字符数组

    那么对于 char name[] = "Sam" 又是什么情况呢?

    我们也来反汇编一下:

    0000000000400596 <main>:
      400596:   55                      push   %rbp
      400597:   48 89 e5                mov    %rsp,%rbp
      40059a:   48 83 ec 10             sub    $0x10,%rsp
      40059e:   64 48 8b 04 25 28 00    mov    %fs:0x28,%rax
      4005a5:   00 00 
      4005a7:   48 89 45 f8             mov    %rax,-0x8(%rbp)
      4005ab:   31 c0                   xor    %eax,%eax
      4005ad:   c7 45 f0 53 61 6d 00    movl   $0x6d6153,-0x10(%rbp)
      4005b4:   c6 45 f0 4a             movb   $0x4a,-0x10(%rbp)
      4005b8:   48 8d 45 f0             lea    -0x10(%rbp),%rax
      4005bc:   48 89 c7                mov    %rax,%rdi
      4005bf:   e8 9c fe ff ff          callq  400460 <puts@plt>
      4005c4:   b8 00 00 00 00          mov    $0x0,%eax
      4005c9:   48 8b 55 f8             mov    -0x8(%rbp),%rdx
      4005cd:   64 48 33 14 25 28 00    xor    %fs:0x28,%rdx
      4005d4:   00 00 
      4005d6:   74 05                   je     4005dd <main+0x47>
      4005d8:   e8 93 fe ff ff          callq  400470 <__stack_chk_fail@plt>
      4005dd:   c9                      leaveq 
      4005de:   c3                      retq   
      4005df:   90                      nop
    

    我们先看字符数组的赋值 char name[] = "Sam"

      4005ad:   c7 45 f0 53 61 6d 00    movl   $0x6d6153,-0x10(%rbp)
    

    需要注意的是,这里的 0x6d6153 并不是一个地址,0x6d 是字符 'm',0x6d6153 翻译成 ASCII 就是 "Sam"(注意字节序,这里是Little-Endian)。这句话就是给 main 函数栈的 -0x10(%rbp) 起始位置存入 "Sam"。

    接下来看修改元素的代码 name[0] = 'J'

      4005b4:   c6 45 f0 4a             movb   $0x4a,-0x10(%rbp)
    

    参考上面 main 函数栈中的 name 的位置,-0x10(%rbp)就是 name[0]0x4a 就是 'J' 字符。

    到这里,我们可以看出与上面操作字符指针的区别:这里操作的不是地址,而是字符

    那这次 "Sam" 存储在什么位置呢?我们同样使用 objdump -s exec_file_name,查看可执行文件中的所有段:

    objdump -s main
    main:     文件格式 elf64-x86-64
    ...省略...
    Contents of section .text:
     4004a0 31ed4989 d15e4889 e24883e4 f0505449  1.I..^H..H...PTI
     4004b0 c7c05006 400048c7 c1e00540 0048c7c7  ..P.@.H....@.H..
     4004c0 96054000 e8b7ffff fff4660f 1f440000  ..@.......f..D..
     4004d0 b8471060 0055482d 40106000 4883f80e  .G.`.UH-@.`.H...
     4004e0 4889e576 1bb80000 00004885 c074115d  H..v......H..t.]
     4004f0 bf401060 00ffe066 0f1f8400 00000000  .@.`...f........
     400500 5dc30f1f 4000662e 0f1f8400 00000000  ]...@.f.........
     400510 be401060 00554881 ee401060 0048c1fe  .@.`.UH..@.`.H..
     400520 034889e5 4889f048 c1e83f48 01c648d1  .H..H..H..?H..H.
     400530 fe7415b8 00000000 4885c074 0b5dbf40  .t......H..t.].@
     400540 106000ff e00f1f00 5dc3660f 1f440000  .`......].f..D..
     400550 803de90a 20000075 11554889 e5e86eff  .=.. ..u.UH...n.
     400560 ffff5dc6 05d60a20 0001f3c3 0f1f4000  ..].... ......@.
     400570 bf200e60 0048833f 007505eb 930f1f00  . .`.H.?.u......
     400580 b8000000 004885c0 74f15548 89e5ffd0  .....H..t.UH....
     400590 5de97aff ffff5548 89e54883 ec106448  ].z...UH..H...dH
     4005a0 8b042528 00000048 8945f831 c0c745f0  ..%(...H.E.1..E.
     4005b0 53616d00 c645f04a 488d45f0 4889c7e8  Sam..E.JH.E.H...
     4005c0 9cfeffff b8000000 00488b55 f8644833  .........H.U.dH3
     4005d0 14252800 00007405 e893feff ffc9c390  .%(...t.........
     4005e0 41574156 4189ff41 5541544c 8d251e08  AWAVA..AUATL.%..
     4005f0 20005548 8d2d1e08 20005349 89f64989   .UH.-.. .SI..I.
     400600 d54c29e5 4883ec08 48c1fd03 e817feff  .L).H...H.......
     400610 ff4885ed 742031db 0f1f8400 00000000  .H..t 1.........
     400620 4c89ea4c 89f64489 ff41ff14 dc4883c3  L..L..D..A...H..
     400630 014839eb 75ea4883 c4085b5d 415c415d  .H9.u.H...[]A\A]
     400640 415e415f c390662e 0f1f8400 00000000  A^A_..f.........
     400650 f3c3  
    ...省略...
    

    可以看到 "Sam" 放在了 .text 段中,地址分别是:0x4005b00x4005b10x4005b2

    我们知道 .text 段也是只读的,那为什么这次操作没有出现段错误呢?

    字符指针和字符数组的区别

    • char *name = "Sam"; 是字符指针,得到的是一个,指向只读空间 "Sam",所在位置的指针变量。
    • char name[] = "Sam"; 是字符数组,得到的是一个栈中的数组,数组中的三个地址分别指向 .text 段中 'S','a','m' 三个字符。当我们修改其中元素时,并没有修改 .text 段中的字符,只是修改了数组中的地址的指向,指向了 .text 段中的另一个字符。

    字符指针和字符数组作为函数参数

    那函数的形参写 char *strchar str[] 有区别吗?
    对于 C 语言来说,声明一个函数的参数是数组的时候,实际上得到的是一个指针。C 语言没有传递数组的方式,通常都是以指针的形式传递。所以 char *strchar str[] 作为函数形参是没有区别的。

    例如:

    #include <stdio.h>
    
    void func1(char *str) {
        str++;
        printf("func1: %s\n", str);
    }
    
    void func2(char str[]) {
        str++;
        printf("func2: %s\n", str);
    }
    
    int main() {
        char *a = "abc";
        func1(a);
        char b[] = "abc";
        func2(b);
        return 0;
    }
    

    编译执行:

    gcc main.c -o main
    ./main
    func1: bc
    func2: bc
    

    来看看参数是怎么传递和存储的:

    0000000000400596 <func1>:
      400596:   55                      push   %rbp
      400597:   48 89 e5                mov    %rsp,%rbp
      40059a:   48 83 ec 10             sub    $0x10,%rsp
      40059e:   48 89 7d f8             mov    %rdi,-0x8(%rbp)
      4005a2:   48 83 45 f8 01          addq   $0x1,-0x8(%rbp)
      4005a7:   48 8b 45 f8             mov    -0x8(%rbp),%rax
      4005ab:   48 89 c6                mov    %rax,%rsi
      4005ae:   bf d4 06 40 00          mov    $0x4006d4,%edi
      4005b3:   b8 00 00 00 00          mov    $0x0,%eax
      4005b8:   e8 b3 fe ff ff          callq  400470 <printf@plt>
      4005bd:   90                      nop
      4005be:   c9                      leaveq 
      4005bf:   c3                      retq   
    
    00000000004005c0 <func2>:
      4005c0:   55                      push   %rbp
      4005c1:   48 89 e5                mov    %rsp,%rbp
      4005c4:   48 83 ec 10             sub    $0x10,%rsp
      4005c8:   48 89 7d f8             mov    %rdi,-0x8(%rbp)
      4005cc:   48 83 45 f8 01          addq   $0x1,-0x8(%rbp)
      4005d1:   48 8b 45 f8             mov    -0x8(%rbp),%rax
      4005d5:   48 89 c6                mov    %rax,%rsi
      4005d8:   bf df 06 40 00          mov    $0x4006df,%edi
      4005dd:   b8 00 00 00 00          mov    $0x0,%eax
      4005e2:   e8 89 fe ff ff          callq  400470 <printf@plt>
      4005e7:   90                      nop
      4005e8:   c9                      leaveq 
      4005e9:   c3                      retq   
    
    00000000004005ea <main>:
      4005ea:   55                      push   %rbp
      4005eb:   48 89 e5                mov    %rsp,%rbp
      4005ee:   48 83 ec 20             sub    $0x20,%rsp
      4005f2:   64 48 8b 04 25 28 00    mov    %fs:0x28,%rax
      4005f9:   00 00 
      4005fb:   48 89 45 f8             mov    %rax,-0x8(%rbp)
      4005ff:   31 c0                   xor    %eax,%eax
      400601:   48 c7 45 e8 ea 06 40    movq   $0x4006ea,-0x18(%rbp)
      400608:   00 
      400609:   48 8b 45 e8             mov    -0x18(%rbp),%rax
      40060d:   48 89 c7                mov    %rax,%rdi
      400610:   e8 81 ff ff ff          callq  400596 <func1>
      400615:   c7 45 f0 61 62 63 00    movl   $0x636261,-0x10(%rbp)
      40061c:   48 8d 45 f0             lea    -0x10(%rbp),%rax
      400620:   48 89 c7                mov    %rax,%rdi
      400623:   e8 98 ff ff ff          callq  4005c0 <func2>
      400628:   b8 00 00 00 00          mov    $0x0,%eax
      40062d:   48 8b 55 f8             mov    -0x8(%rbp),%rdx
      400631:   64 48 33 14 25 28 00    xor    %fs:0x28,%rdx
      400638:   00 00 
      40063a:   74 05                   je     400641 <main+0x57>
      40063c:   e8 1f fe ff ff          callq  400460 <__stack_chk_fail@plt>
      400641:   c9                      leaveq 
      400642:   c3                      retq   
      400643:   66 2e 0f 1f 84 00 00    nopw   %cs:0x0(%rax,%rax,1)
      40064a:   00 00 00 
      40064d:   0f 1f 00                nopl   (%rax)
    

    首先 a 和 b 两个字符串的首地址是放在 main 函数栈的 -0x18(%rbp)-0x10(%rbp) 这个位置的:

      400601:   48 c7 45 e8 ea 06 40    movq   $0x4006ea,-0x18(%rbp)
      ...
      400615:   c7 45 f0 61 62 63 00    movl   $0x636261,-0x10(%rbp)
    

    然后在调用 func1(a)func2(b) 前,程序将这个指针保存在 rdi 寄存器里:

      400609:   48 8b 45 e8             mov    -0x18(%rbp),%rax
      40060d:   48 89 c7                mov    %rax,%rdi
      ...
      40061c:   48 8d 45 f0             lea    -0x10(%rbp),%rax
      400620:   48 89 c7                mov    %rax,%rdi
    

    然后就是调用 func1(char *str)func2(char str[])。进入 func1func2 时先是一顿栈操作,预留了栈空间。

      400596:   55                      push   %rbp
      400597:   48 89 e5                mov    %rsp,%rbp
      40059a:   48 83 ec 10             sub    $0x10,%rsp
    

    然后将上面说的字符串的地址保存在 rdi 寄存器里,接着 func1func2 就把这个地址从 rdi 寄存器里取出来,保存到各自的栈 -0x8(%rbp) 的位置。

      40059e:   48 89 7d f8             mov    %rdi,-0x8(%rbp)
    

    注意这里两个函数的 -0x8(%rbp) 都是相对于各自栈来说,是两个不同的位置,而且和 main 函数的 -0x8(%rbp) 也是不同的。

    然后对各自的变量进行 str++ 操作:

      4005a2:   48 83 45 f8 01          addq   $0x1,-0x8(%rbp)
    

    可以看到,不管是将参数写成 char *str 还是 char str[],编译出来的都没有区别。当然不同的编译器以及不同的编译选项偶可能造成不同的编译结果,但是总体原理是一样的。

    总结

    1. 使用 char *name = "Sam" 代码,name 指针指向的实际上是只读区域,因此,更准确的写法是 const char *name = "Sam"
    2. 使用 char name[] = "Sam"代码,编译器会生成一段初始化数组的代码,该数组存储在栈中,name 指针指向栈中的数组位置,数组的内容当然是可以修改的。
    3. 如果不需要修改数组的内容,使用 char *name = "SamFF" 写法效率会更高。
    4. 函数形参中使用 char *strchar str[] 是没有区别的,都是字符指针。

    参考资料:为什么char *a="xxxxx", *b="xxx"; strcpy(a, b);的用法不行?

    相关文章

      网友评论

          本文标题:C语言中的字符指针和字符数组

          本文链接:https://www.haomeiwen.com/subject/okkkqltx.html