C++ 第六章 类型与声明 - 6.2 类型

6.2 类型

观察下面的式子:

x = y + f(2);

要想让这个式子在C++程序中有效,必须提前声明好名字x、y、f。换句话说,程序员必须确保名字x、y和f对应的实体确实存在,且对于它们的类型来说 = (赋值)、+ (加法) 和 ( ) (函数调用) 是有意义的。

C++程序中的每个名字(标识符)都对应一种数据类型。该类型决定了这个名字(即改名字代表的实体)能执行哪些运算以及如何执行这些运算。例如:

float x;		//x是一个浮点型变量
int y = 7;		//y是一个整型变量,他的初始值是7
float f(int);	//f是一个函数,它接受一个整数类型的参数,返回一个浮点数

有了这几条声明语句,一开始的那个式子就有意义了。因为我们把y声明成int类型,所以能给它赋值,也能把它作为+的运算对象;因为我们把f声明成接受int参数的函数,所以能用整数值2调用它。

本章介绍基本类型(见6.2.1节)和声明(见6.3节),其中的示例程序仅用于描述语法特性,并不能解决什么实际问题。后面的章节会陆续引入一些应用面广且有实际意义的例子。本章介绍的内容是构成C++程序所需的最基本元素,读者必须了解这些元素以及与之有关的技术和语法,这样才有能力用C++编写出一段真正的代码,同时也才有可能读懂别人编写的代码。不过,也不是说必须掌握本章提到的每一个细节之后才能继续学习后续章节。建议读者先泛读本章,记下有哪些欸主要的概念,在以后用到的时候再返回来深入研究。

6.2.1 基本类型

C++包含一套基本类型(fundamental type),这些类型对应计算机最基本的存储单元并且展现了如何利用这些单元存储数据。

  • 6.2.2节 布尔值类型(bool)
  • 6.2.3节 字符类型(比如char和wchar_t)
  • 6.2.4节 整数类型(比如int和long long)
  • 6.2.5节 浮点数类型(比如double和long double)
  • 6.2.7节 void类型,用以表示类型信息缺失

基于上述类型,我们可以用声明构造出更多类型

  • 7.2节 指针类型(比如int*)
  • 7.3节 数组类型(比如char[])
  • 7.7节 引用类型(比如double& 和vector&&)

除此之外,用户还能自定义类型:

  • 8.2节 数据结构和类(第16章)
  • 8.4节 枚举类型,用以表示特定值的集合(enum 和 enum class)

其中,布尔值、字符和整数统称为整型(integral type),整型和浮点数进一步统称为算术类型(arithmetic type)。我们把枚举类型和类(第16章)称为用户自定义类型(user-defined type),因为用户必须先定义它们,然后才能使用;这一点显然与基本类型无须声明可以直接使用的方式不同。与之相反,我们把基本类型、指针和引用统称为内置类型(built-in type)。标准库提供了很多种精妙的用户自定义类型(第4章和第5章)。

整型和浮点型包含的具体类型很多,它们的尺寸各不相同,程序员可以根据计算任务所需的存储空间大小、精度和表示范围选择适当的类型(见6.2.8节)。在设计这些类型时,我们假设计算机系统的字节可以存放字符、字可以存放和计算整数值、某些实体可以执行浮点计算,而内存地址可以用来引用或指向上述实体。C++的基本类型以及指针和数组以一种与实现无关的方式把这些机器级别的思想和概念呈现在程序员面前,供他们使用。

对于大多数应用来说,我们用bool表示布尔值、用char表示字符、用int表示整数值、用double表示浮点数。其他基本类型可以看做上述类型的变形,只有当程序在性能优化、兼容性或其他方面有所要求时才会用到它们;一般情况下,前面四种类型已经足够了。

6.2.2 布尔值

一个布尔变量(bool)的取值或者是true或者是false,布尔变量常用于表示逻辑运算的结果。例如:

void f(int a, int b)
{
    
    
	bool b1 {
    
    a==b};
	//...
}

如果a和b的值相等,则b1变成true;否则b1的值是false。

有的函数被用于检验某个条件(谓词)是否成立,我们常常将这类函数的返回值类型设为bool。例如:

