问题

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

回答
你问的这个问题,其实是C语言中一个非常基础但又常常让人头疼的陷阱。简单来说,`strcpy(a, b);` 这样的操作,在 `char a = "xxxxx";` 和 `char b = "xxx";` 的情况下,为什么不行,核心原因在于 内存的性质。

咱们一步一步来拆解它,让你彻底明白是怎么回事。

首先,认识一下 C 语言中的字符串和指针

在 C 语言里,字符串其实就是一串连续的字符,最后以一个空字符 `` 结尾。而 `char ` 呢,就是指向这串字符的第一个字符的指针。

当你写下:

```c
char a = "xxxxx";
char b = "xxx";
```

这里发生的事情是:

1. 字符串常量存储在哪里?
编译器在编译代码时,会把像 `"xxxxx"` 这样的字符串字面量,放到一个叫做 只读数据段 (readonly data segment) 的内存区域。这个区域的特点就是,你 不可以 对它进行修改。想象成把它放在了一个玻璃柜里,你想往里头塞东西或者换东西,玻璃会碎的。

2. `char a` 和 `char b` 指向哪里?
`a` 这个指针变量,它存储的是字符串 `"xxxxx"` 在内存中首地址。
`b` 这个指针变量,它存储的是字符串 `"xxx"` 在内存中首地址。

所以,此时的 `a` 指向一个只读的内存区域,里面是 `"xxxxx"`。而 `b` 指向另一个只读的内存区域,里面是 `"xxx"`。

为什么 `strcpy(a, b);` 会出问题?

`strcpy(char dest, const char src)` 函数的作用是把 `src` 指向的字符串 复制 到 `dest` 指向的内存空间。

我们来看这个调用:`strcpy(a, b);`

`dest` 就是 `a`
`src` 就是 `b`

`strcpy` 函数会做的事情是:
它会从 `b` 指向的内存区域(也就是 `"xxx"`)开始,一个字符一个字符地把内容复制到 `a` 指向的内存区域去。

问题就出在这里:
`a` 指向的内存区域是只读的!

当你尝试用 `strcpy` 向 `a` 指向的只读内存区域写入数据时,就像你试图在玻璃柜里敲个洞把东西塞进去一样,会发生什么?

内存访问违例 (Segmentation Fault)。

你的程序会崩溃,操作系统会阻止这种非法操作,因为它破坏了内存的保护机制。这通常会表现为一个错误信息,比如 "Segmentation fault (core dumped)"。

如果我想要修改字符串,应该怎么做?

正确的做法是,你需要确保你的目标字符串(`dest`)指向的是一块 可写 的内存空间,并且这块空间足够大,能够容纳下源字符串(`src`)以及它后面的那个空字符 ``。

方法一:使用字符数组 (char array)

这是最常见也最推荐的做法:

```c
char a[] = "xxxxx"; // 声明一个字符数组,编译器会在栈上分配一块可写内存
char b[] = "xxx"; // 同样是可写内存

strcpy(a, b); // 现在是合法的,因为a指向的是可写内存
```

在这里,`char a[] = "xxxxx";` 的意思是:
编译器看到你定义了一个字符数组 `a`,并且初始化它为 `"xxxxx"`。编译器会在 栈 (stack) 或者 全局/静态数据段 (global/static data segment) 为 `a` 分配一块 可读写 的内存空间,并且将 `"xxxxx"` 复制进去,最后加上 ``。

所以,当 `strcpy(a, b);` 被调用时:
`a` 指向的是这块可写内存,`strcpy` 可以安全地将 `b` 的内容(`"xxx"`)复制到 `a` 指向的这块内存中。

重要提示:数组大小
你需要确保数组 `a` 的大小足够容纳 `b` 的内容。如果 `b` 比 `a` 长很多,`strcpy` 就会发生 缓冲区溢出 (buffer overflow),这同样是严重的安全漏洞,会导致未定义的行为,比如覆盖其他变量,甚至引发崩溃。

例如,如果你这样写:
```c
char a[5]; // 只能容纳 "xxxx" + ''
char b[] = "xxxxxx";
strcpy(a, b); // 悲剧发生了,缓冲区溢出!
```

更安全的做法是使用 `strncpy` 或 `strlcpy`(如果你的系统支持的话),它们允许你指定要复制的最大字符数,防止溢出。

