[C++]C++中char、signed char、unsigned char、wchar_t、char16_t、char32_t、char8_t区别

C++20中引入了char8_t,目前看来,C++中字符类型全家应该都到齐了。

不妨,从头梳理一下,看看不发散的情况下,能写多少东西...

C++ 中字符类型

C++中的字符类型非常非常乱,根源在于C语言是上世纪70年代创建的,当时的char和现在语言比如Java、C#的char完全不是一个概念。

先列个表格,看看全家福:

类型引入C标准引入C++标准备注说明
charK&R CC++98表示一个字节的字符
signed charK&R CC++98有符号一个字节字符
unsigned charK&R CC++98无符号一个字节字符
wchar_tC89C++98宽字符
char16_tC11C++1116位Unicode字符
char32_tC11C++1132位Unicode字符
char8_tC23C++20UTF-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_tchar32_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.cppcl 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 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 use  wchar_t for storing Unicode text. The  wchar_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++标准引入u8char8_t只是试图解决执行字符集问题。

执行字符集和源码字符集

要在C++中正确使用中文,必须要了解下面两个概念:

编码备注
源码字符集(the source character set)源码文件是使用何种编码保存的
执行字符集(the execution character set)可执行程序内保存的是何种编码(程序执行时内存中字符串编码)
  • C++98: 既没有规定源码字符集,也没有规定执行字符集
  • C++03:引入了wchar_t,问题依旧
  • C++11:引入char16_tchar32_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_tchar16_tchar32_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显得好看。

  • 16
    点赞
  • 20
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

FL1768317420

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值