C++基本数据类型
在C++中,基本数据类型包含以下几种:
-
空类型:表示空指针类型,即
nullptr_t
类型,占用一个指针的大小(平台相关)。 -
布尔类型:表示
真
或假
,即true
或是false
(占用一个字节)。 -
字符类型:表示单一的字符。根据不同的宽度,又可以细分为以下三种:
char
:单字节字符集中的一个字符。char16_t
:该类型用于表示utf16的一个码点,宽度为两个字节。char32_t
:该类型用于表示utf32的一个码点,宽度为四个字节。wchar_t
:该类型用于表示适合于编译器所属平台的单字符类型,取值是char16_t
或char_32_t
之一。
-
整数类型:表示整数值。根据不同的宽度,又可以细分为以下几种:
short
:编译器最少要提供两个字节的宽度。int
:编译器最少要提供和short
类型相同的宽度。long
:编译器最少要提供4个字节,且至少要提供和int
类型相同的宽度。long long
:编译器至少要提供和long
类型相同的宽度。
-
浮点数类型:表示浮点数。根据不同的精度,又可以细分为以下两种:
float
:单精度浮点数,占用四个字节。double
:双精度浮点数,占用八个字节。
1. 空类型
此类型是C++11标准引入的。在C语言中,通常使用宏NULL
来表示空指针:
#define NULL ((void *)0)
【注意】这里之所以要进行类型转换,是因为"0"是一个整数类型,而非指针类型。如果不进行强制类型转换,那么在进行如下函数调用的时候可能会产生问题:
//$1,C++中使用这种形式的定义。 #define NULL 0 //$2,C语言中使用这种形式的定义。 #define NULL ((void *)0) void func(int); //#1 void func(void*); //#2 int main(void) { func(NULL); // 如果使用$1,此函数将会调用#1;但是原意要调用的却是#2。 // 如果使用$2,此函数将会调用#2,因为对0进行了强制类型转换。 return 0; }
本身使用宏NULL
看上去还不错,但是C++语言是一种强类型的语言,语言本身要求每个对象的类型必须是固定的,要尽最大的力气解决掉可能的二义性。但是"0"这个值实在是太特殊了!由于历史原因,它不仅可以表示整数0,还可以表示空指针,以及布尔值"假",甚至有时还将0直接作为字符串的结束字符使用(因为它和字符’\0’
的码值是一样的)。
如果上面的例子不足以打动你,那么请看下面的一个例子:
#include <mutex>
#include <memory>
int f1(std::shared_ptr<int> ptr) { return 0; }
double f2(std::unique_ptr<int> &ptr) { return 0; }
bool f3(int *ptr) { return 0; }
template<typename FuncType, //内部要转发调用的函数类型。
typename MutexType, //内部互斥量类型。
typename PtrType> //内部转发要调用的函数参数类型。
auto func(FuncType&& func, MutexType&& mutex, PtrType&& ptr)
-> decltype(func(std::forward<PtrType>(ptr))) //c++11后置返回类型。
{
//锁定互斥量。
std::unique_lock<MutexType> guard(std::forward<MutexType>(mutex));
//转发调用参数指定的函数。
return func(std::forward<PtrType>(ptr));
}
int main()
{
std::mutex m1, m2, m3;
func(f1, m1, 0); //int(std::shared_ptr<int>),编译失败,因为第三个参数被推断为int。
func(f1, m1, NULL); //int(std::shared_ptr<int>),编译失败,因为第三个参数被推断为int。
func(f2, m2, 0); //double(std::unique_ptr<int>),编译失败,因为第三个参数被推断为int。
func(f2, m2, NULL); //double(std::unique_ptr<int>),编译失败,因为第三个参数被推断为int。
func(f3, m3, 0); //bool(int*),编译失败,因为第三个参数被推断为int。
func(f3, m3, NULL); //bool(int*),编译失败,因为第三个参数被推断为int。
return 0;
}
函数模板func
的功能是接受一个回调函数、一个互斥量和一个指针类型的参数后,锁定该互斥量,并以该指针为参数调用回调函数。
然而前两个调用却无法通过编译,因为无论是0还是NULL,都被编译器推导为int
类型,无法强制类型转换为指针类型。
为了解决掉这个老大难问题,C++11引入了新类型std::nullptr_t
以及关键字nullptr
,专门用来表示空指针类型。类型std::nullptr_t
可以被隐式类型转换为任何一种指针类型,或者是布尔值"假"。也就是说,任何类型的指针都可以指向nullptr
,但是与宏NULL
不同的是,nullptr
并不会被理解为整数。因此,针对上面的例子,以下代码可以通过编译:
func(f1, m1, nullptr); //nullptr会被正确地推导为指针类型。
func(f2, m2, nullptr); //nullptr会被正确地推导为指针类型。
func(f3, m3, nullptr); //nullptr会被正确地推导为指针类型。
【注意】以空指针表示布尔值"假"是一个特例,这种写法在C语言时代就已经深入人心。C++的设计目标之一是兼容C语言的写法,因此这个概念被保留。
template<typename T> struct ListNode { T* value; ListNode<T>* next; }; int main() { ListNode<int> head = { nullptr, nullptr }; //... ListNode<int>* iter = head.next; //使用指针是否为空,来判断链表遍历是否完成。这种写法已经深入人心,不可更改。 while (iter) //真正想表达的意思是:while(iter != nullptr) { //... iter = iter->next; } return 0; }
总结:C++11提供了新类型std::nullptr_t
和新的关键字nullptr
来表示空指针,其语义较0
和NULL
都更为精准。因此,在新的C++代码中不应该再使用0
或者NULL
。
2. 布尔类型
C++语言提供了原生的布尔类型:true
和false
来表示真与假。如果你犹记得第一部分中“空类型”中我所展现出的问题,那么理解引入布尔类型的必要性也可举一反三。诚然,有很多实现自己布尔类型的方式,例如使用枚举等方式,然而为了避免和整数类型的冲突,还是应该使用原生的布尔类型。
3. 字符类型
任何现代的高级语言都应该提供表示字符的方式。然而,C/C++在这方面表现得确实不够理想:原生的char
类型只能用于表示单字节字符。这并非没有原因,因为在C语言刚被发明出来的时候,只支持英语和基本标志字符就好了。然而, 不同的地区有着不同的字符集,不同的字符集又有着不同的编码方式。这就导致早期软件的国际化变得异常困难。Unicode
字符集解决了这个问题,它几乎容纳了国际上所有的字符,因此也成为了使用得最多的字符集。字符集虽然确定了,但是如此庞大的字符集,它的编码方式却又存在多种。因此,在早期不得不使用字节数组来表示一个字符码点,并使用特殊的字符集转换函数库来进行字符串的编解码操作。
C++11引入了编解码接口codecvt
,但是在随后的C++17中又将其移除了,理由是"此接口原本的设计就不佳(言外之意是当初就不该把它加进标准中),开发人员应该使用专业的字符编解码程序库"😂。从我个人的使用体验来看,属实是不咋地,因此强烈不推荐使用。
char
类型用于表示ASCII
单字节字符集中的一个字符。signed char
类型在大多数平台上都和char
类型相同,但是标准并未规定这一点。如果遇到了char
类型的警告,可以考虑修改为signed char
类型,其他情况下基本不用考虑。unsigned char
类型很特殊,它的本意和字符无关,而是表示字节。如果需要使用字节类型,那么可以使用它。char16_t
类型用于表示utf-16
编码方式中的一个码点。char32_t
类型用于表示utf-32
编码方式中的一个码点。wchar_t
类型给了编译器平台一些选择,既可以代表char16_t
,也可以代表char32_t
。这个类型在不同平台下的长度是不同的。例如,在Windows下,其长度是2个字节(char16_t
),但在linux下,其长度是4个字节(char32_t
)。
一个有趣的规律是,所有以_t
结尾的类型名称都是使用typedef
(或是using
)重命名来的,这是一个C语言中的小传统。
那么引入char16_t
和char32_t
的目的是什么呢?主要就是为了跨平台。因为在这之前,不同的平台可以采取不同的类型实现方式(例如直接使用unsigned short
类型的变量,或是两个unsigned char
类型的变量实现char16_t
)。这导致不同的字符串处理函数库要根据不同的平台来编写不同的实现,十分麻烦。
需要强调的是,char
类型并不等同于signed char
类型,这点和整数类型不同。
最后一点是,C标准库只针对char
和wchar_t
类型提供了字符串处理函数。这是可以理解的,因为这两个类型的具体实现可以让编译器根据目标平台进行动态地调整,用户理论上不需要关心这些细节。如果要进行字符串的传输,可以使用统一的接口:
size_t mbstowcs(wchar_t *pwcs, const char *str, size_t n); //将字节数组转换为平台相关的Unicode字符串。
size_t wcstombs(char *str, const wchar_t *pwcs, size_t n); //将平台相关的Unicode字符串转换为字节数组。
尽管看上去设计的很好,但是仍然存在问题。试想当程序要与一个Java服务器通讯时,有时要传递一个utf-8
编码的字符串,除非Java
服务器使用了Google Protobuf
之类的编解码工具。但是一旦涉及到传输JSON
,那么C/C++程序就需要一个utf-8
转码函数库。
字符集和编码方式有什么区别?“回家”这个动作,每个人都会做,它具有统一的执行方式。但是,在中国不同地区的人对这个动作的称呼不同。例如,北京人喜欢说“回家”,天津人喜欢说“家走”。看,同样的一个动作,却有着不同的称呼方式。这就是字符集和编码方式的区别。
早期,全世界的各个国家都在为自己的文字编码,并且这些编码方式之前互不相同。因此,同一个字符串,拷贝至不同语言的机器上解析时,就可能成了乱码,于是人们就想:我们能不能定义一个超大的字符集,它可以容纳全世界所有的文字字符,再对它们统一进行编码,让每一个字符都对应一个不同的编码值。这样,从其他地区的机器上拷贝一个字符串回来时,最多只是显示的外国文字我看不懂,但是不会再有乱码了。
这是一项艰巨且辛苦的工作,但是总要有人来完成。早期有两个组织在做这件事,一个是国际标准化组织(ISO),他们制订了UCS(Universal Character Set)计划,并最终完成了ISO10646标准;另一个是统一编码联盟,这是由Apple等商业公司所组建而成的组织,他们制订了Unicode字符集标准。
20世纪90年代初,两个组织认为世界上应该只存在统一的标准。于是,他们开始合并彼此的工作成果,并创立一个单一的编码表协同工作,以做到大体上的一致。
- UCS-4:这是在
Unicode
和ISO10646
尚未合并之前,由ISO
指定的一个编码方式,采取了4个字节定长的方式对字符进行编码,它可以编码20多亿个字符。- UTF-32:
UTF-32
只做了一件事:取UCS-4的(0—0x10FFFF)的码点,之后的全不要。随后ISO
为了兼容UTF-32
,也保证不对UCS-4
的0x10FFFF之后的码点赋值。也就是说,UTF-32
是UCS-4
的子集,但是它俩的使用基本是一致的。- UCS-2:见名知意,这是
ISO
指定的另一个编码方式,采取了2个字节定长的方式对字符进行编码,它可以编码65535个字符。但是全球的文字远不止65535个,所以UCS-2
只能编码相对比较常用的字符。- UTF-16:既然
UCS-2
的两个字节不能编码全部的字符串,UTF-16
提供了一种折中的方式:使用和UCS-2
前两个字节相同的码点来表示这些字符,而采取四个字节来表示其余使用较少的字符。也就是说,UCS-2
是UTF-16
的一个子集,而UTF-16
本身是一种变长的编码方式,不支持随机访问。- UTF-8:
UTF-8
完全被设计用来进行存储以及网络传输使用,它采取一至四个字节的变长编码方式来保存所有Unicode字符集中的字符。它的传输效率高、边界明显、纠错简单,但是无法进行随机访问。为什么
Java
等高级语言中直接使用不支持随机访问的UTF-16
编码字符串,而不牺牲一些内存空间,使用访问速度更快的UTF-32
?这是因为历史的原因。在20世纪90年代,Java
等语言刚刚推出的时候,UCS-2
已经可以编码当时所有的字符了,完全没有必要采用UTF-32
。然而随着时代的发展,Unicode
字符集的数量爆炸,Java
等语言才被迫使用UCS-2
的超集UTF-16
。不过从现在来看,各种优化之后性能也还可以😃。
4. 整数类型
4.1 常用整数类型
C/C++中的整数类型存在的问题是:其长度总是与平台相关。所以,很多程序为了获取跨平台的特性,重新定义了自己的整数类型。例如,如果你需要规定自己的程序中整数类型为4个字节,那么可能就会在某个头文件中出现如下语句:
typedef long my_int_t; //16位平台下,long类型是4个字节,但是int类型是2个字节。
C++包含了一系列跨平台的不同精度的通用整数类型,在cstdint
中提供。
#include <cstdint>
uint8_t uint8; //单字节无符号整数。
uint16_t uint16; //双字节无符号整数。
uint32_t uint32; //四字节无符号整数。
uint64_t uint64; //八字节无符号整数。
int8_t int8; //单字节有符号整数。
int16_t int16; //双字节有符号整数。
int32_t int32; //四字节有符号整数。
int64_t int64; //八字节有符号整数。
然而, 由于历史原因,并不是所有的整数类型都适合使用上述这几个类型,例如:
int main(int argc, char *argv[]);
这个main
函数声明,语言规定就是这样,难道还要写成下面这个样子吗?怪怪的。
int32_t main(int32_t argc, char *argv[]);
【建议】只有在确实需要规定整数宽度的时候(例如制定网络通讯协议等场合)才使用上面介绍的类型,否则使用普通基本类型。
除此之外,还有一组非常好用的数据类型,定义在<cstddef>
中:
#include <cstddef>
size_t size; //长度"足够"的无符号整数类型,表示"容量"的语义。
ptrdiff_t diff; //长度"足够"的无符号整数类型,表示"两个指针之间距离"的语义。
4.2 整数类型的表示法
整数类型采取补码的方式进行存储,最高位是符号位(1表示负数,0表示正数),这种表示法给整数类型带来了以下特性:
- 有符号数的正数表示范围要比同宽度的无符号数小很多
- 整数类型的最大值加一等于其最小值,最小值减一等于其最大值
- 对于带符号的整数类型,负数右移在最高位补1,正数右移在最高位补0
让自己习惯补码是非常有好处的,因为这会引导你逐渐地站在CPU的角度思考问题。最简单的例子是:对于32位有符号整数而言,其最大值是:2147483647
。这个东西很难记,但是如果你习惯了补码,那么它就是:0x7FFFFFFF
;同理,最小值就是:0x80000000
。优雅多了。
5. 浮点数类型
C++确实提供了<cfenv>
来针对浮点数运算进行了微调,但是浮点数仍然存在其固有的问题:由于底层采取的是IEEE754规定的符号位+底数+指数的表示法,因此其值域是不连续的,而且无法精确地表示一个值。这也就解释了为什么同样是4个字节,为什么float
比int
表示范围大得多。如果你需要为金融行业编写程序,那么还是采取其他的方式来表示这些数字吧😂。