sprintf是否有方法确定要写入多少个字符?

16

我在使用C++进行工作。

我希望使用sprintf(特别是安全的计数版本_snprintf_s)编写一个可能非常长的格式化字符串。由于在编译时无法确定其近似长度,因此我将不得不使用一些动态分配的内存而不是依赖于大型静态缓冲区。有没有办法确定特定sprintf调用需要多少个字符,以便我始终确保有足够大的缓冲区?

我的备选方案是获取格式化字符串的长度,将其乘以2,然后尝试。如果可以,就很好;如果不能,我将只需将缓冲区的大小加倍,然后重试。重复此过程,直到适合为止。不是最聪明的解决方案。

看起来C99支持将NULL传递给snprintf以获取长度。如果没有其他方法,我想我可以创建一个模块来包装该功能,但我对此并不热衷。

也许fprintf到“/dev/null”/“nul”可能有效?还有其他想法吗?

编辑:或者,是否有任何方式可以“分块”sprintf,使其从中间开始写入?如果可能的话,它可以填充缓冲区,处理它,然后从上次离开的位置重新填充。

7个回答

26

snprintf 的 man 手册中说:

   返回值
       当函数成功返回时,它们返回打印的字符数(不包括用于结束字符串输出的尾随 '\0')。函数 snprintf 和 vsnprintf 不会写入超过 size 字节(包括结尾的 '\0')。如果由于此限制而截断输出,则返回值是字符数(不包括结尾的'\0'),如果有足够的空间可用,则将这些字符写入最终字符串。因此,大小或更大的返回值意味着输出被截断。(另请参见下面的 NOTES)。如果遇到输出错误,则返回负值。

这意味着您可以使用大小为 0 的参数调用 snprintf。什么也不会被写入,而返回值将告诉您需要分配多少空间给您的字符串:

int how_much_space = snprintf(NULL, 0, fmt_string, param0, param1, ...);

最好先尝试输出到固定大小的堆栈变量,因为绝大多数 printf 的输出都不会超过一定的大小。这意味着绝大多数情况下只需要打印而不需要 print/malloc/print/free 的过程。只有少数超出限制的需要完整的操作。 - paxdiablo
@Pax:可能是真的,但这是一种性能优化。通常情况下是适当的,但并非总是如此。当它不适用的一个例子是:你没有太多的堆栈空间,并且你期望大部分打印内容都比你愿意放在堆栈上的任何缓冲区大小都要大。所以你总是使用堆。 - Steve Jessop
鉴于这种情况,为什么不在堆中预先分配一个“专用”内存部分来进行打印,并从性能优化中受益呢? - user666412
1
sprintf如何计算所需的实际字节数? - Prashanth

5
正如其他人所提到的,snprintf()将返回缓冲区中所需的字符数,以防止输出被截断。您可以简单地使用0缓冲区长度参数调用它以获取所需的大小,然后使用适当大小的缓冲区。
为了稍微提高效率,您可以使用足够大的缓冲区来调用它,只有在输出被截断时才进行第二次调用snprintf()。为了确保在这种情况下缓冲区得到正确释放,我经常使用一个auto_buffer<>对象来处理动态内存(并且在正常情况下具有默认缓冲区以避免堆分配)。
如果您正在使用Microsoft编译器,则MS具有非标准的_snprintf(),其严重限制是不总是对缓冲区进行空终止,并且不指示缓冲区应该有多大。
为了解决Microsoft的非支持问题,我使用Holger Weiss的近乎公共领域的snprintf()
当然,如果您的非MS C或C ++编译器缺少snprintf(),则上面链接中的代码同样有效。

4
我会采用两阶段方法。通常,输出字符串的大部分百分比将低于某个特定阈值,只有少数字符串会更大。
第一阶段,使用合理大小的静态缓冲区,例如4K。由于snprintf()可以限制写入多少个字符,因此不会发生缓冲区溢出。从snprintf()返回的是它如果您的缓冲区足够大,将要写入的字符数。
如果调用snprintf()返回小于4K,则使用缓冲区并退出。如上所述,绝大多数调用应该只是这样做。
有些不会,这就是您进入第二阶段的时候。如果调用snprintf()无法适合4K缓冲区,则至少现在知道需要多大的缓冲区。
使用malloc()分配一个足够大的缓冲区,然后再次对其进行snprintf()。完成缓冲区后,请释放它。
我们曾经在没有snprintf()的日子里使用过一个系统,并通过连接到/dev/null的文件句柄并使用fprintf()来实现相同的结果。 /dev/null始终保证接收与给定数据相同的数据,因此我们实际上会从中获取大小,然后根据需要分配缓冲区。
请注意,并非所有系统都具有snprintf()(例如,我了解到在Microsoft C中为_snprintf()),因此您可能必须找到执行相同作业的函数,或者恢复使用fprintf /dev/null解决方案。
如果在检查大小的snprintf()和实际snprintf()之间可以更改数据(即,请注意线程)。如果尺寸增加,您将获得缓冲区溢出损坏。
如果遵循数据一旦交给一个函数,就完全属于该函数直到交回的规则,则不会有问题。