bool is_open(File*);
bool greater(int a, int b){
    
    return a>b};

根据定义,当我们把布尔值转换成整数时,true转为1而false转为0。反之,整数值也能在需要的时候隐式地转换成布尔值,其中非0整数值对应true而0对应false。例如:

bool b1 = 7;	//因为7 != 0,所以b1被赋值为true
bool b2{
    
    7};		//错误:发生了窄化转换(见2.2.2节和10.5节)
int i1 = true;	//i1被赋值为1
int i2{
    
    true};	//i2被赋值为1

如果你既想使用{}-初始化器列表防止窄化转换的发生,同时又确实想把int转换为bool,则可以显式声明如下:

void f(int i)
{
    
    
	bool b{
    
    i != 0};
	//...
};

在算术逻辑表达式和位逻辑表达式中,bool被自动转换成int,编译器在转换后的值上执行整数算术运算以及逻辑运算。如果最终的计算结果需要转换或bool,则与之前介绍的一样,0转换成false而非0值转换成true。例如:

bool a = true;
bool b = true;

bool x = a+b;	//a+b的结果是2,因此x的最终取值是true
bool y = a||b;	//a||b的结果是1,因此y的值是true(“||”的含义是“或”)
bool z = a-b;	//a-b的结果是0,因此z的最终取值是false

如有必要,指针也能被隐式地转换成bool(见10.5.2.5节)。其中,非空指针对应true,值为nullptr的指针对应false。例如:

void g(int* p)
{
    
    
	bool b = p;		//窄化成true或false
	bool b2{
    
    p != nullptr};	//显式地检查指针是否为非空

	if(p){
    
    		//等价于p != nullptr
		//...
	}
}

与if(p != nullptr)相比,我觉得if§更好,它不但简洁而且可以直接表达“p是否有效”的含义,使用if§也不太容易出错。

6.2.3 字符类型

常用的字符集和字符集编码方式有很多。为了反映和描述这种多样性,C++提供了一系列字符类型:

  • char:默认的字符类型,用于程序文本。char是C++实现所用的字符集,通常占8位。
  • signed char:与char类似,但是带有符号;换句话说,它既可以存放正值也可以存放负值。
  • unsigned char:与char类似,但是不带符号。
  • wchar_t:用于存放Unicode(见7.3.2.2节)等更大的字符集。wchar_t的尺寸依赖于实现,确保能够支持实现环境中用到的最大字符集(第39章)。
  • char16_t:该类型存放UTF_16等16位字符集。
  • char32_t:该类型存放UTF_32等32位字符集。
    这是6种不同的字符类型(除了后缀_t常用于指代别名之外,见6.5节)。在具体的实现版本中,char类型可能会和signed char或者unsigned char完全等效。但不管怎么样,我们还是把这3个名字看成完全独立的3种类型。

一个char类型的变量存放一个字符,字符的种类由实现版本所用的字符集决定。例如:

char ch = ‘a’;

绝大多数情况下char占8个二进制位,因此可以保存256个不同的值。一般来说,该字符集是ISO-646的某个变种,比如ASCII,你所用的键盘上的字符应该都会包含在内。由于该字符集只实现了部分标准化,所以有时候会带来一些问题。

字符集一些可能的变化必须引起我们的重视,比如支持不同自然语言的字符集,以及虽然支持的自然语言是同一种,但实现方式不同的字符集。我们最关心的是这些区别是否会影响C++的语法规则。如何在多语言、多字符集的环境中编程是一个更大也更有趣的命题,我们对它稍有涉及(见6.2.3节,36.2.1节和第39章);但基本上这个问题已经远远超出了本书讨论的范围。

我们不妨认为所有字符集都包含十进制数字、英语的26个字母以及一些最基本的标点符号。但是下面这些对字符集的假设不一定成立,有可能带来一些问题:

  • 8位字符集中字符总数不超过127个(某些字符集提供了255个字符)。
  • 字符集中只包含英文字母,没有其他字母(大多数欧洲大陆的语言都含有更多字母)。
  • 字母都是紧密相连的(EBCDIC在字母’i’和’j’之间留有空位)
  • 编写C++代码所需的字符都是可用的(某些国家的字符集中不含{、}、[、]、|和)。
  • char占用一个字节。某些嵌入式处理器没有按字节访问内存的硬件,因此char占4个字节。另外,程序员也可以用16位Unicode字符集编码基本char类型。

