1

我想连接两个定义如下的字符串:

char hello[] = { 'H', 'e', 'l', 'l', 'o', '\0' };
char world[] = { ',', ' ', 'W', 'o', 'r', 'l', 'd', '!', '\0' };

我知道我应该跑过第一个,找到'\0'标志,而不是开始第二个字符串。功能是否strcat以相同的方式工作?

我正在使用的代码:

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];
        }
    }
}

编译后出现这样的错误:

* 检测到堆栈粉碎 * : ./run 终止

我究竟做错了什么?

4

5 回答 5

14

我的答案最初不会专注于正确连接字符串;而是尝试解决代码中的一些问题,并为您提供一些背景思想,这可能有助于阐明如何在 C 中思考事物。然后我们将研究连接字符串

在开始之前,关于 C 字符串结构的一些思考

用 C 语言思考很像像计算机一样思考(CPU、内存等);因此,对于在 CPU 上原生工作的数据类型,C 有字符(单字节事物)、shorts(双字节字)、longs(4 字节字)、int、float 和 double,所有这些都是 CPU 原生理解的。以及创建这些事物的数组或指向这些类型存在的内存位置的指针的能力。

那么我们如何创建一个字符串呢?我们创建一个新类型吗?

好吧,由于 CPU 不理解字符串,C 也不...无论如何都不是最原始的形式(C 解析器没有与字符串关联的类型)。

但是字符串非常有用,因此必须有一个相当简单的概念来确定字符串应该是什么。

所有的 C 字符串都是顺序内存中不包含 NUL 字符的字节;

NUL(发音类似于nool)是我们为内存中值为 0 的字节赋予 value 的名称。在 C 中,这由 表示\0。所以如果我写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,最后都会出现一个字节。在您的情况下,它是您在数组中明确声明的;<hello>在第二种情况下,它是由 C 编译器在发出与和<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循环中的循环是错误的);

但与其指出问题,让我们只看基本的正确解决方案。

当您为字符串编程时,您不知道它们有多大;所以处理字符串i < N的循环中的表单条件不是通常的方法。for

这是一种遍历字符串中字符的方法(以 结尾的 char 数组\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(这是一个内存地址)将在循环中语句的 END 处增加其值;所以现在循环进入了{ ... }它的事情,最后++发生了,我们再次进入条件检查:

  for (p = str; *p != 0; p++)
                ^^^^^^^

所以你可以看到这将p指向 'H' 'e' 'l' 'l' 'o' '\0'; 的内存位置。然后它点击'\ 0'它将退出。

连接字符串:

所以现在我们知道我们想要连接“Hello”和“, World!”。

首先我们需要找到结尾,Hello然后我们需要开始粘贴“,世界!” 到最后:

好吧,我们知道for上面的循环找到了 hello 的结尾;所以如果我们在它的末尾什么都不做,*p将指向末尾的 '\0'Hello是:

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 中完全消失,我们必须在最后注入;请注意,我们仍然必须在最后增加pandq并继续循环 while *q != 0

现在循环结束了,我们在最后加上一个'\0',因为我们破坏了我们拥有的那个:

*p = 0;

这就是串联。

关于记忆的重要部分

如果您在上面的汇编程序输出中注意到;Hello\0占用 6 个字节并从数据段中的, World\0地址开始0000000006(hello 开始于 000000000)。

这意味着如果您写入超出 str1[] 的字节数并且它没有足够的空间这是我们的情况(为什么在下面解释),我们最终将覆盖属于其他东西的部分内存(str2 []) 例如;

我们没有足够内存的原因是因为我们刚刚声明了一个足够大的字符数组来保存我们的初始化值:

char str[] = "Foofoo";

将使 str 正好是 7 个字节。

但是我们可以要求 C 给予更多str的空间,而不仅仅是初始化值。例如,

char str[20] = "Foofoo";

这将提供str20 个字节,并将前七个设置为“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(另一篇文章的主题)请求内存位置。

让我们看看一个可行的解决方案:

连接.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
于 2013-05-29T07:38:28.347 回答
2

将只分配 6 个字节的内存hello。所以尝试为新的连接字符串创建新的内存。

参考这里strcat()实现。

于 2013-05-29T06:05:00.777 回答
2

您可以通过分配足够的内存来解决数组越界的访问...

char hello[14] = "Hello";
于 2013-05-29T07:11:32.017 回答
2

您不需要以如此细致的方式定义您的字符串。这也有效:

char hello[] = "Hello";
char world[] = ", World!";

C 将为您处理空终止它们。

您也可以并行进行复制,一个常见的习惯用法是:

while(*destination++ = *source++)
    ;

这将首先分配source当前指向的 char,destination然后增加两个指针(只有指针,而不是里面的内容)。这是因为取消引用优先于递增。两个指针并行递增。

例如,在第一次运行 while 循环后,两者destinationsource都将指向内存中包含相同字符的地址。

\0在某一时刻,它们将评估while循环评估为的false结果,并且将停止复制它们(因为表达式将不再评估为true)。

由于这(和strcat())被认为有些不安全,因此在执行此操作之前,请确保您在目的地有足够的空间。或者,使用strncat()可以限制复制应该持续多长时间的地方(如果字符串不是以空值结尾的并且你让它“撕裂”,可以说没有限制,可能会发生不好的事情)。

你可以像这样使用上面的:

void strcopycst(char* destination, char* source)
{
    while((*destination++ = *source++))
    ;
}

在你的主要:

char dest [25];
char source = "Hello, World!";

strcopycst(dest, source);

编辑:正如评论者所说,我没有正确解决串联问题。基于上面的代码,这里是一个粗略的 strcat 函数:

void cstmstrcat(char* dest, char* source1, char* source2) /* dest must be big enough */
{
    while((*dest++ = *source1++))
        ;

    --dest; /* backtrack the pointer as after the above 
               it will point to some random memory value */

    while((*dest++ = *source2++))
        ;
}

以下是它的使用方法:

int main()
{
    char source1 [] = "Hello";
    char source2 [] = ", World!";
    char dest [50];

    cstmstrcat(dest, source1, source2);

    printf("%s\n", dest);

    return 0;
}

它打印“你好,世界!”。

于 2013-05-29T06:09:53.387 回答
0

您正在尝试存储超出该数组范围的数据。

char hello[] = { 'H', 'e', 'l', 'l', 'o', '\0' };

char你能存多少个hello?让我们检查一下。

#include <stdio.h>
int main(void) {
    char hello[] = { 'H', 'e', 'l', 'l', 'o', '\0' };
    printf("%zu\n", sizeof hello);
}

输出:6。这意味着hello[0]通过 tohello[5]是有效的索引。hello[6]及以上无效。您需要声明一个足够大的数组来存储连接的结果,如下所示:

#include <stdio.h>
#include <string.h>
int main(void) {
    char hello[] = { 'H', 'e', 'l', 'l', 'o', '\0' };
    char world[] = { ',', ' ', 'W', 'o', 'r', 'l', 'd', '!', '\0' };

    /* Don't forget to add 1 for NUL */
    char hello_world[strlen(hello) + strlen(world) + 1];

    strcpy(hello_world, hello);
    strcat(hello_world, world);
    puts(hello_world);
}
于 2013-05-29T15:16:33.110 回答