方法二:动态分配内存 (malloc)

如果你需要在运行时确定字符串的大小,或者字符串很大,可以使用 `malloc` 来分配可写内存:

```c
include
include
include

int main() {
char a;
char b = "xxx";

// 为 a 分配足够的内存空间,包括字符串内容和终止符 ''
// 这里我们假设知道 b 的长度,但更一般情况下,你可能需要更复杂的逻辑
a = (char )malloc(strlen(b) + 1); // +1 for the null terminator

if (a == NULL) {
perror("Failed to allocate memory");
return 1;
}

strcpy(a, b); // 现在是合法的
printf("a now contains: %s ", a);

// 使用完后记得释放内存
free(a);

return 0;
}
```
在这种情况下,`malloc` 返回的指针指向的是堆 (heap) 上的一块动态分配的内存,这块内存是可写和可用的。

总结一下

`char a = "xxxxx";` 这种写法声明了一个指针 `a`,它指向的是一个位于只读区域的字符串常量。`strcpy` 试图往这个只读区域写数据,所以会引起内存访问违例。

要安全地复制字符串,目标内存区域必须是 可写 的,并且有足够的空间。通常,通过声明字符数组 (`char a[] = ...;`) 或动态分配内存 (`malloc`) 来获得可写内存。

记住这一点,就能避免很多 C 语言中的“坑”了。

网友意见

user avatar

你知道

       char a[] = "hello";     

       char *a = "hello";     

两种写法的区别吗?

正文

让我们看一下下面的程序:

       #include <stdio.h>  int main(int argc, char *argv[]) {         char *a = "aaaaa";         char b[] = "55";          b[0] = 'f';         a[0] = 'f';          printf("a=%s, b=%s
", a ,b);          return 0; }     

编译执行:

       # gcc -o mytest mytest.c -Wall -g # ./mytest Segmentation fault (core dumped)     

直接segmentation fault了。我们看一下是哪一步触发的segfault:

       # gdb mytest core.2125.11.18446744073709551615.1.2125 ... Core was generated by `./mytest'. Program terminated with signal SIGSEGV, Segmentation fault. #0  0x000000000040114f in main (argc=1, argv=0x7fff9b3e4a08) at mytest.c:9 9               a[0] = 'f'; (gdb)     

不用看更多了,gdb直接就告诉了我们之前我们运行的./mytest那个进程因为触发了Segmentation fault,已经被终止运行了。问题行出在main函数里的

       a[0] = 'f';     

这行。

那么问题就来了,为什么这步会触发非法地址访问呢?而它前面那句:

       b[0] = 'f';     

就没事?

我相信很多人都看过各种解释和说法,但是你看到的那些“解释”是怎么具体体现在可执行程序里的呢?

分析

这就是最开始我们问的问题,

       char *a = "aaaaa"; char b[] = "55";     

有什么区别?我们先看一下编译后的程序是什么样的

       # objdump -DS mytest int main(int argc, char *argv[]) {   401126:       55                      push   %rbp   401127:       48 89 e5                mov    %rsp,%rbp   40112a:       48 83 ec 20             sub    $0x20,%rsp   40112e:       89 7d ec                mov    %edi,-0x14(%rbp)   401131:       48 89 75 e0             mov    %rsi,-0x20(%rbp)         char *a = "aaaaa";   401135:       48 c7 45 f8 10 20 40    movq   $0x402010,-0x8(%rbp)   40113c:       00          char b[] = "55";   40113d:       66 c7 45 f5 35 35       movw   $0x3535,-0xb(%rbp)   401143:       c6 45 f7 00             movb   $0x0,-0x9(%rbp)          b[0] = 'f';   401147:       c6 45 f5 66             movb   $0x66,-0xb(%rbp)         a[0] = 'f';   40114b:       48 8b 45 f8             mov    -0x8(%rbp),%rax   40114f:       c6 00 66                movb   $0x66,(%rax) …… ……     

这就很显而易见了:

首先main函数初始化栈,分了一些内存空间作为main函数的栈。关于函数调用栈是怎么回事,参考下文:

