本文主要讲解Windows下字符编码的转换方法。

一、概念

  • 码位:Unicode中的编码,如“汉”对应的U+6C49。
  • 编码单元:已编码文本的最小比特组合。例如,UTF-8、UTF-16 和 UTF-32 分别使用 8 比特、16 比特和 32 比特编码单元。

二、总结

  1. 宽字符(wide char),数据类型为wchar_t,代表的是UTF-16,被Windows下称为Unicode。

    • 在visual studio中,项目属性中的Character Set修改为Unicode时,底层调用的就是带w的函数。
  2. MultiByte指的是ANSI字符串,和code page有关系。
  3. ASCII字符串,即最早的128个英文字符。
  4. UTF-8的编码单元是字节,先读取一个字节,然后根据字节的头部位数,再决定读取后面的字节,所以:

    • UTF-8没有字节序的概念,没有大端字节序、小端字节序。
    • std::string可以存储UTF-8,std::string只是个容器,里面放的内容,如何解析,它不管。
    • Unicode官方不太推荐对UTF-8文件添加BOM
  5. Windows应用程序应该抛弃code page的概念,使用UTF-8来表示字符串。

    • 但是原生API不支持UTF-8,如何解决?
  6. Qt、Java、C#、Python以及 ICU,内部都使用 UTF-16 来表示字符串。
  7. 在visual studio中

    • 调试时,监视窗口显示字符串,是根据当前Windows系统的code page来解析的。
    • 项目属性,字符集中可以选择多字节字符集、Unicode字符集,这个选项仅定义了一个UNICODE宏,影响调用Windows底层API的版本,决定是否带w,如CreateFileA、CreateFileW。
  8. C++中string、wstring的区别

    • string存储窄字符(narrow characters),通常是ASCII或者UTF-8编码,每个字符占用1个字节,适用于跨平台或者网络通信;

      • 注意:使用string存储UTF-8字符串时,不要使用查找、下标等方法,因为每个字符占用多个字节,所以不准确。
    • wstring存储宽字符(wide characters),通常是UTF-16或者UTF-32编码,每个字符占用2或者4个字节,比较适用于Windows环境中,因为Windows的API通常使用宽字符(UTF-16)。
    • 结论:在日常开发中,尽量使用std::string,避免使用std::wstring,除非在和Windows的API打交道的场景,不得已使用std::wstring。在调用Windows的API的时候,调用转换函数进行转换一下,将UTF-8转换成UTF-16(宽字符)。
  9. 关于最佳实践:

    • 微软建议在C++程序中,总是使用宽字符串来处理。wchar_t,std::wstring,当和其他系统在交互式,需要字符集转换时,再调用Windows API提供了一些函数,比如MultiByteToWideCharWideCharToMultiByte,用于在UTF-8和UTF-16之间进行转换。
    • UTF-8 Everywhere:建议和微软正好相反,建议在程序内部长期持有UTF-8字符串,在和Windows系统打交道的时候,再将UTF-8转换成宽字符串。

三、转换

3.1 Windows

3.1.1 WideCharToMultiByte系列
不动态分配内存
// 将std::wstring转换为std::string
// codePage:CP_UTF8代表utf-8,CP_ACP代表当前代码页
 inline std::string wstrToStr(UINT codePage, const std::wstring &wstr) {
    if (wstr.empty()) {
       return std::string();
    }

    int sizeNeeded = WideCharToMultiByte(codePage, 0, wstr.c_str(), int(wstr.size()), NULL, 0, NULL, NULL);
    if (sizeNeeded == 0) {
       return std::string();
    }

    std::string str(sizeNeeded, 0);
    int ret = WideCharToMultiByte(codePage, 0, wstr.c_str(), int(wstr.size()), &str[0], str.capacity(), NULL, NULL);
    if (ret == 0) {
       return std::string();
    }
    return str;
 }

