第二章 变量和基本类型
1. 基本内置类型
C++定义了一组表示整数、浮点数、单个字符和布尔值的算术类型,另外还定义了一种称为void的特殊类型。void 类型没有对应的值,仅用在有限的一些情况下,通常用作无返回值函数的返回类型。
算术类型的存储空间依机器而定。C++标准规定了每个算术类型的最小存储空间,但它并不阻止编译器使用更大的存储空间。
整型:表示整数、字符和布尔值的算术类型合称为整型。
字符类型有两种:char和wchar_t。char类型保证了有足够的空间,能够存储机器基本字符集中任何字符相应的数值,因此,char类型通常是单个机器字节(byte)。wchar_t 类型用于扩展字符集,比如汉字和日语,这些字符集中的一些字符不能用单个char表示。
short、int和long类型都表示整型值,存储空间的大小不同。一般, short 类型为半个机器字长,int 类型为一个机器字长,而 long 类型为一个或两个机器字长(在 32 位机器中 int 类型和 long 类型通常字长是相同的)。
bool 类型表示真值 true 和 false。可以将算术类型的任何值赋给 bool 对象。0 值算术类型代表 false,任何非 0 的值都代表 true。
除 bool 类型外,整型可以是带符号的(signed)也可以是无符号的(unsigned)。
整型 int、short 和 long 都默认为带符号型。要获得无符号型则必须指定该类型为 unsigned,比如 unsigned long。unsigned int 类型可以简写为 unsigned,也就是说,unsigned 后不加其他类型说明符意味着是 unsigned int 。
char 有三种不同的类型:plain char、unsigned char和signed char。虽然char有三种不同的类型,但只有两种表示方式。可以使用unsigned char或 signed char表示char类型。使用哪种char表示方式由编译器而定。
C++ 标准并未定义 signed 类型如何用位来表示,而是由每个编译器自由决定如何表示 signed 类型。这些表示方式会影响signed 类型的取值范围。8位signed类型的取值肯定至少是从-127到127,但也有许多实现允许取值从-128到127。
当把一个超出其取值范围的值赋给一个指定类型的对象时,对于 unsigned 类型来说,编译器必须调整越界值使其满足要求。编译器会将该值对 unsigned 类型的可能取值数目求模,然后取所得值。比如 8 位的 unsigned char,其取值范围从 0 到 255(包括 255)。如果赋给超出这个范围的值,那么编译器将会取该值对 256 求模后的值。例如,如果试图将 336 存储到 8 位的 unsigned char 中,则实际赋值为 80,因为 80 是 336 对 256 求模后的值。对于 unsigned 类型来说,负数总是超出其取值范围。unsigned 类型的对象可能永远不会保存负数。有些语言中将负数赋给 unsigned 类型是非法的,但在 C++ 中这是合法的。其结果是该负数对该类型的取值个数求模后的值。所以,如果把 -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 位有效数字,能满足大多数计算的需要。
2. 字面值常量
称之为字面值是因为只能用它的值称呼它,称之为常量是因为它的值不能修改。每个字面值都有相应的类型,例如:0是int型,3.14159是double型。只有内置类型存在字面值,没有类类型的字面值。因此,也没有任何标准库类型的字面值。
定义字面值整数常量可以使用以下三种进制中的任一种:十进制、八进制和十六进制。当然这些进制不会改变其二进制位的表示形式。例如,能将值 20 定义成下列三种形式中的任意一种:
20 // decimal
024 // octal
0x14 // hexadecimal
字面值整数常量的类型默认为int或long类型。其精度类型决定于字面值——其值适合int就是int类型,比int大的值就是long类型。通过增加后缀,能够强制将字面值整数常量转换为 long、unsigned 或 unsigned long 类型。如:
128u /* unsigned */ 1024UL /* unsigned long */
1L /* long */ 8Lu /* unsigned long */
没有 short 类型的字面值常量。
通常可以用十进制或者科学计数法来表示浮点字面值常量。使用科学计数法时,指数用 E 或者 e 表示。默认的浮点字面值常量为 double 类型。在数值的后面加上 F 或 f 表示单精度。同样加上 L 或者 l 表示扩展精度。下面每一组字面值表示相同的值:
3.14159F .001f 12.345L 0.
3.14159E0f 1E-3F 1.2345E1L 0e0
单词 true 和 false 是布尔型的字面值:
可打印的字符型字面值通常用一对单引号来定义:
'a' '2' ',' ' ' // blank
在字符字面值前加 L 就能够得到 wchar_t 类型的宽字符字面值。如:L'a'
字符串字面值常量用双引号括起来的零个或者多个字符表示。不可打印字符表示成相应的转义字符。为了兼容 C 语言,C++ 中所有的字符串字面值都由编译器自动在末尾添加一个空字符:
"A" // 表示包含字母 A 和空字符两个字符的字符串。
也存在宽字符串字面值,一样在前面加“L”,如L"a wide string literal"宽字符串字面值是一串常量宽字符,同样以一个宽空字符结束。
两个相邻的仅由空格、制表符或换行符分开的字符串字面值(或宽字符串字面值),可连接成一个新字符串字面值。这使得多行书写长字符串字面值变得简单:
std::cout << "a multi-line "
"string literal "
"using concatenation"
<< std::endl;
执行这条语句将会输出:a multi-line string literal using concatenation
如果连接字符串字面值和宽字符串字面值其结果是未定义的,也就是说,连接不同类型的行为标准没有定义。这个程序可能会执行,也可能会崩溃或者产生没有用的值,而且在不同的编译器下程序的动作可能不同:std::cout << "multi-line " L"literal " << std::endl;
处理长字符串有一个更基本的(但不常使用)方法,这个方法依赖于很少使用的程序格式化特性:在一行的末尾加一反斜线符号可将此行和下一行当作同一行处理:
std::cou\
t << "Hi" << st\
d::endl;
等价于std::cout << "Hi" << std::endl;
可以使用这个特性来编写长字符串字面值:
std::cout << "a multi-line \
string literal \
using a backslash"
<< std::endl;
注意反斜线符号必须是该行的尾字符——不允许有注释或空格符。同样,后继行行首的任何空格和制表符都是字符串字面值的一部分。正因如此,长字符串字面值的后继行才不会有正常的缩进。
3. 变量
变量提供了程序可以操作的有名字的存储区。C++ 中的每一个变量都有特定的类型,该类型决定了变量的内存大小和布局、能够存储于该内存中的值的取值范围以及可应用在该变量上的操作集。C++程序员常常把变量称为“变量”或“对象(object)”。
左值(发音为 ell-value):左值可以出现在赋值语句的左边或右边。
右值(发音为 are-value):右值只能出现在赋值的右边,不能出现在赋值语句的左边。
变量是左值,因此可以出现在赋值语句的左边。数字字面值是右值,因此不能被赋值。
左值出现的上下文决定了左值是如何使用的。例如,表达式
units_sold = units_sold + 1; units_sold 变量被用作两种不同操作符的操作数。
C++ 所有的关键字
asm do if return try auto double inline short typedef bool int signed typeid break else long sizeof typename case
enum mutable static union catch explicit namespace static_cast unsigned
char export new struct using class extern operator switch virtual
const false private template void const_cast float protected this
volatile continue for public throw wchar_t default friend register true
while delete goto reinterpret_cast dynamic_cast
C++保留了一些词用作各种操作符的替代名。这些替代名用于支持某些不支持标准C++操作符号集的字符集。它们也不能用作标识符:
and bitand compl not_eq or_eq xor_eq
and_eq bitor not or xor
C++ 标准还保留了一组标识符用于标准库。标识符不能包含两个连续的下划线,也不能以下划线开头后面紧跟一个大写字母。有些标识符(在函数外定义的标识符)不能以下划线开头。
多个变量可以定义在同一条语句中:
double salary, wage; // defines two variables of type double
int month,
day, year; // defines three variables of type int
std::string address; // defines one variable of type std::string
C++ 支持两种初始化变量的形式:复制初始化和直接初始化。复制初始化语法用等号(=),直接初始化则是把初始化式放在括号中:
int ival(1024); // direct-initialization
int ival = 1024; // copy-initialization
初始化内置类型的对象只有一种方法:提供一个值,并且把这个值复制到新定义的对象中。对内置类型来说,复制初始化和直接初始化几乎没有差别。
当定义没有初始化式的变量时,系统有时候会帮我们初始化变量。这时,系统提供什么样的值取决于变量的类型,也取决于变量定义的位置。
内置类型变量是否自动初始化取决于变量定义的位置。在函数体外定义的变量都初始化成 0,在函数体里定义的内置类型变量不进行自动初始化。除了用作赋值操作符的左操作数,未初始化变量用作任何其他用途都是没有定义的。未初始化变量引起的错误难于发现。建议每个内置类型的对象都要初始化。虽然这样做并不总是必需的,但是会更加容易和安全,除非你确定忽略初始化式不会带来风险。
C++ 程序通常由许多文件组成。为了让多个文件访问相同的变量,C++ 区分了声明和定义。在 C++ 语言中,变量必须且仅能定义一次,而且在使用变量之前必须定义或声明变量。
变量的定义用于为变量分配存储空间,还可以为变量指定初始值。在一个程序中,变量有且仅有一个定义。
声明用于向程序表明变量的类型和名字。定义也是声明:当定义变量时我们声明了它的类型和名字。可以通过使用extern关键字声明变量名而不定义它。不定义变量的声明包括对象名、对象类型和对象类型前的关键字extern:
extern int i; // declares but does not define i
int i; // declares and defines i
extern 声明不是定义,也不分配存储空间。事实上,它只是说明变量定义在程序的其他地方。程序中变量可以声明多次,但只能定义一次。
如果声明有初始化式,那么它可被当作是定义,即使声明标记为 extern:
extern double pi = 3.1416; // definition
只有当 extern 声明位于函数外部时,才可以含有初始化式。
4. const 限定符
魔数(magic number),它的意义在上下文中没有体现出来。好像这个数是魔术般地从空中出现的。
因为常量在定义后就不能被修改,所以定义时必须初始化:
const std::string hi = "hello!"; // ok: initialized
const int i, j = 0; // error: i is uninitialized const
在全局作用域里定义非 const 变量时,它在整个程序中都可以访问。我们可以把一个非 const 变更定义在一个文件中,假设已经做了合适的声明,就可在另外的文件中使用这个变量:
// file_1.cc
int counter; // definition
// file_2.cc
extern int counter; // uses counter from file_1
++counter; // increments counter defined in file_1
与其他变量不同,除非特别说明,在全局作用域声明的 const 变量是定义该对象的文件的局部变量。此变量只存在于那个文件中,不能被其他文件访问。
通过指定 const 变更为 extern,就可以在整个程序中访问 const 对象:
// file_1.cc
// defines and initializes a const that is accessible to other files
extern const int bufSize = fcn();
// file_2.cc
extern const int bufSize; // uses bufSize from file_1
// uses bufSize defined in file_1
for (int index = 0; index != bufSize; ++index)
// ...
非 const 变量默认为 extern。要使 const 变量能够在其他的文件中访问,必须地指定它为 extern。
5. 引用
引用就是对象的另一个名字。在实际程序中,引用主要用作函数的形式参数。引用是一种复合类型,通过在变量名前添加“&”符号来定义。复合类型是指用其他类型定义的类型。在引用的情况下,每一种引用类型都“关联到”某一其他类型。不能定义引用类型的引用,但可以定义任何其他类型的引用。
非const引用必须用与该引用同类型的对象初始化:
int ival = 1024;
int &refVal = ival; // ok: refVal refers to ival
int &refVal2; // error: a reference must be initialized
int &refVal3 = 10; // error: initializer must be an object
当引用初始化后,只要该引用存在,它就保持绑定到初始化时指向的对象。不可能将引用绑定到另一个对象。初始化是指明引用指向哪个对象的唯一方法。
const引用是指向const对象的引用:
const int ival = 1024;
const int &refVal = ival; // ok: both reference and object are const
int &ref2 = ival; // error: 将普通的引用绑定到 const 对象是不合法的
可以读取但不能修改refVal ,因此,任何对 refVal 的赋值都是不合法的。这个限制有其意义:不能直接对 ival 赋值,因此不能通过使用 refVal 来修改 ival。
const 引用可以初始化为不同类型的对象或者初始化为右值,如字面值常量:
int i = 42;
// legal for const references only
const int &r = 42;
const int &r2 = r + i;
6. typedef 名字
typedef 可以用来定义类型的同义词:
typedef double wages; // wages is a synonym for double
typedef wages salary; // indirect synonym for double
typedef 通常被用于以下三种目的:
①为了隐藏特定类型的实现,强调使用类型的目的。
②简化复杂的类型定义,使其更易理解。
③允许一种类型用于多个目的,同时使得每次使用该类型的目的明确。
7. 枚举
枚举不但定义了整数常量集,而且还把它们聚集成组。
可以为一个或多个枚举成员提供初始值,用来初始化枚举成员的值必须是一个常量表达式。常量表达式是编译器在编译时就能够计算出结果的整型表达式。
枚举成员值可以是不唯一的。
不能改变枚举成员的值。枚举成员本身就是一个常量表达式,所以也可用于需要常量表达式的任何地方。枚举类型的对象的初始化或赋值,只能通过其枚举成员或同一枚举类型的其他对象来进行:
enum Points { point2d = 2, point2w, point3d = 3, point3w };
Points pt3d = point3d; // ok: point3d is a Points enumerator
Points pt2w = 3; // error:把3赋给Points对象是非法的,即使3与一个Points枚举成员相关联。
pt2w = polygon; // error: polygon is not a Points enumerator
pt2w = pt3d; // ok: both are objects of Points enum type
8. 类类型
类定义以关键字 class 开始,其后是该类的名字标识符。类体位于花括号里面。花括号后面必须要跟一个分号。
访问标号负责控制使用该类的代码是否可以使用给定的成员。类的成员函数可以使用类的任何成员,而不管其访问级别。访问标号 public、private 可以多次出现在类定义中。给定的访问标号应用到下一个访问标号出现时为止。
定义类的数据成员和定义普通变量存在非常重要的区别:一般不能把类成员的初始化作为其定义的一部分。当定义数据成员时,只能指定该数据成员的名字和类型。类不是在类定义里定义数据成员时初始化数据成员,而是通过构造函数控制初始化。
struct:如果使用class关键字来定义类,那么定义在第一个访问标号前的任何成员都隐式指定为 private;如果使用struct关键字,那么这些成员都是 public。使用 class 还是 struct 关键字来定义类,仅仅影响默认的初始访问级别。
9. 编写自己的头文件
头文件为相关声明提供了一个集中存放的位置。头文件一般包含类的定义、extern 变量的声明和函数的声明。使用或定义这些实体的文件要包含适当的头文件。
头文件用于声明而不是用于定义:因为头文件包含在多个源文件中,所以不应该含有变量或函数的定义。
extern int ival = 10; // initializer, so it's a definition
double fica_rate; // no extern, so it's a definition
对于头文件不应该含有定义这一规则,有三个例外。头文件可以定义类、值在编译时就已知道的 const 对象和 inline 函数。这些实体可在多个源文件中定义,只要每个源文件中的定义是相同的。在头文件中定义这些实体,是因为编译器需要它们的定义(不只是声明)来产生代码。
一些 const 对象定义在头文件中,const 变量默认时是定义该变量的文件的局部变量。当我们在头文件中定义了 const 变量后,每个包含该头文件的源文件都有了自己的 const 变量,其名称和值都一样。但是在实践中,大部分的编译器在编译时都会用相应的常量表达式替换这些 const 变量的任何使用。所以,在实践中不会有任何存储空间用于存储用常量表达式初始化的 const 变量。
#include 设施是C++ 预处理器的一部分。预处理器处理程序的源代码,在编译器之前运行。#include 指示只接受一个参数:头文件名。预处理器用指定的头文件的内容替代每个 #include。我们自己的头文件存储在文件中。系统的头文件可能用特定于编译器的更高效的格式保存。
头文件经常需要其他头文件,因此,设计头文件时,应使其可以多次包含在同一源文件中。必须保证多次包含同一头文件不会引起该头文件定义的类和对象被多次定义。使得头文件安全的通用做法,是使用预处理器定义头文件保护符。头文件保护符用于避免在已经见到头文件的情况下重新处理该头文件的内容。
预处理器允许我们自定义变量。预处理器变量 的名字在程序中必须是唯一的。任何与预处理器变量相匹配的名字的使用都关联到该预处理器变量。预处理器变量有两种状态:已定义或未定义。定义预处理器变量和检测其状态所用的预处理器指示不同。#define 指示接受一个名字并定义该名字为预处理器变量。#ifndef 指示检测指定的预处理器变量是否未定义。如果预处理器变量未定义,那么跟在其后的所有指示都被处理,直到出现 #endif。
可以使用这些设施来预防多次包含同一头文件:
#ifndef SALESITEM_H
#define SALESITEM_H
// Definition of Sales_itemclass and related functions goes here
#endif
如果 SALESITEM_H 未定义,那么 #ifndef 测试成功,跟在 #ifndef 后面的所有行都被执行,直到发现 #endif。相反,如果 SALESITEM_H 已定义,那么 #ifndef 指示测试为假,该指示和 #endif 指示间的代码都被忽略。
#include 指示接受以下两种形式:
#include <standard_header>
#include "my_file.h"
如果头文件名括在尖括号(< >)里,那么认为该头文件是标准头文件。编译器将会在预定义的位置集查找该头文件,这些预定义的位置可以通过设置查找路径环境变量或者通过命令行选项来修改。使用的查找方法因编译器的不同而差别迥异。如果头文件名括在一对引号里,那么认为它是非系统头文件,非系统头文件的查找通常开始于源文件所在的路径。