乱码 - c++字符串的编码?
怪我咯
怪我咯 2017-04-17 13:41:13
0
1
633

c++ 字符串加载到内存里面是什么编码格式的?

win7中文系统下,控制台默认是GBK编码的,用GBK格式保存的源文件,中文字符串在vs2010下编译输出到控制台会正常输出

但是vs2010里面采用utf-8无BOM的源文件 输出中文字符串到终端就出现乱码了

所以 是不是c++把字符串的值加载到内存中时 是按照cpp文件的编码保存的? 也就是说utf-8编码的cpp文件,编译后字符串加载到内存时是utf-8编码的?

怪我咯
怪我咯

走同样的路,发现不同的人生

全部回复(1)
迷茫

这个问题想要说清楚还是挺复杂的,题主可以参考 New Options for Managing Character Sets in the Microsoft C/C++ Compiler 这篇文章。具体来说,源代码文件中的字符串常量能否正确的显示在控制台窗口中与以下几个因素有关:

  • 源代码文件(.cpp)保存时所使用的字符集(下文用 C1 代替)

  • 编译器在读取源代码文件时所使用的内部字符集(source character set)(下文用 C2 代替)

  • 编译器在生编译时所使用的字符集(execution character set)(下文用 C3 代替)

  • 运行可执行程序的控制台窗口所使用的字符集(下文用 C4 代替)

第一,“源代码文件保存时所使用的字符集”决定了源代码文件中的抽象字符保存到硬盘中是什么样的字节。这里所说的抽象字符,就是指人所能识别的文字(如“张”)。而字节则是由抽象字符按照字符集的规定映射的,不同的字符集映射的结果不一样(如“张”在 UTF-8 下映射的字节是E5 BC A0三个字节,而在 GBK 下映射的字节是D5 C5两个字节)。比如下面一行代码:

char *s = "张三";

其中的字符串常量"张三"在使用不同的字符集(C1)保存时,映射的字节是不同的。

第二,“编译器在读取源代码文件时所使用的内部字符集”决定了编译器如何把读入的源代码文件字节流进行转换。我所谓的转换就是指把字节流从一种编码转到另一种编码。举例来说,对于抽象字符“张”,如果采用 GBK 编码,映射的字节流就是D5 C5,而转换到 UTF-8 编码,就是把这个字节流转换成E5 BC A0

这个字符集(C2)是由编译器决定的,标准中并未规定。不同的编译器可能采用不同的内部字符集,同样的编译器不同版本也可能采用不同的内部字符集。在 Visual C++ 的某些版本中,内部字符集是 UTF-8,编译器会尝试判断源文件所使用的字符集(C1),并将其转换成内部字符集(C2),如果编译器不能判断出文件所使用的字符集,则会默认其(C1)为当前操作系统的代码页(default code page)这里如果判断错误,就会造成乱码或编译出错)。

比如:源文件如果是 UTF-8 with BOM,则能够正确的被 Visual C++ 识别,其中的抽象字符“张”映射的字节流E5 BC A0就会被正确的转换成E5 BC A0(没变)。而源文件如果是 UTF-8 without BOM,则不能正确的被 Visual C++ 识别,编译器会采用当前的代码页来进行转换,其中的抽象字符“张”映射的字节流E5 BC A0就会被当作 GBK 编码,错误的转换成其它字节流E5 AF AE ...(寮)。

第三,“编译器在编译时所使用的字符集”决定了编译器如何把源代码中使用内部字符集(C2)编码的字符/字符串常量转还到编译时所使用的字符集(C3)。这个字符集(C3)也是由编译器决定的,标准中并未规定。C++ 中的字符常量和字符串常量有不同的类型,它们对应于不同的 C3。

在 Visual C++ 中,参考 String and Character Literals 和前文提到的博客,可以推知不同类型的字符/字符串常量对应的 C3:

// Character literals
auto c0 =   'A'; // char, encoded as default code page
auto c1 = u8'A'; // char, encoded as UTF-8
auto c2 =  L'A'; // wchar_t, encoded as UTF-16LE
auto c3 =  u'A'; // char16_t, encoded as UTF-16LE
auto c4 =  U'A'; // char32_t, encoded as UTF-32LE

// String literals
auto s0 =   "hello"; // const char*, encoded as default code page
auto s1 = u8"hello"; // const char*, encoded as UTF-8
auto s2 =  L"hello"; // const wchar_t*, encoded as UTF-16LE
auto s3 =  u"hello"; // const char16_t*, encoded as UTF-16LE
auto s4 =  U"hello"; // const char32_t*, encoded as UTF-32LE

编译器根据字符串常量的类型把其从 C2 转换到 C3(前面如果判断错误,这里就会继续保留错误)。

比如:auto s1 = "张";,抽象字符“张”在 C2 中(UTF-8)映射的字节流E5 BC A0就会被转换成在 C3 中(CP936,GBK)映射的字节流D5 C5

auto s2 = u8"张";,抽象字符“张”在 C2 中(UTF-8)映射的字节流E5 BC A0就会被转换成在 C3 中(UTF-8)映射的字节流E5 BC A0(不变)。

