如何连接两个C风格(以null结尾)的字符串?

时间:2013-05-29 06:03:11

标签: c concatenation null-terminated

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

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

我做错了什么?

5 个答案:

答案 0 :(得分:13)

我的回答最初并没有集中精力正确连接字符串;而是试图解决你的代码中的一些问题,并给你一些背景知识,可能有助于澄清如何思考C中的事情。然后我们将看看连接字符串

在我们开始之前,对C-strings

的结构有一些想法

用C思考非常像计算机(CPU,内存等);因此,对于在CPU上本机工作的数据类型,C具有字符(单字节事物),短路(双字节字),长字(4字节字),整数,浮点数和双精度数,这些都是CPU本身可以理解的。并且能够创建这些东西的数组或指向存在这些类型的内存位置的指针。

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

好吧,因为CPU不理解字符串,所以C ...也不是最原始的形式(C解析器没有与字符串相关的类型)。

但字符串非常有用,因此必须有一个相当简单的概念,即字符串应该由它决定。

所有C字符串都是顺序存储器中的一个字节,它不包含NUL字符;

NUL (发音类似于 nool )是我们赋予内存中值为0的字节值的名称。在C中,这由{{表示1}}。因此,如果我写 NUL ,则意味着字符\0;

注1:这与C NULL 不同,后者是值为零的内存地址;

注2: NUL 当然不是字符零(&#39; 0&#39;),其值为48;

因此,对字符串起作用的任何函数都会启动char *指向的内存位置(读取char指针);并继续按字节(字符)继续执行其操作字节(字符),直到它为表示字符串结尾的字节的值为0。在那个时候,希望它停止做它正在做的事情,因为字符串已经结束并返回其操作的结果。

因此,如果我们将字符串定义为以0结尾的字符数组,我们完全避免在此之外创建任何人为的字符串概念。

这正是C的作用;它只是将这个概念作为惯例来解决;并且编译器只提供了一个简单的快捷方式来声明使用双引号终止NUL的字符数组。 C中的字符串没有特殊类型。

因此,考虑到所有这一切,让我们看看您的代码:

\0

您声明了两个单字节数组(char)并使用\ 0终止它们; 这对于以下C语句是IDENTICAL:

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

在64位英特尔计算机上运行的Linux计算机上编译时,您的配对和上面的配对都会发出以下(相同的)机器代码输出:

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

如果你正在使用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!. 个字节。在你的情况下,你在数组中明确声明了它;在第二种情况下,当发出与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结尾的字符数组):

\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; ... ^^^^^^^^^ 是一个char指针。在开始时,我们将其指向p(这是运行程序时变量hello在内存中加载的位置)并检查此内存位置的值(由hello获得)&#39 ; s等于&#39; \ 0&#39;与否:

*p

如果我们不进行 for (p = str; *p != 0; ...) ^^^^^^^ 循环,因为条件为真;在我们的案例for中,我们进入循环:

*p=='H'

现在我们先做增量/减量/其他事情。但在这种情况下, for (p = str; *p != 0; p++) ^^^ 运算符后缀++;所以p(这是一个内存地址)将在循环语句的END处增加其值;所以现在循环进入p进行它的事情,最后发生{ ... },我们再次进入条件检查:

++

因此,您可以看到这会将 for (p = str; *p != 0; p++) ^^^^^^^ 设置为指向&#39; H&#39;的内存位置。 &#39; E&#39; &#39;升&#39; &#39;升&#39; &#39;○&#39; &#39; \ 0&#39 ;;然后点击&#39; \ 0&#39;它会退出。

连接字符串:

现在我们知道我们想要连接&#34;你好&#34;和&#34;,世界!&#34;。

首先我们需要找到p的结尾,然后我们需要开始坚持&#34;,World!&#34;到最后:

我们知道上面的Hello循环找到你好的结尾;因此,如果我们在其末尾没有执行任何操作,for将指向&#39; \ 0&#39;在*p的末尾是:

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; } 指向&#39; \ 0&#39;在str1的末尾,所以当我们指定*p时,&#39; \ 0&#39;取而代之的是&#39;,&#39 ;;和&#39; \ 0&#39;完全从str1消失,我们必须在最后注入;请注意,我们仍需在结尾处增加*p = *qp,并在q时继续循环。

现在循环结束了,我们坚持了一个&#39; \ 0&#39;最后,因为我们摧毁了我们的那个:

*q != 0

这就是连接。

关于记忆的重要部分

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

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

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

0000000006

将使str恰好为7个字节。

但我们可以要求C为char str[] = "Foofoo"; 提供更多空间,而不仅仅是初始化值。例如,

str

这将给char str[20] = "Foofoo"; 20个字节,并将前七个设置为&#34; Foofoo \ 0&#34;。其余的通常也设置为str;

所以上面的反汇编看起来像是:

\0

记住在C中你必须像电脑一样思考。如果你没有明确要求记忆,你就不会拥有记忆。因此,如果我们要进行连接,我们必须使用一个足够大的数组,因为我们明确地声明了它:

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                                         ....

或者我们使用 char foo[1000]; /* Lots of room */ (另一篇帖子的主题)在运行时请求内存位置。

让我们看看一个有效的解决方案:

concat.c:

malloc

在Linux上反汇编:

如果你将上面的内容编译成JUST一个目标文件并且不将它链接到一个可执行文件,你就会把事情弄得一团糟:

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

您可以使用对象转储反汇编concat.o:

  gcc -c concat.c -o concat.o

你会注意到转储中涉及printf语句的很多不必要的代码:

  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>

一次。现在,您将获得更清晰的输出;

gcc -O3 -c concat.c -o concat.o 删除了一些帧指针(MUCH后期主题)相关指令,汇编程序将特定于您的代码库:

以下是使用上面编译并使用以下方法转出的concat.o输出:

-O3

答案 1 :(得分:2)

仅为hello分配6个字节的内存。因此,尝试为新的连接字符串创建新内存。

请参阅here了解strcat()实施。

答案 2 :(得分:2)

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

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

C将负责为您终止它们。

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

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

这将首先将source当前指向的char分配给destination,然后递增两个指针(仅指针,而不是内部指针)。这是因为取消引用优先于增量。两个指针并行递增。

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

有一次,他们会评估\0 while循环评估为false并且它将停止复制它们(因为表达式将不再评估为true )。

由于这(和strcat())被认为有些不安全,请确保在执行此操作之前在目的地有足够的空间。或者使用strncat(),你可以限制复制应该持续多长时间(如果字符串不是空终止的,你可以让它'r'无限制地说,可能会发生不好的事情。)

您可以像这样使用上述内容:

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!”。

答案 3 :(得分:2)

您可以通过分配足够的内存来解决超出数组范围的访问...

char hello[14] = "Hello";

答案 4 :(得分:0)

您正试图将数据存储在该数组的范围之外。

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

char可以存储多少hello个?{1}}我们来看看。

#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);
}