***************************************************
更多精彩,欢迎进入:http://shop115376623.taobao.com
***************************************************
2.3 使用命名空间、头文件和实现文件
使新的throttle类满足程序的需求将是非常有意义的(毕竟,我们绝不会知道何时将需要节流阀类)。我们将在此前提下继续完善throttle类,并隐藏类的实现细节。此外,也不应当使其他程序员担心他们自己选择的变量名称是否会碰巧与我们使用的名称发生冲突。
可以通过以下3个步骤来实现上述目标:
(1) 创建命名空间。
(2) 编写头文件。
(3) 编写实现文件。
下面将讨论上述每个步骤的实现意图和相关技术,并阐明另一个程序员如何使用根据这些技术编写的数据项。
2.3.1 创建命名空间
当程序使用了由多个不同程序员编写的不同的类时,将有可能发生命名冲突。举例来说,我们在前面编写了一个节流阀类,但也许国家航空航天局(NASA)也编写了一个节流阀类,而一个程序可能会需要同时使用这两个类。也许用节流阀的例子并不能说明问题,但是普通的现实名称确实会经常发生冲突。
可以利用称为命名空间的程序组织技术来解决这一问题。命名空间(namespace)是一个程序员所选择的用于区分属于他或她的工作的名称。这个名称应该为描述性的,但它也应该包含程序员的真实姓名或email地址的一部分,从而尽可能地避免造成命名冲突。第2章中的第一个命名空间是main_savitch_2A;在本章的后面,还将会使用main_savitch_2B;其他章节中还将使用类似的名称。
属于命名空间的所有内容必须存放于命名空间分组(namespace grouping)中,并采用如下的形式:
namespace main_savitch_2A
{
|| The usual class definition appears here.
}
单词namespace是一个C++关键字。单词main_savitch_2A是为命名空间选择的名称,它可以是任何合法的C++标识符。所有其他代码出现在两个花括号的内部。举例来说,throttle的类声明和throttle的成员函数实现都可以放置在命名空间中。
一个单独的命名空间(例如main_savitch_2A)可以拥有多个不同的命名空间分组。举例来说,throttle的类定义可以出现在main_savitch_2A的一个命名空间分组中,它位于程序中的某一个位置。随后,当准备实现throttle的成员函数时,可以为main_savitch_2A打开第二个命名空间分组,并将函数实现放在第二个分组中。这两个命名空间分组都属于main_savitch_2A命名空间,但它们并不需要存在于同一个文件中。典型的情况是,它们位于两个单独的文件中:
● 类的定义位于头文件(header file)中,头文件提供了程序员所需要的有关如何使用类的全部信息。
● 成员函数的定义位于一个单独的实现文件(implementation file)中。
本章剩余的部分将介绍throttle类的头文件和实现文件的格式,并通过一个示例讲解程序如何使用命名空间中的数据项。
2.3.2 头文件
类的头文件提供了有关程序员如何使用类的所有信息。事实上,有关如何使用类的所有信息都应当出现在头文件顶端的头文件注释(header file comment)中。如果要使用类,程序员只需阅读这段信息的注释即可。注释应当包含所有公有成员函数的列表,而且每个函数都应附带前置条件/后置条件协议(如果函数没有前置条件,那么通常可以省略之,并只列出函数的后置条件)。注释中不应当列出任何私有成员,因为使用类的程序员并不关心私有成员。
类定义位于头文件注释后面的命名空间分组中,但仅仅是类定义出现在这里—— 成员函数的实现并不出现在这里(除了内联函数之外)。
将类定义置于头文件中还有其他一些问题。其中一个问题是:使用类的程序员可能会认为他们不得不阅读类的详细定义才能使用这个类。实际上并非如此,因为有关如何使用类的所有信息都包含在头文件注释中。但是C++要求把类定义放在头文件中,因此对于此问题并没有办法解决。
另一个问题源于头文件的使用方式。在后面的章节中我们将会看到,一个程序有时会多次包含同一个头文件,其结果是类定义也出现了多次,这使得编译过程会由于“重复的类定义”而造成失败。可以将所有的头文件定义放置在称为宏保护(macro guard)的编译器指令内部来避免重复的类定义。在我们的命名空间中,带有宏保护的throttle类声明的完整形式如下所示:
#ifndef MAIN_SAVITCH_THROTTLE_H
#define MAIN_SAVITCH_THROTTLE_H
namespace main_savitch_2A
{
class throttle
{
|| The usual class definition appears here.
};
}
#endif
第一行中的#ifndef MAIN_SAVITCH_THROTTLE_H表明宏保护的开始,从这里至#endif之间出现的所有语句都处于宏保护的控制范围之内。只有当编译器在此之前没有遇到单词MAIN_SAVITCH_THROTTLE_H的时候,这段语句才会被编译。
那么这又是如何避免类的重复定义呢?在这段代码第一次出现时:
● 类定义被编译。
● 单词MAIN_SAVITCH_THROTTLE_H也被定义(通过语句#define MAIN_SAVITCH_ THROTTLE_H定义)。
此时,如果这段代码再次出现,那么第二次的类定义将会被略过(因为MAIN_SAVITCH_THROTTLE_H已经被定义了)。图2-5显示的是throttle类的头文件throttle.h。在过去,大多数程序员使用.h作为头文件名称的结尾(例如throttle.h),尽管这种做法现在已经变得越来越不通用了,因为标准头文件(例如iostream)不再使用.h作为结尾。然而,本书将会继续使用.h,因为某些文本编辑程序或者编译器基于.h文件类型提供了一些特殊模式。
类的头文件
当设计和实现一个类时,应当提供单独的头文件。
在头文件的顶端,放置有关程序员如何使用该类的所有文档信息。
类定义位于文档信息之后。但仅仅是类定义出现在此处,成员函数的实现并不出现在此(不包括内联函数)。
将类定义置于一个命名空间之内,并用“宏保护”包围整段代码。宏保护用于防止意外的重复定义。
头文件
// FILE: throttle.h
// CLASS PROVIDED: throttle (part of the namespace main_savitch_2A)
//
// CONSTRUCTORS for the throttle class:
// throttle( )
// Postcondition: The throttle has one position above the shut_off
// position, and it is currently shut off.
//
// throttle(int size)
// Precondition: size > 0.
// Postcondition: The throttle has size positions above the shut_off
// position, and it is currently shut off.
//
// MODIFICATION MEMBER FUNCTIONS for the throttle class:
// void shut_off( )
//成员函数通常没有前置条件。
// Postcondition: The throttle has been turned off.
//
// void shift(int amount)
// Postcondition: The throttle's position has been moved by
// amount (but not below 0 or above the top position).
//
// CONSTANT MEMBER FUNCTIONS for the throttle class:
// double flow( ) const
// Postcondition: The value returned is the current flow as a
// proportion of the maximum flow.
//
// bool is_on( ) const
// Postcondition: If the throttle's flow is above 0 then
// the function returns true; otherwise it returns false.
//
// VALUE SEMANTICS for the throttle class:
// Assignments and the copy constructor may be used with throttle objects.
#ifndef MAIN_SAVITCH_THROTTLE
//宏保护的开始
#define MAIN_SAVITCH_THROTTLE
namespace main_savitch_2A
//命名空间分组的开始
{
class throttle
{
public:
// CONSTRUCTORS
throttle( );
throttle(int size);
// MODIFICATION MEMBER FUNCTIONS
图2-5 throttle类的头文件
void shut_off( ) { position = 0; }
void shift(int amount);
// CONSTANT MEMBER FUNCTIONS
double flow( ) const { return position / double(top_position); }
bool is_on( ) const { return (position > 0); }
private:
int top_position;
int position;
};
}
//命名空间分组的结束
#endif
//宏保护的结束
图2-5 (续)
在头文件中描述类的值语义
类的值语义(value semantic)决定了如何将值从一个对象复制到另一个对象。在C++中,值语义包含两种操作:赋值操作符和复制构造函数。
赋值操作符(assignment operator) 对于两个对象x和y,赋值语句y = x将x的值复制到y中。对于任何新定义的类,允许类似这样的赋值。对于一个新类,C++通常用如下的方法完成赋值:简单地把赋值操作符右侧对象的成员变量逐个复制到赋值操作符左侧的对象中。这种复制方法称为自动赋值操作符(automatic assignment operator)。在后面我们将会展示一些自动赋值操作符不起作用的示例,但是目前,我们的新类可以使用这种自动赋值操作符。
复制构造函数(copy constructor) 复制构造函数是只具有一个参数的构造函数,而参数的数据类型与构造函数的类相同。例如,throttle的复制构造函数具有一个参数,而参数就是throttle本身。复制构造函数通常的用途是将一个新的对象初始化为另一个已存在对象的严格副本。举例来说,下面的一段代码创建了一个称为x的具有100个位置的节流阀,并将x调节到它的中间位置;然后代码声明了第二个节流阀,并将其初始化为x的一个严格副本:
throttle x(100);
x.shift(50);
throttle y(x);
//节流阀y被初始化为x的一个副本,使得两个节流阀都位于100个位置中的第50个位置。
高亮的语句激活throttle的复制构造函数,以便将y初始化为x的一个严格副本。在初始化之后,x和y可能会执行不同的操作,并处于不同的燃料流量;但是在此刻,两个节流阀都被置于100个位置中的第50个位置。
还有另外一个替代的语法用于调用复制构造函数。除了使用throttle y(x);之外,也可以使用throttle y = x;来调用复制构造函数。这种替代语法看起来就像是赋值语句,但是切记,其实际的效果有一些不同。赋值y = x;只是将x复制到已存在的对象y中。而另一方面,声明throttle y = x;不仅声明了一个新对象y,而且还调用了复制构造函数以便将y初始化为x的一个副本。程序设计中通常使用原始形式throttle y(x);,因为这种形式不易与普通的赋值语句相混淆。
类的实现者可以像编写其他的构造函数一样,编写一个复制构造函数——后面的章节中将会这么做。但是在这里,我们可以利用一个C++的特性:C++提供了一个自动复制构造函数(automatic copy constructor)。自动复制构造函数仅仅通过复制已存在对象的所有成员变量来初始化一个新对象。举例来说,在声明throttle y(x);中,自动复制构造函数将已存在的节流阀x中的两个成员变量复制到新节流阀y中。
对于许多类来说,自动赋值操作符和自动复制构造函数都能很好地工作,但是前文也曾经警告过,后面我们将会展示一些这种自动版本发生失效的类。仅仅复制成员变量并不总是足够的,正因为如此,程序员对使用赋值和复制构造函数非常谨慎。为了解决这一问题,建议在头文件的注释文档中包含一则注释,用于表明可以放心使用类的值语义。
编程提示
注明值语义
在实现一个类时,文档中应当包含一则注释,用于表明可以放心使用类的值语义。举例来说,在throttle的头文件中,编写了如下代码:
// VALUE SEMANTICS for the throttle class:
// Assignments and the copy constructor may be used with throttle objects.
2.3.3 实现文件
类的实现文件(implementation file)包含以下几项:首先出现的是一小段注释,表明类的具体说明文档存放于头文件中。接着是一条include指令,促使编译器从头文件中获取类的定义。在节流阀的示例中,这一include指令如下所示:
#include "throttle.h"
在书写头文件名称throttle.h的时候,此处使用了双引号,而不是尖括号。尖括号只适用于包含标准库中的工具(例如include指令:#include <iostrem>),而双引号则表明使用的是自己编写的头文件。
在include指令之后,程序重新打开命名空间,并给出类成员函数的实现。重新打开命名空间使用的语法与在头文件中所使用的语法相同:
namespace main_savitch_2A
{
|| The definitions of the member functions are written here.
}
大多数编译器需要特定的实现文件后缀名,例如.cpp或.C。而本书将使用.cxx作为实现文件的后缀名,图2-6显示的就是完整的实现文件throttle.cxx。
类的实现文件
每个类都有一个独立的实现文件,其中包含了类成员函数的实现。请访问www.cs.colorado.edu/~main/separation.html获取更多有关实现文件和头文件的信息。
实现文件
// FILE: throttle.cxx
// CLASS IMPLEMENTED: throttle (See throttle.h for documentation)
#include <cassert> // Provides assert
#include "throttle.h" // Provides the throttle class definition
namespace main_savitch_2A
{
throttle::throttle( )
{ // A simple on-off throttle
top_position = 1;
position = 0;
}
throttle::throttle(int size)
// Library facilities used: cassert
{
assert(size > 0);
top_position = size;
position = 0;
}
void throttle::shift(int amount)
{
position += amount;
if (position < 0)
position = 0;
else if (position > top_position)
position = top_position;
}
}
图2-6 throttle类的实现文件
2.3.4 使用命名空间中的数据项
当头文件和实现文件编写完毕之后,任何程序就可以使用这个新定义的类。为了使用新定义的类,需要在程序的顶部放置include指令用于包含头文件,如上述示例所示:
#include "throttle.h"
注意,此处只是包含了头文件,而没有包含实现文件。
在include指令之后,程序可以使用下述3种方法中的任何一种来使用命名空间中定义的数据项:
(1) 放置一条using语句,以便使命名空间的所有数据项可用。语句格式如下:
using namespace main_savitch_2A;
using语句使特定命名空间(main_savitch_2A)中的所有数据项可用,这与前面提到过的获取标准库中所有可用数据项的技术(使用语句using namespace std;)相同。
(2) 如果只需要命名空间中的特定数据项,那么using语句的格式则为:在关键字using之后紧跟命名空间的名称,然后是两个冒号,最后是想要使用的数据项。例如:
using main_savitch_2A::throttle;
这种方法将允许我们使用命名空间中的throttle;如果命名空间中还有其他的数据项,那么我们仍旧无法使用这些数据项。
(3) 在不使用using语句的情况下,只需在使用数据项的地方为数据项增加前缀—— 命名空间名称和::,就仍然能够使用任何数据项。举例来说,可以使用如下语句声明一个throttle变量:
main_savitch_2A::throttle apollo;
::的使用是生存空间解析操作符(在前面讲述过)的一个示例,它澄清了我们请求使用的到底是哪一个特殊的throttle。
图2-7总结了创建和使用命名空间的相关内容,其中包括一则警告,即不要在头文件中放置using语句。
1. 全局命名空间:没有被显式地放置于某一个命名空间的任何数据项都将成为所谓的全局命名空间(global namespace)的一部分。可以在任何地方使用这些数据项,且无须再用using语句或生存空间解析操作符。
2. C++标准库:如果使用新的C++头文件名称(例如<iostream>或者<cstdlib>),那么C++标准库中的所有数据项都将自动地作为std命名空间的一部分。使用这些数据项的最简单方法就是在include语句的后面放置一个using指令:using namespace std;。另一方面,如果使用旧的C++头文件名称(例如<iostream.h>或<stdlib.h>),那么所有的数据项都将会作为全局命名空间的一部分,因此也就不再需要using语句或者生存空间解析操作符。
3. 创建自己的命名空间:如果要创建一个新的命名空间,则需将数据项放置到命名空间分组中,并使用如下的形式:
namespace < The name for the namespace >
{
|| Any item that belongs to the namespace is written here.
}
单词namespace是一个C++关键字。命名空间的名称可以是任意的C++标识符,但也应当作谨慎的选择,以避免与其他命名空间发生冲突(可以使用我们的真实姓名或email地址)。一个单独的命名空间可以包含多个不同的命名空间分组,并存在于不同的文件中。举例来说,头文件中的一个命名空间分组包含了类定义,实现文件中的另一个命名空间分组包含了成员函数的定义,而两个命名空间分组都属于同一个命名空间。
4. 使用命名空间:
● 如果要使用命名空间中的所有数据项,应当在所有include语句的后面放置一个
using指令,如下所示:
using namespace <The name for the namespace>;
● 如果要使用命名空间中的一个数据项,应当在所有include语句的后面放置一个特
殊的using指令,如下所示:
using <The name for the namespace>::<The name of the item>;
● 如果不使用using指令,但仍然要在程序中使用某个数据项,则应当在该数据项的
前面放置命名空间的名称和::。
隐患
不要将using语句放置在头文件中
某些时候,头文件自身也需要使用命名空间中的内容,在这种情况下,应当使用上述的第三种方式,而绝不能在头文件中使用using语句(如果其他的程序包含了这个头文件,那么将会发生意想不到的结果)。
图2-7 创建和使用命名空间的总结
图2-8列出了使用修改后的throttle类的完整程序文档。当实际编译这个完整程序时,可能需要提供一些额外的信息,以便指明在何处能够找到实现文件throttle.cxx的编译后的版本。这个过程称为链接(linking),它会因编译器的不同而情况各异(参见附录D)。
程序
// FILE: demo2.cxx
// This small demonstration shows how the revised throttle class is used.
#include <iostream> // Provides cout and cin
#include <cstdlib> // Provides EXIT_SUCCESS
#include "throttle.h" // Provides the throttle class
using namespace std; // Allows all Standard Library items to be used
using main_savitch_2A::throttle;
const int DEMO_SIZE = 5; // Number of positions in a demonstration throttle
int main( )
{
throttle sample(DEMO_SIZE); // A throttle to use for our demonstration
int user_input; // The position that we set the throttle to
// Set the sample throttle to a position indicated by the user.
cout << "I have a throttle with " << DEMO_SIZE << " positions." << endl;
cout << "Where would you like to set the throttle?" << endl;
cout << "Please type a number from 0 to " << DEMO_SIZE << ": ";
cin >> user_input;
sample.shift(user_input);
// Shift the throttle down to zero, printing the flow along the way.
while (sample.is_on( ))
{
cout << "The flow is now " << sample.flow( ) << endl;
sample.shift(-1);
}
cout << "The flow is now off" << endl;
return EXIT_SUCCESS;
}
示范对话
I have a throttle with 5 positions.
Where would you like to set the throttle?
Please type a number from 0 to 5: 3
The flow is now 0.6
The flow is now 0.4
The flow is now 0.2
The flow is now off
图2-8 使用修改后的throttle类的示范程序
2.3.5 自测习题
13. 如果程序员要学习如何使用一个新类,应当阅读什么内容?
14. 宏保护的目的是什么?
15. 如果x和y均为对象,则赋值y = x通常会发生什么操作?
16. 假设x是一个节流阀,那么声明throttle y(x)将会产生什么影响?
17. 试编写一个使用throttle类的主程序,其中必须包含#include指令和using语句。
18. 设计并实现类circle_location,用于跟踪以圆形轨道运行的点的位置。这个类对象使用角度坐标记录点的位置,角度坐标以圆的顶端作为起点,以顺时针方向作为正方向。该类应当包含下述公有成员函数:
● 默认构造函数,其作用是将点放置到圆的顶端。
● 另一个构造函数,其作用是将点放置到指定位置。
● 用于将点围绕圆心转动指定角度的函数。正的参数表示顺时针方向转动,负的参数表示逆时针方向转动。
● 用于返回点的当前位置的函数,其返回值是点相对于圆的顶端顺时针转过的角度。
需要完成的内容包括:独立的头文件、实现文件和使用该类的主程序。
19. 设计并实现类clock。clock对象保存着一个时间值的实例,例如9:48 P.M.。该类至少具有以下公有成员函数:
● 默认构造函数,其作用是将时间设置为午夜。
● 用于将clock显式地设置为指定的时间的函数(试为该函数选取合适的参数)。
● 用于查询信息的函数,需要查询的信息包括:当前小时和当前分钟。判断函数,用于确定当前时间是否小于或等于正午时间,其返回值为布尔类型。
● 用于根据给定的分钟值向前调节时间的函数。
20. 什么是全局命名空间?
21. 如果头文件中需要使用命名空间的数据项,那么应当采用2.3.4节中的三种方法的哪一种。