如何在C语言中连接常量/字面字符串?

425

我正在使用C语言编程,需要将一些东西拼接起来。

目前我有以下代码:

message = strcat("TEXT ", var);

message2 = strcat(strcat("TEXT ", foo), strcat(" TEXT ", bar));

如果你有C语言的经验,你肯定知道当你运行这段代码时会产生分段错误。那么我该如何解决这个问题?


9
建议您使用 strlcat 而不是 strcat!http://www.gratisoft.us/todd/papers/strlcpy.html - activout.se
7
我想重申一下那个建议。Strcat会导致缓冲区溢出漏洞,有人可能给你的程序提供数据,导致它执行任意代码。 - Brian
17个回答

475
在C语言中,“字符串”只是普通的char数组。因此,您不能直接将它们与其他“字符串”连接起来。
您可以使用strcat函数,该函数将src指向的字符串附加到dest指向的字符串的末尾:
char *strcat(char *dest, const char *src);

这里是一段来自cplusplus.com的示例

char str[80];
strcpy(str, "these ");
strcat(str, "strings ");
strcat(str, "are ");
strcat(str, "concatenated.");

对于第一个参数,您需要提供目标缓冲区本身。目标缓冲区必须是一个 char 数组缓冲区。例如:char buffer[1024];

确保第一个参数有足够的空间来存储您要复制到其中的内容。如果可用,则更安全地使用像strcpy_sstrcat_s这样的函数,您明确需要指定目标缓冲区的大小。

注意:不能将字符串字面值用作缓冲区,因为它是常量。因此,您总是需要为缓冲区分配 char 数组。

strcat 的返回值可以简单地忽略,它仅返回与作为第一个参数传递的指针相同的指针。它只是为了方便起见,并允许您将调用链接成一行代码:

strcat(strcat(str, foo), bar);

那么你的问题可以如下解决:

char *foo = "foo";
char *bar = "bar";
char str[80];
strcpy(str, "TEXT ");
strcat(str, foo);
strcat(str, bar);

78
请问您能否让我将“Be very careful that…”加粗?这点非常重要。误用strcat、strcpy和sprintf是不稳定/不安全软件的根源。 - plinth
14
警告:按照这样写的话,这段代码将为缓冲区溢出攻击留下一个巨大的漏洞。 - Brian
14
上述示例中不存在缓冲区溢出攻击。是的,总的来说我同意如果foo和bar的字符串长度不确定,就不应该使用上述示例。 - Brian R. Bondy
2
不要多次使用 strcatstrcat 必须检查你已经连接的所有前面的字节(搜索 '\0'),这是无用的处理。 - dolmen
23
同意@dolmen的观点,Joel Spolsky针对这个问题写了一篇相当详细的文章,应该强制阅读。;-) - peter.slizik
显示剩余4条评论

292

避免在C代码中使用strcat。最清晰,也是最重要的安全方式是使用snprintf

char buf[256];
snprintf(buf, sizeof(buf), "%s%s%s%s", str1, str2, str3, str4);

一些评论者提出了一个问题,即参数的数量可能与格式字符串不匹配,但代码仍将编译通过,但是大多数编译器已经在这种情况下发出警告。


3
他说的是在sizeof参数的“buf”周围加括号不是必要的,如果参数是一个表达式。但我不明白为什么你被downvote了。我认为你的答案是最好的,尽管它是c99。(也许因为这个他们不同意!蠢货!)+1 - Johannes Schaub - litb
7
sizeof() 只适用于 char buf[...] 这种数组类型,而不适用于 char * buf = malloc(...) 这种指针类型。虽然数组和指针之间并没有太大的区别,但这是其中之一! - Mr.Ree
2
此外,他正在尝试执行串联操作。使用 snprintf() 进行串联是绝对不可取的。 - Leonardo Herrera
6
指针和数组之间的差异是广泛而完整的!它们之间的区别在于如何使用它们,并不总是不同的。此外,指针和动态内存分配真正是正交的概念。 - Lightness Races in Orbit
45
我最讨厌的事情之一就是像@unwind这样的人坚持毫无意义的“sizeof(x)”和“sizeof x”的区别。加上括号的表示法总是有效的,而不加括号的表示法只有在某些情况下才有效,因此请始终使用加括号的表示法;这是一个易于记忆且安全的简单规则。这会引发一场宗教性的争论——我曾与那些反对者进行过讨论——但“始终使用括号”的简洁性超越了不使用它们的任何优点(当然,这只是我的个人看法)。这里提供平衡观点。 - Jonathan Leffler
显示剩余13条评论

