snprintf用于字符串拼接

23

我正在使用snprintf将一个字符串连接到一个字符数组:

char buf[20] = "";
snprintf(buf, sizeof buf, "%s%s", buf, "foo");
printf("%s\n", buf);
snprintf(buf, sizeof buf, "%s%s", buf, " bar");
printf("%s\n", buf);
问题在于第二个将"bar"连接到buf的操作,而不是将其添加到"foo"后面。输出结果如下:
foo
bar

第一个%s应该保持buf(在这种情况下它保存了"foo")。而第二个%s应该附加"bar"。对吗?

我做错了什么?


4
对于 C++,你应该咬紧牙关,直接使用 std::string。当然,这并不能解决你关于 C 部分的问题。 - paxdiablo
1
@Scooter:我不在乎有多少人同意你的观点。在互联网上找到三个错误的人并不难。专家共识是标签应该与使用的编译器相匹配。 - Ben Voigt
@BenVoigt,你能告诉我发帖人说他们正在使用C++编译器的那一行吗?而且很容易找到“专家”犯错,比如那些建议给他们知道无法偿还的人提供抵押贷款的人。我要警告你不要盲目地认为任何关于如何使用C标准库例程的问题都是C++用户想看到的。 - Scooter
1
@Jermin:不要这样做。根据您的评论,它实际上并没有编译为C++,现在我将删除c ++标签。 - Ben Voigt
“如果你不彻底了解C语言,怎么能成为C++专家呢?” 很容易。C++专家应该熟悉C++标准库,并建议使用std::string+=运算符(或append函数)进行字符串连接。他们不会知道任何关于snprintf的东西,肯定不会推荐使用它。C和C++之间有很多差异,特别是对于字符串和标准算法等方面。在提问时请打上相应的标签以获取您想要的答案! - Cody Gray
显示剩余4条评论
4个回答

35
你违反了 snprintfrestrict 契约,该契约规定没有其他参数可以重叠缓冲区。
将输入复制到自身本身也是浪费精力的。无论如何,snprintf 返回要格式化的字符数,因此可以利用这一点进行追加:
char buf[20] = "";
char *cur = buf, * const end = buf + sizeof buf;
cur += snprintf(cur, end-cur, "%s", "foo");
printf("%s\n", buf);
if (cur < end) {
    cur += snprintf(cur, end-cur, "%s", " bar");
}
printf("%s\n", buf);

1
有没有相关的文档可以供我参考,了解这个“restrict”合约? - Jermin Bazazian
1
+= 的问题在于它不安全(如果有这样的短语)。我正在使用上述方法来扩展 buf 的大小,以防止溢出。 - Jermin Bazazian
1
@JerminBazazian- buf 是静态分配的,你无法扩展它。你能解释一下你的意思吗? - bta
1
@Jermin:snprintf不会溢出。你需要给它缓冲区中剩余的空间,它不会写入多于这些字符的内容。 - Ben Voigt
1
@BenVoigt:这正是我使用snprintf的原因。你是在使用+=进行连接,对吗?这就是我所说的不安全溢出。 - Jermin Bazazian
显示剩余7条评论

4

试试这个:

char buf[20];
snprintf(buf, sizeof buf, "%s", "foo");
printf("%s\n", buf);
int len = strlen(buf);
snprintf(buf+len, (sizeof buf) - len, "%s", " bar");
printf("%s\n", buf);

输出结果为“foo bar”。snprintf的第一个参数是指向字符的指针,它是开始填充字符的位置。它不关心缓冲区中已经有什么内容。但是函数strlen会关心,它计算在nul(0)之前snprintf放置的字符数。因此,不要传递buf,而是传递buf+strlen(buf)。您还可以使用strncat,这样会更有效率。
我看到你的问题标记了C++标签。请查找std::string。那更好。

3

虽然已接受的答案还可以,但更好的(我个人认为)答案是连接字符串是错误的。您应该在单个 snprintf 调用中构建整个输出。这就是使用格式化输出函数的全部意义,而且比进行指针算术和多次调用更有效率和安全。例如:

snprintf(buf, sizeof buf, "%s%s%s", str_a, str_b, str_c);

我在curl中使用这种方法作为写入回调,并且所有字符串都事先不知道。 我每次有两个字符串。现有缓冲区和要连接的新缓冲区。因此,它必须是多个调用而不是一个。 - Jermin Bazazian
5
这个想法完全不兼容循环和条件语句。所以我认为它实际上并没有太大帮助,除非在非常有限的情况下。 - Ben Voigt
@BenVoigt:我认为凭经验而言,“有限的情况”涵盖了实际使用的相当大一部分,除非代码被编写成为简单高效而不是过度抽象。如果可以直接使用静态格式字符串构建字符串,而用循环和条件语句构建字符串只会让阅读代码的人更难理解它在做什么。当然也有很多例外情况(例如,如果要格式化“自然”为数组或复杂数据结构的数据),但是OP的代码只是简单的连续调用。 - R.. GitHub STOP HELPING ICE
2
@R..:我非常确定真正的代码中不包含“foo”和“bar”。这很像是一个最小化的复制。 - Ben Voigt

2

为什么不使用 strncat()?它专门设计用于此目的:

char buf[20] = "";
strncat(buf, "foo", sizeof buf);
printf("%s\n", buf);
strncat(buf, " bar", sizeof buf - strlen(buf));
printf("%s\n", buf);

如果您的系统支持,可以使用strncat_s()代替strncat,因为它具有额外的溢出保护级别,并避免了计算输出缓冲区中剩余字节数的需要。
如果您必须使用snprintf,则需要创建一个单独的指针来跟踪字符串的末尾。这个指针将是您传递给snprintf的第一个参数。您当前的代码总是使用buf,这意味着它将始终打印到该数组的开头。您可以使用strlen在每个snprintf调用后找到字符串的末尾,或者您可以使用snprintf的返回值来增加指针。

正如这里所建议的,snprintf是最干净的方法。虽然我不太确定为什么。我的意思是strncat也是安全的。 - Jermin Bazazian
1
@JerminBazazian- 在旧的(C99之前的)系统上推荐使用snprintf,因为strncat不可用且最好的选择是strcat,但它存在溢出问题。而strncat则没有这个问题。 - bta
我认为使用snprintf是因为它提供了许多格式化功能,而strncat则没有。 - Ben Voigt
1
我不确定,但我认为您的缓冲区大小存在一个偏移错误,因为根据您提供的链接,它不包括始终附加的NUL字节。 - Ben Voigt
strncat并不是为了保证输出缓冲区的安全而设计的。它是为了将非C字符串文本数据(来自固定大小的非空终止字段)不安全地附加到C字符串中而设计的。 - R.. GitHub STOP HELPING ICE
显示剩余3条评论

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