C语言 有没有更好的方法来调整用于打印整数的缓冲区的大小



我想创建一个缓冲区来sprintf一个整数(在本例中为unsigned int)。一种简单而误导的方法是:

char buf[11];
sprintf(buf, "%u", x);

如果我们知道unsigned int最多只有33位宽,这很有效,但如果我们想适应所有奇怪的架构怎么办?我能想到的最好的是:

char buf[(CHAR_BIT*sizeof(unsigned)+5)/3];
sprintf(buf, "%u", x);

我很有信心这将适用于任何实现。CHAR_BIT*sizeof(unsigned)是(上限)unsigned中的位数。然后我加 2 并除以 3 以找到八进制表示中的位数,最后为 NUL 终止加一。这意味着缓冲区足以以八进制打印数字,并且由于十进制表示使用的数字不超过八进制,因此对于十进制表示也足够了。

有没有更好的方法可以做到这一点?更好的我的意思是,无论x有什么价值,都不会冒缓冲区溢出的风险(即使面对恶意构建但符合标准的实现)。我的方法将为 32 位unsigned生成一个 12char缓冲区,尽管事实上11就足够了。

编译不同的相关注释,最值得注意的是:

  • 数学问题。
  • Martin R的评论总结得很好:"n个二进制数字需要ceil(n*ln(2)/ln(10))≈ceil(n * 0.301)">

你有你的答案:

#define MAX_DECIMAL_SIZE(x)  ((size_t)(CHAR_BIT * sizeof(x) * 302 / 1000) + 1)
char buffer[MAX_DECIMAL_SIZE(unsigned int) + 1];
sprintf(buffer, "%u", x);
/* MAX_DECIMAL_SIZE(uint8_t) => 3
* MAX_DECIMAL_SIZE(uint16_t) => 5
* MAX_DECIMAL_SIZE(uint32_t) => 10
* MAX_DECIMAL_SIZE(uint64_t) => 20
* MAX_DECIMAL_SIZE(__uint128_t) => 39 */

302/1000来自ln(2)/ln(10),四舍五入。你可以从0.3010299956639812…中获取更多的数字以获得更高的精度,但在你使用32768位左右的系统之前,这是矫枉过正的。继续分数也有效(见下面的马丁R的评论)。无论哪种方式,请注意CHAR_BIT * sizeof(x) * <your chosen numerator>不要太大,并记住结果必须大于实际值。

如果你真的坚持八进制表示,只需将乘数更改为ln(2)/ln(8)(即 1/3),你就会有所需的八进制数字数。

如果您同意动态分配的内存,则可以改用asprintf。 此函数将分配适当的内存量来保存字符串。

char *buf;
int result = asprintf(&buf, "%u", x);
if (result == -1) {
perror("asprintf failed");
} else {
...
free(buf);
}

如果数组应该在所有实际计算机上工作,则int可以是 2 或 4 个字节。没有其他替代方法(*)。

这意味着它可以容纳的最大值是 65535 或 4.29*10^9。这反过来意味着您的数组需要容纳 5 位或 10 位数字。

这反过来意味着数组可以声明为:

char buf [sizeof(int)/2 * 5 + 1];

它将扩展到 5+1 或 10+1,涵盖世界上所有已知的计算机。

更好,更专业的解决方案是使用stdint.h中的固定宽度类型。然后,您始终可以提前确切地知道需要多少位数字,因此可以摆脱上述"幻数"。


(*) 在 C 语言标准理论中,int可以是 2 个字节或更大的任何内容。但是,由于现实世界中不会存在这样的系统,因此使您的代码可移植到它们是没有意义的。C 语言已经引入了longlong long是有原因的。

那些担心移植到异国情调的、完全虚构的系统的人是被误导的,他们大多是喜欢摆姿势的C语言律师。你不应该让这种理论上的废话影响你为现实世界的计算机编写专业程序的方式。


编辑

"C 语言律师装腔作势者"版本如下所示:

#include <stdio.h>
#include <limits.h>
#define STRINGIFY(s) #s
#define GET_SIZE(n) sizeof(STRINGIFY(n))
#define DIGITS(type) _Generic((type), unsigned int: GET_SIZE(INT_MAX) )
int main(void) 
{
unsigned int x;
char buf [DIGITS(x)];
printf("%zu", sizeof(buf));
return 0;
}