读者最好不要对对象的表示形式做出任何主观假设,对于字符尤其如此。

在编码环境中所用的字符集中,每个字符都对应一个整数值。例如,在ASCII字符集中字符’b’的值是98。下面这个小程序的功能是,你可以查看任意字符对应的整数值:

void intval()
{
    
    
	for(char c; cin>>c;)
		cout<<”the value of ‘”<<c<<”’ is ”<<int{
    
    c}<<’\n’;
}

我们用符号int{c}得到字符c对应的整数值(“用c构建的int”)。既然char能转换成int,那么随之而来的问题是:char是有符号的还是无符号的?一个8位的字节所能容纳的256个值既可以被看成0255,也能被看成是-127127。注意:不是像你想象的-128~127。因此C++标准支持使用补码的硬件设备,而补码会排除掉一个值,所以如果我们使用-128的话代码就不容易移植了。不幸的是,char到底是带符号的还是无符号的是个依赖于实现的问题。为了解决这一问题,C++提供了两种含义更明确的字符类型:signed char存放-127~127之间的值;unsigned char存放0255之间的值。幸运的是,问题只出现在0127之外的值,而绝大多数常用的字符事实上不会受到干扰。

把超过上述范围的值存入一个普通的char会带来一定移植性方面的问题。如果你需要使用几种不同的char或者你需要把整数值存在char变量中,请参阅6.2.3.1节。

请注意,字符类型属于整型(见6.2.1节)。因此,我们可以在字符类型上执行算术运算和位逻辑运算(见10.3节)。例如:

void digits()
{
    
    
	for(int i = 0; i != 10; ++i)
		cout<<static_cast<char>(0+ i);
}

上面的代码把10个阿拉伯数字输出到cout。字符字面值常量’0’先转换成它对应的整数值,再与i相加;所得的int再转回char并被输出到cout。’0’+i得到的结果本来是一个int,因此如果不加上static_cast的话,输出的结果将会是48,49…,而不是0,1,2…

6.2.3.1 带符号字符和无符号字符

char类型到底带不带符号是依赖于实现的,这可能会带来一些意料之外的糟糕结果。例如:

char c = 255;	//255的二进制表示是“全1形式”,对应的十六进制是0xFF
int i = c;

i的值是几?不幸的是,答案是未定义的。如果在运行环境中一个字节占8位,则答案依赖于char的“全1形式”在转换为int时是何含义。若机器的char是无符号的,则答案是255;反之,若机器的char是带符号的,则答案是-1。在此例中,因为字面值255有可能会转换成char值-1,所以编译器可能会发出警告。但是C++并没有某种通用的机制来检测这种问题。一个可能的解决方案是放弃使用普通char而只使用特定的char类型。不过像strcmp()这样的标准库函数通常只接受普通的char(见43.4节)。

虽然从本质上来说,char的行为无非与signed char一致或者与unsigned char一致,但这3个名字代表的类型的确各不相同。我们不能混用指向这3种字符类型的指针,例如:

void f(char c, signed char sc, unsigned char uc)
{
    
    
	char* pc = & uc;			//错误:不存在对应的指针转换规则
	signed char* psc = pc;		//错误:不存在对应的指针转换规则
	unsigned char* puc = pc;	//错误:不存在对应的指针转换规则
	psc = puc;					//错误:不存在对应的指针转换规则
}

3种char类型的变量可以相互赋值,但是把一个特别大的值赋值给带符号的char(见10.5.2.1节)是未定义的行为。例如:

void g(char c, signed char sc, unsigned char uc)
{
    
    
	c = 255;		//如果普通的char是带符号的且占8位,则该语句的行为依赖于具体实现
	c = sc;			//OK
	c = uc;			//如果普通的char是带符号的且uc的值特别大,则该语句的行为依赖于具体实现
	sc = uc;		//如果uc的值特别大,则该语句的行为依赖于具体实现
	uc = sc;		//OK:转换成无符号类型
	sc = c;			//如果普通的char是无符号的且uc的值特别大,则该语句的行为依赖于具体实现
	uc = c;			//OK:转换成无符号类型
}

