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

时间:2017-05-17 11:12:14

标签: c printf buffer

我想为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具有什么值(即使面对恶意构造但标准的符合实现),产生较小缓冲区的方式也不会产生缓冲区溢出的风险。尽管char已足够,但我的方法会为32位unsigned生成12 - 11缓冲区。

5 个答案:

答案 0 :(得分:1)

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

  • math question
  • Martin R的评论总结得很好:“n二进制数字需要ceil(n * ln(2)/ ln(10))≈ceil(n * 0.301)”< / em>的

你有答案:

#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)(即&#39; s and),你就可以得到所需的八进制数字。

答案 1 :(得分:0)

如果您对动态分配的内存感到满意,则可以使用asprintf代替。此函数将分配适当的内存量来保存字符串。

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

答案 2 :(得分:0)

如果阵列应该适用于所有真实世界的计算机,那么int可以是2或4个字节。没有其他替代方案(*)。

意味着它可以容纳的最大值是65535或4.29 * 10 ^ 9。这反过来意味着你的阵列需要保持5或10位数。

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

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

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

更好,更专业的解决方案是使用stdint.h中的固定宽度类型。然后,你总是事先知道需要多少位数,便携式,因此可以摆脱上述&#34;魔术数字&#34;。

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

那些担心对极度异国情调,完全虚构系统的可移植性的人是误导的,他们大多是喜欢冒充的C语言律师。你不应该让这种理论上的废话影响你如何为现实世界的计算机编写专业程序。

修改

&#34; C语言 - 律师装腔作势者&#34;版本看起来像这样:

#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内部被定义为表达式。

答案 3 :(得分:0)

需要这样的情况很少见:可能是某些微控制器代码,通过某些串行协议传输值。在这种情况下,使用任何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()来避免缓冲区溢出,缓冲区大小为#34; guesstimated&#34;。例如,

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

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

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

答案 4 :(得分:0)

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

  

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

即使是简短的分析表明,当打印十进制时,unsigned所需的位数增加log10(2)或约0.30103 ....用于打印八进制。 OP的代码使用三分之一或0.33333 ...

1/3

考虑:

  1. 如果缓冲紧张问题属实,那么十进制打印的缓冲区需要单独考虑,而不是以八进制打印。

  2. 正确性:除非代码使用unsigned x; char buf[(CHAR_BIT*sizeof(unsigned)+5)/3]; sprintf(buf, "%u", x); 的奇怪的区域设置,否则最宽sprintf()的转换({1}}适用于所有平台。

  3. 清晰度:unsigned未经过修饰,并未表明5和3的合理性。

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

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

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

  7. 转角案例:使用5/3表示签名的类型和八进制是一个问题,因为int应该是int。示例:尝试通过某个函数(不是(CHAR_BIT*sizeof(unsigned)+5)/3)将(CHAR_BIT*sizeof(unsigned) + 5)/3 + 1转换为基本8文本时出现问题:“ - 100000”。需要的缓冲区为8,而int -32768可能为7。

  8.   

    有更好的方法吗?

    基地10的候选人:

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

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

    sprintf(... "%o" ...)

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

    签名类型的宏可以使用

    CHAR_BIT*sizeof(unsigned)+5)/3

    避免出现一个问题:我建议在宏名称中使用#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); 来传达所需的缓冲区 size ,而不是最大字符串长度。

    基地8的候选人:

    一旦需要非基数10的计算大小,我所做的应用程序通常需要一个缓冲区来处理任何基数2及以上。有一天考虑#define INT_BUFFER_SIZE(type) (1+1+ (CHAR_BIT*sizeof(type)-1)*LOG10_2_N)/LOG10_2_D + 1) 可能允许SIZE。因此,对于通用目的缓冲区来处理整数到文本,任何基数,任何符号建议:

    printf()