请注意,这假定INT_MAX扩展为整数常量而不是表达式。使用 GCC 时,我得到了非常奇怪的结果UINT_MAX,因为该宏在内部被定义为表达式,在 limits.h 内。

需要这样的东西的情况很少见:也许是一些微控制器代码,通过某种串行协议传输值。在这种情况下,使用任何printf()系列函数都可能会增加最终二进制文件的大小。

(在典型的 C 开发环境中,C 库是动态加载的,尝试避免使用标准 C 库函数绝对没有任何好处。它不会减小程序大小。

所以,如果我需要这样的代码,我可能会写一个头文件,

#if defined(INTTYPE) && defined (UINTTYPE) && defined (FUNCNAME)
#ifndef DECIMAL_DIGITS_IN
#define DECIMAL_DIGITS_IN(x) ((CHAR_BIT * sizeof (x) * 28) / 93 + 2)
#endif
char *FUNCNAME(const INTTYPE value)
{
static char buffer[DECIMAL_DIGITS_IN(value) + 1];
char       *p = buffer + sizeof buffer;
UINTTYPE    left = (value < 0) ? -value : value;
*(--p) = '';
do {
*(--p) = '0' + (left % 10);
left /= 10;
} while (left > 0);
if (value < 0)
*(--p) = '-';
return p;
}
#undef FUNCNAME
#undef INTTYPE
#undef UINTTYPE
#endif

对于我需要的每种类型,我都会使用

#define FUNCNAME int2str
#define INTTYPE  int
#define UINTTYPE unsigned int
#include "above.h"

在更普通的代码中,最好的方法是使用snprintf()来避免缓冲区溢出,缓冲区大小是"猜测的"。例如

unsigned int x;
char  buffer[256];
int   len;
len = snprintf(buffer, sizeof buffer, "Message with a number %u", x);
if (len < 0 || (size_t)len >= sizeof buffer - 1) {
/* Abort! The buffer was (almost certainly) too small! */
} else {
/* Success; we have the string in buffer[]. */
}

buffer[]是否比必要的大几十个甚至几百个字节,在典型程序中是完全无关紧要的。 只需使其足够大,并在错误情况下输出一条错误消息,告知哪个缓冲区(文件和函数)不够长,因此在不太可能的情况下很容易修复它太短。


正如dbush所提到的,asprintf()GNU扩展是一个可行的选择。它返回动态分配的字符串。

在GNU系统之外 - 这也是我建议OP考虑的 - 一个人可以实现自己的asprintf(),使用vsnprintf()(在 C99 及更高版本的 C 库中可用,在 POSIX.1 C 库中可用)。

我更喜欢类似于 POSIX.1 getline() 的变体,即将指针指向动态分配的缓冲区的指针和该缓冲区的大小作为额外参数,并在必要时调整该缓冲区的大小:

#include <stdlib.h>
#include <stdarg.h>
#include <string.h>
#include <stdio.h>
#include <errno.h>
size_t dynamic_printf(char **dataptr, size_t *sizeptr, const char *format, ...)
{
va_arg  args;
char   *data;
size_t  size;
int     len;
if (!dataptr || !sizeptr || !format) {
errno = EINVAL;
return 0;
}
if (!*sizeptr) {
*dataptr = NULL;
*sizeptr = 0;
}
data = *dataptr;
size = *sizeptr;
va_start(args, format);
len = vsnprintf(data, size, format, args);
va_end(args);
if (len < 0) {
errno = EINVAL;
return 0;
} else
if ((size_t)len < size) {
errno = 0;
return (size_t)len;
}
/* Need to reallocate the buffer. */
size = (size_t)len + 1;
data = realloc(data, size);
if (!data) {
errno = ENOMEM;
return 0;
}
*dataptr = data;
*sizeptr = size;
va_start(args, format);
len = vsnprintf(data, size, format, args);
va_end(args);
if (len != (int)(size - 1)) {
errno = EINVAL;
return 0;
}
errno = 0;
return (size_t)len;
}

这个想法是,您可以在多个dynamic_printf()调用中重用相同的动态缓冲区:

char   *data = NULL;
size_t  size = 0;
size_t  len;
/* Some kind of loop for example */
len = dynamic_printf(&data, &size, "This is something I need in a buffer");
if (errno) {
/* Abort! Reason is strerror(errno) */
} else {
/* data is non-NULL, and has len chars in it. */
}
/* Strings are no longer used, so free the buffer */
free(data);
data = NULL;
size = 0;

请注意,在调用之间运行free(data); data = NULL; size = 0;是完全安全的。free(NULL)什么都不做,如果缓冲区指针NULL且大小为零,则该函数将仅动态分配新的缓冲区。

在最坏的情况下(当缓冲区不够长时),该函数会"打印"字符串两次。在我看来,这是完全可以接受的。

OP的解决方案最低限度地满足了设计目标。

有没有更好的方法来调整用于打印整数的缓冲区的大小?

即使是简短的分析也表明,unsigned所需的位数在打印十进制时每个值位增长log10(2)倍或大约0.30103...,打印八进制时增加1/3位。 OP 的代码使用三分之一或 0.33333 的因子...

unsigned x;
char buf[(CHAR_BIT*sizeof(unsigned)+5)/3];
sprintf(buf, "%u", x);

考虑:

  1. 如果缓冲区密封性问题是真实的,那么十进制打印的缓冲区值得单独考虑,而不是八进制打印。

  2. 正确性:除非代码使用带有sprintf()奇怪语言环境,否则最宽unsigned的转换,UINT_MAX适用于所有平台。

  3. 清晰度:...5)/3没有装饰,并不表示 5 和 3 的合理性。

  4. 效率。 缓冲区大小略大。 对于单个缓冲区来说,这不是问题,但对于缓冲区数组,建议使用更严格的值。

  5. 通用性:宏只设计为一种类型。

  6. 潜在危险:通过代码重用,代码外推可能会使用相同的 5 和 3 进行int,而无需适当考虑。 OP 的 5/3 也适用于int,所以这不是问题。

  7. 极端情况:对有符号类型和八进制使用 5/3 是一个问题,因为应该(CHAR_BIT*sizeof(unsigned) + 5)/3 + 1(CHAR_BIT*sizeof(unsigned)+5)/3。 示例:尝试通过某些函数(不是sprintf(... "%o" ...))将int -32768转换为基数 8 文本时出现问题:"-100000"。 所需的缓冲区是 8,而CHAR_BIT*sizeof(unsigned)+5)/3可能是 7。


