字符编码总结
本文主要讲解Windows下字符编码的转换方法。
一、概念
- 码位:Unicode中的编码,如“汉”对应的U+6C49。
- 编码单元:已编码文本的最小比特组合。例如,UTF-8、UTF-16 和 UTF-32 分别使用 8 比特、16 比特和 32 比特编码单元。
二、总结
宽字符(wide char),数据类型为wchar_t,代表的是UTF-16,被Windows下称为Unicode。
- 在visual studio中,项目属性中的Character Set修改为Unicode时,底层调用的就是带w的函数。
- MultiByte指的是ANSI字符串,和code page有关系。
- ASCII字符串,即最早的128个英文字符。
UTF-8的编码单元是字节,先读取一个字节,然后根据字节的头部位数,再决定读取后面的字节,所以:
- UTF-8没有字节序的概念,没有大端字节序、小端字节序。
- std::string可以存储UTF-8,std::string只是个容器,里面放的内容,如何解析,它不管。
- Unicode官方不太推荐对UTF-8文件添加BOM
Windows应用程序应该抛弃code page的概念,使用UTF-8来表示字符串。
- 但是原生API不支持UTF-8,如何解决?
- Qt、Java、C#、Python以及 ICU,内部都使用 UTF-16 来表示字符串。
在visual studio中
- 调试时,监视窗口显示字符串,是根据当前Windows系统的code page来解析的。
- 项目属性,字符集中可以选择多字节字符集、Unicode字符集,这个选项仅定义了一个UNICODE宏,影响调用Windows底层API的版本,决定是否带w,如CreateFileA、CreateFileW。
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(宽字符)。
关于最佳实践:
- 微软建议在C++程序中,总是使用宽字符串来处理。wchar_t,std::wstring,当和其他系统在交互式,需要字符集转换时,再调用Windows API提供了一些函数,比如
MultiByteToWideChar
和WideCharToMultiByte
,用于在UTF-8和UTF-16之间进行转换。 - UTF-8 Everywhere:建议和微软正好相反,建议在程序内部长期持有UTF-8字符串,在和Windows系统打交道的时候,再将UTF-8转换成宽字符串。
- 微软建议在C++程序中,总是使用宽字符串来处理。wchar_t,std::wstring,当和其他系统在交互式,需要字符集转换时,再调用Windows API提供了一些函数,比如
三、转换
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字符;