C++基础之头文件
类
C++中我们通过定义类来定义自己的数据结构。类机制是C++中最重要的特征之一。事实上,C++设计的主要焦点就是使所定义的类类型的行为可以像内置类型一样自然。我们看到的像 istream 和 ostream 这样的库类型,都是定义为类的,也就是说,它们严格说来不是语言的一部分。
使用类时我们需要回答三个问题:
- 类的名字是什么?
- 它在哪里定义?
- 它支持什么操作?
每个类都定义了一个接口和一个实现。接口由使用该类的代码需要执行的操作组成。实现一般包括该类所需要的数据。实现还包括定义该类需要的但又不供一般性使用的函数。
定义类时,通常先定义该类的接口,即该类所提供的操作。通过这些操作,可以决定该类完成其功能所需要的数据,以及是否需要定义一些函数来支持该类的实现。
定义一个类
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 是一种新的类型,而且程序也可以定义该类型的变量。
每一个类都定义了它自己的作用域。也就是说,数据和操作的名字在类的内部必须唯一,但可以重用定义在类外的名字。
类的数据成员
定义类的数据成员和定义普通变量有些相似。我们同样是指定一种类型并给该成员一个名字:
std::string isbn;
unsigned units_sold;
double revenue;
这个类含有三个数据成员:一个名为 isbn 的 string 类型成员,一个名为 units_sold 的 unsigned 类型成员,一个名为 revenue 的 double 类型成员。类的数据成员定义了该类类型对象的内容。当定义 Sales_item 类型的对象时,这些对象将包含一个 string 型变量,一个 unsigned 型变量和一个 double 型变量。
定义变量和定义数据成员存在非常重要的区别:一般不能把类成员的初始化作为其定义的一部分。当定义数据成员时,只能指定该数据成员的名字和类型。类不是在类定义里定义数据成员时初始化数据成员,而是通过称为构造函数的特殊成员函数控制初始化。
访问标号
访问标号负责控制使用该类的代码是否可以使用给定的成员。类的成员函数可以使用类的任何成员,而不管其访问级别。访问标号 public、private 可以多次出现在类定义中。给定的访问标号应用到下一个访问标号出现时为止。
类中public 部分定义的成员在程序的任何部分都可以访问。一般把操作放在 public 部分,这样程序的任何代码都可以执行这些操作。
不是类的组成部分的代码不能访问 private 成员。通过设定 Sales_item 的数据成员为 private,可以保证对 Sales_item 对象进行操作的代码不能直接操纵其数据成员。就像我们在第一章编写的程序那样,程序不能访问类中的 private 成员。Sales_item 类型的对象可以执行那些操作,但是不能直接修改这些数据。
使用 struct 关键字
C++ 支持另一个关键字 struct,它也可以定义类类型。struct 关键字是从 C 语言中继承过来的。
如果使用 class 关键字来定义类,那么定义在第一个访问标号前的任何成员都隐式指定为 private;如果使用 struct 关键字,那么这些成员都是 public。使用 class 还是 struct 关键字来定义类,仅仅影响默认的初始访问级别。
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;
};
Tips: 用 class 和 struct 关键字定义类的唯一差别在于默认访问级别:默认情况下,struct 的成员为 public,而 class 的成员为 private。
编写自己的头文件
一般类定义都会放入头文件。在本节中我们将看到怎样为 Sales_item 类定义头文件。
事实上,C++ 程序使用头文件包含的不仅仅是类定义。回想一下,名字在使用前必须先声明或定义。到目前为止,我们编写的程序是把代码放到一个文件里来处理这个要求。只要每个实体位于使用它的代码之前,这个策略就有效。但是,很少有程序简单到可以放置在一个文件中。由多个文件组成的程序需要一种方法连接名字的使用和声明,在 C++ 中这是通过头文件实现的。
为了允许把程序分成独立的逻辑块,C++ 支持所谓的分别编译。这样程序可以由多个文件组成。为了支持分别编译,我们把 Sales_item 的定义放在一个头文件里面。像 main 这样使用 Sales_item 对象的函数放在其他的源文件中,任何使用 Sales_item 的源文件都必须包含 Sales_item.h 头文件。
设计自己的头文件
头文件为相关声明提供了一个集中存放的位置。头文件一般包含类的定义、extern 变量的声明和函数的声明。使用或定义这些实体的文件要包含适当的头文件。
头文件的正确使用能够带来两个好处:保证所有文件使用给定实体的同一声明;当声明需要修改时,只有头文件需要更新。
设计头文件还需要注意以下几点:头文件中的声明在逻辑上应该是统一的。编译头文件需要一定的时间。如果头文件太大,程序员可能不愿意承受包含该头文件所带来的编译时代价。
为了减少处理头文件的编译时间,有些 C++ 的实现支持预编译头文件。
头文件用于声明而不是用于定义
当设计头文件时,记住定义和声明的区别是很重要的。定义只可以出现一次,而声明则可以出现多次。
对于头文件不应该含有定义这一规则,有三个例外。头文件可以定义类、值在编译时就已知道的 const 对象和 inline 函数(后面介绍 inline 函数)。这些实体可在多个源文件中定义,只要每个源文件中的定义是相同的。
在头文件中定义这些实体,是因为编译器需要它们的定义(不只是声明)来产生代码。例如:为了产生能定义或使用类的对象的代码,编译器需要知道组成该类型的数据成员。同样还需要知道能够在这些对象上执行的操作。类定义提供所需要的信息。在头文件中定义 const 对象则需要更多的解释。
Tips:
一般来说,常量表达式是编译器在编译时就能够计算出结果的表达式。当 const 整型变量通过常量表达式自我初始化时,这个 const 整型变量就可能是常量表达式。而 const 变量要成为常量表达式,初始化式必须为编译器可见。为了能够让多个文件使用相同的常量值,const 变量和它的初始化式必须是每个文件都可见的。而要使初始化式可见,一般都把这样的 const 变量定义在头文件中。那样的话,无论该 const 变量何时使用,编译器都能够看见其初始化式。
预处理器的简单介绍
既然已经知道了什么应该放在头文件中,那么我们下一个问题就是真正地编写头文件。我们知道要使用头文件,必须在源文件中#include该头文件。为了编写头文件,我们需要进一步理解 #include 指示是怎样工作的。#include 设施是C++ 预处理器的一部分。预处理器处理程序的源代码,在编译器之前运行。C++ 继承了 C 的非常精细的预处理器。现在的 C++ 程序以高度受限的方式使用预处理器。
头文件经常 #include 其他头文件。头文件定义的实体经常使用其他头文件的设施。例如,定义 Sales_item 类的头文件必须包含 string 库。Sales_item 类含有一个 string 类型的数据成员,因此必须可以访问 string 头文件。
包含其他头文件是如此司空见惯,甚至一个头文件被多次包含进同一源文件也不稀奇。例如,使用 Sales_item 头文件的程序也可能使用 string 库。该程序不会(也不应该)知道 Sales_item 头文件使用了 string 库。在这种情况下,string 头文件被包含了两次:一次是通过程序本身直接包含,另一次是通过包含 Sales_item 头文件而间接包含。
因此,设计头文件时,应使其可以多次包含在同一源文件中,这一点很重要。我们必须保证多次包含同一头文件不会引起该头文件定义的类和对象被多次定义。使得头文件安全的通用做法,是使用预处理器定义头文件保护符。头文件保护符用于避免在已经见到头文件的情况下重新处理该头文件的内容。
在编写头文件之前,我们需要引入一些额外的预处理器设施。预处理器允许我们自定义变量。
预处理器变量的名字在程序中必须是唯一的。任何与预处理器变量相匹配的名字的使用都关联到该预处理器变量。
为了避免名字冲突,预处理器变量经常用全大写字母表示。
预处理器变量有两种状态:已定义或未定义。定义预处理器变量和检测其状态所用的预处理器指示不同。#define 指示接受一个名字并定义该名字为预处理器变量。#ifndef 指示检测指定的预处理器变量是否未定义。如果预处理器变量未定义,那么跟在其后的所有指示都被处理,直到出现 #endif。
可以使用这些设施来预防多次包含同一头文件:
#ifndef SALESITEM_H
#define SALESITEM_H
// Definition of Sales_itemclass and related functions goes here
#endif
为了保证头文件在给定的源文件中只处理过一次,我们首先检测 #ifndef。第一次处理头文件时,测试会成功,因为 SALESITEM_H 还未定义。下一条语句定义了 SALESITEM_H。那样的话,如果我们编译的文件恰好又一次包含了该头文件。#ifndef 指示会发现 SALESITEM_H 已经定义,并且忽略该头文件的剩余部分。
当没有两个头文件定义和使用同名的预处理器常量时,这个策略相当有效。我们可以为定义在头文件里的实体(如类)命名预处理器变量来避免预处理器变量重名的问题。一个程序只能含有一个名为 Sales_item 的类。通过使用类名来组成头文件和预处理器变量的名字,可以使得很可能只有一个文件将会使用该预处理器变量。
#include 指示接受以下两种形式:
#include <standard_header>
#include "my_file.h"
如果头文件名括在尖括号(< >)里,那么认为该头文件是标准头文件。编译器将会在预定义的位置集查找该头文件,这些预定义的位置可以通过设置查找路径环境变量或者通过命令行选项来修改。使用的查找方法因编译器的不同而差别迥异。如果头文件名括在一对引号里,那么认为它是非系统头文件,非系统头文件的查找通常开始于源文件所在的路径。