从一个单元测试用例来说说编程中的编码问题

2021-08-06 14:40:57 浏览数 (1)

在编程中,大多数程序员都离不开编码问题: 系统的默认区域和语言设置,代码文件的编码,以及代码中字符串的编码。

编码简述以及Windows默认配置

一提到编码大家最熟悉的莫过于ASCII(American Standard Code for Information Interchange), 其采用7个bit表示128个字符,包含了常见的英文字符、数字,控制字符等。但是ASCII不包含中文,日文等文字的编码,便出现了针对中文的编码GB2312,GBK等编码,针对日文的Shift_JIS编码,他们都兼容ASCII编码,微软]称为ANSI(American National Standards Institute)编码。但是有个问题,就是各个编码之间不兼容,比如我们都知道一个字符的编码说到底都是二进制表示,那么0xB182GB2312中编码为,但是在Shift_JIS编码中为。说到这里读者是不是会有两个问题:

  1. 上述的编码并不涵盖世界上所有语言的字符。于是这个时候出现了Unicode编码方案,而对应的编码方式主要有UTF-8, UTF-16, UTF-32.
  2. 上述例子中编码值0xB182GB2312Shift_JIS编码方式中有不同的字符表示。这对于对于程序员来说处理起来不是很友好了,比如0xB182这个字符保存的文本,在你的操作系统中用notepad打开会显示什么字符呢?这个时候你也许会发现,怎么在不同人的机器上会显示不同的字符样式呢?

比如在我的系统上显示的字符为:

同一个文件在另一个Windows系统上打开可能显示字符:

然后同一个文件在另一个Windows系统上也可能显示乱码。

Notepad在解析的时候,是根据当前的Windows的默认配置的区域有关系,在控制面板时钟和区域->区域->管理->更改系统区域设置 (修改后会提示重启生效)

这个配置关联着一个相应的Code Page, 这个就表明使用的编码方式。比如我本机配置的是中文(简体,中国),那么通过命令行chcp得到代码页为936,通过微软的MSDN可以查询到为GB2312(ANSI/OEM Simplified Chinese (PRC, Singapore); Chinese Simplified (GB2312))。

根据我当前的配置Code Page936,那边便在GB2312相应字符集中找到对应的字符进行显示啦。 如果Code Page932(ANSI/OEM Japanese; Japanese (Shift-JIS)),那边便从Shift-JIS相应的字符相应的字符集中找到字符进行显示。 如果Code Page437 (OEM United States), 把每个字节当成一个单独的字符为‚±乱码样式。

一个单元测试

有一定编码经验的同学一定听说过URL Encoding,在RFC1738中规定URL中的除了字母和数字[0-9a-zA-Z],特殊符号$-_. !*'(),以及一些保留字可以不做编码,对于其他的字符需要对其进行编码,比如汉字程序员对应三个字对应的UTF-8编码为E7A88B,E5BA8FE59198(字节按照从低到高排序), 其对应的UTF-8的URL Encoding(Percent-Encoding)的编码为程序员

URL Encoding不是本章节的重点,本章节的重点在于通过一个单元测试用例,来看一看Visual Studio中字符串的编码(本文基于Visual Studio 2015)。

那么先上一个基于gtest的测试用例,测试用主要测试了原型为std::string UrlEncoding(const std::string& strInput)函数,对输入的字符串进行Url Encoding并且返回结果。

代码语言:javascript复制
TEST(URL_ENCODING_CHINESE_CHAR_TEST, PURE_CHINESE_CHAR)
{
    EXPECT_EQ(UrlEncoding("程序员"), "程序员");
}

一开始对于编码概念还不是很熟悉的同学,先通过网络查找了程序员对应的Url Encoding的编码为程序员,很期待的在自己机器上运行了这个测试用例,结果程序报错了。这里暂停下,各位同学思考一下哪里可能会导致这个错误呢?如果你还不够了解,一起来理一理:

  1. 首先要理解我们从网站上获取的Url Encoding是基于程序员这三个字的Utf-8编码的,而且Url Encoding是基于每个字节做的编码。
  2. 那我们的测试用例的 std::string strTest = "程序员"这个的编码是Utf-8编码吗?

这个时候通过测试用例查看UrlEncoding("程序员")的返回结果是����Ա, 这个不就是GB2312对应的编码吗?这个时候我们需要输入的是一个Utf-8编码的字符串进行测试,可以用C 11的语法如下,指定程序员Utf-8编码。

