C 和 C++ 字符数据类型简史






4.82/5 (15投票s)
字符数据类型的目的、历史和使用场景
引言
本文的主题是大多数编程语言中被认为是基本的内容:字符和string
类型。然而,这并不是一篇关于C和C++中字符数据使用方法的初学者文章。我的目标是阐明各种字符数据类型,它们的目的、历史和使用场景。
可能从阅读本文中受益的人包括
- 有其他编程语言经验,但对C和C++感兴趣的程序员
- 不同经验水平的C和C++程序员,他们不确定何时或如何使用不同的字符和
string
类型 - 编程爱好者,他们可能会觉得了解C和C++中字符和字符串数据类型开发背后的历史很有趣
原始的char数据类型
原始的char
类型诞生于1971年,当时位于新泽西的贝尔实验室的Dennis Ritchie开始扩展B编程语言以增加类型。char
是他添加到该语言的第一个类型,该语言最初称为NB
("new B"),后来更名为C。
char
是一种类型,表示机器上可寻址的最小单元,它可以包含基本字符集。除了字符数据,它还经常用作“byte
”类型——例如,二进制数据通常声明为char
数组。
对于习惯于Java或C#等较新编程语言的开发者来说,C版本的char
显得过于灵活且说明不足。例如,在Java中,char
类型是原始整型,保证可以容纳16位无符号整数,表示UTF-16代码单元。相比之下,C/C++的char
类型
- 大小为一个字节,但其位数至少保证为8位。实际上存在(或至少曾经存在)
char
位数超过8位——多达32位——的实际平台; - 可以是带符号或无符号的。默认值通常取决于平台,并且可以通过编译器标志进行更改。
signed char
和unsigned char
是存在的,它们分别保证是无符号和带符号的,但它们都与char
是不同的类型。 - 可以包含任何单字节字符集中的一个字符,或多字节字符集中编码
string
的一个字节。
为了说明前面的几点——在Java中
char a = (char)65; // A
char b = (char)174; // ®
char c = (char)1046; // Ж
我们知道a
包含一个16位无符号整型值65,代表拉丁大写字母A
;b
是一个16位无符号整型值174,代表注册商标符号;c
是一个16位无符号整型值1046,代表西里尔字母大写zhe。
在C或C++中,我们没有这样的保证。
char a = (char)65;
a
至少有8位,所以我们可以确定它的值是65,而不管char
的“符号性”如何。该值代表什么则有待解释。对于大多数基于ASCII的字符集,a
将代表拉丁大写字母A
,就像在Java中一样。然而,在使用EBCDIC编码的IBM大型机系统上,它将没有任何意义。
char b = (char)174;
假设char
是8位宽,b
的值可能是174
,或者溢出为类似-82
的值,这取决于char
类型是带符号还是无符号。此外,它实际代表的字符甚至在各种基于ASCII的字符集中也会有所不同。例如,在ISO-8859-1(Latin-1编码)中,它代表注册商标符号(如Java);在ISO 8859-2(Latin-2)中,它代表“Ş”(S-cedilla);在ISO 8859-5(Latin/Cyrillic)中,它代表“Ў”(短U)等等。
char c = (char)1046;
c
在现代硬件架构下几乎肯定会溢出,其值将是无意义的。
实际上,我们很少将整数值赋给char
,而是使用字符字面量。例如
char a = 'A';
这将起作用并将一个整数值赋给a
。现在,你能猜出存储在变量中的实际数值是多少吗?这取决于编译器、它的选项以及源文件的编码。对于当今大多数使用的平台,存储在a
中的值将是65
。如果你在IBM大型机上编译代码,很可能的值是193
,但这也要取决于编译器设置。
那么
char b = '®';
根据编译器和源文件编码的不同,它可能成功编译并将类似174
的值存储到b
中,或者导致编译器错误。例如,我使用的Linux上的clang 14.0
期望UTF-8源文件,它报告
error: character too large for enclosing character literal type
类似
char c = 'Ж';
如果源文件保存为ISO-8859-5代码页并且编译器被设置为使用该编码,理论上可以工作。Clang再次因相同的原因因相同错误而失败。
char
字面量的另一个有趣特性是,在C中它的类型是int
——而不是char
。例如,sizeof('a')
很可能返回类似4
的值。在C++中,字面量的类型是char
,而sizeof('a')
保证为1
。
C风格字符串
显然,字符数据最常被用作字符的string
,而不是单个字符。在C中,string
是一个字符数组,以值为0
的字符结尾。有一些C库实现了所谓的“Pascal风格”string
,其中字符数组前面是其长度,但它们很少见,因为语言本身偏爱空终止的string
。例如,字符串字面量“abc
”的类型将是char[4]
(在C中,而不是C++中)——它将包含为尾随零留下的空间。
在最简单的情况下,C风格string
将使用单字节字符集进行编码,在这种情况下,每个char
对应一个可以在屏幕上显示的“字母”。显然,char
数组不携带关于字符集的信息,因此需要单独提供文本才能正确显示。
只要不需要嵌入零,C风格string
就可以很好地处理多字节编码。在这种情况下,单个char
包含一个字节,该字节可以对应于单个字符,也可以是多字节编码字符的一部分。
C标准库假定string
是零终止的。例如,strlen()
函数的一个简单实现可能如下
size_t strlen(const char *str)
{
const char *c = str;
while (*c != 0)
++c;
return (c - str);
}
让我们看看上面提到的char
类型的特性如何影响数据string
。如果我们查看strlen
示例,可以看到
- 它不受
char
大小的影响。无论有多少位,它都会正确计数直到遇到0
; - 它不受
char
符号的影响。无论char
是带符号还是无符号,它都将返回相同的值; - 取决于字符集,
strlen
返回的值可能与调用者期望的一致,也可能不一致。也就是说,该函数将始终返回数组中char
的数量,如果字符集是单字节的,这通常就是字符的数量。对于多字节字符集,返回的char
数量通常会与用户感知的字符数量不同。
C++字符串类
C++标准库提供了模板类std::basic_string<>
,它可以为各种字符类型进行实例化。它与typedef
声明一起在<string>
头文件中,用于字符类型实例化。char
类型的typedef
std::basic_string<char>
是std::string
。
历史上,string
类早于C++中的模板和命名空间。在1998年C++标准被采纳之前,它只是众多string
类之一。在其早期,string
类通常采用写时复制(copy-on-write)语义实现,这导致了各种问题,尤其是在多线程环境中,最终被C++11标准禁止。
如今,std::basic_string
已被广泛采用和使用。它的实现通常包含“短字符串优化”——一种对于短string
使用基于栈的缓冲区技术。随着移动语义(move semantics)的采纳,string
与C++容器配合良好,而不会引入不必要的复制。在现代C++中,使用char*
来表示string
很少有意义,除非是在API层面。
wchar_t
在20世纪80年代末,一项旨在引入一种通用字符集以取代所有基于8位代码单元的旧式字符编码的倡议启动了。其理念是将流行的ASCII字符集从7位扩展到16位,这被认为足以涵盖所有世界语言的字符。新的编码标准称为Unicode,第一版于1991年底发布。
为了支持新的“宽”字符,C90标准增加了一个新类型——wchar_t
。它被定义为“一个整型,其值范围能够表示所有支持的区域设置中指定的最大扩展字符集的所有成员的唯一代码”。wchar_t
中的“w
”表示“wide
”(宽),以强调wchar_t
通常(但不一定!)比char
更宽。“_t
”部分来自于这样一个事实:在C中,wchar_t
不是一个独立的编译器类型,而是另一个整型(如unsigned short
)的typedef
。wchar_t
在wchar.h头文件中声明,并提供了各种用于处理宽string
的函数,如wcslen()
、wprintf()
等。
在标准C++之前,wchar_t
也从typedef
开始,但很快决定它必须是一个独立的编译器类型。即使在标准化之后,它也仍然与“底层类型”绑定,而该底层类型是其他整型之一。
实际上(但并非根据任何标准),wchar_t
总是无符号的,并且有两种大小
- 在Microsoft Windows和IBM AIX上,它是16位
- 在几乎所有其他平台上,它是32位
这种大小差异是一个不幸的历史遗留问题——Unicode的早期采用者选择了与Unicode 1.0标准兼容的16位大小。在后来的Unicode标准版本引入了补充平面后,wchar_t
在Windows和AIX上用于UTF-16编码格式,在其他较晚采用Unicode的平台上用于UTF-32编码格式。
与wchar_t
一起,还引入了新的宽字符字面量:L''用于宽字符,L""用于宽string
。
C++标准将std::wstring
类定义为basic_string
类模板的实例化,它使用wchar_t
作为字符类型。
C11 / C++11 字符类型
在C11中,引入了两种新的字符类型:char16_t
和char32_t
;它们都声明在<uchar.h>
头文件中,并且是无符号整型的typedef
。前者用于存储16位字符,并且宽度至少为16位。后者用于32位字符,并且宽度至少为32位。
与wchar_t
一样,也引入了新的字面量:u''用于char16_t
,U''用于char32_t
。与wchar_t
不同的是,没有新的string
函数等同于char
的那些函数能与新类型配合使用;没有char16_t
的strlen()
。
引入的第三种新字面量是u8''。它与旧的char
类型一起工作,用于UTF-8编码格式。
随着C11的出现,wchar_t
基本变得无用(尽管未被官方弃用)。这些字符类型旨在用于以下场景
char
用于UTF-8 Unicode编码格式、各种单字节和多字节遗留编码,以及作为字节类型char16_t
用于UTF-16 Unicode编码格式char32_t
用于UTF-32 Unicode编码格式
不出所料,C++11引入了两个同名的字符类型。与C不同,它们是独立的内置类型,而不是typedef
,并且是新的关键字。
C++11引入了两个新的typedef
,用于两个新类型的std::basic_string
实例化
std::u16string
——std::basic_string<char16_t>
的typedefstd::u32string
——std::basic_string<char32_t>
的typedef
C++20 char8_t
C++20引入了一种专门用于UTF-8编码字符数据的新字符类型:char8_t
。它与unsigned char
的大小和符号性相同,但与其不同。u8''字符字面量和 u8"" 字符串字面量已被修改为返回新类型。引入了一个新的typedef
std::u8string
,用于std::basic_string<char8_t>
。
即将发布的C标准(可能是C23)包含了一个关于char8_t
的提案,它是一个unsigned char
的typedef
。
结论
C和C++的字符和字符串类型反映了这两种语言悠久的历史。
原始的char
类型仍然得到最广泛的使用。在新代码中,它应该用于遗留的单字节和多字节编码,以及非字符二进制数据。它与UTF-8编码的字符串配合良好,并且可以用于它们,特别是对于不支持char8_t
类型的编译器。
wchar_t
由于Unicode规范的不断变化而成为牺牲品。今天在新代码中使用它没有太多理由,即使是与旧编译器一起使用。
char16_t
应该用于UTF-16编码的string
,在过去使用各种“widechar
”typedef
的地方。
char32_t
应该用于UTF-32编码的string
。尽管由于其内存效率低下,很少看到UTF-32编码的string
,但单个代码点经常是UTF-32编码的,而char32_t
是实现该目的的理想类型。
char8_t
是最近才引入C++的,并且只在C中被提议。尚不清楚它与普通char
在编码UTF-8string
方面的优势是否足以使其得到广泛使用。
历史
- 2022年11月25日:初始版本