标签:style blog color 使用 io 文件 数据 问题
class Person { public: Person(const string& name, const Date& birthday, const Address& addr, const Country& country); virtual ~Person(); ... // 简化起见,省略了拷贝构造 // 函数和赋值运算符函数 string name() const; string birthDate() const; string address() const; string nationality() const; private: string name_; // 实现细节 Date birthDate_; // 实现细节 Address address_; // 实现细节 Country citizenship_; // 实现细节 };
Person的实现用到了一些类,即string, Date,Address和Country;Person要想被编译,就得让编译器能够访问得到这些类的定义。这样的定义一般是通过#include指令来提供的,所以在定义Person类的文件头部,可以看到象下面这样的语句:
#include <string> // 用于string类型 (参见条款49) #include "date.h" #include "address.h" #include "country.h"
遗憾的是,这样一来,定义Person的文件和这些头文件之间就建立了编译依赖关系。所以如果任一个辅助类(即string, Date,Address和Country)改变了它的实现,或任一个辅助类所依赖的类改变了实现,包含Person类的文件以及任何使用了Person类的文件就必须重新编译。对于Person类的用户来说,这实在是令人讨厌,因为这种情况用户绝对是束手无策。
那么,你一定会奇怪为什么C++一定要将一个类的实现细节放在类的定义中。例如,为什么不能象下面这样定义Person,使得类的实现细节与之分开呢?
class string; // "概念上" 提前声明string 类型 // 详见条款49 class Date; // 提前声明 class Address; // 提前声明 class Country; // 提前声明 class Person { public: Person(const string& name, const Date& birthday, const Address& addr, const Country& country); virtual ~Person(); ... // 拷贝构造函数, operator= string name() const; string birthDate() const; string address() const; string nationality() const; };
如果这种方法可行的话,那么除非类的接口改变,否则Person 的用户就不需要重新编译。大系统的开发过程中,在开始类的具体实现之前,接口往往基本趋于固定,所以这种接口和实现的分离将大大节省重新编译和链接所花的时间。
可惜的是,现实总是和理想相抵触,看看下面你就会认同这一点:
int main() { int x; // 定义一个int Person p(...); // 定义一个Person // (为简化省略参数) ... }
当看到x的定义时,编译器知道必须为它分配一个int大小的内存。这没问题,每个编译器都知道一个int有多大。然而,当看到p的定义时,编译器虽然知道必须为它分配一个Person大小的内存,但怎么知道一个Person对象有多大呢?唯一的途径是借助类的定义,但如果类的定义可以合法地省略实现细节,编译器怎么知道该分配多大的内存呢?
原则上说,这个问题不难解决。当定义一个对象时,只分配足够容纳这个对象的一个指针的空间。也就是说,对应于上面的代码,就象这样做:
int main() { int x; // 定义一个int Person *p; // 定义一个Person指针 ... }
实现Person接口和实现的分离的技术:
首先,在声明Person类的头文件中只放下面的东西:
// 编译器还是要知道这些类型名, // 因为Person的构造函数要用到它们 class string; // 对标准string来说这样做不对, // 原因参见条款49 class Date; class Address; class Country; // 类PersonImpl将包含Person对象的实 // 现细节,此处只是类名的提前声明 class PersonImpl; class Person { public: Person(const string& name, const Date& birthday, const Address& addr, const Country& country); virtual ~Person(); ... // 拷贝构造函数, operator= string name() const; string birthDate() const; string address() const; string nationality() const; private: PersonImpl *impl; // 指向具体的实现类 };
现在Person的用户程序完全和string,date,address,country以及person的实现细节分家了。那些类可以随意修改,而Person的用户却落得个自得其乐,不闻不问。更确切的说,它们可以不需要重新编译。另外,因为看不到Person的实现细节,用户不可能写出依赖这些细节的代码。这是真正的接口和实现的分离。
为了降低编译依赖性,我们只要知道这么一条就足够了:只要有可能,尽量让头文件不要依赖于别的文件;如果不可能,就借助于类的声明,不要依靠类的定义。其它一切方法都源于这一简单的设计思想。
下面就是这一思想直接深化后的含义:
· 如果可以使用对象的引用和指针,就要避免使用对象本身。定义某个类型的引用和指针只会涉及到这个类型的声明。定义此类型的对象则需要类型定义的参与。
· 尽可能使用类的声明,而不使用类的定义。因为在声明一个函数时,如果用到某个类,是绝对不需要这个类的定义的,即使函数是通过传值来传递和返回这个类:
class Date; // 类的声明 Date returnADate(); // 正确 ---- 不需要Date的定义 void takeADate(Date d);
不要在头文件中再(通过#include指令)包含其它头文件,除非缺少了它们就不能编译。相反,要一个一个地声明所需要的类,让使用这个头文件的用户自己(通过#include指令)去包含其它的头文件,以使用户代码最终得以通过编译。
Person类仅仅用一个指针来指向某个不确定的实现,这样的类常常被称为句炳类(Handle class)或信封类(Envelope class)。(对于它们所指向的类来说,前一种情况下对应的叫法是主体类(Body class);后一种情况下则叫信件类(Letter class)。)
你一定会好奇句炳类实际上都做了些什么。答案很简单:它只是把所有的函数调用都转移到了对应的主体类中,主体类真正完成工作。例如,下面是Person的两个成员函数的实现:
#include "Person.h" // 因为是在实现Person类, // 所以必须包含类的定义 #include "PersonImpl.h" // 也必须包含PersonImpl类的定义, // 否则不能调用它的成员函数。 // 注意PersonImpl和Person含有一样的 // 成员函数,它们的接口完全相同 Person::Person(const string& name, const Date& birthday, const Address& addr, const Country& country) { impl = new PersonImpl(name, birthday, addr, country); } string Person::name() const { return impl->name(); }
请注意Person的构造函数怎样调用PersonImpl的构造函数(隐式地以new来调用,参见条款5和M8)以及Person::name怎么调用PersonImpl::name。这很重要。使Person成为一个句柄类并不改变Person类的行为,改变的只是行为执行的地点。
除了句柄类,另一选择是使Person成为一种特殊类型的抽象基类,称为协议类(Protocol class)。根据定义,协议类没有实现;它存在的目的是为派生类确定一个接口(参见条款36)。所以,它一般没有数据成员,没有构造函数;有一个虚析构函数(见条款14),还有一套纯虚函数,用于制定接口。Person的协议类看起来会象下面这样:
class Person { public: virtual ~Person(); virtual string name() const = 0; virtual string birthDate() const = 0; virtual string address() const = 0; virtual string nationality() const = 0; };
Person类的用户必须通过Person的指针和引用来使用它,因为实例化一个包含纯虚函数的类是不可能的(但是,可以实例化Person的派生类----参见下文)。和句柄类的用户一样,协议类的用户只是在类的接口被修改的情况下才需要重新编译。
条款34: 将文件间的编译依赖性降至最低,布布扣,bubuko.com
标签:style blog color 使用 io 文件 数据 问题
原文地址:http://www.cnblogs.com/ljygoodgoodstudydaydayup/p/3919490.html