第四,“运行可执行程序的控制台窗口所使用的字符集”决定了如何把编译好的可执行程序中的字节流转换成抽象字符显示在控制台中。比如,在上一步中的s1映射的字节流就会通过 C4(CP 936)映射回抽象字符“张”,在我们看来就是正确的。而上一步中的s2映射的字节流就会通过 C4(CP 936)映射回抽象字符“寮”,在我们看来就是乱码。

以上,就是我理解的 C++ 中字符/字符串的编码处理方式,如果有误还请指出:-)

题主可以尝试在 Visual C++ 中把以下代码分别保存成 CP936、UTF-8 with BOM、UTF-8 without BOM 的格式,看看输出结果是什么。

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

int main() {
  char *s1 = u8"张";
  char *s2 = "张";
  cout << "s1 " << sizeof(s1) << " " << strlen(s1) << " -> " << s1 << endl;  // Error in console
  cout << "s2 " << sizeof(s2) << " " << strlen(s2) << " -> " << s2 << endl;  // OK in console

  ofstream os("s1.txt");
  if (os.is_open()) {
    os << "s1 " << sizeof(s1) << " " << strlen(s1) << " -> " << s1 << endl;  
    os.close();
  }
  ofstream os2("s2.txt");
  if (os2.is_open()) {
    os2 << "s2 " << sizeof(s2) << " " << strlen(s2) << " -> " << s2 << endl;  
    os2.close();
  }
  ofstream os3("s3.txt");
  if (os3.is_open()) {
    os3 << "s1 " << sizeof(s1) << " " << strlen(s1) << " -> " << s1 << endl;
    os3 << "s2 " << sizeof(s2) << " " << strlen(s2) << " -> " << s2 << endl;
    os3.close();
  }

  cin.get();
  return 0;
}

输出的三个文件中,前两个文件s1.txts2.txt都能够被正常的文本编辑器猜出其编码格式,从而正确显示内容,但是第三个文件s3.txt会显示成部分或全部乱码,因为其中既包含了 UTF-8 编码的字节流又包含了 GBK 编码的字节流,所以文本编辑器就不知道该用什么编码来把字节流映射回抽象文本了。


Character Encoding Model


标准引用和参考文档

来自 ISO C++11 § 2.3/1
基本源字符集由 96 个字符组成:空格字符、代表水平制表符、垂直制表符、换页符的控制字符和换行符,加上以下 91 个图形字符:

雷雷

来自 ISO C++11 § 2.3/2
universal-character-name 构造提供了一种命名其他字符的方法。

雷雷

universal-character-name UNNNNNNNN 指定的字符是 ISO/IEC 10646 中字符短名称为 NNNNNNNN 的字符;通用字符名 uNNNN 指定的字符是 ISO/IEC 10646 中字符短名称为 0000NNNN 的字符。 ...

来自 ISO C++11 § 2.3/3
基本执行字符集基本执行宽字符集 应各自包含所有成员基本源字符集的字符集,加上表示警报、退格键和回车符的控制字符,再加上一个空字符(分别为空宽字符),其表示形式全部为零。
...
执行字符集执行宽字符集分别是基本执行字符集和基本执行宽字符集的实现定义的超集。执行字符集成员的值和附加成员集是特定于语言环境的。

来自 ISO C++11 § 2.2/1
翻译语法规则的优先级由以下阶段指定。

  1. 如果需要,物理源文件字符会以实现定义的方式映射到基本源字符集(引入换行符作为行尾指示符)。接受的物理源文件字符集是实现定义的。 ...不在基本源字符集 (2.3) 中的任何源文件字符都将替换为指定该字符的通用字符名。 ...

  2. ...

  3. ...

  4. ...

  5. 字符文字或字符串文字中的每个源字符集成员,以及字符文字或非原始字符串文字中的每个转义序列和通用字符名称,都会转换为相应的成员执行字符集(2.14.3、2.14.5);如果没有相应的成员,则将其转换为除空(宽)字符之外的实现定义的成员。

  6. ...

来自 ISO C++11 § 2.14.3/5
通用字符名称被转换为指定字符在适当的执行字符集中的编码。如果没有这样的编码,则通用字符名称将转换为实现定义的编码。 [ 注意: 在翻译阶段 1 中,只要在源文本中遇到实际的扩展字符,就会引入通用字符名称。因此,所有扩展字符都按照通用字符名进行描述。然而,实际的编译器实现可能会使用自己的本机字符集,只要获得相同的结果即可。 — 尾注 ]

来自 ISO C++11 § 2.14.5/6 7 15
6 在翻译阶段 6 之后,不以编码前缀开头的字符串文字是普通字符串文字,并被初始化
7 以 u8 开头的字符串文字,例如 u8"asdf",是 UTF-8 字符串文字,并使用以 UTF-8 编码的给定字符进行初始化。
...
15 非原始字符串文字中的转义序列和通用字符名称与字符文字 (2.14.3) 中的含义相同,但单引号 可以由自身或转义序列表示,双引号 " 前面应有 。在窄字符串文字中,由于多字节编码,通用字符名称可能映射到多个 char 元素。 ...

  • 字符集

  • 字符串和字符文字

  • 在 Microsoft C/C++ 编译器中管理字符集的新选项

热门教程
更多>
最新下载
更多>
网站特效
网站源码
网站素材
前端模板