Post

【C++】字符串编码问题

C++的源代码字符集处理是一个复杂的过程。如果程序中使用了中文,而字符集设置得不正确,就会出现乱码。本文介绍C++的字符串编码问题,以及如何正确地设置字符集。

1.字符类型

C++支持多种不同的字符类型。除了基本字符类型char,C++还提供了表示宽字符的wchar_t以及表示Unicode字符的char8_tchar16_tchar32_t,详见Character types

字符类型sizeof字符串类型字符串常量前缀编码
char1string编译器决定
wchar_t2或4wstringLUTF-16或UTF-32
char8_t (C++20)1u8stringu8UTF-8
char16_t (C++11)2u16stringuUTF-16
char32_t (C++11)4u32stringUUTF-32

标准库字符串是一个模板std::basic_string<CharT>,每种字符类型都有对应的字符串类型。最常用的std::string实际上就是std::basic_string<char>的别名。

1
2
3
4
5
using string = basic_string<char>;
using wstring = basic_string<wchar_t>;
using u8string = basic_string<char8_t>;
using u16string = basic_string<char16_t>;
using u32string = basic_string<char32_t>;

2.字符串编码

在GCC编译器中,源文件的字符集称为输入字符集(input character set),可以使用-finput-charset选项指定(默认为UTF-8)。预处理器会将源文件转换为源字符集(source character set)(UTF-8)用于内部处理。预处理完成后,字符和字符串常量会再次被转换为执行字符集(execution character set)(决定在内存中的表示),可以使用-fexec-charset选项指定(默认为UTF-8)。

上述几种字符串类型分别采用不同的编码。

2.1 std::string

std::string底层保存的是字节数组,因此没有特定的编码(类似于Python的bytes和Java的byte[])。字符串在内存中的表示取决于编译器的执行字符集(对于字符串常量)或输入文件的编码(对于从文件读取的字符串)。例如:

1
2
3
4
5
6
7
8
9
10
11
12
#include <iostream>
#include <iomanip>
#include <string>

int main() {
    std::string s = "你好";
    std::cout << s.length() << '\n';
    for (unsigned char c : s)
        std::cout << std::hex << std::setw(2) << std::setfill('0') << static_cast<int>(c) << ' ';
    std::cout << '\n' << s << '\n';
    return 0;
}

如果源文件和控制台的编码均为UTF-8,则输出如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
$ g++ -o test test.cpp && ./test
6
e4 bd a0 e5 a5 bd 
你好

$ g++ -fexec-charset=GBK -o test test.cpp && ./test
4
c4 e3 ba c3 
���

$ g++ -finput-charset=GBK -o test test.cpp && ./test
9
e6 b5 a3 e7 8a b2 e3 82 bd 
浣犲ソ

可以看到,打印std::string时,如果执行字符集与控制台的编码不一致,就会输出乱码。Windows CMD的编码为GBK;Linux Shell的编码为UTF-8。

如果输入字符集与源文件编码不一致,则可能输出乱码,或者编译器报错 “error: converting to execution character set: Illegal byte sequence” 。

2.2 UTF字符串

C++11和C++20引入的UTF字符串(u8stringu16stringu32string)采用特定的UTF编码(UTF-8、UTF-16和UTF-32),而不是由执行字符集决定,每个字符表示一个代码单元。编译器会根据输入字符集将UTF字符串常量转换为对应编码的二进制表示。

注:关于UTF编码、代码单元等概念,参见《Java核心技术》笔记 第3章 3.3.4和3.6.6节以及《Python基础教程》笔记 第1章 1.10.4节。

例如:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
#include <iostream>
#include <iomanip>
#include <string>

template<class S, class C = typename S::value_type>
void print(const char* name, const S& s) {
    std::cout << name << ": sizeof char = " << sizeof(C) << ", length = " << s.length() << '\n';
    int w = sizeof(C) * 2;
    for (C c : s)
        std::cout << std::hex << std::setw(w) << std::setfill('0') << static_cast<int>(c) << ' ';
    std::cout << '\n';
}

int main() {
    std::u8string u8str = u8"你好";
    std::u16string u16str = u"你好";
    std::u32string u32str = U"你好";
    print("u8string", u8str);
    print("u16string", u16str);
    print("u32string", u32str);
    return 0;
}

无论在Windows还是Linux系统上,无论执行字符集是什么,程序都会产生一致的输出:

1
2
3
4
5
6
u8string: sizeof char = 1, length = 6
e4 bd a0 e5 a5 bd 
u16string: sizeof char = 2, length = 2
4f60 597d 
u32string: sizeof char = 4, length = 2
00004f60 0000597d 

2.3 宽字符串

在C++11之前,宽字符wchar_t和宽字符串std::wstring是C++中表示Unicode字符的唯一方式,详见wide strings。与UTF字符串不同的是,宽字符串的编码不是固定的:在Windows上采用UTF-16编码(等价于std::u16string),在Linux和macOS上采用UTF-32编码(等价于u32string)。

例如:

1
2
3
4
5
6
7
8
9
10
11
12
13
#include <iostream>
#include <iomanip>
#include <string>

int main() {
    std::wstring s = L"你好";
    std::cout << "sizeof char = " << sizeof(wchar_t) << ", length = " << s.length() << '\n';
    int w = sizeof(wchar_t) * 2;
    for (wchar_t c : s)
        std::cout << std::hex << std::setw(w) << std::setfill('0') << static_cast<int>(c) << ' ';
    std::cout << '\n';
    return 0;
}

在Windows上输出如下:

1
2
sizeof char = 2, length = 2
4f60 597d 

在Linux上输出如下:

1
2
sizeof char = 4, length = 2
00004f60 0000597d 

注意,要打印宽字符串本身,应该使用std::wcout而不是std::out。但字符串并不会显示出来,因为控制台的编码不是UTF-16或UTF-32。

3.总结

在C++中,为了避免乱码问题,需要正确地设置字符集:

  • 输入字符集应该与源文件编码一致
  • 执行字符集应该与控制台编码一致

参考

This post is licensed under CC BY 4.0 by the author.