snprintf()函数总是以空字符结尾吗?

111

snprintf是否总是以空字符结尾目标缓冲区?

换句话说,这个是否足够:

char dst[10];

snprintf(dst, sizeof (dst), "blah %s", somestr);

或者你必须这样做,如果somestr足够长?

char dst[10];

somestr[sizeof (dst) - 1] = '\0';
snprintf(dst, sizeof (dst) - 1, "blah %s", somestr);

我对标准规定以及一些流行的libc可能会做出与标准不同的行为都感兴趣。


注:libc指C语言标准库。

在第二个例子中,你是指将 somestr 或 dst 置为 null 终止吗? - Hudson
@chux,Martin Ba在被接受的答案中已经涵盖了这一点。 :) - Prof. Falken
@chux 我认为它很好,你的评论让它非常清楚:如果 dest i 0 long,则什么也不会被写入。我将每个评论都视为与 fellow stackoverflowers 聊天的潜在借口。 :) - Prof. Falken
1
@Prof. Falken同意该评论是可以的和明确的,但它与答案重复了 - 我在审查中忽略了这一点。 - chux - Reinstate Monica
Visual Studio现在支持*snprintf()*函数。 - Prof. Falken
5个回答

91
如其他答案所述:它应该

snprintf ... 将结果写入字符字符串缓冲区。(...) 除非buf_size为零,否则将以空字符终止。

因此,你要注意的是不要向它传递大小为零的缓冲区,因为(显然)它无法向“无处”写入零。
然而,请注意小心,Microsoft 的库中没有称为snprintf的函数,而是历史上只有一个名为_snprintf(注意前导下划线)的函数,它不会追加终止符号。这里是文档 (VS 2012, ~~ VS 2013):

http://msdn.microsoft.com/en-us/library/2ts7cx93%28v=vs.110%29.aspx

返回值

令 len 为格式化数据字符串的长度(不包括终止空值)。对于_snprintf,len 和count以字节计算;对于_snwprintf,宽字符数。

  • 如果len < count,则在缓冲区中存储 len 个字符,附加一个空终止符号,并返回 len。

  • 如果len = count,则在缓冲区中存储 len 个字符,不会追加终止符号,并返回 len。

  • 如果len > count,则在缓冲区中存储 count 个字符,不会追加终止符号,并返回负值。

Visual Studio 2015(VC14)显然引入了符合规范的snprintf函数,但具有前导下划线和非空终止行为的旧版仍然存在:

snprintf函数在len大于或等于count时截断输出,通过在buffer[count-1]放置一个空终止符。 (...)

对于所有不是snprintf的函数,如果len = count,则将len个字符存储在buffer中,不会追加空终止符,(...)


30
当微软工程师引入 _snprintf 函数并默默地去除了 snprintf 的一个关键安全特性,使字符串没有以空字符结尾时,阿斯兰在想些什么? - Colin D Bennett
2
@ColinDBennett - 这很奇怪,非常恼人,我不知道是否有人考虑过 :-) - Martin Ba
2
@MartinBa 对不起,我测试的是 template <size_t size> int _snprintf_s(char (&buffer)[size], size_t count, const char *format [, argument] ...); 我还应该提到这只会在使用 /GS(安全检查)编译标志时发生。该函数知道大小、计数和长度。 - sekmet64
4
请注意,mingw64在未指定其他情况下使用微软的_snprintf实现作为“正常”的snprintf。https://nvd.nist.gov/vuln/detail/CVE-2018-1000101 - domenukk
3
@Sajjon 这是一个承认有些愚蠢(也许是完全原创的)的沮丧呼喊(https://idioms.thefreedictionary.com/in+the+name+of+God),可能略带委婉的誓言(https://en.wikipedia.org/wiki/Minced_oath)。另一个例子可能是“What in the name of Zeus...?!”(https://forum.wordreference.com/threads/in-the-name-of-zeus.2132965/)。 - Colin D Bennett
显示剩余7条评论

23
根据snprintf(3)手册。函数snprintf()和vsnprintf()最多将size个字符(包括结尾的空字符('\0'))写入str中。因此,如果size>=1,则无需终止字符串。

5
谢天谢地,这是唯一明智的设计。这些函数被检查的版本的整个重点在于保证安全,如果你必须手动执行所有的终止操作将会很糟糕。 - Kerrek SB
2
我建议在依赖此功能前,先在你正在使用的平台上进行测试。即使它“应该”写入空字节,但我知道有时会遇到没有这样做的实现(可能是在使用旧版 MS 运行库的 MinGW 中)。 - Dmitri
2
我不相信你引用的短语实际上意味着你认为它意味着什么。作为一个以英语为母语的人,它显然意味着只要字符串及其空终止符在大小范围内或以下,就会全部被写入。但是,单凭这句话本身绝对不能描述如果大小不足以写入所有内容时所写的内容。 - Swiss Frank

15
根据C标准,除非缓冲区大小为0,否则vsnprintf()snprintf()都将在其输出末尾添加空字符。

snprintf()函数等同于sprintf()函数,并增加了一个n参数来说明s所引用的缓冲区大小。如果n为零,则不会写入任何东西,s可以是空指针。否则,超过第n-1个字符的输出字节将被丢弃而不是被写入数组,并且在实际写入数组的字节末尾写入空字符。

因此,如果您需要知道要分配多大的缓冲区,请使用大小为零,并使用空指针作为目标。请注意,我链接到了POSIX页面,但这些页面明确表示在涵盖相同内容时,标准C和POSIX之间没有意图产生任何分歧:

本参考页面描述的功能与ISO C标准一致。在此处描述的要求与ISO C标准之间的任何冲突均是无意的。此POSIX.1-2008卷遵循ISO C标准。

要注意微软版本的vsnprintf()。当缓冲区空间不足时,它与标准C版本显然有所不同(返回-1,而标准函数返回所需长度)。在错误情况下,微软版本是否完全将其输出置空并不清楚,而标准C版本则会这样做。

请注意,自本答案最初编写以来,Microsoft已更改了规则(vsnprintf()):

从Visual Studio 2015和Windows 10中的UCRT开始,vsnprintf不再与_vsnprintf完全相同。 vsnprintf函数符合C99标准; _vnsprintf保留了与旧版Visual Studio代码的向后兼容性。

snprintf()sprintf()也有类似的情况。

还要注意Do you use the TR 24731 safe functions?的答案(参见MSDN 上的Microsoft版本的vsprintf_s())和Mac solution for the safe alternatives to unsafe C standard library functions?


哦,那真是太棒了,我从未想过这一点。另一方面... :) - Prof. Falken
啊,我觉得 MS 的 vsprintf() 坑了我,然后我养成了那个 -1 的习惯。 - Prof. Falken