再举个例子,假设char占8位:

signed char sc = -160;
unsigned char uc = sc;		//uc==116(因为256-140==116)
cout<<uc;					//输出’t’

char count[256];			//假设是占8位的char(未初始化的)
++count[sc];				//严重错误:越界访问
++count[uc];				//OK

如果你从始至终都使用普通的char并且尽量避免负值,则上面这些潜在的错误不太可能发生。

6.2.3.2 字符字面值常量

字符字面值常量(character literal)是指单引号内的一个字符,如’a’和’0’等。字符字面值常量的数据类型是char,它可以隐式地转换成当前机器所用字符集中对应地整数值。例如,如果你的机器使用地是ASCII字符集,则’0’的值是48。建议程序员尽量使用字符字面值常量,而不要直接使用对应的十进制数量,虽然前者的可移植性更强。

一些字符有一个以反斜线\开头的标准名字,我们称之为转义字符:

字符名字 ASCII名字 C++名字
换行 NL(LF) \n
横向制表 HT \v
纵向制表 VT \n
退格 BS \b
回车 CR \r
换页 FF \f
警告 BEL \a
反斜线 \ \\
问号 ? \?
单引号 \'
双引号 \"
八进制数 ooo \ooo
十六进制数 hhh \xhhh

不要被它们的外表迷惑,它们都是货真价实的单字符。

我们可以把字符集中的字符表示成一个1~3位的八进制数(\后紧跟八进制数字)或者表示成十六进制数( \x 后紧跟十六进制数字)。其中,序列里十六进制数字的数量没有限制。如果遇到了第一个不是八进制数字或十六进制数字的字符,则表明当前的八进制序列或十六进制序列已经结束。例如:

八进制 十六进制 十进制 ASCII
'\6' '\x6' 6 ACK
'\60' '\x30' 48 '0'
'\137' '\x05f' 95 '_'

上述规则使得我们不但可以设法表示出字符集中的每一个字符,而且能把这些字符嵌入到一个长字符串中(见7.3.2节)。但是,一旦我们在程序中使用了字符对应的数字形式,这样的程序就无法在使用不同字符集的机器间移植了。

有时候程序会把多个字符放在一对单引号内,比如’ab’。这种用法已经过时,它的效果完全依赖于实现,我们最好避免这种用法。多字符字面值常量的数据类型是int。

当我们在字符串中嵌入八进制数字常量时,常规的做法是使用3个数字。这样的用法稳定且易于解读,我们不必担心常量之后的字符到底是不是数字。对于十六进制数字常量来说,我们使用两个数字。考虑如下的示例:

char v1[] = “a\xah\129;	//6个字符:’a’ ‘\xa’ ‘h’ ‘\12’ ‘9’ ‘\0’
char v2[] = “a\xah\127;	//5个字符:’a’ ‘\xa’ ‘h’ ‘\127’ ‘\0’
char v3[] = “a\xad\127;	//4个字符:’a’ ‘\xad’ ‘\127’ ‘\0’
char v4[] = “a\xad\0127;	//5个字符:’a’ ‘\xad’ ‘\012’ ‘7’ ‘\0’

宽字符字面值常量形如L’ab’,它的数据类型是wchar_t。单引号内字符的数量及其含义依赖于具体实现。

C++程序可以操作Unicode等其他字符集,这些字符集的规模远不止ASCII的127个字符那么多。大字符集中的字面值常量通常表示成4个或8个十六进制数字,其前缀是u或者U。例如:

U’\UFADEBEEF’
u’\uDEAD’
u’\xDEAD’

对于任意的十六进制数字X而言,较短的表示形式u’\uXXXX’与较长的表示形式U’\U0000XXXX’是等价的。但是长度值不能是4和8之外的其他数字,否则会造成词法错误。ISO/IEC 10646标准定义了上述十六进制数值的含义,这样的值称为通用字符名字(universal character name)。在C++标准中,§iso.2.2、§iso.2.3、§iso.2.14.3、§iso.2.14.5和§iso.E等处解释了通用字符名字。

