在编程中,大多数程序员都离不开编码问题: 系统的默认区域和语言设置,代码文件的编码,以及代码中字符串的编码。
编码简述以及Windows默认配置
一提到编码大家最熟悉的莫过于ASCII
(American Standard Code for Information Interchange), 其采用7个bit
表示128个字符,包含了常见的英文字符、数字,控制字符等。但是ASCII
不包含中文,日文等文字的编码,便出现了针对中文的编码GB2312
,GBK
等编码,针对日文的Shift_JIS
编码,他们都兼容ASCII
编码,微软]称为ANSI
(American National Standards Institute)编码。但是有个问题,就是各个编码之间不兼容,比如我们都知道一个字符的编码说到底都是二进制表示,那么0xB182
在GB2312
中编码为偙
,但是在Shift_JIS
编码中为こ
。说到这里读者是不是会有两个问题:
- 上述的编码并不涵盖世界上所有语言的字符。于是这个时候出现了
Unicode
编码方案,而对应的编码方式主要有UTF-8
,UTF-16
,UTF-32
. - 上述例子中编码值
0xB182
在GB2312
和Shift_JIS
编码方式中有不同的字符表示。这对于对于程序员来说处理起来不是很友好了,比如0xB182
这个字符保存的文本,在你的操作系统中用notepad
打开会显示什么字符呢?这个时候你也许会发现,怎么在不同人的机器上会显示不同的字符样式呢?
比如在我的系统上显示的字符为偙
:
同一个文件在另一个Windows系统上打开可能显示字符こ
:
然后同一个文件在另一个Windows系统上也可能显示乱码。
Notepad在解析的时候,是根据当前的Windows的默认配置的区域有关系,在控制面板时钟和区域->区域->管理->更改系统区域设置
(修改后会提示重启生效)
这个配置关联着一个相应的Code Page
, 这个就表明使用的编码方式。比如我本机配置的是中文(简体,中国)
,那么通过命令行chcp
得到代码页为936
,通过微软的MSDN
可以查询到为GB2312
(ANSI/OEM Simplified Chinese (PRC, Singapore); Chinese Simplified (GB2312)
)。
根据我当前的配置Code Page
为936
,那边便在GB2312
相应字符集中找到对应的字符偙
进行显示啦。
如果Code Page
为932
(ANSI/OEM Japanese; Japanese (Shift-JIS)
),那边便从Shift-JIS
相应的字符相应的字符集中找到字符こ
进行显示。
如果Code Page
为437
(OEM United States
), 把每个字节当成一个单独的字符为‚±
乱码样式。
一个单元测试
有一定编码经验的同学一定听说过URL Encoding,在RFC1738
中规定URL中的除了字母和数字[0-9a-zA-Z]
,特殊符号$-_. !*'(),
以及一些保留字可以不做编码,对于其他的字符需要对其进行编码,比如汉字程序员
对应三个字对应的UTF-8
编码为E7A88B
,E5BA8F
和E59198
(字节按照从低到高排序), 其对应的UTF-8
的URL Encoding(Percent-Encoding)的编码为程序员
。
URL Encoding不是本章节的重点,本章节的重点在于通过一个单元测试用例,来看一看Visual Studio
中字符串的编码(本文基于Visual Studio 2015
)。
那么先上一个基于gtest
的测试用例,测试用主要测试了原型为std::string UrlEncoding(const std::string& strInput)
函数,对输入的字符串进行Url Encoding
并且返回结果。
TEST(URL_ENCODING_CHINESE_CHAR_TEST, PURE_CHINESE_CHAR)
{
EXPECT_EQ(UrlEncoding("程序员"), "程序员");
}
一开始对于编码概念还不是很熟悉的同学,先通过网络查找了程序员
对应的Url Encoding
的编码为程序员
,很期待的在自己机器上运行了这个测试用例,结果程序报错了。这里暂停下,各位同学思考一下哪里可能会导致这个错误呢?如果你还不够了解,一起来理一理:
- 首先要理解我们从网站上获取的Url Encoding是基于
程序员
这三个字的Utf-8
编码的,而且Url Encoding是基于每个字节做的编码。 - 那我们的测试用例的
std::string strTest = "程序员"
这个的编码是Utf-8
编码吗?
这个时候通过测试用例查看UrlEncoding("程序员")
的返回结果是����Ա
, 这个不就是GB2312
对应的编码吗?这个时候我们需要输入的是一个Utf-8
编码的字符串进行测试,可以用C 11
的语法如下,指定程序员
为Utf-8
编码。
TEST(URL_ENCODING_CHINESE_CHAR_TEST, PURE_CHINESE_CHAR)
{
EXPECT_EQ(UrlEncoding(u8"程序员"), "程序员");
}
这个时候这个同学很欢快的跑了一下单元测试,哇果然成功了,开开心心的把自己的代码提交到了代码仓库。可是故事到这里并没有结束,一般在软件发布版本的打包或者部署,都是在统一的系统中,而这些系统中都集成了单元测试
,如果单元测试失败就会让整个发布失败。在进行软件部署或者新发布打包的时候,发现单元测试失败了。
这位同学有了疑问,为什么在自己的机器跑的没问题,但是在集成系统里面却跑失败了呢?同样的代码啊,而且还指定了程序员
为Utf-8
编码。这个时候思考如下问题:
u8"程序员"
你指定了程序的字符串为Utf-8
编码,但是源码文件保存的时候一定是Utf-8
吗?答案是不一定,比如你的源文件编码为GB2312
, 在你指定了u8"程序员"
并不会影响文件编码(这个应该很好理解吧),而只是告诉编译器,程序未来运行的时候这个字符串是Utf-8
编码的。接着往下看。- 这位同学查看了自己的源码文件的编码为
gb2312
,莫非是编译器读取源码的时候首先识别出来了gb2312
的编码,然后将gb2312
编码的程序员
转换为Utf-8
的程序员
编码,从而编译/链接进可执行文件?这样似乎也不对,如果这位同学的机器上的编译器可以识别出gb2312
并转换到utf-8
,那应该在统一集成的环境中同样的编译器应该行为一致才对. 那这个时候又回到上一个章节的思考了,那是不是Visual Studio
是根据系统默认配置的Code Page
去识别源码文件编码的吗?
这样一想再看一下集成环境的机器默认的Code Page
为437
(OEM United States
), 那么我们理一下:
因为集成环境编译机器的Code Page
为437
, 读取的源码文件为gb2312
, 但是编译器并没有认为这个是一个gb2312
的编码文件(这个很正常,一般一个文件如果没有标识,编译器或者其他的编辑器不一定能够识别出源文件的编码),那么编译器就以源文件编码为机器默认编码437
,而在转换gb2312
编码的程序员
到utf-8
编码的时候,会有一个错误就是转换的时候认为源文件中的程序员
为437
编码的,并对其进行转换到Utf-8
,那么这个时候实际上转化出来的并不是正确的utf-8
编码的程序员
。
如果还有没有明白的读者,用下面例子来说明下,用Windows API
MultiByteToWideChar
,可以将指定编码的字符串转换为UTF-16
编码的字符串。看看函数原型, 其中也要指定输入的字符串对应的Code Page
。
int MultiByteToWideChar(
UINT CodePage,
DWORD dwFlags,
_In_NLS_string_(cbMultiByte)LPCCH lpMultiByteStr,
int cbMultiByte,
LPWSTR lpWideCharStr,
int cchWideChar
);
这个时候我们写一个样例程序, 运行在系统默认Code Page
为936
(GB2312
)的机器上,输入的程序员
为GB2312
编码的,如果对其进行编码转换,转换的时候假设其分别为IBM437
(OEM United States
), GB2312
, UTF-8
,都转换为UTF-16
。
#include <iostream>
#include <windows.h>
#include <locale.h>
#include <string>
#include <memory>
#define CUSTOM_CODE_PAGE_IBM437 437 //OEM United States
#define CUSTOM_CODE_PAGE_GD2312 936 //GB2312
#define CUSTOM_CODE_PAGE_UTF_8 65001 //UTF-8
std::wstring AnsiToWChar(const std::string& strInputAnsiString, UINT uCodePage)
{
int iInputStrSize = ::MultiByteToWideChar(uCodePage,
0,
strInputAnsiString.c_str(),
strInputAnsiString.size(),
0,
0);
std::wstring wstrOutput;
if (iInputStrSize > 0)
{
wstrOutput.resize(iInputStrSize);
MultiByteToWideChar(uCodePage,
0,
strInputAnsiString.c_str(),
strInputAnsiString.size(),
&wstrOutput[0],
wstrOutput.size());
}
return wstrOutput;
}
void OutputStringHexChar(const std::wstring& wstrInput, const std::string& strDescription)
{
printf("%s ", strDescription.c_str());
for (auto&& ch : wstrInput)
wprintf(L"X ", ch);
printf("n");
}
int main()
{
setlocale(LC_ALL, "zh-cn");
std::string strTest = "程序员";
std::wstring wstrASCIIToWString = AnsiToWChar(strTest, CUSTOM_CODE_PAGE_IBM437);
std::wstring wstrGB2312ToWString = AnsiToWChar(strTest, CUSTOM_CODE_PAGE_GD2312);
std::wstring wstrUtf8ToWString = AnsiToWChar(strTest, CUSTOM_CODE_PAGE_UTF_8);
OutputStringHexChar(wstrASCIIToWString, R"(GB2312 String "程序员", 以Code Page IBM437 转换为Utf-16)");
OutputStringHexChar(wstrGB2312ToWString, R"(GB2312 String "程序员", 以Code Page GB2312 转换为Utf-16)");
OutputStringHexChar(wstrUtf8ToWString, R"(GB2312 String "程序员", 以Code Page UTF8 转换为Utf-16)");
return 0;
}
输出如下图所示。程序员
的实际的UTF-16
的编码为7A0B 5E8F 5458
,可以看出在进行编码转换的时候,必须指定输入的字符串编码是正确的,才能得到正确的Utf-16
编码的字符串。所以这里指定输入字符串程序员
的Code Page
为GB2312
的方法,转换到了正确的UTf-16
的程序员
,而其他的都转换错误。
到这里应该理解了,上述为什么编译器指定了Utf-8
的u8"程序员"
,在运行的时候却不是真正的Utf-8
编码。如果还不明白,可以找我一起讨论讨论哈。
接下来就要看如何设定,可以让这个单元测试
不管在哪个编译机器上都能够编译出来都能过通过。这个时候我们可以在Visual Studio
中讲文件保存为UTF-8 with signature
。所谓的signature
就是在文件开头加了一个BOM
头,而一般BOM
头是用来标记大小端的(如果不清楚的可以自行去搜索下),而UTF-8
的BOM
头不是用来标记大小端的,就是用来表明这个文件是一个UTF-8
编码的。这样编译器看到UTF-8
的BOM
头就很容易识别出来这个文件的编码为UTF-8
了。
还有一种方法, 你可以用/source-charset
去指定源码文件的编码,这里再提一个就是默认的"程序员"
这个字符串运行时是和编译机器的默认Code Page
相同的,当然你也可以通过/execution-charset
去指定,运行时的字符串是什么编码。
其他
对于编码的处理有一个权威的开源库ICU(International Components for Unicode
),比如编码识别,编码转换等等。
http://site.icu-project.org/home
另外对于编码的知识的了解,强烈推荐看网友刨根究底学编程
写的系列文章《刨根究底字符编码》
https://www.cnblogs.com/benbenalin/category/1005679.html
参考文档
- Code Page Identifiers: https://ss64.com/nt/chcp.html#:~:text=CHCP.com Code page , 10 more rows
- CHPH: https://ss64.com/nt/chcp.html#:~:text=CHCP.com Code page , 10 more rows
- 关于URL编码: http://www.ruanyifeng.com/blog/2010/02/url_encoding.html
- String and character literals (C ): https://docs.microsoft.com/en-us/cpp/cpp/string-and-character-literals-cpp?redirectedfrom=MSDN&view=msvc-160
- Set Source and Executable character sets to UTF-8: https://docs.microsoft.com/en-us/cpp/build/reference/utf-8-set-source-and-executable-character-sets-to-utf-8?view=msvc-160#:~:text=Open the project Property Pages dialog box. For,preferred encoding. Choose OK to save your changes