有没有更好的方法可以做到这一点?

基数 10 的候选者:

28/93 (0.301075...) 是 log10(2) 非常接近且更大的近似值。 当然,代码可以使用更明显的分数,如 30103/100000。

通用性:一个好的宏也会适应其他类型的宏。 下面是用于各种无符号类型的一个。

#define LOG10_2_N 28
#define LOG10_2_D 93
//                              1 for the ceiling                          1 for 
#define UINT_BUFFER10_SIZE(type) (1 + (CHAR_BIT*sizeof(type)*LOG10_2_N)/LOG10_2_D + 1)

unsigned x;
char bufx[UINT_BUFFER10_SIZE(x)];
sprintf(bufx, "%u", x);
size_t z;
char bufz[UINT_BUFFER10_SIZE(z)];
sprintf(bufz, "%zu", z);

对于整数大小为 1 到 92 位,28/93 分数给出与 log10(2) 相同的答案整数结果,因此对于缓冲区数组来说空间有效。 它永远不会太小。

有符号类型的宏可以使用

#define INT_BUFFER_SIZE(type) (1+1+ (CHAR_BIT*sizeof(type)-1)*LOG10_2_N)/LOG10_2_D + 1)

避免逐一问题:我建议在宏名称中使用SIZE来传达所需的缓冲区大小,而不是最大字符串长度。

基数 8 的候选者:

一旦需要非基数10 的计算大小,我创建的应用程序通常需要一个缓冲区来处理任何基数 2 及以上。 考虑一下printf()有一天也可能允许%b。 因此,对于处理整数到文本的通用缓冲区,任何基数、任何符号都建议:

#define INT_STRING_SIZE(x)  (1 /* sign */ + CHAR_BIT*sizeof(x) + 1 /*  */)
int x = INT_MIN;
char buf[INT_STRING_SIZE(x)];
my_itoa(buf, sizeof buf, x, 2);
puts(buf); --> "-10000000000000000000000000000000"  (34 char were needed)

最新更新