// 将std::string转换为std::wstring
// codePage:CP_UTF8代表utf-8,CP_ACP代表当前代码页
 inline std::wstring strToWstr(UINT codePage, const std::string &str) {
    if (str.empty()) {
       return std::wstring();
    }

    int sizeNeeded = MultiByteToWideChar(codePage, 0, str.c_str(), int(str.size()), NULL, 0);
    if (sizeNeeded == 0) {
        return std::wstring();
    }

    std::wstring wstr(sizeNeeded, 0);
    int ret = MultiByteToWideChar(codePage, 0, str.c_str(), int(str.size()), &wstr[0], wstr.capacity());

    if (ret == 0) {
       return std::wstring();
    }
    return wstr;
 }
动态分配内存
  
/**
 * @brief 将Microsoft Unicode转换成codePage编码的char*
 *        调用者需要使用free释放内存
 *        参考:sqlite3.c
 * @param codePage 目的字符串编码,如CP_ACP,CP_UTF8
 * @param wstr 源字符串,可处理为NULL,为空字符串的情况
 * @return char* 目的字符串
 */
char *wstrToStr(UINT codePage, const wchar_t *wstr)
{
    int nByte;
    char* str;

    nByte = WideCharToMultiByte(codePage, 0, wstr, -1, 0, 0, 0, 0);
    str = (char*)malloc(nByte);
    if (str == 0)
    {
        return 0;
    }

    nByte = WideCharToMultiByte(codePage, 0, wstr, -1, str, nByte, 0, 0);
    if (nByte == 0)
    {
        free(str);
        str = 0;
    }
    return str;
}

/**
 * @brief 将codePage编码的char*转换成Microsoft Unicode
 *        调用者需要使用free释放内存
 *        参考:sqlite3.c
 * @param codePage 源字符串编码,如CP_ACP,CP_UTF8
 * @param str 源字符串,可处理为NULL,为空字符串的情况
 * @return wchar_t* 目的字符串
 */