6.2.4 整数类型

与char类似,整数类型也包含“普通的”int、signed int和unsigned int。整数还可以划分成另外4种形式:short int、“普通的”int、long int 和long long int。其中,long int即long,而long long int即long long。类似地,short是short int的同义词,unsigned是unsigned int的同义词,而signed是signed int的同义词。读者要注意,千万不能想当然地揣测long short int等价于int,压根儿没有long short int这种类型。

当我们把存储空间看成是二进制位地数组时,可以考虑使用unsigned整数类型。但是有的程序员试图用unsigned取代int来表示正整数,并且宣称这样做可以多利用1位,这样的想法有点不切实际。尽管他们试图通过声明unsigned类型的变量来确保某些值为正,但是这种担保并不可靠,因为程序中随处都是隐式类型转换(见10.5.1节和10.5.2.1节),一旦发生了隐式类型转换,谁也不知道后果会怎样。

一个不加修饰的int通常是带符号的,这一点与char不太一样。带符号的int和普通int不是两种类型,前者更像是后者的等价词,只不过语义上更明确一些。

如果你需要更精确地控制整数的尺寸,可以使用中定义的别名(见43.7节)。这些别名包括int64_t(明确规定占用64位的带符号整数)、uint_fast16_t(至少占用16位的无符号整数,一般被认为是最快的整数)和int_least32_t(至少占用32位的带符号整数,类似于int)等。事实上,常规的几种整数类型都对最小尺寸做了很好的定义(见6.2.8节),因此显得有点儿多余,建议程序员谨慎使用。

在标准整数类型之外,一个具体的实现还可能提供某些扩展整数类型(extended integer type,带符号的以及无符号的)。这些新类型的行为必须与标准整数类似并且可以参与类型转换,也对应整数字面值常量。唯一的区别是它们的表示范围更大(占用更多空间)。

6.2.4.1 整数字面值常量

整数字面值常量分为3种:十进制、八进制和十六进制。其中十进制字面值常量最常见,也最符合用户的使用习惯:

7  1234  976  12345678901234567890

当字面值常量表示的数值太大以至于在C++中无法表达时,编译器会发出警告;但是只有使用{}初始化器的形式才会报错(见6.3.5节)。

以x或X(0x或0X)开头的字面值常量表示一个十六进制数值(基是16);以0开头但是后面没有x或X的字面值常量表示一个八进制数值(基是8)。例如:

十进制 八进制 十六进制
0 0x
2 02 0x2
63 077 0x3f

在十六进制中,a、b、c、d、e、f及其大写形式分别表示10、11、12、13、14、15。用八进制和十六进制表示二进制比较有效,但如果用它们表示纯数字,可能会产生意想不到的后果。举个例子,如果某台机器用16位补码表示int,则0xffff对应十进制-1;但如果表示int的二进制位不只16位,则0xffff对应的值就可能变成65535了。

后缀U用于显式指定unsigned字面值常量,与之类似,后缀L用于显式指定long字面值常量。例如,3是一个int,3U的类型是unsigned int而3L的类型是long int。

多个后缀可以组合在一起使用,例如:

cout<<0xF0UL<< ‘ ’ <<0LU << ‘\n’;

如果没有显式地指定后缀,编译器会根据整数字面值常量的值以及当前实现的整数尺寸为它分配一种合适的类型(见6.2.4.2节)。

不要滥用含义不明显的常量,最好只在给const(见7.5节)、constexpr(见10.4节)和枚举(见8.4节)赋初值时使用。

6.2.4.2 整数字面值常量的类型

