C++20中引入了char8_t,目前看来,C++中字符类型全家应该都到齐了。
不妨,从头梳理一下,看看不发散的情况下,能写多少东西...
C++ 中字符类型
C++中的字符类型非常非常乱,根源在于C语言是上世纪70年代创建的,当时的char和现代语言比如Java、C#中的char完全不是一个概念。
先列个表格,看看全家福:
类型 | 引入C标准 | 引入C++标准 | 备注说明 |
---|---|---|---|
char | K&R C | C++98 | 表示一个字节的字符 |
signed char | K&R C | C++98 | 有符号一个字节字符 |
unsigned char | K&R C | C++98 | 无符号一个字节字符 |
wchar_t | C89 | C++98 | 宽字符 |
char16_t | C11 | C++11 | 16位Unicode字符 |
char32_t | C11 | C++11 | 32位Unicode字符 |
char8_t | C23 | C++20 | UTF-8编码字符 |
如果历史可以重来:
unsigned char
改为byte
char16_t
改为char
- 其他去掉
只是,历史改不掉,C、C++也不可能像Python2到Python3那样变革,唯有接受现实,了解历史,规避各种坑
- char、signed char、unsigned char 是三国演义
- 不要思考char是有符号还是无符号
- char的字面量在 C 与 C++ 下不一样
wchar_t
跨平台很多坑:宽度不定,字符编码不定,是否独立类型不定wchar_t
在Windows下应用的非常非常普遍,但很多人用了而不自知char16_t
和char32_t
姗姗来迟,正视执行字符集- 你好
char8_t
,2011年你去哪儿了 - 执行字符集
- 源码字符集
char、signed char、unsigned char
由于历史原因,C语言从一开始就引入了这三个不同的类型。C++从一开始又从C语言继承了这三个类型。
首先这是三个不同的类型
它们很容易被误解成2个类型,因为signed int 和 int 是同一个类型。容易造成错误联想。
可以使用如下断言进行验证:
1 2 3 4 5 6 7 8 |
|
也可以直接输出类型看看:
1 2 3 4 5 6 7 8 9 10 11 |
|
尽管在不同的编译器下,看到的具体内容不同。但是输出的三行信息总是不同的。
最直观的验证方式,使用函数重载:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 |
|
结果:
1 2 3 |
|
不要思考 char 有符号还是无符号
char 和 signed char、unsigned char 是不同的类型。使用char的时候,不要去思考它有没有符号。
但是,如果你特别想把它作为一个整数来用的话:
1 2 3 4 5 6 7 8 9 10 |
|
结果是什么??
- -1
- 255
两个皆有可能,取决于你的编译器选项
- 对g++来说:
1 2 3 |
|
- 对MSVC来说
1 2 |
|
如果你真想让char作为整数来用,最好在代码中转成unsigned char或signed char而后再用,不要使用此处列出的编译器选项。后者是
uint8_t
和int8_t
的替身,而char不是!
char字面量在C与C++行为不同
一个简单的例子,保存成 .c 还是 .cpp,结果不一样的:
1 2 3 4 5 6 7 |
|
g++ debao.c
或cl debao.c
1 |
|
g++ debao.cpp
或cl debao.cpp
1 |
|
C++和C毕竟是两门语言,所以存在很多情况,尽管语法在两个语言下都对,但是运行效果不同。
wchar_t 的坑
Unicode1.0是1991年发布,wchar_t
是89年进入C标准,98年进入C++标准的。
wchar_t
与Unicode擦肩而过,如果当初能明确它对应UTF-16或者UCS2,压根就不会有后面一堆的事。
但是wchar_t
从一开始就继承了C语言的优良传统:它的宽度是多少,编译器自己决定就可以,只要不比char窄就行了。别的!别的?啥都没说。
Unicode 4.0标准的5.2节是如何说的:
"The width of
wchar_t
is compiler-specific and can be as small as 8 bits. Consequently, programs that need to be portable across any C or C++ compiler should not usewchar_t
for storing Unicode text. Thewchar_t
type is intended for storing compiler-defined wide characters, which may be Unicode characters in some compilers."
如果你的程序想要跨平台,就不应该使用wchar_t
。
GCC下的wchar_t
看个例子,该类型的宽度是多少?
1 2 3 4 5 6 7 8 9 |
|
猜猜结果是多少?
- 2
- 4
结果都可以,却决于你的编译选项
1 2 |
|
不光宽度不定,更头大的是,在GCC下,还有-fwide-exec-charset
这个选项(字符集也不确定,默认值都是4选1):
1 2 3 |
|
你就想吧,wchar_t
到底是个多么不可控的东西——它可以设置成你能想象到的任何字符集(只要iconv支持)。
MSVC下 wchar_t
你可能知道,MSVC下,或者说在Windows下,wchar_t
用的非常非常多,因为Windows操作系统的API接口在广泛使用它。尽管很多人都是通过TCHAR,WCHAR或其他宏的形式在用它,没有直接手敲wchar_t
。
在MSVC下:
- 好处,
wchar_t
执行字符集是确定的——UTF-16 - 麻烦:
wchar_t
是否是独立类型?
但不慌,看个例子:
1 2 3 4 5 6 7 8 |
|
结果是什么?
- false
- true
都有可能,取决于你的编译器选项
1 2 |
|
如果你需要同时用两个预编译的第三方的C++库,接口都暴露了
wchar_t
,但是二者的编译选项不一致。可就有得玩了。
平心而论,微软将 wchar_t
作为UTF-16来用,是很明智的。如果当时其他平台也跟进,弄成事实标准,就没有char16_t
什么事情了。但是微软要确保wchar_t
执行字符集是UTF-16,必须要明确源代码采用的什么编码存储的,编译器才能干活。为此,一个简单到极致的字符集问题,微软从2003年wchar_t
进入C++标准,一直折腾到MSVC2015Update1,才折腾完。这十几年,Visual Studio的几乎每个版本,处理策略都不一样(真怀疑和IE一版本一变一样,他们就是故意的,故意的!)。
另外,注意:MSVC下的/Zc:wchar_t-
和GCC下的-fshort-wchar
完全不是同一个东西,不要类比,不要混淆,GCC下不管加什么选项,它都是独立类型。
char16_t、char32_t
在wchar_t
被玩废的情况下,不管C++在国际化上到底又多烂,C++11总算往前走了一大步。
都到了2011年,C++中连个现代意义上的字符都没有。由于char这个关键词一开始被C使用了,C++11只好引入了这两个长相怪异的兄弟。至此:
- 源码字符集(仍然被C++标准丢到一边)
- 执行字符集(提上日程)
1 2 3 4 5 6 7 8 9 10 11 |
|
这两个东西,特别是char16_t
,很有用,但尴尬之处在于:我想写个小例子输出它,都不知道怎么写。
你倒是把 std::cout 这种基础设施弄好啊!
你好 char8_t
难以理解,char8_t
2020年才加入C++。
2011年引入u8"1+1=10"
这种写法的时候,为什么不顺便把这个类型弄进来呢?
看个例子:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
|
这是例子怎么写都无所谓,因为里面用的都是ASCII字符。
但是,不少人喜欢写下面的东西(国内的不少教材从这儿把大家带歪了):
1 |
|
一下子,档次就上去了(快看看我,非ASCII字符存进 char 里面啦):
- 你用的源码字符集是什么东西?
- 你用的执行字符集是什么东西?
啊?这两个概念都不知道,我们就敢这么写C++程序?就敢在C++中直接敲中文?甚至是就敢在注释中用中文?
1 2 |
|
这两个字符集的概念,一时半会是说不清楚了。
C++标准引入u8
和char8_t
只是试图解决执行字符集问题。
执行字符集和源码字符集
要在C++中正确使用中文,必须要了解下面两个概念:
编码 | 备注 |
---|---|
源码字符集(the source character set) | 源码文件是使用何种编码保存的 |
执行字符集(the execution character set) | 可执行程序内保存的是何种编码(程序执行时内存中字符串编码) |
- K&R C: 既没有规定源码字符集,也没有规定执行字符集
- C++98:引入了
wchar_t
,问题依旧 - C++11:引入
char16_t
,char32_t
试图 规范化执行字符集UTF16、UTF32、以及UTF8 - C++20:引入
char8_t
试图规范化执行字符集 UTF8
例子
这个要求高么?
一个简单的C++程序,只是希望它能在简体中文Windows、正体中文Windows、英文版Windows、Linux、MAC OS...下的结果一致。
1 2 3 4 5 6 |
|
可以试着反问自己两个问题
- 这个源码文件是何种编码保存的?(有确定答案么?)
- mystr中是什么内容?(有确定答案么?)
对C++来说,以上两点都不确定。
实际情况,实际上更有趣,单以MSVC2005为例(假定位于不同的国家的工程师都安装了MSVC2005,然后源码统一用不带BOM的本地字符集或UTF-8):
- 情况1:德国工程师正常代码,原封不动拷贝到中国,编译报错!
- 情况2:德国工程师正常代码,原封不动拷贝到中国,编译通过,输出乱码!
- 情况3:德国工程师正常代码,原封不动拷贝到中国,编译通过,运行结果不同!
- 情况4:在中国,代码注释中加入三个中文字符,编译报错!
- 情况5:在中国,代码注释中加入三个中文字符,编译通过,运行逻辑错误
- 情况6:...
如果两个人的MSVC的版本不一致,那么更火爆
如果一个人MSVC,一个GCC,矛盾简直不可调和(同情一下:一路走来的C++跨平台库Qt)...
GCC
在GCC下,这两个都可以使用你自己喜好的编码(如果不指定,默认都是UTF8)
1 2 |
|
MSVC
在MSVC2015Update1之前,MSVC没有类似GCC前面的选项,需要灵魂拷问:
问题 | 方案 |
---|---|
源码字符集如何解决? | 有BOM么,有则按BOM解释,无则使用本地Locale字符集(随系统设置而变) |
执行字符集如何解决? | 使用本地Locale字符集(随系统设置而变) |
MSVC2015Update1之后,引入类似gcc命令行选项,尽管只针对utf8的,但够用了。
这才两个编译器,看起来就这么复杂了。而C++编译器的数目远大于2.
要想跨平台,必须确保这两个字符集都是“确定”的,而能胜任该任务的字符集,似乎理想的也只能是...
UTF-8 执行字符集
标准通过引入新的类型来解决执行字符集的问题。很好!
但是新的类型我还是用不惯,因为第三方配套还不行!
我还是是不是要写下面这样的代码:
1 |
|
好处在于:我现在不用考虑傻白甜的MSVC2003了,不用头疼怎么处理油盐不进的MSVC2005了,不用纠结怎么给MSVC2008打补丁了,甚至不用考虑MSVC2010提供的如下方案了:
1 2 3 |
|
MSVC2015Update1开始,MSVC不光承诺二进制兼容性,而且还提供命令行选项了
1 2 3 |
|
中日韩民众总算迎来一个好时代:C++执行字符集可以正大光明地用UTF-8,终于可以放心用本国字符而不用担心放到其他国家乱码了。
UTF-8 源码字符集
C++标准引入类型
char8_t
、char16_t
和char32_t
,明确规定了utf8、utf16和utf32这3种执行字符集。可是C++并没有规定源码字符集
1 |
|
C++标准对编译器说,我不管这个文件的具体编码是什么,但你必须给我生成对应utf8编码的字节流。
编译器似乎有点傻了吧?不知道源文件的编码,我如何转换
于是:
GCC说:我认为你就是utf8编码,除非通过命令行通知我其他编码。
MSVC说:我就当你是本地locale的编码,除非你有BOM,或者通过命令行告诉我是utf8。
2015年之前,为了跨GCC和MSVC等平台,源码需要保存中带BOM的UTF8。2015年之后,完全不需要BOM了,命令行参数更好使。
突然之间,微软MSVC开始守规矩了,喜欢乱搞的IE也被干掉了,还真有点不适应。希望大家呼吁一下:什么时候微软能把系统API再搞一搞,以符合posix标准啊。
其他
C++中,char、signed char、unsigned char 三兄弟肩负了太多的职责。
除了没做好的字符的职责外,它们还承担寻址单位byte 和算数类型8位整数的职责:
std::byte
直到2017年,C++才开始剥离三兄弟的内存字节属性。
C++17 引入为名 byte 新类型。注意,这是个枚举class类型,不是整数:
1 |
|
用于标识内存中的字节类型,与char或整数不同,它不是字符类型,且不支持算术类型。只支持位操作符。
使用举例:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
|
int8_t、uint8_t
C++11 引入的类型别名。
只是类型别名,需要8位整数的时候,比用signed char、unsigned char显得好看。
注意,不管有符号,还是无符号,都不会用 char 的!
在不同的编译器下typedef是不同的。可以以 MSVC2019 为例,看一下:
1 2 3 4 5 6 7 8 |
|
C++ 整数类型
其实,这一堆的字符类型,在C++中,都属于整数类型。
可以写个代码验证一下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 |
|
结果如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
|
不同于Java等语言,对于各类型宽度,C++标准只保证:
1 2 |
|
也就是说 char 和 long long 可以一样长,只要sizeof是 1就行。
C中的字符类型别名?
标题用了C++,所以前面主要在说C++,但考虑C++和C渊源关系,为了完整性,最后还是看一眼C。
不同于C++,截至到2024年,在C中:
- 只有
char
、signed char
、unsigned char
是关键字 wchar_t
、char16_t
、char32_t
与char8_t
都不是关键词,也不是原生类型- 它们只是类型别名!
伪代码如下:
1 2 3 4 5 6 |
|
参考
- https://gcc.gnu.org/onlinedocs/gcc-13.2.0/gcc/Preprocessor-Options.html
- https://stackoverflow.com/questions/2324658/how-to-determine-the-version-of-the-c-standard-used-by-the-compiler
- https://blog.csdn.net/dbzhang800/article/details/7540905
- https://learn.microsoft.com/en-us/cpp/build/reference/utf-8-set-source-and-executable-character-sets-to-utf-8?view=msvc-170
- https://en.wikipedia.org/wiki/Wide_character
- https://en.cppreference.com/w/cpp/language/type
- https://en.cppreference.com/w/cpp/language/types
- https://www.moria.us/articles/wchar-is-a-historical-accident/