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

Is there a better way to size a buffer for printing integers?

我想为 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 中的位数(上限)。然后我加二并除以 3 以找到八进制表示的位数,最后加一作为 NUL 终止符。这意味着缓冲区足以打印八进制数字,并且由于十进制表示使用的数字不多于八进制,因此对于十进制表示也足够了。

有更好的方法吗?更好的意思是一种产生更小缓冲区的方法,无论 x 具有什么值,都不会冒缓冲区溢出的风险(即使面对恶意构造但符合标准的实现)。尽管 11 就足够了,但我的方法将为 32 位 unsigned 生成一个 12-char 缓冲区。

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

  • math question.
  • 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 位左右的系统之前,这太过分了。连分数也适用(请参阅下面 Martin R 的评论)。无论哪种方式,请注意 CHAR_BIT * sizeof(x) * <your chosen numerator> 不要太大,记住结果必须大于实际值。

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

如果您可以使用动态分配的内存,则可以改用 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 中的固定宽度类型。然后你总是提前知道需要多少位数,可移植,因此可以摆脱上面的 "magic numbers".


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

关注可移植性到奇异的、完全虚构的系统的人被误导了,他们大多是喜欢冒充的 C 语言律师。你不应该让这些理论上的废话影响你为现实世界的计算机编写专业程序的方式。


编辑

"C language-lawyer poser" 版本如下所示:

#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 扩展为整数常量而不是表达式。使用 UINT_MAX 时,我从 GCC 得到了非常奇怪的结果,因为该宏在内部定义为表达式,在 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) = '[=10=]';
    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() 来避免缓冲区超过运行s,缓冲区大小为"guesstimated"。例如,

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[]是否比必要的大几十个甚至几百个字节,在典型程序中完全不相关。只要让它足够大,并在错误情况下输出一条错误消息,告诉哪个缓冲区(文件和函数)不够长,所以在不太可能的情况下很容易修复它太短了。


所述,asprintf() GNU 扩展是一个可行的替代方案。它 returns 一个动态分配的字符串。

在 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 且大小为零,函数将动态分配一个新缓冲区。

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

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

Is there a better way to size a buffer for printing integers?

即使是简短的分析也表明 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. 正确性:除非代码使用奇怪的localesprintf(),最宽的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 应该是 (CHAR_BIT*sizeof(unsigned) + 5)/3 + 1。示例:尝试通过某些函数(而非 sprintf(... "%o" ...))将 int -32768 转换为基数 8 文本时出现问题:“-100000”。所需的缓冲区是 8,其中 CHAR_BIT*sizeof(unsigned)+5)/3 可能是 7.


Is there a better way to do this?

基数 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 [=11=]
#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) 相同的答案整数结果,因此 space 对于缓冲区数组有效。它永远不会太小。

可以使用签名类型的宏

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

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

基地8的候选人:

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

#define INT_STRING_SIZE(x)  (1 /* sign */ + CHAR_BIT*sizeof(x) + 1 /* [=13=] */)

int x = INT_MIN;
char buf[INT_STRING_SIZE(x)];
my_itoa(buf, sizeof buf, x, 2);
puts(buf); --> "-10000000000000000000000000000000"  (34 char were needed)