在Windows控制台上输出UTF-8

11
以下代码在我的机器上表现出意外的行为(在Windows XP上测试了Visual C++ 2008 SP1和在Windows 7上测试了VS 2012):
#include <iostream>
#include "Windows.h"

int main() {
    SetConsoleOutputCP( CP_UTF8 );
    std::cout << "\xc3\xbc";
    int fail = std::cout.fail() ? '1': '0';
    fputc( fail, stdout );
    fputs( "\xc3\xbc", stdout );
}

我只是使用 cl /EHsc test.cpp 编译了代码。

Windows XP: 在控制台窗口中的输出是ü0ü(转换为 Codepage 1252,最初在默认的 Codepage 中显示一些线条绘图字符,可能是437)。当我更改控制台窗口的设置,使用 "Lucida Console" 字符集并再次运行我的 test.exe,输出将变为 ,这意味着:

  • 字符 ü 可以使用 fputs 和其 UTF-8 编码 C3 BC 进行写入
  • std::cout 不起作用,原因不明
  • 尝试写入该字符后,流的 failbit 设置为 true

Windows 7: 使用 Consolas 输出 ��0ü。更有趣的是,正确的字节已经被写入,可能会(至少在将输出重定向到文件时)流状态正常,但两个字节被分别写入为单独的字符)。

我曾试图在“Microsoft Connect”上提出此问题(请参见此处),但是 MS 并未提供太多帮助。你也可以看看这里,因为之前有类似的问题被问到过。

你能复现这个问题吗?

我做错了什么?std::coutfputs 不应该有相同的效果吗?

已解决:(在某种程度上)按照 mike.dld 的想法,我实现了一个 std::stringbuf,在 sync() 中进行 UTF-8 到 Windows-1252 的转换,并用此转换器替换了 std::cout 的 streambuf(请参见我的评论 mike.dld 的答案)。


我以前在使用C++的iostreams时遇到过问题。有很多隐藏的问题会引起麻烦。虽然这不是答案,但当iostreams出现问题时,可以使用C的stdio,因为我以前也遇到过类似的问题需要这样做。 - Matt Joiner
是的,使用iostream比stdio更复杂,甚至有关于此的全文书籍。但是iostream提供了很大的灵活性,我很乐意使用它们。 - mkluwe
这不是Windows控制台的问题吗?我记得它根本不支持Unicode,从而导致了很多这样的问题... - Philippe F
正如您所看到的,我可以在Windows控制台中输出UTF-8编码的字符串(通过fputs),并且我可以使用type命令键入UTF-8编码的文件(在执行chcp 65001之后)。因此,我认为它可以处理这种编码... - mkluwe
5个回答

6

我知道这个问题已经很久了,但如果有人仍然感兴趣,下面是我的解决方案。我实现了一个相当简单的std::streambuf派生类,然后在程序执行的最开始将其传递给每个标准流。

这使您可以在程序中的任何地方使用UTF-8。在输入时,数据从控制台以Unicode形式获取,然后转换并以UTF-8形式返回给您。在输出时,相反的操作会被执行,从您那里获取数据以UTF-8形式,将其转换为Unicode并发送到控制台。到目前为止没有发现任何问题。

此外,请注意,该解决方案不需要进行任何代码页修改,无论是使用SetConsoleCPSetConsoleOutputCP 还是chcp等其他方法都不需要。

这就是流缓冲区:

class ConsoleStreamBufWin32 : public std::streambuf
{
public:
    ConsoleStreamBufWin32(DWORD handleId, bool isInput);

protected:
    // std::basic_streambuf
    virtual std::streambuf* setbuf(char_type* s, std::streamsize n);
    virtual int sync();
    virtual int_type underflow();
    virtual int_type overflow(int_type c = traits_type::eof());

private:
    HANDLE const m_handle;
    bool const m_isInput;
    std::string m_buffer;
};

ConsoleStreamBufWin32::ConsoleStreamBufWin32(DWORD handleId, bool isInput) :
    m_handle(::GetStdHandle(handleId)),
    m_isInput(isInput),
    m_buffer()
{
    if (m_isInput)
    {
        setg(0, 0, 0);
    }
}