通常情况下,整数字面值常量的类型由它的形式、取值和后缀共同决定:

  • 如果它是十进制数且没有后缀,则它的类型是下面几种类型中能够表达它的值且尺寸最小的那个:int、long int、long long int。
  • 如果它是八进制数或十六进制数且没有后缀,则它的类型是下面几种类型中能够表达它的值且尺寸最小的那个:int,unsigned int,long int,unsigned long int,long long int, unsigned long long int。
  • 如果它的后缀是u或U,则它的类型是下面几种类型中能够表达它的值且尺寸最小的那个:unsigned int,unsigned long int,unsigned long long int。
  • 如果它是十进制数且后缀是l或L,则它的类型是下面几种类型中能够表达它的值且尺寸最小的那个:long int,long long int。
  • 如果它是八进制数或十六进制数且后缀是l或L,则它的类型是下面几种类型中能够表达它的值且尺寸最小的那个:long int,unsigned long int,long long int,unsigned long long int。
  • 如果它的后缀是ul,lu,uL,Lu,Ul,lU,UL或LU,则它的类型是下面几种类型中能够表达它的值且尺寸最小的那个:unsigned long int,unsigned long long int。
  • 如果它是八进制数或十六进制数且后缀是ll或LL,则它的类型是下面几种类型中能够表达它的值且尺寸最小的那个:long long int,unsigned long long int。
  • 如果它的后缀是llu,llU,ull,Ull,LLu,LLU,uLL或ULL,则它的类型是unsigned long long int。

例如,对于字面值常量100000来说,在32位int的机器上它的类型是int,而在16位int和32位long的机器上它的类型是long int。类似地,0XA000在32位int的机器上类型是int而在16位int的机器上的类型是unsigned int。我们可以使用后缀来规避上述对于实现的依赖性:在任何机器上100000L的类型都是long int,0XA000U的类型都是unsigned int。

6.2.5 浮点数类型

浮点数类型用于表示浮点数。浮点数是实数在有限内存空间上的一种近似表示。有3种浮点数类型:float(单精度)、double(双精度)和long double(扩展精度)。

所谓单精度、双精度和扩展精度的确切含义是依赖于具体实现的。程序员只有对浮点运算有非常深刻的理解才能在解决实际问题时做出最好的选择。如果你做不到这一点,最好向有经验的程序员寻求建议或者自学。实在不行就优先选择double类型,这是一种折中的选择,比较稳妥。

6.2.5.1 浮点数字面值常量

默认情况下,浮点数字面值常量的类型是double。再说一次,编译器应该会在发现数据类型不足以表示给定值的时候发出警告。下面是一些浮点数字面值常量的示例:

1.23  .23  0.23  1.  1.0  1.2e10  1.23e-15

谨记在浮点数字面值常量内部不允许出现空格。例如,65.43e-21不是一个浮点数字面值常量,它更像是4个独立的词汇单元(并且会导致语法错误):

65.43  e  -  21

如果你希望定义一个float类型的浮点数字面值常量,则必须加上后缀f或F:

3.14159265f  2.0f  2.997925F  2.9e-3f

类似地,如果你希望定义一个long double类型的浮点数字面值常量,加上后缀l或L:

3.14159265L  2.0L  2.997925L  2.9e-3L

6.2.6 前缀和后缀

有一些前缀和后缀常被用来限定字面值常量的类型:
在这里插入图片描述
请注意,上表中的“字符串”是指“字符串字面值常量”(见7.3.2节),而非数据类型“std::string”。

我们当然可以把 . 和e看成是中缀,同时把R”和u8”看成分隔符的一部分,不过怎么命名并不重要。通过上表,我们的最终目标是把字面值常量的各种情况总结在一起,供读者了解和学习。

后缀l和L可以与u和U结合在一起使用,表达的数据类型是unsigned long。例如:

1LU		//unsigned long
2UL		//unsigned long
3ULL	//unsigned long long
4LLU	//unsigned long long
5LUL	//错误

同样,后缀l和L也能用于表示浮点数字面值常量,表达的类型是long double。例如:

1L		//long int
1.0L	//long double

几种前缀R、L和u能结合在一起,如uR“(foo(bar))”。读者一定要对符号U的两种用法加以区分:一种是字符的前缀U,表示unsigned,另一种是字符串的前缀U,表示UTF-32编码(见7.3.2.2节)。

此外,用户可以为自定义类型定义新的后缀。例如,我们可以定义一个新的字面值常量运算符(见19.2.6节):

