确定sprintf缓冲区大小 - 有什么标准吗?

52

当进行如下整数转换时:

char a[256];
sprintf(a, "%d", 132);

确定变量 a 应该多大的最佳方法是什么?我认为手动设置它是可以的(因为我已经在很多地方看到了这样的做法),但应该设置多大呢?32位系统上最大的整数值是多少,有没有一些巧妙的方法可以动态计算它?


8
@Robert: 如果可以使用Python的话,那么你可以使用str ;-p - Steve Jessop
3
@Robert: 这是一项大学作业。在这门课之前,我讨厌C语言。现在我喜欢它的简单易懂。虽然它不太容错,但非常令人满意,因为我通常只涉及Python/托管语言,所以需要花费很多时间去学习。 - Dominic Bou-Samra
3
我认为,如果你不得不思考这种问题,除了C语言或汇编语言之外,没有其他语言能够满足你的要求。任何其他语言都会有巨大难以预测的堆栈使用、堆碎片等问题。 - R.. GitHub STOP HELPING ICE
4
在GNU世界中,有一个名为asprintf的函数,它会内部调用malloc来分配所需的内存空间。 - utopianheaven
相关:https://dev59.com/_W865IYBdhLWcg3whe5f - Ciro Santilli OurBigBook.com
显示剩余6条评论
7个回答

110

一些人认为这种方法过于复杂,对于将整数转换为字符串,我可能更倾向于赞同他们的观点。但是当找不到合理的字符串大小限制时,我见过这种方法被使用,并且我自己也使用过。

int size = snprintf(NULL, 0, "%d", 132);
char * a = malloc(size + 1);
sprintf(a, "%d", 132);

下面我会解释这里发生了什么。

  1. 在第一行中,我们需要确定需要多少个字符。调用snprintf函数时,前两个参数告诉它将结果写入NULL指针的0个字符中。这样做时,snprintf不会实际写入任何字符,只会返回本应写入的字符数。这正是我们想要的。
  2. 在第二行中,我们动态分配了一个char指针所需的内存。确保需要的大小加1(因为有一个结尾的\0终止字符)。
  3. 现在,由于已经分配了足够的内存到char指针上,我们可以安全地使用sprintf将整数写入char指针。

当然,如果您愿意,您可以让它更简洁。

char * a = malloc(snprintf(NULL, 0, "%d", 132) + 1);
sprintf(a, "%d", 132);

除非这是一个“快速而肮脏”的程序,否则您总是要确保释放使用malloc调用的内存。这就是使用C进行动态处理变得复杂的地方。然而,在我看来,如果您不想在大多数情况下只使用很小一部分时分配巨大的char指针,则我认为这不是坏方法。


