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>
int main()
{
static_assert( !std::is_same_v<char, unsigned char> );
static_assert( !std::is_same_v<char, signed char> );
return 0;
}
也可以直接输出类型看看:
#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作为整数来用,最好在代码中转成unsigned char或signed char而后再用,不要使用此处列出的编译器选项。后者是
uint8_t
和int8_t
的替身,而char不是!
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
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
看个例子,该类型的宽度是多少?
#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
这个选项(字符集也不确定,默认值都是4选1):
-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
到底是个多么不可控的东西——它可以设置成你能想象到的任何字符集(只要iconv支持)。
MSVC下 wchar_t
你可能知道,MSVC下,或者说在Windows下,wchar_t
用的非常非常多,因为Windows操作系统的API接口在广泛使用它。尽管很多人都是通过TCHAR,WCHAR或其他宏的形式在用它,没有直接手敲wchar_t
。
在MSVC下:
- 好处,
wchar_t
执行字符集是确定的——UTF-16 - 麻烦:
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
,但是二者的编译选项不一致。可就有得玩了。
平心而论,微软将 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++标准丢到一边)
- 执行字符集(提上日程)
#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 = "你好";
一下子,档次就上去了(快看看我,非ASCII字符存进 char 里面啦):
- 你用的源码字符集是什么东西?
- 你用的执行字符集是什么东西?
啊?这两个概念都不知道,我们就敢这么写C++程序?就敢在C++中直接敲中文?甚至是就敢在注释中用中文?
const char8_t *name = u8"你好";
//const char *name = u8"你好";
这两个字符集的概念,一时半会是说不清楚了。
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...下的结果一致。
//main.cpp
int main()
{
char mystr[] = "老老实实的学问,来不得半点马虎";
return sizeof mystr;
}
可以试着反问自己两个问题
- 这个源码文件是何种编码保存的?(有确定答案么?)
- mystr中是什么内容?(有确定答案么?)
对C++来说,以上两点都不确定。
实际情况,实际上更有趣,单以MSVC2005为例(假定位于不同的国家的工程师都安装了MSVC2005,然后源码统一用不带BOM的本地字符集或UTF-8):
- 情况1:德国工程师正常代码,原封不动拷贝到中国,编译报错!
- 情况2:德国工程师正常代码,原封不动拷贝到中国,编译通过,输出乱码!
- 情况3:德国工程师正常代码,原封不动拷贝到中国,编译通过,运行结果不同!
- 情况4:在中国,代码注释中加入三个中文字符,编译报错!
- 情况5:在中国,代码注释中加入三个中文字符,编译通过,运行逻辑错误
- 情况6:...
如果两个人的MSVC的版本不一致,那么更火爆
如果一个人MSVC,一个GCC,矛盾简直不可调和(同情一下:一路走来的C++跨平台库Qt)...
GCC
在GCC下,这两个都可以使用你自己喜好的编码(如果不指定,默认都是UTF8)
-finput-charset=charset
-fexec-charset=charset
MSVC
在MSVC2015Update1之前,MSVC没有类似GCC前面的选项,需要灵魂拷问:
问题 | 方案 |
---|---|
源码字符集如何解决? | 有BOM么,有则按BOM解释,无则使用本地Locale字符集(随系统设置而变) |
执行字符集如何解决? | 使用本地Locale字符集(随系统设置而变) |
MSVC2015Update1之后,引入类似gcc命令行选项,尽管只针对utf8的,但够用了。
这才两个编译器,看起来就这么复杂了。而C++编译器的数目远大于2.
要想跨平台,必须确保这两个字符集都是“确定”的,而能胜任该任务的字符集,似乎理想的也只能是...
UTF-8 执行字符集
标准通过引入新的类型来解决执行字符集的问题。很好!
但是新的类型我还是用不惯,因为第三方配套还不行!
我还是是不是要写下面这样的代码:
char mystr[] = "老老实实的学问,来不得半点马虎";
好处在于:我现在不用考虑傻白甜的MSVC2003了,不用头疼怎么处理油盐不进的MSVC2005了,不用纠结怎么给MSVC2008打补丁了,甚至不用考虑MSVC2010提供的如下方案了:
#if _MSC_VER >= 1600
#pragma execution_character_set("utf-8")
#endif
MSVC2015Update1开始,MSVC不光承诺二进制兼容性,而且还提供命令行选项了
/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了,命令行参数更好使。
突然之间,微软MSVC开始守规矩了,喜欢乱搞的IE也被干掉了,还真有点不适应。希望大家呼吁一下:什么时候微软能把系统API再搞一搞,以符合posix标准啊。
其他
C++中,char、signed char、unsigned char 三兄弟肩负了太多的职责。
除了没做好的字符的职责外,它们还承担寻址单位byte 和算数类型8位整数的职责:
std::byte
直到2017年,C++才开始剥离三兄弟的内存字节属性。
C++17 引入为名 byte 新类型。注意,这是个枚举class类型,不是整数:
enum class byte : unsigned char {};
用于标识内存中的字节类型,与char或整数不同,它不是字符类型,且不支持算术类型。只支持位操作符。
使用举例:
#include <iostream>
#include <cstddef>
int main() {
std::byte b1 = std::byte(0x41);
std::byte b2 = std::byte(0b01010101);
std::byte b3{0x41};
std::byte b4 = b1 | b2;
std::cout << "Result of bitwise OR: " << static_cast<int>(b4) << std::endl;
auto intValue = static_cast<int>(b3);
std::cout << "Integer value of b3: " << intValue << std::endl;
return 0;
}
int8_t、uint8_t
C++11 引入的类型别名。
只是类型别名,需要8位整数的时候,比用signed char、unsigned char显得好看。
注意,不管有符号,还是无符号,都不会用 char 的!
在不同的编译器下typedef是不同的。可以以 MSVC2019 为例,看一下:
typedef signed char int8_t;
typedef unsigned char uint8_t;
typedef signed char int_least8_t;
typedef unsigned char uint_least8_t;
typedef signed char int_fast8_t;
typedef unsigned char uint_fast8_t;
C++ 整数类型
其实,这一堆的字符类型,在C++中,都属于整数类型。
可以写个代码验证一下:
#include <iostream>
#include <type_traits>
#define TYPE_CHECK(type) check_integral<type>(#type)
template <typename T>
void check_integral(const char* typeName) {
if (std::is_integral<T>::value) {
std::cout << "integral type: " << typeName << std::endl;
} else {
std::cout << "non integral type: " << typeName << std::endl;
}
}
int main() {
TYPE_CHECK(bool);
TYPE_CHECK(char);
TYPE_CHECK(signed char);
TYPE_CHECK(unsigned char);
TYPE_CHECK(short);
TYPE_CHECK(unsigned short);
TYPE_CHECK(int);
TYPE_CHECK(unsigned int);
TYPE_CHECK(long);
TYPE_CHECK(unsigned long);
TYPE_CHECK(long long);
TYPE_CHECK(unsigned long long);
TYPE_CHECK(wchar_t);
TYPE_CHECK(char16_t);
TYPE_CHECK(char32_t);
TYPE_CHECK(char8_t);
TYPE_CHECK(float);
TYPE_CHECK(double);
TYPE_CHECK(std::byte);
return 0;
}
结果如下:
integral type: bool
integral type: char
integral type: signed char
integral type: unsigned char
integral type: short
integral type: unsigned short
integral type: int
integral type: unsigned int
integral type: long
integral type: unsigned long
integral type: long long
integral type: unsigned long long
integral type: wchar_t
integral type: char16_t
integral type: char32_t
integral type: char8_t
non integral type: float
non integral type: double
non integral type: std::byte
不同于Java等语言,对于各类型宽度,C++标准只保证:
1 == sizeof(char) ≤ sizeof(short) ≤ sizeof(int) ≤ sizeof(long) ≤ sizeof(long long)
1 == sizeof(char) ≤ sizeof(wchar_t)
也就是说 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
都不是关键词,也不是原生类型- 它们只是类型别名!
伪代码如下:
typedef int wchar_t;
// typedef short unsigned int wchar_t;
typedef short unsigned int char16_t;
typedef unsigned int char32_t;
typedef unsigned char char8_t;
参考
-
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/