std::streambuf* ConsoleStreamBufWin32::setbuf(char_type* /*s*/, std::streamsize /*n*/)
{
    return 0;
}

int ConsoleStreamBufWin32::sync()
{
    if (m_isInput)
    {
        ::FlushConsoleInputBuffer(m_handle);
        setg(0, 0, 0);
    }
    else
    {
        if (m_buffer.empty())
        {
            return 0;
        }

        std::wstring const wideBuffer = utf8_to_wstring(m_buffer);
        DWORD writtenSize;
        ::WriteConsoleW(m_handle, wideBuffer.c_str(), wideBuffer.size(), &writtenSize, NULL);
    }

    m_buffer.clear();

    return 0;
}

ConsoleStreamBufWin32::int_type ConsoleStreamBufWin32::underflow()
{
    if (!m_isInput)
    {
        return traits_type::eof();
    }

    if (gptr() >= egptr())
    {
        wchar_t wideBuffer[128];
        DWORD readSize;
        if (!::ReadConsoleW(m_handle, wideBuffer, ARRAYSIZE(wideBuffer) - 1, &readSize, NULL))
        {
            return traits_type::eof();
        }

        wideBuffer[readSize] = L'\0';
        m_buffer = wstring_to_utf8(wideBuffer);

        setg(&m_buffer[0], &m_buffer[0], &m_buffer[0] + m_buffer.size());

        if (gptr() >= egptr())
        {
            return traits_type::eof();
        }
    }

    return sgetc();
}

ConsoleStreamBufWin32::int_type ConsoleStreamBufWin32::overflow(int_type c)
{
    if (m_isInput)
    {
        return traits_type::eof();
    }

    m_buffer += traits_type::to_char_type(c);
    return traits_type::not_eof(c);
}

使用方法如下:
template<typename StreamT>
inline void FixStdStream(DWORD handleId, bool isInput, StreamT& stream)
{
    if (::GetFileType(::GetStdHandle(handleId)) == FILE_TYPE_CHAR)
    {
        stream.rdbuf(new ConsoleStreamBufWin32(handleId, isInput));
    }
}

// ...

int main()
{
    FixStdStream(STD_INPUT_HANDLE, true, std::cin);
    FixStdStream(STD_OUTPUT_HANDLE, false, std::cout);
    FixStdStream(STD_ERROR_HANDLE, false, std::cerr);

    // ...

    std::cout << "\xc3\xbc" << std::endl;

    // ...
}

遗漏了 wstring_to_utf8utf8_to_wstring 可以很容易地用 WideCharToMultiByteMultiByteToWideChar WinAPI 函数实现。


那是一个有帮助的想法。为了输出,我最终使用从std::stringbuf派生的类(这样我就不必自己进行缓冲),并只实现了执行转换的sync()。我的sync()不是在代码中硬编码输出接收器,而是将转换后的字符串插入到流的原始streambuf中。 - mkluwe
1
不错的解决方案!在我的Windows7系统中,我发现使用SetConsoleOutputCP函数调用无效。Mike.dld的答案有效!我在这里找到了一个wstring_to_utf8utf8_to_wstring实现:将wstring转换为UTF-8编码的字符串,希望能帮助其他人。 - ollydbg23
嗨,我发现使用这种方法时,std::cout可以正常工作,但我刚刚尝试了printf("\xc3\xbc");函数,它无法正常工作。你能帮忙解决 printf() 的问题吗?谢谢。 - ollydbg23
好的,我有一种解决printf()问题的方法,我刚把我的解决方案作为答案添加到了这个问题中。 - ollydbg23
@mike.dld:这个解决方案很好,但不幸的是,使用情况 std::string line; std::getline( std::cin, line ); 不会百分之百地工作。尽管输入正确读取到了 line 中,但如果将输入的 UTF-16 表示形式组装为 UTF-16 代理对,则回显输出将是垃圾。如果只需要一个 UTF-16 字节,则一切正常。可以在“Windows 终端”中使用命令提示符或 PowerShell 轻松重现此问题,然后在程序等待 std::getline 时将其粘贴到程序中。ReadConsoleW(w. ENABLE_LINE_INPUT)无法处理 UTF-16 代理对。 - TeaAge Solutions