“foo bar”s	//是一个std::string类型的字面值常量
123_km		//是一个Distance类型的字面值常量

不以_开始的后缀仅存在于标准库中。

6.2.7 void

从语法结构上来说,void属于基本类型。但是它只能被用作其他复杂类型的一部分,不存在任何void类型的对象。void有两个作用:一是作为函数的返回类型用以说明函数不返回任何实际的值;二是作为指针的基本类型部分以表明指针所指对象的类型未知。例如:

void x;		//错误:不存在void类型的对象
void& r;	//错误:不存在void的引用
void f();	//函数f不返回任何实际的值(见12.1.4节)
void* pv;	//指针所指的对象类型未知(见7.2.1节)

当我们声明一个函数时,必须指明返回结果的数据类型。从逻辑上来说,如果某个函数不返回任何值,也许我们会希望直接忽略掉返回值部分。但其实这种想法并不可行,它会违反C++的语法规则( § iso.A)。因此,我们使用void表示函数的返回值为空,此时void可以看成是一种“伪返回类型”。

6.2.8 类型尺寸

C++基本类型的某些方面是依赖于实现的(见6.1节),其中一个例子是int类型的尺寸。我曾经不止一次指出过这种依赖性的存在,并且建议程序员应该尽量避免依赖性带来的问题或者设法减少它对程序结果的影响。为什么呢?通常,如果程序员在几种不同的系统中编程或者使用不同的编译器编辑,则他们必须特别关注依赖性的问题,否则的话,他们有可能得花费大量时间来定位并修改许多隐藏较深、不易察觉的程序错误。有的人宣称他们不太介意可移植性的问题,原因是这些人基本上只在一种系统上编程,而且武断地认为“当前编译器实现地就是真正的C++语言,没有其他了”。这显然是非常狭隘和短视的观点。如果你的程序真的有用,它不可避免地会被移植到其他系统中,此时别的程序员就不得不费时费力地去寻找和改正程序中受实现依赖影响的部分。此外,在一个大系统中,某部分程序常常需要用其他编译器编译;而且即使是你一直使用的编译器,它的不同版本之间也可能会有差异。显然,在编写程序的时候就对实现依赖性的问题给予足够重视并设法减少其负面影响要比事后弥补容易得多。

相对来说,限制依赖于实现的语言特性的影响比较容易;而要想限制依赖于系统的标准库功能就难多了。一种可行的措施是尽量使用那些比较通用的标准库功能,

我们之所以为整数类型、无符号类型和浮点数类型都分别设计了几种不同形式,目的是允许程序员从中选择最恰当的一种以充分利用硬件的特性。在许多机器上,不同的基本类型之间差异很大,这些差异体现在内存需求、内存访问时间和计算时间等方面。如果你深入了解某一机器,则做出抉择的过程会比较容易,比如很容易为某一变量选择一种适用于当前机器的整数类型。显然,要想编写真正具有可移植性的程序非常困难。

下面展示了一组基本类型的集合以及一个字符串字面值常量(见7.3.2节):
在这里插入图片描述
如果以上图的比例来看(0.2英寸对应1个字节),则1M内存大概要向右延伸3英里(5千米)。

所有C++对象的尺寸都可以表示成char英寸的整数倍,因此如果我们令char的尺寸为1,则使用sizeof运算符(见10.3节)就能得到任意类型或对象的尺寸。下面是C++对于基本类型尺寸的一些规定:

1sizeof(char)sizeof(short)sizeof(int)sizeof(long)sizeof(long long)
1sizeof(bool)sizeof(long)
sizeof(char)sizeof(wchar_t)sizeof(long)
sizeof(float)sizeof(double)sizeof(long double)
sizeof(N)sizeof(signed N)sizeof(unsigned N)

