在编程中,大多数程序员都离不开编码问题: 系统的默认区域和语言设置,代码文件的编码,以及代码中字符串的编码。
编码简述以及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)的编码为%E7%A8%8B%E5%BA%8F%E5%91%98 。
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("程序员"), "%E7%A8%8B%E5%BA%8F%E5%91%98");
}
 一开始对于编码概念还不是很熟悉的同学,先通过网络查找了程序员 对应的Url Encoding 的编码为%E7%A8%8B%E5%BA%8F%E5%91%98 ,很期待的在自己机器上运行了这个测试用例,结果程序报错了。 这里暂停下,各位同学思考一下哪里可能会导致这个错误呢? 如果你还不够了解,一起来理一理:
- 首先要理解我们从网站上获取的Url Encoding是基于
程序员 这三个字的Utf-8 编码的,而且Url Encoding是基于每个字节做的编码。 - 那我们的测试用例的
std::string strTest = "程序员" 这个的编码是Utf-8 编码吗?
这个时候通过测试用例查看UrlEncoding("程序员") 的返回结果是%B3%CC%D0%F2%D4%B1 , 这个不就是GB2312 对应的编码吗? 这个时候我们需要输入的是一个Utf-8 编码的字符串进行测试,可以用C++ 11 的语法如下,指定程序员 为Utf-8 编码。
TEST(URL_ENCODING_CHINESE_CHAR_TEST, PURE_CHINESE_CHAR)
{
EXPECT_EQ(UrlEncoding(u8"程序员"), "%E7%A8%8B%E5%BA%8F%E5%91%98");
}
这个时候这个同学很欢快的跑了一下单元测试,哇果然成功了,开开心心的把自己的代码提交到了代码仓库。可是故事到这里并没有结束,一般在软件发布版本的打包或者部署,都是在统一的系统中,而这些系统中都集成了单元测试 ,如果单元测试失败就会让整个发布失败。 在进行软件部署或者新发布打包的时候,发现单元测试失败了。
这位同学有了疑问,为什么在自己的机器跑的没问题,但是在集成系统里面却跑失败了呢?同样的代码啊,而且还指定了程序员 为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
#define CUSTOM_CODE_PAGE_GD2312 936
#define CUSTOM_CODE_PAGE_UTF_8 65001
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"%04X ", 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 ),比如编码识别,编码转换等等。
另外对于编码的知识的了解,强烈推荐看网友刨根究底学编程 写的系列文章《刨根究底字符编码》
参考文档
- Code Page Identifiers
- CHPH
- 关于URL编码
- String and character literals (C++)
- Set Source and Executable character sets to UTF-8
|