Windows 控制台上的 UTF-8 输出

UTF-8 output on Windows console

提问人:mkluwe 提问时间:11/2/2009 最后编辑:Communitymkluwe 更新时间:10/13/2023 访问量:9838

问:

以下代码显示了我的计算机上的意外行为(在 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中:控制台窗口中的输出是(转换为代码页 1252,最初显示一些线条图 默认代码页中的字符,可能是 437)。当我更改设置时 的控制台窗口以使用“Lucida 控制台”字符集并运行我的 test.exe,输出再次更改为 ,这意味着ü0ü

  • 可以使用及其 UTF-8 编码编写字符üfputsC3 BC
  • std::cout无论出于何种原因都不起作用
  • 尝试写入字符后正在设置流failbit

视窗 7:使用 Consolas 的输出为 。更有趣。可能写入了正确的字节(至少在将输出重定向到文件时)并且流状态正常,但这两个字节被写入为单独的字符)。��0ü

我试图在“Microsoft Connect”上提出这个问题(见这里), 但多发性硬化症并没有太大帮助。你不妨看看这里,因为以前有人问过类似的事情。

你能重现这个问题吗?

我做错了什么?不应该 和 有一样的 影响?std::coutfputs

已解决:(有点)按照 mike.dld 的想法,我实现了从 UTF-8 到 Windows-1252 的转换,并用这个转换器替换了 streambuf(请参阅我对 mike.dld 答案的评论)。std::stringbufsync()std::cout

C++ visual-studio-2008 UTF-8 Windows-XP 控制台

评论

0赞 Matt Joiner 11/2/2009
我以前在使用 C++ IOSTREAMS 时遇到过问题。有很多隐藏的肮脏会导致问题。这不值得回答,但是当 IOSTREAMS 给您带来麻烦时,请使用 C 的 stdio,我以前曾多次遇到过这样的问题。
0赞 mkluwe 11/2/2009
是的,使用 iostreams 比 stdio 更复杂,甚至有关于此的完整教科书。但是 iostreams 为您提供了很大的灵活性,我很乐意使用它。
0赞 Philippe F 11/2/2009
这不是 Windows 控制台的问题吗?我记得它无论如何都不能识别 unicode,从而产生很多这样的问题......
0赞 mkluwe 11/2/2009
如您所见,我可以在 Windows 控制台中输出 UTF-8 编码的字符串(通过 ),并且可以使用命令键入 UTF-8 编码的文件(完成后)。因此,我认为它可以处理这种编码......fputstypechcp 65001

答:

1赞 chris prosser 11/4/2009 #1

哎呀。恭喜您找到了一种从程序内部更改控制台代码页的方法。我不知道那个电话,我总是不得不使用 chcp。

我猜 C++ 默认语言环境正在参与其中。默认情况下,它将使用 GetThreadLocale() 提供的代码页来确定非 wstring 内容的文本编码。这通常默认为 CP1252。您可以尝试使用 SetThreadLocale() 来获取 UTF-8(如果它这样做了,则无法调用),希望 std::locale 默认为可以处理您的 UTF-8 编码的东西。

评论

0赞 mkluwe 11/5/2009
绝对不是解决方案,而是我以前没有想过的事情。几天后我回到工作岗位时会尝试一下(在家里我使用的是 Linux......
0赞 mkluwe 11/30/2009
我又看了一遍,但 SetThreadLocale 不处理编码,或者我不理解文档 msdn.microsoft.com/en-us/library/dd374051(VS.85).aspx。我尝试了一点std::cout.imbue,但无济于事。这个问题仍未解决...
1赞 mkluwe 6/19/2013 #2

现在是时候关闭它了。斯蒂芬·T·拉瓦维(Stephan T. Lavavej),这种行为是“有意为之”的,尽管我无法遵循这种解释。

我目前的知识是:UTF-8 代码页中的 Windows XP 控制台不适用于 C++ iostreams。

Windows XP 现在已经过时了,VS 2008 也是如此。我很想听听较新的 Windows 系统上是否仍然存在该问题。

在 Windows 7 上,这种效果可能是由于 C++ 流式传输输出字符的方式。如在 Windows 控制台中正确打印 utf8 字符的答案所示,当一个字节接一个字节地打印时,UTF-8 输出也会失败并显示 C stdio。也许这就是C++流在这里的作用。putc('\xc3'); putc('\xbc');

评论

0赞 eraxillan 5/11/2014
它存在于:(我试图在 stackoverflow.com/questions/23584160/ 中找到解决方法......欢迎您:)
6赞 mike.dld 2/12/2014 #3

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

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

另请注意,此解决方案不需要使用 、 、 或其他内容进行任何代码页修改。SetConsoleCPSetConsoleOutputCPchcp

这就是流缓冲区:

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;

    // ...
}

省略了,可以很容易地用 WinAPI 函数实现。wstring_to_utf8utf8_to_wstringWideCharToMultiByteMultiByteToWideChar

评论

0赞 mkluwe 2/20/2014
这是一个有用的主意。对于输出,我最终得到了一个派生的类(所以我不必自己做缓冲),并且只是实现了转换。我没有在代码中硬连接输出接收器,而是将转换后的字符串插入到流的原始 streambuf 中。std::stringbufsync()sync()
1赞 ollydbg23 8/2/2022
不错的解决方案!在我的 Windows7 系统中,我发现使用函数调用不起作用。Mike.dld 的答案有效!我在这里找到了一个实现:将 wstring 转换为以 UTF-8 编码的字符串,希望可以帮助其他人。SetConsoleOutputCPwstring_to_utf8utf8_to_wstring
0赞 ollydbg23 8/2/2022
嗨,我发现通过使用这种方法,可以正常工作,但是我只是尝试了该功能,它不起作用。你能帮忙解决这个问题吗?谢谢。std::coutprintf("\xc3\xbc");printf()
0赞 ollydbg23 8/2/2022
好的,我有一种方法可以解决这个问题,我只是在这个问题中添加了我的解决方案作为答案。printf()
0赞 TeaAge Solutions 12/4/2022
@mike.dld:这个解决方案很棒,但不幸的是,用例不能 100% 工作。尽管正确读取到回显输出的输入是垃圾,但如果输入的 UTF-16 表示形式被组装为 UTF-16 代理项对。如果只需要一个 UTF-16 字节,一切都很好。这可以在“Windows 终端”中使用命令提示符或 PowerShell 轻松重现,然后在程序等待时粘贴🚀 🍀 🔥到程序中。 (w.)在 UTF-16 代理项对上失败。std::string line; std::getline( std::cin, line );linestd::getlineReadConsoleWENABLE_LINE_INPUT
0赞 ollydbg23 8/2/2022 #4

我只是按照 mike.dld 在这个问题中的回答,并添加对字符串的支持。printfUTF-8

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

以下是完整的测试代码:

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

using namespace std;

// https://stackoverflow.com/questions/4358870/convert-wstring-to-string-encoded-in-utf-8
#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://stackoverflow.com/questions/1660492/utf-8-output-on-windows-console
// 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://stackoverflow.com/questions/7315936/which-of-sprintf-snprintf-is-more-secure
    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;
}

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

ü
ü
0赞 boludoz 10/8/2023 #5

我之所以做出这个回答,是因为它确实是一个令人头晕目眩的东西,而且它比看起来要简单得多。

#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

在适用于 Linux 的 Windows 子系统中测试:

g++ main.cpp -o test.elf --std=c++20