其中,最后一行的N可以是char、short、int、long或者long long。C++规定char至少占8位,short至少占16位,long至少占32位。char应该能存放机器字符集中的任意字符,它的实际类型依赖于实现并确保是当前机器上最适合保存和操作字符的类型。通常情况下,char占据一个8位的字节。与之类似,int的实际类型也是依赖于实现的,并确保是当前机器上最适合保存和操作整数的类型。int通常占据一个4字节(32位)的字。上面这些假设是比较恰当的,但是我们很难做更多设定。比如,我们只能说char“通常”占据8位,因为确实也存在char占32位的机器。又比如我们决不能假定int和指针的尺寸一样大,因为在很多机器上(“64位体系结构”)指针的尺寸比整数大。最后请注意,下面两条假设并不成立:sizeof(long)<sizeof(long long)和sizeof(double)<sizeof(long double)。

通过使用sizeof函数,我们能发现基本类型的某些依赖于实现的特性,更多这样的特性包含在中。例如:

#include<limits>	//见40.2节
#include<iostream>
int main()
{
    
    
	cout<< “size of long<<sizeof(1L)<<’\n’;
	cout<<”size of long long<<sizeof(1LL)<<’\n’;

	cout<<”largest float ==<<std::numeric_limits<float>::max()<<’\n’;
	cout<<char is signed ==<<std::numeric_limits<char>::is_signed<<’\n’;
}

因为中定义的函数(见40.2节)是constexpr(见10.4节),所以可以用在需要常量表达式的上下文中,并且不会带来额外的运行时开销。

在赋值语句及表达式中可以自由地使用基本类型,编译器随时随地计算并转换变量的值以尽量做到不损失信息(见10.5节)。

如果某个值v能用T类型的变量确切地表达,则把v的类型转换成T是值保护的(value-preseving)。我们最好避免使用那些做不到值保护的类型转换(见2.2.2节和10.5.2.6节)。

如果你需要使用某种特定尺寸的整数类型(比如16位的整数),应该事先#include标准库头文件。在中定义了很多类型(或类型别名,见6.5节),例如:

int16_t x{
    
    0xaabb};				//2字节
int64_t xxxx{
    
    0xaaaabbbbccccdddd};	//8字节
int_least16_t y;				//至少2字节(与int类型)
int_least32_t yy;				//至少4字节(与long类型)
int_fast32_t z;					//是最快的整数类型,至少包含4个字节

在标准库头文件中定义了一个别名,它被广泛用于标准库声明及用户代码:size_t是一个依赖于实现的无符号整数类型,用于表示任意对象所占的字节数。我们可以在需要保存对象尺寸的时候使用size_t,例如:

void* allocate(size_t n);			//获得n个字节

类似地,还定义了一个带符号的整数类型ptrdiff_t,两个指针相减所得的元素数量可以保存在ptrdiff_t中。

6.2.9 对齐

对象首先应该有足够的空间存放对应的变量,但这还不够。在一些机器的体系结构中,存放变量的字节必须保持一种良好的对齐(alignment)方式,以便硬件在访问数据资源时足够高效(在极端情况下一次性访问所有数据)。例如,4字节的int应该按字(4字节)的边界排列,而8字节的double有时也应该按字(8字节)的边界排列。当然这些约定都是依赖于实现且用户不可见的,你也许写了几十年漂亮的C++代码却从来没有为对齐问题担心过。对齐只有在涉及对象布局的问题中比较明显:有时候我们会让struct包含一些“空洞”以提升整齐程度(见8.2.1节)。

alignof()运算符返回实参表达式的对齐情况,例如:

auto ac = alignof(‘c’);	//char的对齐情况
auto ai = alignof(1);	//int的对齐情况
auto ad = alignof(2.0);	//double的对齐情况

int a[20];
auto aa = alignof(a);	//int的对齐情况

有时我们需要在声明语句中使用对齐,但是不允许形如alignof(x+y)的表达式;此时,我们可以使用类型说明符alignas:alignas(T),它的含义是“像T那样对齐”。例如,我们用下面的语句为一些类型X的变量留出未初始化的存储空间:

void user(const vector<X>& vx)
{
    
    
	constexpr int bufmax = 1024;
	alignas(X) buffer[bufmax];	//未初始化的

	const int max = min(vx.size(), bufmax/sizeof(X));
	uninitialized_copy(vx.begin(), vx.begin()+max, buffer);
}

猜你喜欢

转载自blog.csdn.net/qq_40660998/article/details/121788377
今日推荐