【问题标题】:How do I concatenate two C-style (null-terminated) strings?如何连接两个 C 风格(以空结尾)字符串?
【发布时间】:2013-05-24 06:45:30
【问题描述】:

我想连接两个这样定义的字符串:

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 终止

我做错了什么?

【问题讨论】:

  • there 是关于 C 中字符串连接的好线程
  • 这不是一个坏问题,实际上 OP 正在询问关于猫如何运作的内部原理。我认为这是一个有效的问题 :)
  • @AhmedMasud 是的,你是对的。但找不到能准确解释我的答案。
  • 我已经为你开始了一个答案,阅读它,我在最后添加更多内容
  • 我还使用示例粗连接函数更新了我的答案:)

标签: c concatenation null-terminated


【解决方案1】:

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

请参阅 here 以了解 strcat() 实现。

【讨论】:

    【解决方案2】:

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

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

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

    你也可以并行复制,一个常见的习惯是:

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

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

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

    在某一时刻,它们将计算为 \0while 循环计算为 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;
    }
    

    它打印“Hello, World!”。

    【讨论】:

    • “成语”宁愿是while(*destination != '\0') { *destination = *source; destination++; source++; }。这是完全相同的,并且产生完全相同的机器代码,它只是更具可读性,并且不使用 if 语句中的赋值,这是危险和糟糕的风格。
    • 很公平,在这种情况下,让我链接一个更恰当的解释:stackoverflow.com/questions/810129/how-does-whiles-t-work 但你不能说它不常见 :)
    • 顺便说一句,他要求连接而不是复制
    • 孩子吃地上的东西很常见。这是否意味着这是个好主意?
    • @Lundin 不,它没有。如果来自未初始化变量destination 的第一个字节的不确定值是'\0',那将无法正常工作。也许您的意思是 for (*destination = *source; *destination != '\0'; source++, destination++, *destination = *source); 或其他什么...
    【解决方案3】:

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

    char hello[14] = "Hello";
    

    【讨论】:

      【解决方案4】:

      我的答案最初不会专注于正确连接字符串;而是尝试解决代码中的一些问题,并为您提供一些背景想法,这可能有助于阐明如何在 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 编译器在发出与 &lt;hello&gt;&lt;world&gt; 符号对应的数据时隐式注入的。

      好的,现在您了解了它的工作原理;你可以看到:

      // 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 &lt; 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 中完全消失,我们必须在最后注入;请注意,我们仍然必须在最后增加 pq 并在 *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
      

      【讨论】:

      • 你的回答太棒了。这超出了我的预期。太好了谢谢。我希望你完成它:) 是的,我正在使用 Linux,并且有兴趣查看字符串的机器表示。如果您将指定命令,那将非常好:)
      • 核心完成了,我来输入反汇编命令
      • 添加了拆解指南。我可能最终会用它来做 C 讲座 :)
      • 1.你听说过“抽象机器”吗?它与您的想法相矛盾,即 C 一定会以某种方式实现 CPU 的原生功能。哪个 CPU 有 sizeof 运算符? 2.你读过c-faq.com吗?有一个问题我想你可能会感兴趣:Seriously, have any actual machines really used nonzero null pointers, or different representations for pointers to different types?。 3. 你知道EBCDIC 中'0' 的值是250,而不是48? C 被合理化为可移植的。
      • Related question 关于 NULL 与空指针。此外,com.lang C FAQ 有一个关于该主题的精彩章节。
      【解决方案5】:

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

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

      hello 中可以存储多少个char?让我们检查一下。

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

      输出:6。这意味着 hello[0]hello[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);
      }
      

      【讨论】:

        猜你喜欢
        • 1970-01-01
        • 2010-12-23
        • 2013-04-07
        • 1970-01-01
        • 2011-06-06
        • 1970-01-01
        • 2015-06-10
        • 1970-01-01
        • 1970-01-01
        相关资源
        最近更新 更多