52

字符串也可以在编译时连接。

#define SCHEMA "test"
#define TABLE  "data"

const char *table = SCHEMA "." TABLE ; // note no + or . or anything
const char *qry =               // include comments in a string
    " SELECT * "                // get all fields
    " FROM " SCHEMA "." TABLE   /* the table */
    " WHERE x = 1 "             /* the filter */ 
                ;

31

使用strncpy()、strncat()或snprintf()。
超出缓冲区空间将破坏后面的内存内容!
(记得为尾随的空字符'\0'留出空间!)


3
你不仅需要记得为 NULL 字符留出空间,还要记得添加NULL字符。strncpy 和 strncat 不会为你自动添加它。 - Graeme Perrow
3
@unwind,我认为Graeme的观点是,如果缓冲区太小,strncpy或strncat将不会添加终止符“\0”。 - quinmars
更好的是,建议使用OpenBSD的strlcpy、strlcat函数(容易移植到你自己的项目中)。 - Alex B
2
snprintf很好,strncpy/strncat是最糟糕的建议,strlcpy/strlcat要好得多。 - Robert Gamble
11
不要使用strncpy()。它并不是strcpy()的“更安全”的版本。目标字符数组可能会不必要地填充额外的'\0'字符,或者更糟糕的是,可能会没有终止符(即,不是一个字符串)。(它是为一个现在很少用到的数据结构设计的,该数据结构是以零个或多个'\0'字符填充到末尾的字符数组。) - Keith Thompson
显示剩余4条评论

16

如果你事先不知道有多少字符串要连接,那么malloc和realloc非常有用。

#include <stdio.h>
#include <string.h>

void example(const char *header, const char **words, size_t num_words)
{
    size_t message_len = strlen(header) + 1; /* + 1 for terminating NULL */
    char *message = (char*) malloc(message_len);
    strncat(message, header, message_len);

    for(int i = 0; i < num_words; ++i)
    {
       message_len += 1 + strlen(words[i]); /* 1 + for separator ';' */
       message = (char*) realloc(message, message_len);
       strncat(strncat(message, ";", message_len), words[i], message_len);
    }

    puts(message);

    free(message);
}

1
num_words>INT_MAX时,这将导致无限循环,也许你应该使用size_t来代替i - 12431234123412341234123

7

如果不想限制缓冲区大小,最好使用asprintf()方法。

char* concat(const char* str1, const char* str2)
{
    char* result;
    asprintf(&result, "%s%s", str1, str2);
    return result;
}

2
你应该返回 char *,而不是 const char *。返回值需要传递给 free - Per Johansson
2
很遗憾,asprintf 只是 GNU 的扩展。 - Calmarius

6
如果您有C语言经验,那么您会注意到字符串只是char数组,其中最后一个字符是空字符。
现在这很不方便,因为您必须找到最后一个字符才能添加内容。`strcat`可以帮助您实现这一点。
所以`strcat`会在第一个参数中搜索空字符。然后它将用第二个参数的内容替换它(直到它以空字符结尾)。
现在让我们来看看您的代码:
message = strcat("TEXT " + var);

在这里,您正在将某些内容添加到指向文本“TEXT”的指针上(“TEXT”的类型是const char*,即指针类型)。

通常情况下,这样做是行不通的。修改“TEXT”数组也不起作用,因为它通常位于常量段中。

message2 = strcat(strcat("TEXT ", foo), strcat(" TEXT ", bar));

这种方法可能更好,但您再次尝试修改静态文本。strcat没有为结果分配新内存。

我建议改为以下方式:

sprintf(message2, "TEXT %s TEXT %s", foo, bar);

阅读 sprintf 的文档以查看其选项。

现在是一个重要的点:

确保缓冲区有足够的空间来容纳文本和空字符。有几个函数可以帮助你,例如 strncat 和特殊版本的 printf 可为您分配缓冲区。 不确保缓冲区大小将导致内存损坏和远程可利用的错误。