1

嗨,恭喜你找到了一种从程序内部更改控制台代码页的方法。我不知道那个调用,我总是要使用chcp。

我猜测C++默认区域设置正在发挥作用。默认情况下,它将使用GetThreadLocale()提供的代码页来确定非wstring内容的文本编码。这通常默认为CP1252。您可以尝试使用SetThreadLocale()来获取UTF-8(如果它甚至这样做的话,我记不清了),希望std::locale默认为能够处理您的UTF-8编码的内容。


绝对不是解决方案,但这是我之前没有想过的事情。我会在几天后回到工作中尝试一下(在家里我使用的是Linux...)。 - mkluwe
我再次查看了这个问题,但是SetThreadLocale并没有处理编码,或者我没有理解文档http://msdn.microsoft.com/en-us/library/dd374051(VS.85).aspx。我尝试使用std::cout.imbue,但是没有成功。这个问题仍然没有解决... - mkluwe

1

现在是关闭它的时候了。Stephan T. Lavavej 表示 这种行为是“按设计而来”,但我无法理解这个解释。

我的当前认识是:Windows XP 控制台在 UTF-8 代码页下无法使用 C++ iostreams。

现在 Windows XP 和 VS 2008 已经过时了。我很想知道这个问题在更新的 Windows 系统上是否仍然存在。

在 Windows 7 上,这种效果可能是由于 C++ 流输出字符的方式。正如在 在 Windows 控制台中正确打印 UTF8 字符 的答案中所看到的那样,当像 putc('\xc3'); putc('\xbc'); 这样连续输出一个字节时,C 标准 IO 输出 UTF-8 失败。也许这就是 C++ 流在这里所做的事情。


