首页 > 解决方案 > swprintf 截断导致意外输出

问题描述

我正在修复在 linux 和 windows 上运行的遗留代码,在某些情况下,应该包含格式化内容的缓冲区小于该内容。

该代码使用 swprintf 根据文档

size - 最多可以写入 size - 1 个字符,加上空终止符

确实会截断字符串,但是在coliru上尝试时遇到了意想不到的结果:

#include <iostream> 
#include <string> 
#include <cwchar> 

int main()
{

    wchar_t wide[5];

    std::swprintf(wide, sizeof wide/sizeof *wide, L"%ls", L"111111111");

    std::wcout << wide;
}

将导致1111??

#include <iostream> 
#include <string> 
#include <cwchar> 

int main()
{

    wchar_t wide[20];

    std::swprintf(wide, sizeof wide/sizeof *wide, L"%ls", L"111111111");

    std::wcout << wide;
}

工作得很好。

怎么了 ?

PS 我希望我可以将所有内容更改为 C++ 流/字符串,但我不能,wchar_t到处都在使用数组

标签: c++printf

解决方案


tl; dr:出于某种原因,那些空终止语义依赖于函数调用是否成功,并且swprintf只有在缓冲区足够大时才会成功。因此,您第一次尝试的数组不是以空值结尾的。


这很微妙,但swprintf不像snprintf. 它不会写“最多 N-1 个字符”,并认为在所有情况下都是成功的。

以下是同一文档中关于 from 的返回值的说明swprintf

返回值:如果成功则写入的宽字符数(不包括终止的空宽字符),如果发生编码错误或要生成的字符数等于或大于 size(包括 size 为零时),则返回负值

而且,确实,您的尝试返回 -1

从这个(以及引用下面的注释)我们可以确定如果提供的输出缓冲区中没有足够的字节,则swprintf认为操作失败。它不会溢出该缓冲区,但它也可能无法完成其工作,其工作包括编写一个 NULL 终止符。如果没有那个 NULL 终止符,wchar_t*您 [有效地] 传递给std::wcout将超出范围,并且您的程序具有未定义的行为。


我承认,在随意阅读时,这似乎与围绕size参数的语义相矛盾,C11 指出:

n写入宽字符,包括一个终止的空宽字符,它总是被添加(除非n是零)。

…没有说明函数调用是否成功的任何条件。

可能会将此称为标准中的编辑缺陷或实现错误。即使两者都不是真的,你的函数调用被认为是不成功的,我认为你不应该相应地依赖结果。

我们至少可以从Formatted Output Functions 的手册页中看到libc意图与上述破败相匹配:

返回值是为给定输入生成的字符数,不包括尾随的 null。如果不是所有输出都适合提供的缓冲区,则返回负值。您应该使用更大的输出字符串再试一次。注意:这与 snprintf 处理这种情况的方式不同。


您将不得不注意上述注意事项:

虽然窄字符串提供 std::snprintf,这使得确定所需的输出缓冲区大小成为可能,但宽字符串没有等价物,为了确定缓冲区大小,程序可能需要调用 std::swprintf,检查结果值,并重新分配一个更大的缓冲区,再试一次,直到成功。

…或完全切换到其他功能。


推荐阅读