以及相关系列文章。我这里就不再解释函数栈的原理了。我们通过汇编语言可以看到char *a和char b[]现在位于main函数栈的下列位置:

       +--------+ |    a   |   <--- -0x8(%rbp) +--------+ |   b[2] |   <--- -0x9(%rbp) +--------+ |   b[1] |   <--- -0xa(%rbp) +--------+ |   b[0] |   <--- -0xb(%rbp) +--------+  注: b[0~2]每个占一个字节,但变量a会占更多字节(我只是没画出来),因为a是一个指针变量,占用的字节数和体系结构有关。     

我们先看char b[] = "55";

               char b[] = "55";   40113d:       66 c7 45 f5 35 35       movw   $0x3535,-0xb(%rbp)   401143:       c6 45 f7 00             movb   $0x0,-0x9(%rbp)     

0x35就是字符'5',所以0x3535就是"55",所以“movw $0x3535,-0xb(%rbp)”这句就是给main函数栈的-0xb(%rbp)起始的位置存两个字节的"55",也就是b[0]='5'; b[1]='5';

然后下面一句就很简单了,编译器为字符串自动补,所以“movb $0x0,-0x9(%rbp)”就相当于b[2]='';这样char b[] = "55";就出来了。

接着我们再看char *a = "aaaaa";

               char *a = "aaaaa";   401135:       48 c7 45 f8 10 20 40    movq   $0x402010,-0x8(%rbp)     

为什么它只有一句“movq $0x402010,-0x8(%rbp)”? 上面我们说0x3535是"55",但是这个"0x402010"是什么?它显然不等于"aaaaa",而且我们也没看出程序有把"aaaaa"存放进a的过程。

因为这个“$0x402010”是一个地址,这个地址指向的是"aaaaa"这个字符串实际存储的位置,那这个位置在哪呢?

       # objdump -sj .rodata mytest  mytest:     file format elf64-x86-64  Contents of section .rodata:  402000 01000200 00000000 00000000 00000000  ................  402010 61616161 6100613d 25732c20 623d2573  aaaaa.a=%s, b=%s  402020 0a00     

看到“402010: 616161616100 .....”了么?0x61就是ascii的'a',0x00就是''。所以0x402010就是[aaaaa]的地址。这段内容属于只读的数据段,注意“只读”两个字,代表它不可写。

接写来看赋值部分b[0] = 'f':

               b[0] = 'f';   401147:       c6 45 f5 66             movb   $0x66,-0xb(%rbp)     

参考上面我给出的main函数栈中b数组的位置,-0xb(%rbp)就是b[0],0x66就是'f'。所以这句很明显就是b[0] = 'f';

再看a[0] = 'f':

               a[0] = 'f';   40114b:       48 8b 45 f8             mov    -0x8(%rbp),%rax   40114f:       c6 00 66                movb   $0x66,(%rax)     

“-0x8(%rbp)”存储的是*a,也就是上面我们说的地址0x402010,我们先把这个地址放在AX寄存器中。然后寻址AX寄存器的内容为地址的地址空间,也就是0x402010对应的地址空间,也就是第一个"a"的位置,程序尝试向这个位置写入0x66(也就是'f')。上面我们已经说了,0x402010属于只读数据段,必然不允许写入,所以这这个写入操作就是一个非法的访问。所以直接触发了Segmentation fault。

所以char *a = "hello"; 和char a[] = "hello";的区别就是,char *a = "hello";得到的是一个指向"hello"所在只读空间的指针变量。而char a[] = "hello"得到的是一个栈上的数组。

说到这里我觉得你应该懂了,strcpy(a, b)就是读取字符串b的内容,写入a,当你的a对应的是只读空间时,必然不允许写入。


char *a和char a[]作为函数参数

评论区 Kevin Yang 同学问道:“那函数的形参写char* str和char str[]有区别吗[好奇]”

我觉得这是一个好的引申问题。对于C语言来说(我没说别的语言),当我们声明一个函数的参数是一个数组的时候,我们实际上得到的是一个指针。C语言没有传递数组的方式,通常都是以指针的形式传递。所以char *str和char str[]作为函数形参是没有区别的。

如下面的例子:

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

