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

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

#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);的用法不行?

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 217,657评论 6 505
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 92,889评论 3 394
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 164,057评论 0 354
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,509评论 1 293
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 67,562评论 6 392
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,443评论 1 302
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,251评论 3 418
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 39,129评论 0 276
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,561评论 1 314
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,779评论 3 335
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,902评论 1 348
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,621评论 5 345
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 41,220评论 3 328
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,838评论 0 22
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,971评论 1 269
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 48,025评论 2 370
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,843评论 2 354

推荐阅读更多精彩内容