5

一些旧版本的SunOS在使用snprintf函数时可能会出现奇怪的问题,可能没有将输出以NUL结尾,并且返回值与其他系统不同,但是过去10年中发布的任何版本都遵循C99标准。


我注意到XP发布已经超过10年了。 :-) - Prof. Falken
而今年它已经过时了。 :) - Prof. Falken

3
歧义始于C标准本身。 C99和C11都具有相同的snprintf函数描述。以下是来自C99的描述:
7.19.6.5 snprintf函数
概要 1 #include int snprintf(char * restrict s, size_t n, const char * restrict format, ...);
描述 2 snprintf函数等效于fprintf,不同之处在于输出写入数组(由参数s指定),而不是流。如果n为零,则不会写入任何内容,并且s可以是空指针。否则,超出第n-1个字符的输出将被丢弃,而不是写入数组,并且在实际写入数组的字符末尾写入空字符。如果在重叠对象之间进行复制,则行为未定义。
返回值 3 snprintf函数返回将写入的字符数(不包括终止的空字符),如果发生编码错误,则返回负值。因此,当且仅当返回值为非负且小于n时,已完全写入以空字符结尾的输出。
一方面,以下语句
“否则,超出第n-1个字符的输出将被丢弃,并在实际写入数组的字符末尾写入空字符”
表示,如果(s指向一个长度为3的数组并且)n为3,则将写入2个字符,并且第2个字符之后的字符将被丢弃;然后,在这些字符之后写入空字符(空字符将是第3个写入的字符)。
我相信这回答了原始问题。
答案: 如果在重叠对象之间进行复制,则行为未定义。 如果n为0,则不会将任何内容写入输出; 否则,如果没有遇到编码错误,则输出始终以空字符结尾(无论输出是否适合输出数组;如果不适合,则丢弃一些字符,以使输出数组永远不会溢出); 否则(如果遇到编码错误),输出可能保持非以空字符结尾的状态。
另一方面,最后一句话
因此,仅当返回的值为非负且小于 n 时,空字符结尾的输出才已完全写入。这句话存在歧义(或者说是我的英语不够好)。我可以用至少两种方式来解释这个句子:
1. 当且仅当返回的值为非负且小于 n 时,输出才以空字符结尾(这意味着如果返回的值小于 n,即输出(包括终止的空字符)无法适应数组大小,则输出没有以空字符结尾)。
2. 当且仅当返回的值为非负且小于 n 时,输出完整(没有任何字符被丢弃)。

我认为上述解释1与答案相矛盾,会导致误解和冗长的讨论。这就是为什么最后一个描述 snprintf 函数的句子需要更改,以消除任何歧义(从而提出向C语言标准提案的理由)。
我认为可以从http://en.cppreference.com/w/c/io/fprintf中采用无歧义措辞的例子(请参见4),感谢 @"Martin Ba" 提供的链接。
另请参见问题“snprintf:是否有C标准提案/计划更改此函数的描述?”。

5
我不认为你的解释1有任何可信度。我从这句话中理解到:“如果输出(顺便提一下,它是以 null 作为结尾的)已经完全被写入,那么......”,而我只能理解成#2。 - zwol
1
“null-terminated output has been completely written”的否定形式是“null-terminated output没有被完全写入”。仅此而已。这个否定句本身并不意味着任何东西都已经被写入(包括不完整的空终止输出,不完整的非空终止输出或无色的绿色想法)。标准中的其他地方说明了当输出不完整时究竟写入了什么内容,并且那个地方指出,除非为空(n == 0),否则输出将以空终止。 - n. m.

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