实际上,你经常可以使用alloca而不是malloc。如果生成的代码仍然太臃肿,可以为其制作一个宏。 - thejh
5
alloca函数的可移植性如何?它显然不符合ANSI C标准。 - Daniel Standage
4
那么,C99版本的alloca函数的替代方案就是使用可变长度数组,对吧?比如 int size = ...; char a[size+1]; sprintf(... - Tommy
2
不要忘记处理malloc()返回NULL的情况,除非您不介意程序崩溃或不安全。特别是当大小取决于程序外部输入时,这一点尤为重要。 - Jeff Learman
1
绝对+1,我不明白为什么这不是被接受的答案。它恰好就是手册所解释的内容。 - Marco Bonelli

20

使用C++11/C99中的vsnprintf函数,可以使Daniel Standage的解决方案适用于任意数量的参数。

int bufferSize(const char* format, ...) {
    va_list args;
    va_start(args, format);
    int result = vsnprintf(NULL, 0, format, args);
    va_end(args);
    return result + 1; // safe byte for \0
}

根据c99标准第7.19.6.12节的规定:

vsnprintf函数返回要写入(不包括终止null字符)的字符数,假设n足够大,或者在编码错误时返回负值。


这是纯C99。 - fshp
谢谢。我甚至没有注意到。我已经更新了我的答案。 - Regis Portalez
2
_vscprintf是更好的解决方案。请查看-https://dev59.com/kGox5IYBdhLWcg3wLhii#9369242 - 23W
6
_vscprintf 是仅适用于 Microsoft 的函数。 - Regis Portalez

13

int类型中可能的最大位数为CHAR_BIT * sizeof(int),而一个十进制数字至少“值”3位,因此对于任意int所需的空间的上限为(CHAR_BIT * sizeof(int) / 3) + 3。+3是由于在除法运算时向下取整导致,其中一个用于符号位,一个用于结束符。

如果你说“在32位系统上”,意思是你知道int是32位,则需要12个字节。其中10个字节用于存储数字,一个用于存储符号,一个用于结束符。

对于你特定的情况,即要转换的int132,则需要4个字节。打鼓声。

如果可以使用有合理边界的固定大小缓冲区,则它们是更简单的选项。我并不谦虚地认为上面提到的限制是合理的(32位int为13个字节而不是12个字节,64位int为23个字节而不是21个字节)。但对于复杂的情况,在C99中您只需调用snprintf来获取大小,然后malloc相应的内存。对于这种简单的情况这样做有些过头了。


2
使用malloc是荒谬的。它通过添加必须检查的失败情况来使您的代码过于复杂化--如果它失败了,那么你该怎么办?!?只需像您解释的那样使用正确大小的缓冲区即可。 - R.. GitHub STOP HELPING ICE
3
如果编写代码的人对语言本身的基础知识感兴趣或想要了解它的底层原理,使用malloc并不荒谬。告诉别人只需使用std::string因为所有的艰苦工作都已经完成,这是无知的,即想要了解底层实现方式。也许原帖发布者想知道std::string是如何实现的? - Eric
4
@Eric:这个问题涉及到C语言,与std::string无关。 - Steve Jessop
8
这很危险——经典的潜在缓冲区溢出场景。printf 函数族的确切输出取决于语言环境。例如,某些语言环境可能设置“千位分隔符”。 - Brett Hale
2
@Brett:你让我有一分钟的迷惑,但是当然%d不使用千位分隔符。%'d会使用,但这不是问题的关键。 - Steve Jessop
显示剩余6条评论

7
我看到这个对话已经有几年了,但我在寻找MS VC ++的答案时发现了它,其中无法使用snprintf来查找大小。我将发布我最终找到的答案,以帮助其他人:

VC++具有_scprintf函数,专门用于查找所需字符数。


2
如果您要打印一个简单的整数,且没有其他内容,那么有一种更简单的方法来确定输出缓冲区的大小。计算上至少更简单些,但代码有点晦涩:
char *text;
text = malloc(val ? (int)log10((double)abs(val)) + (val < 0) + 2 : 2);

log10(value)返回在基数为10的情况下存储正非零值所需的数字位数(减1)。对于小于一的数字,它会有点偏离轨道,因此我们指定abs(),并为零编写特殊逻辑(三元运算符,test?truecase:falsecase)。为了存储负数的符号(val <0),添加一个空格,为了弥补log10的差异再加上一个,并且为了使总和达到2个,再添加一个空终止符,这样就可以计算出给定数字所需的确切存储空间,而不必调用snprintf()或等效函数两次来完成任务。此外,它所使用的内存通常比INT_MAX所需的内存更少。

但是,如果您需要非常快速地打印大量数字,请分配INT_MAX缓冲区,然后重复打印到该缓冲区中。较少的内存抖动更好。

还要注意,您可能实际上并不需要(double)而不是(float)。我没有检查。像那样反复转换也可能是个问题。在所有这些方面,您的经验可能会有所不同。但对我来说非常有效。


1
假设您的整数是INT_MIN。那么abs(val)INT_MIN,应用于它的log10返回NaN,并将NaN转换为int是未定义的行为。此外,如果您要打印64位整数,则其中最大的整数不能准确地表示为double - Pascal Cuoq
@Pascal Cuoq -- 有趣的怪癖,谢谢。我想,如果我们尝试为INT_MIN的字符串表示分配存储空间,解决方案并不完整,但我感到懒惰,所以我会把它留给任何真正关心它的人来练习。-- P.S. “未定义行为”是一种轻描淡写。在GCC中,printf(“%d \ n”,(int)log10((double)abs(-2147483647 - 1))); spit出“-2147483648”,而不是预期的“9” - 多么奇怪。 - Pegasus Epsilon

1

很好,你关注了缓冲区大小的问题。为了在代码中应用这个想法,我会使用snprintf

snprintf( a, 256, "%d", 132 );

或者

snprintf( a, sizeof( a ), "%d", 132 );  // when a is array, not pointer

1
snprintf只解决了一半的问题。如果您不确定缓冲区是否足够大,您需要使用snprintf来测试所需的大小,或者确保您的代码在输出被截断时没有错误。Steve的答案更好。 - R.. GitHub STOP HELPING ICE

1

首先,sprintf是个麻烦。如果可以的话,请使用snprintf,否则您可能会破坏内存并使应用程序崩溃。

至于缓冲区大小,它就像所有其他缓冲区一样-尽可能小,必要时尽可能大。在您的情况下,您有一个带符号整数,因此请取最大可能的大小,并随意添加一些安全填充。没有“标准大小”。

它也不取决于您运行的系统。如果您在堆栈上定义了缓冲区(就像在您的示例中),则取决于堆栈的大小。如果您自己创建了线程,则自己确定了堆栈大小,因此您知道限制。如果您期望递归或深度堆栈跟踪,则还需要特别小心。


谢谢。那就用snprintf吧。 - Dominic Bou-Samra
10
使用正确大小的缓冲区来调用 sprintf 函数是没有危险的。对于数值输出,确定缓冲区的大小非常容易,可以参考 Steve 的回答。 - R.. GitHub STOP HELPING ICE
@R.. 假设另一个线程没有在背后破坏一些输入... - tc.
3
在这种情况下,snprintf 无法帮助你。对数据进行不同步访问始终是未定义的行为。如果字符串大小在其下发生更改,snprintf 同样有可能会溢出。 - R.. GitHub STOP HELPING ICE
@R.. 虽然不能保证一定有帮助,但我可以想到明显的情况和实现方式,其中 snprintf() 至少能防止程序崩溃。 - tc.
不一定。snprintf 很可能存在 TOCTOU 竞争,首先检查 strlen,然后执行 strcpy 或类似操作。除非你已经阅读了实现源代码,否则你不知道。如果你的程序如此糟糕以至于进行未同步的数据访问(这是最糟糕的 UB 形式之一,因为它“大多数时间似乎工作正常”),那么在这里使用 snprintf 不会对你的程序的错误产生任何影响。 - R.. GitHub STOP HELPING ICE

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