【问题标题】:Is there a better way to size a buffer for printing integers?有没有更好的方法来调整打印整数的缓冲区大小?
【发布时间】:2017-05-17 11:12:13
【问题描述】:

我想为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 具有什么值(即使面对恶意构造但符合标准的实现)。我的方法会为 32 位 unsigned 生成一个 12-char 缓冲区,尽管 11 就足够了。

【问题讨论】:

  • "然后我加二除以 3 求八进制表示的位数" 嗯...什么?为什么?你想用任何基数或八进制基数打印数字,还是什么?请澄清。
  • @skyking 八进制从何而来?你只是把它扔掉了。 %u 将给出十进制表示,句号。如果你想要八进制,你会使用%o。如果你想要任何基础,你会使用一些完全不同的代码。
  • 除非我记错了,n 个二进制数字需要 ceil(n*ln(2)/ln(10)) ≈ ceil(n * 0.301) 十进制数字(不包括终止 NUL 字符)。
  • @Lundin 我没有说我想支持八进制表示。我希望buf 足够大以保证sprintf 不会溢出。推理中使用八进制表示的原因是它将产生十进制表示所需的字符数的上限。问题是是否有人有更好的解决方案,它不会保留比实际需要多 10% 的内存。
  • 'char buf[256];`。当这是一个实际问题时,您将死去,这是其他人的问题。或者。推断我们多久将整数位数加倍,目标是在您退休前的某个时间缓冲区用完,您就有了不错的退休保险。

标签: c printf buffer


【解决方案1】:

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

  • 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)(即 ⅓),您将获得所需的八进制位数。

【讨论】:

  • 这并不重要,但 16/53 ≈ 0.30188 是一个更好的近似值,分子/分母更小(使用连分数获得:)
  • 确实,添加了注释。无论哪种方式,像这样的神奇数字都需要得到适当的评论。
  • 我更喜欢#define DECIMAL_DIGITS(x) ((CHAR_BIT * sizeof (x) * 28) / 93 + 2)。 28/93 ≈ 0.301075,这是(但仍大于)log(2)/log(10) 的更好近似值; +2 部分包括四舍五入 (+1) 和可能的符号,只是因为很容易意外忘记包含在计算中。然后缓冲区将是char buffer[DECIMAL_DIGITS(x) + 1];,其中 +1 用于终止 nul 字节。
  • @Lundin> 宏不适用于仅 int,它适用于任何无符号数字类型。如果有long long long long int,它的大小也会正确。而现在在现实世界中,至少有 uint8_t、uint16_t、uint32_t、uint64_t 和 __uint128_t,而宏完全正确。
  • @Lundin> 基本上,您只是在建议完全相同的方法,只是您将8 * ln(2) / ln(10) 近似为5 / 2 = 2.5 而不是8 * 302/1000 = 2.416(或8 * 28 / 93 ≈ 2.4086 用于NominalAnimal's version) ,我真的不明白你的意思或如何使它更安全。不过,它确实会在具有非 8 位字符的系统上破坏它,例如 C54x DSP。
【解决方案2】:

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

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

【讨论】:

  • 非常方便,可惜它不是 C 标准的一部分,甚至不是 POSIX 的一部分,它是一个纯粹的 GNU 扩展。不过,当您以 GNU 系统为目标时会很方便。
  • 如果只想使用标准库,可以取snprintf的返回值,然后分配合适的数量
  • @ErikW 关于snprintf() 是对的,但迂腐的代码需要寻找编码错误返回和语言环境问题。
【解决方案3】:

如果数组应该在所有真实世界的计算机上工作,那么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 扩展为整数常量而不是表达式。使用UINT_MAX 时,我从 GCC 得到了非常奇怪的结果,因为该宏在内部定义为一个表达式,在 limits.h 内。

【讨论】:

  • 我同意你的回答,除了最后一段。尽管我更喜欢缓冲区大小参数中的宏,但这并不是因为我假设可移植性;这是因为我认为它使代码更易于管理,尤其是在新程序员阅读的代码中使用时,可能会帮助他们学会避免缓冲区溢出问题——这在 C 语言中非常常见。我可能是在冒充,它是几乎是理论上的,但我绝对不是语言律师! (不过,编辑后的示例只是拖钓。只有白痴才会写 that。)
  • @NominalAnimal 这不亚于“对数”答案。
  • 你没有解释你的魔法5 / 2 来自哪里,如果不是来自近似CHAR_BIT * ln(2) / ln(10)。事实是你做了完全相同的事情,你只是将 CHAR_BIT 值硬编码为 8,并使用了一个不太准确的近似值,你通过反复试验而不是理解底层公式。顺便说一句,它在__uint128_t 上短了(或长了,哈哈)。
  • @spectras “这反过来意味着您的阵列需要保存 5 位或 10 位数字。”只需计算该句子之前发布的数字所需的数字。本回答开头部分重点回答问题,只关注unsigned int。因此,uint128_t 和问题中未提及的其他此类问题未得到解决。最后一个 sn-p 可以通过将它们添加到 _Generic 列表来扩展以涵盖其他类型。
  • (顺便说一下,第二个简单的例子不起作用,因为在我的系统上,*_MAX 宏都没有扩展为文字整数。我的limits.h在值周围有括号 - 但是无论如何,我举这个例子是为了开玩笑)。
【解决方案4】:

需要这样的情况很少见:可能是一些微控制器代码,通过一些串行协议传输值。在这种情况下,使用任何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) = '\0';
    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 并且大小为零,则该函数将动态分配一个新缓冲区。

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

【讨论】:

    【解决方案5】:

    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. 潜在危险:在代码重复使用的情况下,代码外推可能会在没有适当考虑的情况下对int 使用相同的 5 和 3。 OP 的 5/3 也适用于 int,所以这不是问题。

    7. 极端情况:将 5/3 用于 signed 类型和八进制是一个问题,因为 (CHAR_BIT*sizeof(unsigned)+5)/3 应该是 (CHAR_BIT*sizeof(unsigned) + 5)/3 + 1。示例:尝试通过某些函数(不是sprintf(... "%o" ...))将int -32768 转换为base 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 \0
    #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 来传达所需的缓冲区大小,而不是最大字符串长度。

    base 8 的候选人:

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

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

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2023-04-05
      • 1970-01-01
      • 1970-01-01
      • 2010-10-21
      • 1970-01-01
      • 2020-05-15
      相关资源
      最近更新 更多