wchar_t *strToWstr(UINT codePage, const char* str)
{
    int nChar;
    wchar_t* wstr;
    
    nChar = MultiByteToWideChar(codePage, 0, str, -1, NULL, 0);
    wstr = (wchar_t *)malloc(nChar * sizeof(wchar_t));
    if (wstr == 0)
    {
        return 0;
    }
    nChar = MultiByteToWideChar(codePage, 0, str, -1, wstr, nChar);
    if (nChar == 0)
    {
        free(wstr);
        wstr = 0;
    }
    return wstr;
}
  • 转换示例

    • UTF-8、宽字符互相转换
    // UTF-8 -> 宽字符
    MultiByteToWideChar(CP_UTF8,
    
    // 宽字符 -> UTF-8
    WideCharToMultiByte(CP_UTF8,
    • ANSI、宽字符互相转换
    // ANSI -> 宽字符
    MultiByteToWideChar(CP_ACP,
    
    // 宽字符 -> ANSI
    WideCharToMultiByte(CP_ACP,
    • ANSI、UTF-8互相转换
    // 两者之间没办法经过一步直接转换
    // 需要先转换成宽字符,再做对应的转换
    // ANSI -> UTF-8
    MultiByteToWideChar(CP_ACP,
    WideCharToMultiByte(CP_UTF8,
    
    // UTF-8 -> ANSI
    MultiByteToWideChar(CP_UTF8,
    WideCharToMultiByte(CP_ACP,
3.1.2 CA2W系列

另外Windows提供了一些更高级的宏,来实现宽字符、utf-8字符串、ANSI字符串之间的编码转换,其内部实现,仍然是调用的MultiByteToWideChar、WideCharToMultiByte。
这一系列宏有自动化的内存管理,且看起来更简洁一些。

#include <iostream>
#include <string>
using namespace std;

#include <atlstr.h>

int main()
{
    // UTF-8 -> 宽字符
    {
        const char *utf8Str = u8"中文";
        CA2W wstr(utf8Str, CP_UTF8);
        size_t wstrLen = wcslen(wstr);
        std::wstring wStr = std::wstring(wstr);
        cout << wstrLen << endl;
    }

    // 宽字符 -> UTF-8
    {
        const wchar_t *wStr = L"中文";
        CW2A utf8(wStr, CP_UTF8);
        size_t utf8Len = strlen(utf8);
        std::string str = std::string(utf8);
        cout << utf8Len << endl;
    }


    // ANSI -> 宽字符
    {
        const char *ansiStr = "中文";
        CA2W wstr(ansiStr);
        size_t wLen = wcslen(wstr);
        std::wstring wStr = std::wstring(wstr);
        cout << wLen << endl;
    }

    // 宽字符 -> ANSI
    {
        const wchar_t *wstr = L"中文";
        CT2A ansiStr(wstr);
        size_t ansiLen = strlen(ansiStr);
        std::string str = std::string(ansiStr);
        cout << ansiLen << endl;
    }

    // ANSI -> UTF-8
    {
        const char *ansiStr = "中文";
        CA2W wstr(ansiStr);
        CW2A utf8(wstr, CP_UTF8);
        size_t utf8Len = strlen(utf8);
        std::string str = std::string(utf8);
        cout << utf8Len << endl;
    }


    // UTF-8 -> ANSI
    {
        const char *utf8Str = u8"中文";
        CA2W wstr(utf8Str, CP_UTF8);
        CW2A ansiStr(wstr);
        size_t ansiLen = strlen(ansiStr);
        std::string str = std::string(ansiStr);
        cout << ansiLen << endl;
    }

    return 0;
}

3.2 C语言

虽然提供了wcstombs等API,但是依赖local,修改local容易污染全局的local,所以不推荐使用C语言提供的接口进行编码转换。

3.3 C++语言

UTF-8、宽字符互相转换

#include <locale>
#include <codecvt>
// 需要C++11支持,到C++17后标记为废弃
// 所以不推荐使用
// 标准库对编码转换支持太拉垮了

// UTF-8 -> 宽字符
std::wstring UTF8ToUnicode(const std::string &str)
{
    std::wstring ret;
    try 
    {
        std::wstring_convert<std::codecvt_utf8<wchar_t>> wcv;
        ret = wcv.from_bytes(str);
    } 
    catch (const std::exception & e) 
    {
        std::cerr << e.what() << std::endl;
    }
    return ret;
}

// 宽字符 -> UTF-8
std::string UnicodeToUTF8(const std::wstring & wstr)
{
    std::string ret;
    try 
    {
        std::wstring_convert<std::codecvt_utf8<wchar_t>> wcv;
        ret = wcv.to_bytes(wstr);
    } 
    catch (const std::exception & e) 
    {
        std::cerr << e.what() << std::endl;
    }
    return ret;
}

ANSI、宽字符互相转换

// C++标准未提供转换方法,需要依赖Windows的API

#define _CRT_SECURE_NO_WARNINGS
#include <iostream>
#include <cstdlib>
#include <cstring>

int main() {
    // ANSI 编码的字符串
    const char* ansiString = "Hello, 你好!";

    // 获取需要的缓冲区大小
    size_t wideCharCount = mbstowcs(nullptr, ansiString, 0);

    if (wideCharCount == static_cast<size_t>(-1)) {
        std::cerr << "mbstowcs failed" << std::endl;
        return 1;
    }

    // 分配宽字符缓冲区
    wchar_t* wideString = new wchar_t[wideCharCount + 1];  // +1 用于存放 null 结尾

    // 进行转换
    if (mbstowcs(wideString, ansiString, wideCharCount + 1) == static_cast<size_t>(-1)) {
        std::cerr << "mbstowcs failed" << std::endl;
        delete[] wideString;
        return 1;
    }

    // 输出结果
    std::cout << "ANSI: " << ansiString << std::endl;
    std::wcout << L"Wide Char: " << wideString << std::endl;

    // 释放内存
    delete[] wideString;

    return 0;
}

ANSI、UTF-8互相转换

// C++标准未提供转换方法,需要依赖Windows的API

四、调试

Q:在visual studio中调试,如何正确查看不同编码的字符串变量的值?
A:可以通过在监视窗口,添加要监视的变量,然后在监视的变量后面添加逗号,再添加:

  • s8:查看utf-8字符串;
  • su:查看宽字符;
  • 默认什么都不添加为ANSI字符;

五、参考资料

标签: none

添加新评论