“TEXT”的类型是char[5]不是 const char*。在大多数情况下,它会衰变为char*。出于向后兼容性的原因,字符串字面值不是const,但尝试修改它们会导致未定义的行为。(在C++中,字符串字面值是const。) - Keith Thompson

5
不要忘记初始化输出缓冲区。strcat的第一个参数必须是以空字符结尾的字符串,为了得到结果字符串,还需分配足够的额外空间:
char out[1024] = ""; // must be initialized
strcat( out, null_terminated_string ); 
// null_terminated_string has less than 1023 chars

5

企图修改字符串字面量是未定义行为,而这正是类似以下代码的行为:

strcat ("Hello, ", name);

这段代码尝试做某件事情。它会试图将name字符串添加到字符串字面量"Hello, "的末尾,但这是不明确的。

你可以尝试这样做,它能实现你想要的效果:

char message[1000];
strcpy (message, "TEXT ");
strcat (message, var);

这将创建一个缓冲区,允许进行修改,并将字符串文字和其他文本都复制到其中。但是要小心缓冲区溢出。如果你控制输入数据(或在使用前检查它),那么使用固定长度的缓冲区是可以的。

否则,你应该采用缓解策略,例如从堆中分配足够的内存来确保你可以处理它。换句话说,类似于:

const static char TEXT[] = "TEXT ";

// Make *sure* you have enough space.

char *message = malloc (sizeof(TEXT) + strlen(var) + 1);
if (message == NULL)
     handleOutOfMemoryIntelligently();
strcpy (message, TEXT);
strcat (message, var);

// Need to free message at some point after you're done with it.

4
如果var/foo/bar超过1000个字符会发生什么?> :) - Geo
1
然后您将得到一个缓冲区溢出,您可以先添加代码进行检查(例如使用strlen)。但是代码片段的目的是展示某些内容的工作原理,而不会用太多额外的代码来污染它。否则,我将要检查长度、变量/ foo / bar是否为null等。 - paxdiablo
7
但是你在需要提到的问题的回答中甚至没有提及它,这使得你的回答变得危险。此外,你也没有解释为什么这段代码比原始代码更好,除了说它“可以实现与你的原始代码相同的结果”(那还有什么意义呢?原始代码是_有问题的_!),因此这个答案也是不完整的 - Lightness Races in Orbit
希望我解决了您的问题,@PreferenceBean,尽管时间上不太理想 :-) 如果您对答案仍有疑问,请告诉我,我会进一步改进它。 - paxdiablo

5

正如人们所指出的,字符串处理已经得到了很大的改进。因此,您可能希望学习如何使用C ++字符串库,而不是C风格的字符串。然而,这里有一个纯C的解决方案。

#include <string.h>
#include <stdio.h>
#include <stdlib.h>

void appendToHello(const char *s) {
    const char *const hello = "hello ";

    const size_t sLength     = strlen(s);
    const size_t helloLength = strlen(hello);
    const size_t totalLength = sLength + helloLength;

    char *const strBuf = malloc(totalLength + 1);
    if (strBuf == NULL) {
        fprintf(stderr, "malloc failed\n");
        exit(EXIT_FAILURE);
    }

    strcpy(strBuf, hello);
    strcpy(strBuf + helloLength, s);

    puts(strBuf);

    free(strBuf);

}

int main (void) {
    appendToHello("blah blah");
    return 0;
}

我不确定这样做是否正确/安全,但目前在ANSI C中我找不到更好的方法。


<string.h> 是 C++ 风格。你需要 "string.h"。你还计算了 strlen(s1) 两次,这是不必要的。s3 应该有 totalLenght+1 的长度。 - Mooing Duck
6
@MooingDuck说:"string.h"是无意义的。 - sbi
7
@MooingDuck: 那是不正确的。#include <string.h> 才是正确的 C 语言方式。使用尖括号来包含标准和系统库头文件(包括 <string.h>),使用引号来包含你自己程序中的头文件。(如果你没有一个同名的头文件,#include "string.h" 也能工作,但总之还是要使用 <string.h>。) - Keith Thompson
请注意,这取决于C99特定的功能:混合声明和语句以及可变长度数组(VLAs)。还要注意,VLAs不提供检测或处理分配失败的机制;如果没有足够的空间来分配VLA,则程序的行为是未定义的。 - Keith Thompson
已发布修复版本,请重新考虑您的投票。 - Nils
显示剩余4条评论

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接