第二章 变量和基本类型(续3)类类型 & 编写自己头文件

来源:互联网 发布:linux 卸载svn1.7 编辑:程序博客网 时间:2024/05/16 12:47

2.8. 类类型

C++ 中,通过定义来自定义数据类型。类定义了该类型的对象包含的数据和该类型的对象可以执行的操作。标准库类型string、istream和ostream都定义成类。

C++对类的支持非常丰富——事实上,定义类是如此重要,后续会陆续全部用来描述 C++ 对类及类操作的支持。

类定义以关键字 class 开始,其后是该类的名字标识符。类体位于花括号里面。花括号后面必须要跟一个分号。

类体可以为空。类体定义了组成该类型的数据和操作。这些操作和数据是类的一部分,也称为类的成员。操作称为成员函数,而数据则称为数据成员

类也可以包含 0 个到多个 private 或 public 访问标号。访问标号控制类的成员在类外部是否可访问。使用该类的代码可能只能访问 public 成员。

定义了类,也就定义了一种新的类型。类名就是该类型的名字。

每一个类都定义了它自己的作用域。也就是说,数据和操作的名字在类的内部必须唯一,但可以重用定义在类外的名字。

类的数据成员

定义类的数据成员和定义普通变量有些相似。我们同样是指定一种类型并给该成员一个名字:

     std::string isbn;     unsigned units_sold;     double revenue;
这个类含有三个数据成员:一个名为 isbn 的 string 类型成员,一个名为 units_sold 的 unsigned 类型成员,一个名为 revenue 的 double 类型成员。

类的数据成员定义了该类类型对象的内容。当定义 Sales_item 类型的对象时,这些对象将包含一个 string 型变量,一个 unsigned 型变量和一个 double 型变量。

定义变量和定义数据成员存在非常重要的区别:一般不能把类成员的初始化作为其定义的一部分。当定义数据成员时,只能指定该数据成员的名字和类型。类不是在类定义里定义数据成员时初始化数据成员,而是通过称为构造函数的特殊成员函数控制初始化。

访问标号

访问标号负责控制使用该类的代码是否可以使用给定的成员。类的成员函数可以使用类的任何成员,而不管其访问级别。访问标号 publicprivate 可以多次出现在类定义中。给定的访问标号应用到下一个访问标号出现时为止。

类中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,并且没有在花括号后使用关键字 publicstruct 的成员都是 public,除非有其他特殊的声明,所以就没有必要添加 public 标号。

用 class 和 struct 关键字定义类的唯一差别在于默认访问级别:默认情况下,struct 的成员为 public,而 class 的成员为 private


2.9. 编写自己的头文件

事实上,C++ 程序使用头文件包含的不仅仅是类定义。回想一下,名字在使用前必须先声明或定义。到目前为止,我们编写的程序是把代码放到一个文件里来处理这个要求。只要每个实体位于使用它的代码之前,这个策略就有效。但是,很少有程序简单到可以放置在一个文件中。由多个文件组成的程序需要一种方法连接名字的使用和声明,在 C++ 中这是通过头文件实现的。

为了允许把程序分成独立的逻辑块,C++ 支持所谓的分别编译。这样程序可以由多个文件组成。

为了支持分别编译,我们把 Sales_item 的定义放在一个头文件里面。我们后面定义的 Sales_item 成员函数将放在单独的源文件中。

像 main 这样使用 Sales_item 对象的函数放在其他的源文件中,任何使用 Sales_item 的源文件都必须包含 Sales_item.h 头文件。

设计自己的头文件

头文件为相关声明提供了一个集中存放的位置。头文件一般包含类的定义、extern 变量的声明和函数的声明。函数的声明将在后续介绍。使用或定义这些实体的文件要包含适当的头文件。

头文件的正确使用能够带来两个好处:保证所有文件使用给定实体的同一声明;当声明需要修改时,只有头文件需要更新。

设计头文件还需要注意以下几点:头文件中的声明在逻辑上应该是统一的。编译头文件需要一定的时间。如果头文件太大,程序员可能不愿意承受包含该头文件所带来的编译时代价。

《注解》:为了减少处理头文件的编译时间,有些 C++ 的实现支持预编译头文件。

编译和链接多个源文件

要产生可执行文件,我们不但要告诉编译器到哪里去查找 main 函数,而且还要告诉编译器到哪里去查找 Sales_item 类所定义的成员函数的定义。假设我们有两个文件:main.cc 含有 main 函数的定义,Sales_item.cc 含有 Sales_item 的成员函数。我们可以按以下方式编译这两个文件:


$ CC -c main.cc Sales_item.cc # by default generates a.exe                                   # some compilers generate a.out     # puts the executable in main.exe     $ CC -c main.cc Sales_item.cc -o main
其中 $ 是我们的系统提示符,# 开始命令行注释。现在我们可以运行可执行文件,它将运行我们的 main 程序。

如果我们只是修改了一个 .cc 源文件,较有效的方法是只重新编译修改过的文件。大多数编译器都提供了分别编译每一个文件的方法。通常这个过程产生 .o 文件,.o 扩展名暗示该文件含有目标代码。

编译器允许我们把目标文件链接在一起以形成可执行文件。我们所使用的系统可以通过命令名 CC 调用编译。因此可以按以下方式编译程序:

     $ CC -c main.cc              # generates main.o     $ CC -c Sales_item.cc        # generates Sales_item.o     $ CC main.o Sales_item.o     # by default generates a.exe;                                  # some compilers generate a.out     # puts the executable in main.exe     $ CC main.o Sales_item.o -o main
头文件用于声明而不是用于定义

当设计头文件时,记住定义和声明的区别是很重要的。定义只可以出现一次,而声明则可以出现多次。下列语句是一些定义,所以不应该放在头文件里:

     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。同一个程序中有两个以上文件含有上述任一个定义都会导致多重定义链接错误。

