我的答案最初不会专注于正确连接字符串;而是尝试解决代码中的一些问题,并为您提供一些背景想法,这可能有助于阐明如何在 C 中思考事物。然后我们将研究连接字符串
在我们开始之前,关于C-strings结构的一些想法
用 C 语言思考很像像计算机一样思考(CPU、内存等);因此,对于在 CPU 上原生工作的数据类型,C 有字符(单字节事物)、shorts(双字节字)、longs(4 字节字)、int、float 和 double,所有这些都是 CPU 原生理解的。以及创建这些东西的数组或指向这些类型存在的内存位置的指针的能力。
那么我们如何创建一个字符串呢?我们创建一个新类型吗?
嗯,由于 CPU 不理解字符串,C 也不理解...无论如何都不是最原始的形式(C 解析器没有与字符串关联的类型)。
但字符串非常有用,因此必须有一个相当简单的概念来确定字符串应该是什么。
所有的 C 字符串都是顺序内存中不包含 NUL 字符的字节;
NUL(发音类似于 nool)是我们为内存中值为 0 的字节赋予值的名称。在 C 中,这由 @987654325 表示@。所以如果我写 NUL 它意味着字符 \0;
注意 1:这与 C 中的 NULL 不同,后者是一个 内存地址 零值;
注意 2:NUL 当然不是字符零('0'),其值为 48;
因此,任何处理字符串的函数都会启动一个由 char * 指向的内存位置(读取 char 指针);并且只是继续逐字节(字符)地执行其操作,直到它遇到一个表示字符串结尾的字节的值 0。到那时,希望它停止做它正在做的事情,因为字符串已经结束并返回其操作的结果。
因此,如果我们将字符串定义为以 0 结尾的字符数组,那么我们就完全避免了创建任何人为的字符串概念。
这正是 C 所做的;它只是将这个概念确定为使用约定;并且编译器只是提供了一个简单的快捷方式来声明使用双引号以 NUL 结尾的字符数组,仅此而已。 C中的字符串没有特殊的类型。
考虑到所有这些,让我们看看您的代码:
char hello[] = { 'H', 'e', 'l', 'l', 'o', '\0' };
char world[] = { ',', ' ', 'W', 'o', 'r', 'l', 'd', '!', '\0' };
您声明了两个单字节 (char) 数组并用 \0 终止它们;
这与以下 C 语句相同:
char hello[] = "Hello";
char world[] = ", World!";
当在 64 位 Intel 计算机上运行的 Linux 计算机上编译时,您的配对和上面的配对都会发出以下(相同的)机器代码输出:
Disassembly of section .data:
0000000000000000 <hello>:
0: 48 65 6c 6c 6f 00 Hello.
0000000000000006 <world>:
6: 2c 20 57 6f 72 6c 64 21 00 , World!.
如果您使用的是 Linux,您可以尝试一下;让我知道,我会在下面以附录的形式向您展示这些命令。
请注意,在这两种情况下,00 字节都出现在末尾。在您的情况下,它是您在数组中明确声明的;在第二种情况下,它是由 C 编译器在发出与 <hello> 和 <world> 符号对应的数据时隐式注入的。
好的,现在您了解了它的工作原理;你可以看到:
// This is bad: :-)
for (int i = 0; i < 6; i++) {
if (hello[i] == '\0') {
for (int j = 0; j < 9; j++) {
int index = 5 + j;
hello[index] = world[j];
}
}
}
上面的循环很奇怪。实际上它有很多问题(例如嵌套在外部for循环内的循环是错误的);
但与其指出问题,不如只看基本正确的解决方案。
当您为字符串编程时,您不知道它们有多大;所以在处理字符串的for 循环中i < N 形式的条件不是通常的方法。
这是一种遍历字符串中字符的方法(以\0 结尾的字符数组):
char *p; /* Points to the characters in strings */
char str[] = "Hello";
for ( p = str; *p != 0; p++ ) {
printf("%c\n", *p);
}
让我们弄清楚这里发生了什么:
for ( p = str; ...
^^^^^^^^^
p 是一个字符指针。一开始我们将它指向hello(当你运行程序时,变量hello被加载到内存中)并检查这个内存位置的值(由*p获得)是否等于'\0'与否:
for (p = str; *p != 0; ...)
^^^^^^^
如果不是,我们执行for 循环,因为条件为真;在我们的例子中 *p=='H' 所以我们进入循环:
for (p = str; *p != 0; p++)
^^^
在这里,我们现在先进行递增/递减/其他操作。但在这种情况下,++ 运算符是后缀到p;所以p(这是一个内存地址)将在循环中语句的结尾处增加它的值;所以现在循环进入 { ... } 做它的事情,最后 ++ 发生,我们再次进入条件检查:
for (p = str; *p != 0; p++)
^^^^^^^
所以你可以看到这会将p 设置为指向 'H' 'e' 'l' 'l' 'o' '\0' 的内存位置;然后点击 '\0' 就会退出。
连接字符串:
现在我们知道我们想要连接“Hello”和“, World!”。
首先我们需要找到Hello的结尾,然后我们需要开始粘贴“,世界!”到最后:
我们知道上面的 for 循环找到了 hello 的结尾;因此,如果我们在它的末尾什么都不做,*p 将指向 Hello 末尾的 '\0' 所在的位置:
char str1[] = "Hello";
char str2[] = ", World";
char *p; /* points str1 */
char *q; /* points str2 */
for (p = str1; *p!=0; p++) {
/* Skip along till the end */
}
/* Here p points to '\0' in str1 */
/* Now we start to copy characters from str2 to str1 */
for (q = str2; *q != 0; p++, q++ ) {
*p = *q;
}
请注意,在第一遍中,*p 指向 str1 末尾的 '\0',所以当我们分配 *p = *q 时,'\0' 被替换为 ','; '\0' 从 str1 中完全消失,我们必须在最后注入;请注意,我们仍然必须在最后增加 p 和 q 并在 *q != 0 时继续循环。
现在循环结束了,我们在最后加上一个“\0”,因为我们破坏了我们拥有的那个:
*p = 0;
这就是串联。
关于记忆的重要部分
如果您在上面的汇编程序输出中注意到; Hello\0 占用了 6 个字节,, World\0 开始于数据段中的地址 0000000006(hello 开始于 000000000)。
这意味着如果您写入的字节数超出 str1[] 的字节数并且它没有足够的空间,这是我们的情况(为什么会在下面解释),我们最终会覆盖属于某物的部分内存else (str2[]) 例如;
我们没有足够内存的原因是因为我们刚刚声明了一个足够大的字符数组来保存我们的初始化值:
char str[] = "Foofoo";
将使 str 正好为 7 个字节。
但是我们可以要求 C 给str 更多的空间,而不仅仅是初始化值。例如,
char str[20] = "Foofoo";
这将给str 20 个字节,并将前七个设置为“Foofoo\0”。其余的通常也设置为\0;
所以上面的反汇编看起来像:
Disassembly of section .data:
0000000000000000 <str>:
0: 48 65 6c 6c 6f 00 00 00 00 00 00 00 00 00 00 00 Foofoo..........
10: 00 00 00 00 ....
请记住,在 C 语言中,您必须像计算机一样思考。如果您没有明确要求内存,您将不会拥有它。因此,如果我们要进行连接,要么我们必须使用一个足够大的数组,因为我们以这种方式明确声明了它:
char foo[1000]; /* Lots of room */
或者我们在运行时使用malloc(另一个帖子的主题)请求内存位置。
让我们看看一个可行的解决方案:
concat.c:
#include <stdio.h>
char str1[100] = "Hello";
char str2[] = ", World!"; /* No need to make this big */
int main()
{
char *p;
char *q;
printf("str1 (before concat): %s\n", str1);
for (p = str1; *p != 0; p++) {
/* Skip along to find the end */
}
for (q = str2; *q != 0; p++, q++ ) {
*p = *q;
}
*p = 0; /* Set the last character to 0 */
printf("str1 (after concat): %s\n", str1);
return 0;
}
在 Linux 上反汇编:
如果您将上述内容编译成一个目标文件并且不将其链接到可执行文件,那么您将让事情变得不那么混乱:
gcc -c concat.c -o concat.o
您可以使用对象转储来反汇编 concat.o:
objdump -d concat.o
您会在处理 printf 语句的转储中发现很多不必要的代码:
0: 55 push %rbp
1: 48 89 e5 mov %rsp,%rbp
4: 48 83 ec 10 sub $0x10,%rsp
8: be 00 00 00 00 mov $0x0,%esi
d: bf 00 00 00 00 mov $0x0,%edi
12: b8 00 00 00 00 mov $0x0,%eax
17: e8 00 00 00 00 callq 1c <main+0x1c>
所以要摆脱它,只需在代码中注释掉 printf 即可。然后使用该行重新编译
gcc -O3 -c concat.c -o concat.o
再次。现在您将获得更清晰的输出;
-O3 删除了一些与帧指针(稍后主题)相关的指令,并且汇编程序将特定于您的代码库:
这是使用上面编译并转储出来的 concat.o 输出:
objdump -S -s concat.o
concat.o: File format elf64-x86-64
Contents of section .text:
0000 803d0000 000000b8 00000000 740b6690 .=..........t.f.
0010 4883c001 80380075 f70fb615 00000000 H....8.u........
0020 84d2741d b9000000 000f1f80 00000000 ..t.............
0030 4883c101 88104883 c0010fb6 1184d275 H.....H........u
0040 efc60000 31c0c3 ....1..
Contents of section .data:
0000 48656c6c 6f000000 00000000 00000000 Hello...........
0010 00000000 00000000 00000000 00000000 ................
0020 00000000 00000000 00000000 00000000 ................
0030 00000000 00000000 00000000 00000000 ................
0040 00000000 00000000 00000000 00000000 ................
0050 00000000 00000000 00000000 00000000 ................
0060 00000000 2c20576f 726c6421 00 ...., World!.
Contents of section .comment:
0000 00474343 3a202844 65626961 6e20342e .GCC: (Debian 4.
0010 342e352d 38292034 2e342e35 00 4.5-8) 4.4.5.
Contents of section .eh_frame:
0000 14000000 00000000 017a5200 01781001 .........zR..x..
0010 1b0c0708 90010000 14000000 1c000000 ................
0020 00000000 47000000 00000000 00000000 ....G...........
Disassembly of section .text:
0000000000000000 <main>:
0: 80 3d 00 00 00 00 00 cmpb $0x0,0x0(%rip) # 7 <main+0x7>
7: b8 00 00 00 00 mov $0x0,%eax
c: 74 0b je 19 <main+0x19>
e: 66 90 xchg %ax,%ax
10: 48 83 c0 01 add $0x1,%rax
14: 80 38 00 cmpb $0x0,(%rax)
17: 75 f7 jne 10 <main+0x10>
19: 0f b6 15 00 00 00 00 movzbl 0x0(%rip),%edx # 20 <main+0x20>
20: 84 d2 test %dl,%dl
22: 74 1d je 41 <main+0x41>
24: b9 00 00 00 00 mov $0x0,%ecx
29: 0f 1f 80 00 00 00 00 nopl 0x0(%rax)
30: 48 83 c1 01 add $0x1,%rcx
34: 88 10 mov %dl,(%rax)
36: 48 83 c0 01 add $0x1,%rax
3a: 0f b6 11 movzbl (%rcx),%edx
3d: 84 d2 test %dl,%dl
3f: 75 ef jne 30 <main+0x30>
41: c6 00 00 movb $0x0,(%rax)
44: 31 c0 xor %eax,%eax
46: c3 retq