编译执行:

       # gcc -o mytest mytest.c -Wall -g # ./mytest func1: bcdef func2: bcdef     

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

       # objdump -DS mytest ... void func1(char *a) {   401126:       55                      push   %rbp   401127:       48 89 e5                mov    %rsp,%rbp   40112a:       48 83 ec 10             sub    $0x10,%rsp   40112e:       48 89 7d f8             mov    %rdi,-0x8(%rbp)         a++;   401132:       48 83 45 f8 01          addq   $0x1,-0x8(%rbp)         printf("func1: %s
", a); ... ...  void func2(char a[]) {   401150:       55                      push   %rbp   401151:       48 89 e5                mov    %rsp,%rbp   401154:       48 83 ec 10             sub    $0x10,%rsp   401158:       48 89 7d f8             mov    %rdi,-0x8(%rbp)         a++;   40115c:       48 83 45 f8 01          addq   $0x1,-0x8(%rbp)         printf("func2: %s
", a); ... ...  int main(int argc, char *argv[]) {   40117a:       55                      push   %rbp   40117b:       48 89 e5                mov    %rsp,%rbp   40117e:       48 83 ec 20             sub    $0x20,%rsp   401182:       89 7d ec                mov    %edi,-0x14(%rbp)   401185:       48 89 75 e0             mov    %rsi,-0x20(%rbp)         char *a = "abcdef";   401189:       48 c7 45 f8 26 20 40    movq   $0x402026,-0x8(%rbp)   401190:       00          func1(a);   401191:       48 8b 45 f8             mov    -0x8(%rbp),%rax   401195:       48 89 c7                mov    %rax,%rdi   401198:       e8 89 ff ff ff          callq  401126 <func1>         func2(a);   40119d:       48 8b 45 f8             mov    -0x8(%rbp),%rax   4011a1:       48 89 c7                mov    %rax,%rdi   4011a4:       e8 a7 ff ff ff          callq  401150 <func2>          return 0;   4011a9:       b8 00 00 00 00          mov    $0x0,%eax }   4011ae:       c9                      leaveq    4011af:       c3                      retq      

首先字符串的首地址是放在main函数栈的-0x8(%rbp)这个位置的:

               char *a = "abcdef";   401189:       48 c7 45 f8 26 20 40    movq   $0x402026,-0x8(%rbp)     

然后在调用func1(a)和func2(a)前,程序将这个指针保存在%rdi寄存器里(我没有开优化,这里多绕了一下,但是最好还是在rdi寄存器里。因为rax寄存器通常用于保存返回值。):

         40119d:       48 8b 45 f8             mov    -0x8(%rbp),%rax   4011a1:       48 89 c7                mov    %rax,%rdi     

然后就是调用func1()和func2()。进入func1或func2时先是一顿栈操作,预留了func1/2的栈空间。

         401126:       55                      push   %rbp   401127:       48 89 e5                mov    %rsp,%rbp   40112a:       48 83 ec 10             sub    $0x10,%rsp     

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

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

注意这里两个函数的-0x8(%rbp)都是相对于各自的栈来说的,是两个不一样的位置,而且和main函数的-0x8(%rbp)也是不一样的。欲了解函数调用和返回过程,请参考:

醉卧沙场:简单函数的调用原理

醉卧沙场:简单函数的返回原理

(进阶可参考: 醉卧沙场:递归函数的堆栈操作

更多内容可参考:醉卧沙场:README - 专业性文章及回答总索引

我在这里不多叙述函数调用的原理了,接着说这个问题。

上面看到func1和func2都把地址参数保存在了各自的栈的临时变量里,然后对各自的变量进行a++操作:

               a++;   40115c:       48 83 45 f8 01          addq   $0x1,-0x8(%rbp)     

最后都打印出来。

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

类似的话题

  • 回答
    你问的这个问题,其实是C语言中一个非常基础但又常常让人头疼的陷阱。简单来说,`strcpy(a, b);` 这样的操作,在 `char a = "xxxxx";` 和 `char b = "xxx";` 的情况下,为什么不行,核心原因在于 内存的性质。咱们一步一步来拆解它,让你彻底明白是怎么回事。 .............

本站所有内容均为互联网搜索引擎提供的公开搜索信息,本站不存储任何数据与内容,任何内容与数据均与本站无关,如有需要请联系相关搜索引擎包括但不限于百度google,bing,sogou

© 2025 tinynews.org All Rights Reserved. 百科问答小站 版权所有