不幸的是,snprintf() 函数不是标准的 C++。我刚试图在 Visual Studio 2008 Express Edition 中使用它,但编译器报告找不到 snprintf。 - jasonmray
我认为在Microsoft C++中应该是_snprintf()函数。 - FryGuy
@rubancache,那就是使用“fprintf到/dev/null”解决方案的时候。 - paxdiablo

1

就算价值有限,asprintf 是 GNU 扩展,可以管理这个功能。它接受一个指针作为输出参数,还有一个格式字符串和一个可变数量的参数,并将正确分配缓冲区的地址写回指针。

您可以像这样使用它:

#define _GNU_SOURCE
#include <stdio.h>

int main(int argc, char const *argv[])
{
    char *hi = "hello"; // these could be really long
    char *everyone = "world";
    char *message;
    asprintf(&message, "%s %s", hi, everyone);
    puts(message);
    free(message);
    return 0;
}

希望这能帮助到某些人!

0

我也曾经寻找过你所说的相同功能,但据我所知,像C99方法这样简单的东西在C++中是不可用的,因为C++目前没有包含C99中添加的功能(例如snprintf)。

你最好的选择可能是使用stringstream对象。它比清晰编写的sprintf调用要麻烦一些,但它可以工作。


我没有做过这件事,但我可以理解为什么会这样做。你不需要C99,因为已经有snprintf()的PD版本了。或者是因为问题明确要求使用printf()而不是stringstream。我不知道,我早就放弃试图理解那些匆忙投票的人了。 - paxdiablo

0

看一下 CodeProject: 使用标准C++的CString-clone。它使用了您建议的方案来扩大缓冲区大小。

// -------------------------------------------------------------------------
    // FUNCTION:  FormatV
    //      void FormatV(PCSTR szFormat, va_list, argList);
    //
// DESCRIPTION: // 此函数使用sprintf风格格式规范对字符串进行格式化。 // 它通常猜测所需的缓冲区大小,然后尝试越来越大的缓冲区, // 直到找到足够大的缓冲区或超过门槛(MAX_FMT_TRIES)。 // // PARAMETERS: // szFormat - 包含输出格式的PCSTR // argList - 用于变量参数列表的Microsoft特定va_list // // RETURN VALUE: // -------------------------------------------------------------------------

void FormatV(const CT* szFormat, va_list argList) { #ifdef SS_ANSI
int nLen = sslen(szFormat) + STD_BUF_SIZE; ssvsprintf(GetBuffer(nLen), nLen-1, szFormat, argList); ReleaseBuffer(); #else CT* pBuf = NULL; int nChars = 1; int nUsed = 0; size_type nActual = 0; int nTry = 0;
do { // Grow more than linearly (e.g. 512, 1536, 3072, etc)
nChars += ((nTry+1) * FMT_BLOCK_SIZE); pBuf = reinterpret_cast(_alloca(sizeof(CT)*nChars)); nUsed = ssnprintf(pBuf, nChars-1, szFormat, argList);
// Ensure proper NULL termination. nActual = nUsed == -1 ? nChars-1 : SSMIN(nUsed, nChars-1); pBuf[nActual+1]= '\0';
} while ( nUsed < 0 && nTry++ < MAX_FMT_TRIES );
// assign whatever we managed to format
this->assign(pBuf, nActual); #endif }


0

既然你正在使用C++,那么真的没有必要使用任何版本的sprintf。最简单的方法是使用std::ostringstream。

std::ostringstream oss;
oss << a << " " << b << std::endl;

oss.str() 返回一个 std::string,其中包含您写入 oss 的内容。使用 oss.str().c_str() 来获取一个 const char *。从长远来看,这将更容易处理并消除内存泄漏或缓冲区溢出。通常,如果您在 C++ 中担心此类内存问题,则未充分利用该语言,并且应重新考虑设计。


警告:C++在流函数中添加了许多小的额外功能,这些功能可能会对您造成重大影响。特别是,流支持区域设置,可以更改数字格式。在设置为一个区域设置的流中输出的数字无法在使用不同区域设置的流中读取。如果您可以保证永远不会使用其他区域设置,则没问题。我们遇到了这个问题,因为我们正在使用插入到使用区域设置的主机应用程序的DLL。 - AHelps

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