类型是所有程序的基础。类型告诉我们数据代表什么意思以及可以对数据执行哪些操作。
C++ 语言定义了几种基本类型:字符型、整型、浮点型等。C++ 还提供了可用于自定义数据类型的机制,标准库正是利用这些机制定义了许多更复杂的类型,比如可变长字符串string、vector 等。此外,我们还能修改已有的类型以形成复合类型。
类型确定了数据和操作在程序中的意义。
C++ 中对类型的支持是非常广泛的:语言本身定义了一组基本类型和修改已有类型的方法,还提供了一组特征用于自定义类型。
2.1. 基本内置类型
C++ 定义了一组表示整数、浮点数、单个字符和布尔值的 算术类型,另外还定义了一种称为 void 的特殊类型。 void 类型没有对应的值,仅用在有限的一些情况下,通常用作无返回值函数的返回类型。算术类型的存储空间依机器而定。这里的存储空间是指用来表示该类型的位(bit)数。C++标准规定了每个算术类型的最小存储空间,但它并不阻止编译器使用更大的存储空间。事实上,对于int类型,几乎所有的编译器使用的存储空间都比所要求的大。int表 2.1 列出了内置算术类型及其对应的最小存储空间。
Table 2.1. C++: Arithmetic Types
表 2.1. C++ 算术类型
Type 类型 | Meaning 含义 | Minimum Size 最小存储空间 |
---|---|---|
bool | boolean | NA |
char | character | 8 bits |
wchar_t | wide character | 16 bits |
short | short integer | 16 bits |
int | integer | 16 bits |
long | long integer | 32 bits |
float | single-precision floating-point | 6 significant digits |
double | double-precision floating-point | 10 significant digits |
long double | extended-precision floating-point | 10 significant digits |
Because the number of bits varies, the maximum (or minimum) values that these types can represent also vary by machine. 因为位数的不同,这些类型所能表示的最大(最小)值也因机器的不同而有所不同。 |
内置类型的机器级表示
让存储具有结构的最基本方法是用块(chunk)处理存储。大部分计算机都使用特定位数的块来处理存储,块的位数一般是 2 的幂,因为这样可以一次处理 8、16 或 32 位。64 和 128 位的块如今也变得更为普遍。虽然确切的大小因机器不同而不同,但是通常将 8 位的块作为一个字节,32 位或 4 个字节作为一个“字(word)”。
大多数计算机将存储器中的每一个字节和一个称为地址的数关联起来。对于一个 8 位字节和 32 位字的机器,我们可以将存储器的字表示如下:
736424 | 0 | 0 | 0 | 1 | 1 | 0 | 1 | 1 |
736425 | 0 | 1 | 1 | 1 | 0 | 0 | 0 | 1 |
736426 | 0 | 1 | 1 | 0 | 0 | 1 | 0 | 0 |
736427 | 0 | 0 | 1 | 1 | 1 | 0 | 1 | 1 |
在这个图中,左边是字节的地址,地址后面为字节的 8 位。
可以用地址表示从该地址开始的任何几个不同大小的位集合。可以说地址为 736424 的字,也可以说地址为 736426 的字节。例如,可以说地址为736425的字节和地址为 736427 的字节不相等。如果知道地址为 736425 的字节的类型是8位无符号整数,那么就可以知道该字节表示整数 112。另外,如果这个字节是 ISO-Latin-1 字符集中的一个字符,那它就表示小写字母 q。虽然两种情况的位相同,但归属于不同类型,解释也就不同。
*带符号和无符号类型
和其他整型不同,char 有三种不同的类型:plain char 、unsigned char 和signed char。虽然char 有三种不同的类型,但只有两种表示方式。可以使用unsigned char 或 signed char 表示char 类型。使用哪种char 表示方式由编译器而定。
表示 signed 整型类型最常见的策略是用其中一个位作为符号位。符号位为 1,值就为负数;符号位为 0,值就为 0 或正数。一个signed 整型取值是从 -128 到 127。
*整型的赋值
对象的类型决定对象的取值。这会引起一个疑问:当我们试着把一个超出其取值范围的值赋给一个指定类型的对象时,结果会怎样呢?答案取决于这种类型是signed 还是unsigned 的。
对于 unsigned 类型来说,编译器必须调整越界值使其满足要求。编译器会将该值对 unsigned 类型的可能取值数目求模,然后取所得值。比如 8 位的unsigned char,其取值范围从 0 到 255(包括 255)。如果赋给超出这个范围的值,那么编译器将会取该值对 256 求模后的值。例如,如果试图将 336 存储到 8 位的unsigned char 中,则实际赋值为 80,因为 80 是 336 对 256 求模后的值。
对于 unsigned 类型来说,负数总是超出其取值范围。unsigned 类型的对象可能永远不会保存负数。有些语言中将负数赋给unsigned 类型是非法的,但在 C++ 中这是合法的。
C++ 中,把负值赋给 unsigned 对象是完全合法的,其结果是该负数对该类型的取值个数求模后的值。所以,如果把 -1 赋给8位的unsigned char,那么结果是 255,因为 255 是 -1 对 256 求模后的值。
*浮点型
类型 float、 double 和 long double 分别表示单精度浮点数、双精度浮点数和扩展精度浮点数。一般 float 类型用一个字(32 位)来表示, double 类型用两个字(64 位)来表示, long double 类型用三个或四个字(96 或 128 位)来表示。类型的取值范围决定了浮点数所含的有效数字位数。对于实际的程序来说,float 类型精度通常是不够的——float 型只能保证 6 位有效数字,而double 型至少可以保证 10 位有效数字,能满足大多数计算的需要。
《解释:》这个精度是指 有效数字,而不单指小数点后的位数。 至于精度是 6, 是指 最坏情况下,保持的精度至少有 6 位。
这是因为,IEEE标准的 float 类型用 23 bit 来表示浮点数的小数部分。
极限情况下(这时没有使用隐含的高位),这里所能表示的 10 进制的精度是 log10(2^23) = 6.92...
所以最小也能保持 6 位的精度。
建议:使用内置算术类型
实际上,许多人用整型进行计数。例如:程序经常计算像vector 或数组这种数据结构的元素个数。在第三章和第四章中,我们将看到标准库定义了一组类型用于统计对象的大小。因此,当计数这些元素时使用标准库定义的类型总是正确的。其他情况下,使用unsigned 类型比较明智,可以避免值越界导致结果为负数的可能性。当执行整型算术运算时,很少使用short 类型。大多数程序中,使用short 类型可能会隐含赋值越界的错误。这个错误会产生什么后果将取决于所使用的机器。比较典型的情况是值“截断(wrap around)”以至于因越界而变成很大的负数。同样的道理,虽然char 类型是整型,但是char 类型通常用来存储字符而不用于计算。事实上,在某些应用中char 类型被当作signed 类型,在另外一些应用中则被当作unsigned 类型,因此把 char 类型作为计算类型使用时容易出问题。
在大多数机器上,使用int 类型进行整型计算不易出错。就技术上而言,int 类型用 16 位表示——这对大多数应用来说太小了。实际应用中,大多数通用机器都是使用和long 类型一样长的 32 位来表示 int 类型。整型运算时,用 32 位表示 int 类型和用 64 位表示long 类型的机器会出现应该选择int 类型还是long 类型的难题。在这些机器上,用 long 类型进行计算所付出的运行时代价远远高于用int 类型进行同样计算的代价,所以选择类型前要先了解程序的细节并且比较long 类型与int 类型的实际运行时性能代价。
决定使用哪种浮点型就容易多了:使用double 类型基本上不会有错。在float 类型中隐式的精度损失是不能忽视的,而double 类型精度代价相对于float 类型精度代价可以忽略。事实上,有些机器上,double 类型比float 类型的计算要快得多。long double 类型提供的精度通常没有必要,而且还需要承担额外的运行代价。
2.2. 字面值常量
称之为字面值是因为只能用它的值称呼它,称之为常量是因为它的值不能修改。每个字面值都有相应的类型,例如: 0 是 int 型, 3.14159 是 double 型。只有内置类型存在字面值,没有类类型的字面值。因此,也没有任何标准库类型的字面值。
整型字面值规则
定义字面值整数常量可以使用以下三种进制中的任一种:十进制、八进制和十六进制。当然这些进制不会改变其二进制位的表示形式。例如,我们能将值20 定义成下列三种形式中的任意一种:
20 // decimal 024 // octal 0x14 // hexadecimal
以 0(零)开头的字面值整数常量表示八进制,以 0x 或 0X 开头的表示十六进制。
字面值整数常量的类型默认为int 或long 类型。其精度类型决定于字面值——其值适合int 就是 int 类型,比int 大的值就是long 类型。通过增加后缀,能够强制将字面值整数常量转换为long、unsigned 或unsigned long 类型。通过在数值后面加L 或者 l(字母“l”大写或小写)指定常量为 long 类型。
<提示>:定义长整型时,应该使用大写字母 L。小写字母l 很容易和数值 1 混淆。
类似地,可通过在数值后面加 U 或 u 定义 unsigned 类型。同时加L 和U 就能够得到unsigned long 类型的字面值常量。但其后缀不能有空格:
<提示>:
没有 short 类型的字面值常量。
浮点字面值规则
通常可以用十进制或者科学计数法来表示浮点字面值常量。使用科学计数法时,指数用 E 或者 e 表示。默认的浮点字面值常量为double 类型。在数值的后面加上F 或f 表示单精度。同样加上 L 或者 l 表示扩展精度(再次提醒,不提倡使用小写字母l)。下面每一组字面值表示相同的值:
3.14159F .001f 12.345L 0. 3.14159E0f 1E-3F 1.2345E1L 0e0
布尔字面值和字符字面值
单词 true 和 false 是布尔型的字面值:可打印的字符型字面值通常用一对单引号来定义:
'a' '2' ',' ' ' // blank
这些字面值都是 char 类型的。在字符字面值前加 L 就能够得到 wchar_t 类型的宽字符字面值。如:
L'a'
非打印字符的转义序列
有些字符是 不可打印的。不可打印字符实际上是不可显示的字符,比如退格或者控制符。还有一些在语言中有特殊意义的字符,例如单引号、双引号和反斜线符号。不可打印字符和特殊字符都用 转义字符书写。转义字符都以反斜线符号开始,C++ 语言中定义了如下转义字符:
newline 换行符 | \n | horizontal tab 水平制表符 | \t |
vertical tab 纵向制表符 | \v | backspace 退格符 | \b |
carriage return 回车符 | \r | formfeed 进纸符 | \f |
alert (bell) 报警(响铃)符 | \a | backslash 反斜线 | \\ |
question mark 疑问号 | \? | single quote 单引号 | \' |
double quote 双引号 | \" |
我们可以将任何字符表示为以下形式的通用转义字符:
\ooo
\7 (bell) \12 (newline) \40 (blank) \0 (null) \062 ('2') \115 ('M')字符’ \0’通常表示“空字符(null character)”,我们将会看到它有着非常特殊的意义。
同样也可以用十六进制转义字符来定义字符:
\xddd
它由一个反斜线符、一个 x 和一个或者多个十六进制数字组成。
字符串字面值
字符串字面值是一串常量字符字符串字面值常量用双引号括起来的零个或者多个字符表示。不可打印字符表示成相应的转义字符。
"Hello World!" // simple string literal "" // empty string literal "\nCC\toptions\tfile.[cC]\n" // string literal using newlines and tabs
为了兼容 C 语言,C++ 中所有的字符串字面值都由编译器自动在末尾添加一个空字符。字符字面值
'A' // single quote: character literal表示单个字符 A,然而
"A" // double quote: character string literal
表示包含字母 A 和空字符两个字符的字符串。
也存在宽字符串字面值,一样在前面加“L”,如
L"a wide string literal"宽字符串字面值是一串常量宽字符,同样以一个宽空字符结束。
字符串字面值的连接
两个相邻的仅由空格、制表符或换行符分开的字符串字面值(或宽字符串字面值),可连接成一个新字符串字面值。这使得多行书写长字符串字面值变得简单:
// concatenated long string literal
std::cout << "a multi-line "
"string literal "
"using concatenation"
<< std::endl;
执行这条语句将会输出:
a multi-line string literal using concatenation
如果连接字符串字面值和宽字符串字面值,将会出现什么结果呢?例如:
// Concatenating plain and wide character strings is undefined
std::cout << "multi-line " L"literal " << std::endl;
其结果是未定义的,也就是说,连接不同类型的行为标准没有定义。这个程序可能会执行,也可能会崩溃或者产生没有用的值,而且在不同的编译器下程序的动作可能不同。
多行字面值
理长字符串有一个更基本的(但不常使用)方法,这个方法依赖于很少使用的程序格式化特性:在一行的末尾加一反斜线符号可将此行和下一行当作同一行处理。C++ 的格式非常自由。特别是有一些地方不能插入空格,其中之一是在单词中间。特别是不能在单词中间断开一行。但可以通过使用反斜线符号巧妙实现:
// ok: A \ before a newline ignores the line break
std::cou\
t << "Hi" << st\
d::endl;
等价于
std::cout << "Hi" << std::endl;
可以使用这个特性来编写长字符串字面值:
// multiline string literal
std::cout << "a multi-line \
string literal \
using a backslash"
<< std::endl;
注意反斜线符号必须是该行的尾字符——不允许有注释或空格符。同样,后继行行首的任何空格和制表符都是字符串字面值的一部分。正因如此,长字符串字面值的后继行才不会有正常的缩进。
建议:不要依赖未定义行为
使用了未定义行为的程序都是错误的,即使程序能够运行,也只是巧合。未定义行为源于编译器不能检测到的程序错误或太麻烦以至无法检测的错误。
不幸的是,含有未定义行为的程序在有些环境或编译器中可以正确执行,但并不能保证同一程序在不同编译器中甚至在当前编译器的后继版本中会继续正确运行,也不能保证程序在一组输入上可以正确运行且在另一组输入上也能够正确运行。
程序不应该依赖未定义行为。同样地,通常程序不应该依赖机器相关的行为,比如假定int 的位数是个固定且已知的值。我们称这样的程序是不可移植的。当程序移植到另一台机器上时,要寻找并更改任何依赖机器相关操作的代码。在本来可以运行的程序中寻找这类问题是一项非常不愉快的任务。