《注解》:因为头文件包含在多个源文件中,所以不应该含有变量或函数的定义。

对于头文件不应该含有定义这一规则,有三个例外。头文件可以定义类、值在编译时就已知道的 const 对象和 inline 函数。这些实体可在多个源文件中定义,只要每个源文件中的定义是相同的。

在头文件中定义这些实体,是因为编译器需要它们的定义(不只是声明)来产生代码。例如:为了产生能定义或使用类的对象的代码,编译器需要知道组成该类型的数据成员。同样还需要知道能够在这些对象上执行的操作。类定义提供所需要的信息。在头文件中定义 const 对象则需要更多的解释。


一些 const 对象定义在头文件中

回想一下,const 变量默认时是定义该变量的文件的局部变量。正如我们现在所看到的,这样设置默认情况的原因在于允许 const 变量定义在头文件中。

在 C++ 中,有些地方需要放置常量表达式。例如,枚举成员的初始化式必须是常量表达式。

一般来说,常量表达式是编译器在编译时就能够计算出结果的表达式。当 const 整型变量通过常量表达式自我初始化时,这个 const 整型变量就可能是常量表达式。而const 变量要成为常量表达式,初始化式必须为编译器可见。为了能够让多个文件使用相同的常量值,const 变量和它的初始化式必须是每个文件都可见的。而要使初始化式可见,一般都把这样的 const 变量定义在头文件中。那样的话,无论该 const 变量何时使用,编译器都能够看见其初始化式。

但是,C++ 中的任何变量都只能定义一次。定义会分配存储空间,而所有对该变量的使用都关联到同一存储空间。因为 const 对象默认为定义它的文件的局部变量,所以把它们的定义放在头文件中是合法的。

这种行为有一个很重要的含义:当我们在头文件中定义了 const 变量后,每个包含该头文件的源文件都有了自己的 const 变量,其名称和值都一样。

当该 const 变量是用常量表达式初始化时,可以保证所有的变量都有相同的值。但是在实践中,大部分的编译器在编译时都会用相应的常量表达式替换这些 const 变量的任何使用。所以,在实践中不会有任何存储空间用于存储用常量表达式初始化的 const 变量。

如果 const 变量不是用常量表达式初始化,那么它就不应该在头文件中定义。相反,和其他的变量一样,该 const 变量应该在一个源文件中定义并初始化。应在头文件中为它添加 extern 声明,以使其能被多个文件共享。

预处理器的简单介绍

既然已经知道了什么应该放在头文件中,那么我们下一个问题就是真正地编写头文件。我们知道要使用头文件,必须在源文件中#include该头文件。为了编写头文件,我们需要进一步理解 #include 指示是怎样工作的。#include 设施是C++ 预处理器的一部分。预处理器处理程序的源代码,在编译器之前运行。C++ 继承了 C 的非常精细的预处理器。现在的 C++ 程序以高度受限的方式使用预处理器。

    #include 指示只接受一个参数:头文件名。预处理器用指定的头文件的内容替代每个 #include。我们自己的头文件存储在文件中。系统的头文件可能用特定于编译器的更高效的格式保存。无论头文件以何种格式保存,一般都含有支持分别编译所需的类定义及变量和函数的声明。

头文件经常需要其他头文件
头文件经常 #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

The conditional directive

条件指示

     #ifndef SALESITEM_H

测试 SALESITEM_H 预处理器变量是否未定义。如果 SALESITEM_H 未定义,那么 #ifndef 测试成功,跟在 #ifndef 后面的所有行都被执行,直到发现 #endif。相反,如果SALESITEM_H 已定义,那么 #ifndef 指示测试为假,该指示和 #endif 指示间的代码都被忽略。

为了保证头文件在给定的源文件中只处理过一次,我们首先检测 #ifndef。第一次处理头文件时,测试会成功,因为 SALESITEM_H 还未定义。下一条语句定义了SALESITEM_H。那样的话,如果我们编译的文件恰好又一次包含了该头文件。#ifndef 指示会发现 SALESITEM_H 已经定义,并且忽略该头文件的剩余部分。

《注解》:

头文件应该含有保护符,即使这些头文件不会被其他头文件包含。编写头文件保护符并不困难,而且如果头文件被包含多次,它可以避免难以理解的编译错误。

使用自定义的头文件

#include 指示接受以下两种形式:

     #include <standard_header>

如果头文件名括在尖括号(< >)里,那么认为该头文件是标准头文件。编译器将会在预定义的位置集查找该头文件,这些预定义的位置可以通过设置查找路径环境变量或者通过命令行选项来修改。使用的查找方法因编译器的不同而差别迥异。建议你咨询同事或者查阅编译器用户指南来获得更多的信息。

如果头文件名括在一对引号里,那么认为它是非系统头文件,非系统头文件的查找通常开始于源文件所在的路径。





* 几个重点概念:
             link(链接)

一个编译步骤,此时多个目标文件放置在一起以形成可执行程序。链接步骤解决了文件间的依赖,如将一个文件中的函数调用链接到另一个文件中的函数定义。

literal constant(字面值常量)

诸如数、字符或字符串的值,该值不能修改。字面值字符用单引号括住,而字面值字符串则用双引号括住。

object(对象)

具有类型的一段内存区域。变量就是一个有名字的对象。


preprocessor(预处理器)

预处理器是作为 C++ 程序编译的一部分运行的程序。预处理器继承于 C 语言,C++ 的特征大量减少了它的使用,但仍保存了一个很重要的用法:#include 设施,用来把头文件并入程序。

separate compilation(分别编译)
将程序分成多个分离的源文件进行编译。