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 是同一个类型。容易造成错误联想。
最简单的验证方法,直接输出类型看看:
#include <iostream>
#include <typeinfo>
int main()
{
std::cout << typeid(char).name() << std::endl;
std::cout << typeid(signed char).name() << std::endl;
std::cout << typeid(unsigned char).name() << std::endl;
return 0;
}
尽管在不同的编译器下,看到的具体内容不同。但是输入的三行信息总是不同的。
最直观的验证方式,使用函数重载:
#include <iostream>
void foo(char c)
{
std::cout << "char: " << c << std::endl;
}
void foo(signed char c)
{
std::cout << "signed char: " << c << std::endl;
}
void foo(unsigned char c)
{
std::cout << "unsigned char: " << c << std::endl;
}
int main()
{
foo('a'); // char
foo(static_cast<signed char>('a')); // signed char
foo(static_cast<unsigned char>('a')); // unsigned char
return 0;
}
结果:
char: a
signed char: a
unsigned char: a
不要思考 char 有符号还是无符号
char 和 signed char、unsigned char 是不同的类型。使用char的时候,不要去思考它有没有符号。
但是,如果你想把它作为一个整数来用的话:
#include <iostream>
int main()
{
char a = -1;
int aa = a;
std::cout << aa << std::endl;
return 0;
}
结果是什么??
- -1
- 255
两个皆有可能,取决于你的编译器选项
- 对g++来说:
g++ debao.cpp
g++ -fsigned-char debao.cpp
g++ -funsigned-char debao.cpp
- 对MSVC来说
cl debao.cpp
cl /J debao.cpp
char字面量在C与C++行为不同
一个简单的例子,保存成 .c 还是 .cpp,结果不一样的:
#include <stdio.h>
int main()
{
printf(sizeof 'a' == sizeof 1 ? "true" : "false");
return 0;
}
g++ debao.c
或cl debao.c
true
g++ debao.cpp
或cl debao.cpp
false
wchar_t 的坑
Unicode1.0是1991年发布,wchar_t
是89年进入C标准,98年进入C++标准的。wchar_t与Unicode擦肩而过。
但是wchar_t
从一开始就继承了C语言的优良传统:它的宽度是多少,编译器自己决定就可以,只要不比char窄就行了。
Unicode 4.0标准的5.2节是如何说的:
"The width ofwchar_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
看个例子,该类型的宽度是多少?
#include <iostream>
int main()
{
wchar_t a = L'a';
std::cout << sizeof a << std::endl;
return 0;
}
猜猜结果是多少?
- 2
- 4
结果都可以,却决于你的编译选项
g++ -fshort-wchar debao.cpp
g++ debao.cpp
不光宽度不定,更头大的是,在GCC下,还有-fwide-exec-charset
这个选项(字符集也不确定):
-fwide-exec-charset=charset
Set the wide execution character set, used for wide string and character constants. The default is one of UTF-32BE, UTF-32LE, UTF-16BE, or UTF-16LE, whichever corresponds to the width of wchar_t and the big-endian or little-endian byte order being used for code generation. As with -fexec-charset, charset can be any encoding supported by the system’s iconv library routine; however, you will have problems with encodings that do not fit exactly in wchar_t.
你就想吧,wchar_t
到底是个多么不可控的东西
MSVC下 wchar_t
你可能知道,MSVC下,或者说在Windows下,wchar_t
用的非常非常多,因为Windows操作系统的API接口在广泛使用它。尽管很多人都是通过TCHAR,WCHAR或其他宏的形式在用它,没有直接手敲wchar_t
。
在MSVC下:
- 好处,
wchar_t
字符集是确定的,UTF16 - 麻烦:
wchar_t
是否是独立类型?
但不慌,看个例子:
#include <iostream>
int main()
{
std::cout << std::boolalpha;
std::cout << std::is_same_v<wchar_t, unsigned short> << std::endl;
return 0;
}
结果是什么?
- false
- true
都有可能,取决于你的编译器选项
cl debao.cpp
cl /Zc:wchar_t- debao.cpp
如果你需要同时用两个预编译的第三方的C++库,接口都暴露了
wchar_t
,但是二者的编译选项不一致。可就有得玩了。
另外,注意:MSVC下的/Zc:wchar_t-
和GCC下的-fshort-wchar
完全不是同一个东西,不要类比,不要混淆,GCC下它是独立类型。
char16_t、char32_t
在wchar_t
被玩废的情况下,不管C++在国际化上到底又多烂,C++11总算往前走了一大步。
都到了2011年,C++中连个现代意义上的字符都没有。由于char这个关键词一开始被C使用了,C++11只好引入了这两个长相怪异的兄弟。至此:
- 源码字符集(仍然被C++标准丢到一边)
- 执行字符集(提上日程)
#include <iostream>
int main() {
const char *name = "1+1=10";
const wchar_t *name2 = L"1+1=10";
const char16_t *name3 = u"1+1=10";
const char32_t *name4 = U"1+1=10";
// const char *name5 = u8"1+1=10";
rerurn 0;
}
这两个东西,特别是char16_t
,很有用,但尴尬之处在于:我想写个小例子输出它,都不知道怎么写。
你倒是把 std::cout 这种基础设施弄好啊!
你好 char8_t
难以理解,char8_t
2020年才加入C++。
2011年引入u8"1+1=10"
这种写法的时候,为什么不顺便把这个类型弄进来呢?
看个例子:
#include <iostream>
int main()
{
#if __cplusplus >= 202002L
const char8_t *name = u8"1+1=10";
#elif __cplusplus >= 201103L
const char *name = u8"1+1=10";
#else
const char *name = "1+1=10";
#endif
std::cout << (char*)name << std::endl;
return 0;
}
这是例子怎么写都无所谓,因为都是ASCII字符。
但是,不少人喜欢写下面的东西:
const char *name = "你好";
一下子,档次就上去了:
- 你用的源码字符集是什么东西?
- 你用的执行字符集是什么东西?
啊?这两个概念都不知道,我们就敢这么写C++程序?就敢在C++中直接敲中文?
const char8_t *name = u8"你好";
//const char *name = u8"你好";
这两个字符集的概念,一时半会是说不清楚了。
C++标准引入u8
和char8_t
只是试图解决执行字符集问题。
执行字符集和源码字符集
要在C++中正确使用中文,必须要了解下面两个概念:
编码 | 备注 |
---|---|
源码字符集(the source character set) | 源码文件是使用何种编码保存的 |
执行字符集(the execution character set) | 可执行程序内保存的是何种编码(程序执行时内存中字符串编码) |
- C++98: 既没有规定源码字符集,也没有规定执行字符集
- C++03:引入了
wchar_t
,问题依旧 - C++11:引入
char16_t
,char32_t
试图 规范化执行字符集UTF16、UTF32、以及UTF8 - C++20:引入
char8_t
试图规范化执行字符集 UTF8
例子
这个要求高么?
一个简单的C++程序,只是希望它能在简体中文Windows、正体中文Windows、英文版Windows、Linux、MAC OS...下的结果一致。
//main.cpp
int main()
{
char mystr[] = "老老实实的学问,来不得半点马虎";
return sizeof mystr;
}
可以试着反问自己两个问题
- 这个源码文件是何种编码保存的?(有确定答案么?)
- mystr中是什么内容?(有确定答案么?)
对C++来说,这两个都不确定。
GCC
在GCC下,这两个都可以使用你自己喜好的编码(如果不指定,默认都是UTF8)
-finput-charset=charset
-fexec-charset=charset
MSVC
在MSVC2015之前,MSVC没有类似GCC前面的选项,需要灵魂拷问:
问题 | 方案 |
---|---|
源码字符集如何解决? | 有BOM么,有则按BOM解释,无则使用本地Locale字符集(随系统设置而变) |
执行字符集如何解决? | 使用本地Locale字符集(随系统设置而变) |
MSVC2015之后,引入类似gcc命令行选项,尽管只针对utf8的,但够用了。
这才两个编译器,看起来就这么复杂了。而C++编译器的数目远大于2.
要想跨平台,必须确保这两个字符集都是“确定”的,而能胜任该任务的字符集,似乎理想的也只能是...
UTF-8 执行字符集
标准通过引入新的类型来解决执行字符集的问题。很好!
但是新的类型我还是用不惯,因为第三方配套还不行!
我还是是不是要写下面这样的代码:
char mystr[] = "老老实实的学问,来不得半点马虎";
好处在于:我现在不用纠结怎么处理油盐不进的MSVC2005了,不用纠结怎么给MSVC2008打补丁了,甚至不用考虑MSVC2010提供的如下方案了:
#if _MSC_VER >= 1600
#pragma execution_character_set("utf-8")
#endif
MSVC2015Update1开始,不光承诺二进制兼容性,而且还提供命令行选项了
/utf-8
/source-charset:utf-8
/execution-charset:utf-8
中日韩民众活在一个好时代,C++执行字符集可以正大光明地用UTF-8,可以用本国字符不用乱码了。
UTF-8 源码字符集
C++标准引入类型char8_t
、char16_t
和char32_t
,明确规定了utf8、utf16和utf32这3种执行字符集。可是C++并没有规定源码字符集
const char8_t* mystr=u8"中文";
C++标准对编译器说,我不管这个文件的具体编码是什么,但你必须给我生成对应utf8编码的字节流。
编译器似乎有点傻了吧?不知道源文件的编码,我如何转换
于是:
GCC说:我认为你就是utf8编码,除非通过命令行通知我其他编码。
MSVC说:我就认为你是本地locale的编码,除非你有BOM,或者通过命令行告诉我是utf8。
2015年之前,为了跨平台GCC和MSVC等平台,源码需要保存中带BOM的UTF8。2015年之后,完全不需要BOM了,命令行参数更好使。
其他
C++中,char、signed char、unsigned char 三兄弟肩负了太多的职责。
除了没做好的字符的职责外,它们还承担byte 和 8位整数的职责:
std::byte
C++17 引入的新类型。
enum class byte : unsigned char {};
int8_t、uint8_t
只是类型别名,需要8位整数的时候,比用signed char、unsigned char显得好看。