const 限定符把一个对象转换成一个常量。
const int bufSize = 512; // input buffer size
定义 bufSize 为常量并初始化为 512。变量 bufSize 仍然是一个左值,但是现在这个左值是不可修改的。任何修改 bufSize 的尝试都会导致编译错误:
bufSize = 0; // error: attempt to write to const object
因为常量在定义后就不能被修改,所以定义时必须初始化:
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)
// ...
本程序中,file_1.cc 通过函数 fcn 的返回值来定义和初始化 bufSize。而 bufSize 定义为 extern,也就意味着 bufSize 可以在其他的文件中使用。
file_2.cc 中 extern 的声明同样是 extern;这种情况下,extern 标志着 bufSize 是一个声明,所以没有初始化式。
非 const 变量默认为 extern。要使 const 变量能够在其他的文件中访问,必须地指定它为 extern。
引用
引用就是对象的另一个名字。在实际程序中,引用主要用作函数的形式参数。
引用是一种复合类型,通过在变量名前添加“&”符号来定义。复合类型是指用其他类型定义的类型。在引用的情况下,每一种引用类型都“关联到”某一其他类型。不能定义引用类型的引用,但可以定义任何其他类型的引用。
引用必须用与该引用同类型的对象初始化:
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: non const reference to a const object
typedef 定义以关键字 typedef 开始,后面是数据类型和标识符。标识符或类型名并没有引入新的类型,而只是现有数据类型的同义词。typedef 名字可出现在程序中类型名可出现的任何位置。
typedef 通常被用于以下三种目的:
• 为了隐藏特定类型的实现,强调使用类型的目的。
• 简化复杂的类型定义,使其更易理解。
• 允许一种类型用于多个目的,同时使得每次使用该类型的目的明确。
枚举
枚举的定义包括关键字 enum,其后是一个可选的枚举类型名,和一个用花括号括起来、用逗号分开的枚举成员列表。
// input is 0, output is 1, and append is 2
enum open_modes {input, output, append};
默认地,第一个枚举成员赋值为 0,后面的每个枚举成员赋的值比前面的大 1。
可以为一个或多个枚举成员提供初始值,用来初始化枚举成员的值必须是一个常量表达式。常量表达式是编译器在编译时就能够计算出结果的整型表达式。
整型字面值常量是常量表达式,正如一个通过常量表达式自我初始化的 const 对象也是常量表达式一样。
例如,可以定义下列枚举类型:
// shape is 1, sphere is 2, cylinder is 3, polygon is 4
enum Forms {shape = 1, sphere, cylinder, polygon};
在 枚举类型 Forms 中,显式将 shape 赋值为 1。其他枚举成员隐式初始化:sphere 初始化为 2,cylinder 初始化为 3,polygon 初始化为 4。
枚举成员值可以是不唯一的。
// point2d is 2, point2w is 3, point3d is 3, point3w is 4
enum Points { point2d = 2, point2w,
point3d = 3, point3w };
本例中,枚举成员 point2d 显式初始化为 2。下一个枚举成员 point2w 默认初始化,即它的值比前一枚举成员的值大 1。因此 point2w 初始化为 3。枚举成员 point3d 显式初始化为 3。一样,point3w 默认初始化,结果为 4。
不能改变枚举成员的值。枚举成员本身就是一个常量表达式,所以也可用于需要常量表达式的任何地方。
每个 enum 都定义一种唯一的类型
每个 enum 都定义了一种新的类型。和其他类型一样,可以定义和初始化 Points 类型的对象,也可以以不同的方式使用这些对象。枚举类型的对象的初始化或赋值,只能通过其枚举成员或同一枚举类型的其他对象来进行:
Points pt3d = point3d; // ok: point3d is a Points enumerator
Points pt2w = 3; // error: pt2w initialized with int
pt2w = polygon; // error: polygon is not a Points enumerator
pt2w = pt3d; // ok: both are objects of Points enum type
注意把 3 赋给 Points 对象是非法的,即使 3 与一个 Points 枚举成员相关联。
类类型
C++ 中,通过定义类来自定义数据类型。类定义了该类型的对象包含的数据和该类型的对象可以执行的操作。标准库类型 string、istream 和 ostream 都定义成类。
每个类都定义了一个接口和一个实现。接口由使用该类的代码需要执行的操作组成。实现一般包括该类所需要的数据。实现还包括定义该类需要的但又不供一般性使用的函数。
定义类时,通常先定义该类的接口,即该类所提供的操作。通过这些操作,可以决定该类完成其功能所需要的数据,以及是否需要定义一些函数来支持该类的实现。
在 C++ 语言中,定义这种数据类型的方法就是定义类:
class Sales_item {
public:
// operations on Sales_item objects will go here
private:
std::string isbn;
unsigned units_sold;
double revenue;
};
类定义以关键字 class 开始,其后是该类的名字标识符。类体位于花括号里面。花括号后面必须要跟一个分号。
类体可以为空。类体定义了组成该类型的数据和操作。这些操作和数据是类的一部分,也称为类的成员。操作称为成员函数,而数据则称为数据成员。
类也可以包含 0 个到多个 private 或 public 访问标号。访问标号控制类的成员在类外部是否可访问。使用该类的代码可能只能访问 public 成员。
定义了类,也就定义了一种新的类型。类名就是该类型的名字。通过命名 Sales_item 类,表示 Sales_item 是一种新的类型,而且程序也可以定义该类型的变量。
每一个类都定义了它自己的作用域。也就是说,数据和操作的名字在类的内部必须唯一,但可以重用定义在类外的名字。
访问标号
访问标号负责控制使用该类的代码是否可以使用给定的成员。类的成员函数可以使用类的任何成员,而不管其访问级别。访问标号 public、private 可以多次出现在类定义中。给定的访问标号应用到下一个访问标号出现时为止。
类中public部分定义的成员在程序的任何部分都可以访问。一般把操作放在 public 部分,这样程序的任何代码都可以执行这些操作。
不是类的组成部分的代码不能访问 private成员。通过设定 Sales_item 的数据成员为 private,可以保证对 Sales_item 对象进行操作的代码不能直接操纵其数据成员。就像我们在第一章编写的程序那样,程序不能访问类中的 private 成员。Sales_item 类型的对象可以执行那些操作,但是不能直接修改这些数据。
使用 struct 关键字
C++ 支持另一个关键字 struct,它也可以定义类类型。struct 关键字是从 C 语言中继承过来的。
如果使用 class 关键字来定义类,那么定义在第一个访问标号前的任何成员都隐式指定为 private;如果使用 struct 关键字,那么这些成员都是 public。使用 class 还是 struct 关键字来定义类,仅仅影响默认的初始访问级别。
可以等效地定义 Sales_item 类为:
struct Sales_item {
// no need for public label, members are public by default
// operations on Sales_item objects
private:
std::string isbn;
unsigned units_sold;
double revenue;
};
本例的类定义和前面的类定义只有两个区别:这里使用了关键字 struct,并且没有在花括号后使用关键字 public。struct 的成员都是 public,除非有其他特殊的声明,所以就没有必要添加 public 标号。
用 class 和 struct 关键字定义类的唯一差别在于默认访问级别:默认情况下,struct 的成员为 public,而 class 的成员为 private。
编写自己的头文件
头文件为相关声明提供了一个集中存放的位置。头文件一般包含类的定义、extern 变量的声明和函数的声明。函数的声明将在第 7.4 节介绍。使用或定义这些实体的文件要包含适当的头文件。
头文件的正确使用能够带来两个好处:保证所有文件使用给定实体的同一声明;当声明需要修改时,只有头文件需要更新。
设计头文件还需要注意以下几点:
头文件中的声明在逻辑上应该是统一的。
编译头文件需要一定的时间。如果头文件太大,程序员可能不愿意承受包含该头文件所带来的编译时代价。
头文件用于声明而不是用于定义
当设计头文件时,记住定义和声明的区别是很重要的。定义只可以出现一次,而声明则可以出现多次。下列语句是一些定义,所以不应该放在头文件里:
extern int ival = 10; // initializer, so it's a definition
double fica_rate; // no extern, so it's a definition
虽然 ival 声明为 extern,但是它有初始化式,代表这条语句是一个定义。类似地,fica_rate 的声明虽然没有初始化式,但也是一个定义,因为没有关键字 extern。同一个程序中有两个以上文件含有上述任一个定义都会导致多重定义链接错误。
因为头文件包含在多个源文件中,所以不应该含有变量或函数的定义。
在头文件中定义这些实体,是因为编译器需要它们的定义(不只是声明)来产生代码。例如:为了产生能定义或使用类的对象的代码,编译器需要知道组成该类型的数据成员。同样还需要知道能够在这些对象上执行的操作。类定义提供所需要的信息。
C++ 中的任何变量都只能定义一次。定义会分配存储空间,而所有对该变量的使用都关联到同一存储空间。因为 const 对象默认为定义它的文件的局部变量,所以把它们的定义放在头文件中是合法的。
这种行为有一个很重要的含义:当我们在头文件中定义了 const 变量后,每个包含该头文件的源文件都有了自己的 const 变量,其名称和值都一样。
当该 const 变量是用常量表达式初始化时,可以保证所有的变量都有相同的值。但是在实践中,大部分的编译器在编译时都会用相应的常量表达式替换这些 const 变量的任何使用。所以,在实践中不会有任何存储空间用于存储用常量表达式初始化的 const 变量。
如果 const 变量不是用常量表达式初始化,那么它就不应该在头文件中定义。相反,和其他的变量一样,该 const 变量应该在一个源文件中定义并初始化。应在头文件中为它添加 extern 声明,以使其能被多个文件共享。