它存在 :( 我正在尝试在https://dev59.com/bn_aa4cB1Zd3GeqP45AM中找到解决方法。欢迎您的加入 :) - eraxillan

0

我在这个问题中只是按照mike.dld的答案,并为UTF-8字符串添加了printf支持。

正如mkluwe在他的答案中提到的,默认情况下,printf函数会逐字节输出到控制台,而控制台无法正确处理单个字节。我的方法非常简单,我使用snprintf函数将整个内容打印到内部字符串缓冲区,然后将缓冲区转储到std::cout

以下是完整的测试代码:

#include <iostream>
#include <locale>
#include <windows.h>
#include <cstdlib>

using namespace std;

// https://dev59.com/G2855IYBdhLWcg3waDiR
#include <codecvt>
#include <string>

// convert UTF-8 string to wstring
std::wstring utf8_to_wstring (const std::string& str)
{
    std::wstring_convert<std::codecvt_utf8<wchar_t>> myconv;
    return myconv.from_bytes(str);
}

// convert wstring to UTF-8 string
std::string wstring_to_utf8 (const std::wstring& str)
{
    std::wstring_convert<std::codecvt_utf8<wchar_t>> myconv;
    return myconv.to_bytes(str);
}

// https://dev59.com/n3I-5IYBdhLWcg3w0MDx
// mike.dld's answer
class ConsoleStreamBufWin32 : public std::streambuf
{
public:
    ConsoleStreamBufWin32(DWORD handleId, bool isInput);

protected:
    // std::basic_streambuf
    virtual std::streambuf* setbuf(char_type* s, std::streamsize n);
    virtual int sync();
    virtual int_type underflow();
    virtual int_type overflow(int_type c = traits_type::eof());

private:
    HANDLE const m_handle;
    bool const m_isInput;
    std::string m_buffer;
};

ConsoleStreamBufWin32::ConsoleStreamBufWin32(DWORD handleId, bool isInput) :
    m_handle(::GetStdHandle(handleId)),
    m_isInput(isInput),
    m_buffer()
{
    if (m_isInput)
    {
        setg(0, 0, 0);
    }
}

std::streambuf* ConsoleStreamBufWin32::setbuf(char_type* /*s*/, std::streamsize /*n*/)
{
    return 0;
}

int ConsoleStreamBufWin32::sync()
{
    if (m_isInput)
    {
        ::FlushConsoleInputBuffer(m_handle);
        setg(0, 0, 0);
    }
    else
    {
        if (m_buffer.empty())
        {
            return 0;
        }

        std::wstring const wideBuffer = utf8_to_wstring(m_buffer);
        DWORD writtenSize;
        ::WriteConsoleW(m_handle, wideBuffer.c_str(), wideBuffer.size(), &writtenSize, NULL);
    }

    m_buffer.clear();

    return 0;
}

ConsoleStreamBufWin32::int_type ConsoleStreamBufWin32::underflow()
{
    if (!m_isInput)
    {
        return traits_type::eof();
    }

    if (gptr() >= egptr())
    {
        wchar_t wideBuffer[128];
        DWORD readSize;
        if (!::ReadConsoleW(m_handle, wideBuffer, ARRAYSIZE(wideBuffer) - 1, &readSize, NULL))
        {
            return traits_type::eof();
        }

        wideBuffer[readSize] = L'\0';
        m_buffer = wstring_to_utf8(wideBuffer);

        setg(&m_buffer[0], &m_buffer[0], &m_buffer[0] + m_buffer.size());

        if (gptr() >= egptr())
        {
            return traits_type::eof();
        }
    }

    return sgetc();
}

ConsoleStreamBufWin32::int_type ConsoleStreamBufWin32::overflow(int_type c)
{
    if (m_isInput)
    {
        return traits_type::eof();
    }

    m_buffer += traits_type::to_char_type(c);
    return traits_type::not_eof(c);
}

template<typename StreamT>
inline void FixStdStream(DWORD handleId, bool isInput, StreamT& stream)
{
    if (::GetFileType(::GetStdHandle(handleId)) == FILE_TYPE_CHAR)
    {
        stream.rdbuf(new ConsoleStreamBufWin32(handleId, isInput));
    }
}

// some code are from this blog
// https://blog.csdn.net/witton/article/details/108087135

#define printf(fmt, ...) __fprint(stdout, fmt, ##__VA_ARGS__ )

int __vfprint(FILE *fp, const char *fmt, va_list va)
{
    // https://dev59.com/KGw05IYBdhLWcg3wUAM0
    size_t nbytes = snprintf(NULL, 0, fmt, va) + 1; /* +1 for the '\0' */
    char *str = (char*)malloc(nbytes);
    snprintf(str, nbytes, fmt, va);
    std::cout << str;
    free(str);
    return nbytes;
}

int __fprint(FILE *fp, const char *fmt, ...)
{
    va_list va;
    va_start(va, fmt);
    int n = __vfprint(fp, fmt, va);
    va_end(va);
    return n;
}

int main()
{
    FixStdStream(STD_INPUT_HANDLE, true, std::cin);
    FixStdStream(STD_OUTPUT_HANDLE, false, std::cout);
    FixStdStream(STD_ERROR_HANDLE, false, std::cerr);

    // ...

    std::cout << "\xc3\xbc" << std::endl;

    printf("\xc3\xbc");

    // ...
    return 0;
}

源代码以 UTF-8 格式保存,在 Msys2 的 GCC 下构建并在 Windows 7 64位系统下运行。以下是结果:

ü
ü

0
我给出这个答案是因为它确实很复杂,但实际上比看起来要简单得多。
#include <iostream>
#include <string>

#ifdef _WIN32
#include <windows.h>
#endif

int main() {

#ifdef _WIN32
  SetConsoleOutputCP(CP_UTF8);
#endif

  std::string content = "Hello Word ";    
  std::cout << content << std::endl;

  return 0;
}

在Windows 11中进行了测试:
clang main.cpp -o main.exe -std=c++20

在Windows子系统中进行了测试:
g++ main.cpp -o test.elf --std=c++20

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