- 浏览: 642103 次
- 性别:
- 来自: 武汉
文章分类
最新评论
-
lizhuang:
这个方法的内部实现主要是依赖于类加载器,一般的自己实现的类是用 ...
Java中getResourceAsStream的用法 -
prince4426:
回答评论都很精彩
Java中getResourceAsStream的用法 -
kexuetou:
美人如此多娇 写道可能这样总结更好,路径前不带'/',则是相对 ...
Java中getResourceAsStream的用法 -
guoxin91:
...
Java中getResourceAsStream的用法 -
美人如此多娇:
可能这样总结更好,路径前不带'/',则是相对路径;若带,则是绝 ...
Java中getResourceAsStream的用法
假设某一天你打开自己的C++程序代码,然后对某个类的实现做了小小的改动。提醒你,改动的不是接口,而是类的实现,也就是说,只是细节部分。然后你准备重新生成程序,心想,编译和链接应该只会花几秒种。毕竟,只是改动了一个类嘛!于是你点击了一下"Rebuild",或输入make(或其它类似命令)。然而,等待你的是惊愕,接着是痛苦。因为你发现,整个世界都在被重新编译、重新链接!
当这一切发生时,你难道仅仅只是愤怒吗?
问题发生的原因在于,在将接口从实现分离这方面,C++做得不是很出色。尤其是,C++的类定义中不仅包含接口规范,还有不少实现细节。例如:
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对象有多大呢?唯一的途径是借助类的定义,但如果类的定义可以合法地省略实现细节,编译器怎么知道该分配多大的内存呢?
原则上说,这个问题不难解决。有些语言如Smalltalk,Eiffel和Java每天都在处理这个问题。它们的做法是,当定义一个对象时,只分配足够容纳这个对象的一个指针的空间。也就是说,对应于上面的代码,他们就象这样做:
int main() { int x; // 定义一个int Person *p; // 定义一个Person指针 ... }
你可能以前就碰到过这样的代码,因为它实际上是合法的C++语句。这证明,程序员完全可以自己来做到 "将一个对象的实现隐藏在指针身后"。
下面具体介绍怎么采用这一技术来实现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);
当然,传值通常不是个好主意(见条款22),但出于什么原因不得不这样做时,千万不要还引起不必要的编译依赖性。
如果你对returnADate和takeADate的声明在编译时不需要Date的定义感到惊讶,那么请跟我一起看看下文。其实,它没看上去那么神秘,因为任何人来调用那些函数,这些人会使得Date的定义可见。"噢" 我知道你在想,"为什么要劳神去声明一个没有人调用的函数呢?" 不对!不是没有人去调用,而是,并非每个人都会去调用。例如,假设有一个包含数百个函数声明的库(可能要涉及到多个名字空间----参见条款28),不可能每个用户都去调用其中的每一个函数。将提供类定义(通过#include 指令)的任务从你的函数声明头文件转交给包含函数调用的用户文件,就可以消除用户对类型定义的依赖,而这种依赖本来是不必要的、是人为造成的。
· 不要在头文件中再(通过#include指令)包含其它头文件,除非缺少了它们就不能编译。相反,要一个一个地声明所需要的类,让使用这个头文件的用户自己(通过#include指令)去包含其它的头文件,以使用户代码最终得以通过编译。一些用户会抱怨这样做对他们来说很不方便,但实际上你为他们避免了许多你曾饱受的痛苦。事实上,这种技术很受推崇,并被运用到C++标准库(参见条款49)中;头文件<iosfwd>就包含了iostream库中的类型声明(而且仅仅是类型声明)。
Person类仅仅用一个指针来指向某个不确定的实现,这样的类常常被称为句炳类(Handle class)或信封类(Envelope class)。(对于它们所指向的类来说,前一种情况下对应的叫法是主体类(Body class);后一种情况下则叫信件类(Letter class)。)偶尔也有人把这种类叫 "Cheshire猫" 类,这得提到《艾丽丝漫游仙境》中那只猫,当它愿意时,它会使身体其它部分消失,仅仅留下微笑。
你一定会好奇句炳类实际上都做了些什么。答案很简单:它只是把所有的函数调用都转移到了对应的主体类中,主体类真正完成工作。例如,下面是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的派生类----参见下文)。和句柄类的用户一样,协议类的用户只是在类的接口被修改的情况下才需要重新编译。
当然,协议类的用户必然要有什么办法来创建新对象。这常常通过调用一个函数来实现,此函数扮演构造函数的角色,而这个构造函数所在的类即那个真正被实例化的隐藏在后的派生类。这种函数叫法挺多(如工厂函数(factory function),虚构造函数(virtual constructor)),但行为却一样:返回一个指针,此指针指向支持协议类接口(见条款M25)的动态分配对象。这样的函数象下面这样声明:
// makePerson是支持Person接口的 // 对象的"虚构造函数" ( "工厂函数") Person* makePerson(const string& name, // 用给定的参数初始化一个 const Date& birthday, // 新的Person对象,然后 const Address& addr, // 返回对象指针 const Country& country);
用户这样使用它:
string name; Date dateOfBirth; Address address; Country nation; ... // 创建一个支持Person接口的对象 Person *pp = makePerson(name, dateOfBirth, address, nation); ... cout << pp->name() // 通过Person接口使用对象 << " was born on " << pp->birthDate() << " and now lives at " << pp->address(); ... delete pp; // 删除对象
makePerson这类函数和它创建的对象所对应的协议类(对象支持这个协议类的接口)是紧密联系的,所以将它声明为协议类的静态成员是很好的习惯:
class Person { public: ... // 同上 // makePerson现在是类的成员 static Person * makePerson(const string& name, const Date& birthday, const Address& addr, const Country& country);
这样就不会给全局名字空间(或任何其他名字空间)带来混乱,因为这种性质的函数会很多(参见条款28)。
当然,在某个地方,支持协议类接口的某个具体类(concrete class)必然要被定义,真的构造函数也必然要被调用。它们都背后发生在实现文件中。例如,协议类可能会有一个派生的具体类RealPerson,它具体实现继承而来的虚函数:
class RealPerson: public Person { public: RealPerson(const string& name, const Date& birthday, const Address& addr, const Country& country) : name_(name), birthday_(birthday), address_(addr), country_(country) {} virtual ~RealPerson() {} string name() const; // 函数的具体实现没有 string birthDate() const; // 在这里给出,但它们 string address() const; // 都很容易实现 string nationality() const; private: string name_; Date birthday_; Address address_; Country country_;
有了RealPerson,写Person::makePerson就是小菜一碟:
Person * Person::makePerson(const string& name, const Date& birthday, const Address& addr, const Country& country) { return new RealPerson(name, birthday, addr, country); }
实现协议类有两个最通用的机制,RealPerson展示了其中之一:先从协议类(Person)继承接口规范,然后实现接口中的函数。另一种实现协议类的机制涉及到多继承,这将是条款43的话题。
是的,句柄类和协议类分离了接口和实现,从而降低了文件间编译的依赖性。"但,所有这些把戏会带来多少代价呢?",我知道你在等待罚单的到来。答案是计算机科学领域最常见的一句话:它在运行时会多耗点时间,也会多耗点内存。
句柄类的情况下,成员函数必须通过(指向实现的)指针来获得对象数据。这样,每次访问的间接性就多一层。此外,计算每个对象所占用的内存大小时,还应该算上这个指针。还有,指针本身还要被初始化(在句柄类的构造函数内),以使之指向被动态分配的实现对象,所以,还要承担动态内存分配(以及后续的内存释放)所带来的开销 ---- 见条款10。
对于协议类,每个函数都是虚函数,所有每次调用函数时必须承担间接跳转的开销(参见条款14和M24)。而且,每个从协议类派生而来的对象必然包含一个虚指针(参见条款14和M24)。这个指针可能会增加对象存储所需要的内存数量(具体取决于:对于对象的虚函数来说,此协议类是不是它们的唯一来源)。
最后一点,句柄类和协议类都不大会使用内联函数。使用任何内联函数时都要访问实现细节,而设计句柄类和协议类的初衷正是为了避免这种情况。
但如果仅仅因为句柄类和协议类会带来开销就把它们打入冷宫,那就大错特错。正如虚函数,你难道会不用它们吗?(如果回答不用,那你正在看一本不该看的书!)相反,要以发展的观点来运用这些技术。在开发阶段要尽量用句柄类和协议类来减少 "实现" 的改变对用户的负面影响。如果带来的速度和/或体积的增加程度远远大于类之间依赖性的减少程度,那么,当程序转化成产品时就用具体类来取代句柄类和协议类。希望有一天,会有工具来自动执行这类转换。
有些人还喜欢混用句柄类、协议类和具体类,并且用得很熟练。这固然使得开发出来的软件系统运行高效、易于改进,但有一个很大的缺点:还是必须得想办法减少程序重新编译时消耗的时间。
发表评论
-
main中调用dll中的函数,F10单步到main的右大口号时出现user breakpoint called...
2010-03-09 18:42 1784在dll中输出了一个包含string类子对象的类,在DEBUG ... -
VC中链接动态链接库的方法
2010-03-06 17:17 1379方法一:windows提供了一套函数,用于加载动态链接库中的符 ... -
50个C/C++源代码网站
2010-01-30 13:01 3021C/C++是最主要的编程语言。这里列出了50名优秀网站和网页清 ... -
QHttp
2010-01-26 17:06 7032QHttp是Qt所提供有关网络的高阶API,可以协助我们进行H ... -
VC屏蔽Enter和ESC退出程序
2010-01-15 21:51 2437重载PreTranslateMessage函数屏蔽回车和ESC ... -
《Effective C++》条款22:尽量用"传引用"代替"传值"
2010-01-13 11:15 2912c语言中,什么都是通过传值来实现的,c++继承了这一传统并将它 ... -
《高质量C++/C 编程指南》之 内存耗尽怎么办
2010-01-12 14:20 1618如果在申请动态内存时找不到足够大的内存块,malloc ... -
《高质量C++/C 编程指南》之 常见的内存错误及其对策
2010-01-12 14:10 1470发生内存错误是件非 ... -
《高质量C++/C 编程指南》之 有了malloc/free为什么还要new/delete
2010-01-12 13:33 1732malloc与free是C++/C语言的标准库函 ... -
《高质量C++/C 编程指南》之 free和delete把指针怎么啦?
2010-01-12 11:30 1288别看free和delete的名字恶狠狠的(尤其是delete) ... -
《高质量C++/C 编程指南》之 杜绝"野指针"
2010-01-12 11:27 1307“野指针”不是NULL指针,是指向“垃圾”内存的指针。人们一般 ... -
《Effective C++》条款14: 确定基类有虚析构函数
2010-01-12 11:18 5178有时,一个类想跟踪它 ... -
数组名不完全等于指针
2010-01-10 19:12 1829指针是C/C++语言的特色,而数组名与指针有太多的相似,甚至很 ... -
C++字符串完全指引之二
2010-01-09 16:16 1538引言 因为C语言 ... -
C++字符串完全指引之一
2010-01-08 22:53 1264引言 毫无疑问,我们都看到过像 TCHAR, st ... -
BMP文件格式
2010-01-08 22:15 1869一.位图结构如下: ---- 一、BMP文件结构 --- ... -
OpenCV基础数据结构
2010-01-08 21:56 2175图像数据结构: 1) IPL ... -
OpenCV基本知识
2010-01-08 21:31 29881、OpenCV概述 1) 什么是OpenCV 开 ... -
calloc(), malloc(), realloc(), free()
2010-01-08 21:08 1432void *calloc(size_t nobj, size_ ... -
善用GetLastError函数
2010-01-08 21:06 2215在编程过程中,当程序出现错误,却又不知道错误的原因 ...
相关推荐
条款34: 将文件间的编译依赖性降至最低 第六章 继承和面向对象设计 条款35: 使公有继承体现 "是一个" 的含义 条款36: 区分接口继承和实现继承 条款37: 决不要重新定义继承而来的非虚函数 条款38: 决不要重新定义继承...
《Effective C++ Modern》与《Effective Modern C++》这两本书是C++编程领域的经典之作,由Scott Meyers撰写,旨在帮助开发者深入理解C++语言的精髓,提升编程效率和代码质量。以下是对这两个主题的详细解析: 1. *...
### Effective C++中文版知识点概览 #### 从C转向C++ 对于许多已经熟悉C语言的程序员来说,转向C++可能会遇到一些挑战。虽然C是C++的一个子集,但两者之间存在诸多差异,这些差异使得C++拥有更加丰富的特性和更加...
### Effective C++中文版知识点概览 #### 一、从C转向C++ 对于熟悉C语言的程序员来说,转向C++可能会遇到一些挑战。虽然C是C++的一个子集,但两者之间存在诸多差异,需要程序员调整思维方式和编程习惯。 1. **...
《Effective C++》和《More Effective C++》是C++编程领域的两本经典著作,由Scott Meyers撰写,对深入理解和高效使用C++语言具有极高的指导价值。这两本书不仅适合初学者,也深受经验丰富的C++程序员喜爱,因为它们...
### Effective_C++中文版知识点概览 #### 一、从C转向C++ 对于熟悉C语言的程序员来说,转向C++可能会遇到一些挑战。虽然C是C++的一个子集,但两者之间存在诸多差异,这些差异可能导致原有的编程习惯不再适用。 **...
不过,根据文件信息中提及的标题“Effective C++中文第三版”,我可以根据这个主题生成相关的知识点。 《Effective C++中文第三版》是Scott Meyers所著的一本广泛认可的关于C++编程的书籍。这本书主要涵盖了C++编程...
《Effective C++》和《More Effective C++》是C++编程领域的两本经典著作,由Scott Meyers撰写,为程序员提供了提升代码质量和效率的实用建议。这两本书在C++社区中有着广泛的影响,是深入理解C++特性和最佳实践的...
C++风格转换包括`static_cast`、`const_cast`、`dynamic_cast`和`reinterpret_cast`等,这些转换方式更加明确,能够提供更好的类型安全性和编译时检查。 - **Item 3: 不要对数组使用多态** 多态是C++面向对象编程...
《Effective C++》、《More Effective C++》和《Exceptional C++》是C++编程领域中的经典之作,由Scott Meyers撰写。这三本书分别代表了C++学习的三个阶段,旨在帮助开发者逐步提升对C++语言的理解和使用技巧。 ...
《More Effective C++》与《Effective C++》是两本非常经典的C++编程指南,由Scott Meyers撰写,深入探讨了C++编程的最佳实践和常见陷阱。这两本书为程序员提供了提高代码质量、效率和可维护性的实用建议。下面将...
2. **静态与动态绑定**:C++支持静态绑定(编译时绑定)和动态绑定(运行时绑定),理解两者的区别对于编写高效且安全的代码至关重要。 3. **常量与引用**:常量引用能确保对象在传递过程中不被修改,同时避免了...