代码语言:javascript复制
TEST(URL_ENCODING_CHINESE_CHAR_TEST, PURE_CHINESE_CHAR)
{
    EXPECT_EQ(UrlEncoding(u8"程序员"), "程序员");
}

这个时候这个同学很欢快的跑了一下单元测试,哇果然成功了,开开心心的把自己的代码提交到了代码仓库。可是故事到这里并没有结束,一般在软件发布版本的打包或者部署,都是在统一的系统中,而这些系统中都集成了单元测试,如果单元测试失败就会让整个发布失败。在进行软件部署或者新发布打包的时候,发现单元测试失败了。

这位同学有了疑问,为什么在自己的机器跑的没问题,但是在集成系统里面却跑失败了呢?同样的代码啊,而且还指定了程序员Utf-8编码。这个时候思考如下问题:

  1. u8"程序员"你指定了程序的字符串为Utf-8编码,但是源码文件保存的时候一定是Utf-8吗?答案是不一定,比如你的源文件编码为GB2312, 在你指定了u8"程序员"并不会影响文件编码(这个应该很好理解吧),而只是告诉编译器,程序未来运行的时候这个字符串是Utf-8编码的。接着往下看。
  2. 这位同学查看了自己的源码文件的编码为gb2312,莫非是编译器读取源码的时候首先识别出来了gb2312的编码,然后将gb2312编码的程序员转换为Utf-8程序员编码,从而编译/链接进可执行文件?这样似乎也不对,如果这位同学的机器上的编译器可以识别出gb2312并转换到utf-8,那应该在统一集成的环境中同样的编译器应该行为一致才对. 那这个时候又回到上一个章节的思考了,那是不是Visual Studio是根据系统默认配置的Code Page去识别源码文件编码的吗?

这样一想再看一下集成环境的机器默认的Code Page437(OEM United States), 那么我们理一下: 因为集成环境编译机器的Code Page437, 读取的源码文件为gb2312, 但是编译器并没有认为这个是一个gb2312的编码文件(这个很正常,一般一个文件如果没有标识,编译器或者其他的编辑器不一定能够识别出源文件的编码),那么编译器就以源文件编码为机器默认编码437,而在转换gb2312编码的程序员utf-8编码的时候,会有一个错误就是转换的时候认为源文件中的程序员437编码的,并对其进行转换到Utf-8,那么这个时候实际上转化出来的并不是正确的utf-8编码的程序员

如果还有没有明白的读者,用下面例子来说明下,用Windows API MultiByteToWideChar ,可以将指定编码的字符串转换为UTF-16编码的字符串。看看函数原型, 其中也要指定输入的字符串对应的Code Page

代码语言:javascript复制
int MultiByteToWideChar(
  UINT                              CodePage,
  DWORD                             dwFlags,
  _In_NLS_string_(cbMultiByte)LPCCH lpMultiByteStr,
  int                               cbMultiByte,
  LPWSTR                            lpWideCharStr,
  int                               cchWideChar
);

这个时候我们写一个样例程序, 运行在系统默认Code Page936(GB2312)的机器上,输入的程序员GB2312编码的,如果对其进行编码转换,转换的时候假设其分别为IBM437 (OEM United States), GB2312, UTF-8,都转换为UTF-16

代码语言:javascript复制
#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 PageGB2312的方法,转换到了正确的UTf-16程序员,而其他的都转换错误。

到这里应该理解了,上述为什么编译器指定了Utf-8u8"程序员",在运行的时候却不是真正的Utf-8编码。如果还不明白,可以找我一起讨论讨论哈。

接下来就要看如何设定,可以让这个单元测试不管在哪个编译机器上都能够编译出来都能过通过。这个时候我们可以在Visual Studio中讲文件保存为UTF-8 with signature。所谓的signature就是在文件开头加了一个BOM头,而一般BOM头是用来标记大小端的(如果不清楚的可以自行去搜索下),而UTF-8BOM头不是用来标记大小端的,就是用来表明这个文件是一个UTF-8编码的。这样编译器看到UTF-8BOM头就很容易识别出来这个文件的编码为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

参考文档

  1. Code Page Identifiers: https://ss64.com/nt/chcp.html#:~:text=CHCP.com Code page , 10 more rows
  2. CHPH: https://ss64.com/nt/chcp.html#:~:text=CHCP.com Code page , 10 more rows
  3. 关于URL编码: http://www.ruanyifeng.com/blog/2010/02/url_encoding.html
  4. String and character literals (C ): https://docs.microsoft.com/en-us/cpp/cpp/string-and-character-literals-cpp?redirectedfrom=MSDN&view=msvc-160
  5. 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

0 人点赞