⭐ 欢迎来到虫虫下载站! | 📦 资源下载 📁 资源专辑 ℹ️ 关于我们
⭐ 虫虫下载站

📄 9247.htm

📁 C++细节解释
💻 HTM
📖 第 1 页 / 共 2 页
字号:
<P>下面就是这一思想直接深化后的含义:</P> 
<P>· 如果可以使用对象的引用和指针,就要避免使用对象本身。定义某个类型的引用和指针只会涉及到这个类型的声明。定义此类型的对象则需要类型定义的参与。</P> 
<P>· 尽可能使用类的声明,而不使用类的定义。因为在声明一个函数时,如果用到某个类,是绝对不需要这个类的定义的,即使函数是通过传值来传递和返回这个类:</P> 
<P>&nbsp; class Date;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; // 类的声明</P> 
<P>&nbsp; Date returnADate();&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; // 正确 ---- 不需要Date的定义<BR>&nbsp; void takeADate(Date d);&nbsp;&nbsp;&nbsp;&nbsp; </P> 
<P>当然,传值通常不是个好主意(见条款22),但出于什么原因不得不这样做时,千万不要还引起不必要的编译依赖性。</P> 
<P>如果你对returnADate和takeADate的声明在编译时不需要Date的定义感到惊讶,那么请跟我一起看看下文。其实,它没看上去那么神秘,因为任何人来调用那些函数,这些人会使得Date的定义可见。"噢" 我知道你在想,"为什么要劳神去声明一个没有人调用的函数呢?" 不对!不是没有人去调用,而是,并非每个人都会去调用。例如,假设有一个包含数百个函数声明的库(可能要涉及到多个名字空间----参见条款28),不可能每个用户都去调用其中的每一个函数。将提供类定义(通过#include 指令)的任务从你的函数声明头文件转交给包含函数调用的用户文件,就可以消除用户对类型定义的依赖,而这种依赖本来是不必要的、是人为造成的。</P> 
<P>· 不要在头文件中再(通过#include指令)包含其它头文件,除非缺少了它们就不能编译。相反,要一个一个地声明所需要的类,让使用这个头文件的用户自己(通过#include指令)去包含其它的头文件,以使用户代码最终得以通过编译。一些用户会抱怨这样做对他们来说很不方便,但实际上你为他们避免了许多你曾饱受的痛苦。事实上,这种技术很受推崇,并被运用到C++标准库(参见条款49)中;头文件&lt;iosfwd&gt;就包含了iostream库中的类型声明(而且仅仅是类型声明)。</P> 
<P>Person类仅仅用一个指针来指向某个不确定的实现,这样的类常常被称为句炳类(Handle class)或信封类(Envelope class)。(对于它们所指向的类来说,前一种情况下对应的叫法是主体类(Body class);后一种情况下则叫信件类(Letter class)。)偶尔也有人把这种类叫 "Cheshire猫" 类,这得提到《艾丽丝漫游仙境》中那只猫,当它愿意时,它会使身体其它部分消失,仅仅留下微笑。</P> 
<P>你一定会好奇句炳类实际上都做了些什么。答案很简单:它只是把所有的函数调用都转移到了对应的主体类中,主体类真正完成工作。例如,下面是Person的两个成员函数的实现:</P> 
<P>#include "Person.h"&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; // 因为是在实现Person类,<BR>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; // 所以必须包含类的定义</P> 
<P>#include "PersonImpl.h"&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; // 也必须包含PersonImpl类的定义,<BR>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; // 否则不能调用它的成员函数。<BR>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; // 注意PersonImpl和Person含有一样的<BR>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; // 成员函数,它们的接口完全相同</P> 
<P>Person::Person(const string&amp; name, const Date&amp; birthday,<BR>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; const Address&amp; addr, const Country&amp; country)<BR>{<BR>&nbsp; impl = new PersonImpl(name, birthday, addr, country);<BR>}</P> 
<P>string Person::name() const<BR>{<BR>&nbsp; return impl-&gt;name();<BR>}</P> 
<P>请注意Person的构造函数怎样调用PersonImpl的构造函数(隐式地以new来调用,参见条款5和M8)以及Person::name怎么调用PersonImpl::name。这很重要。使Person成为一个句柄类并不改变Person类的行为,改变的只是行为执行的地点。</P> 
<P>除了句柄类,另一选择是使Person成为一种特殊类型的抽象基类,称为协议类(Protocol class)。根据定义,协议类没有实现;它存在的目的是为派生类确定一个接口(参见条款36)。所以,它一般没有数据成员,没有构造函数;有一个虚析构函数(见条款14),还有一套纯虚函数,用于制定接口。Person的协议类看起来会象下面这样:</P> 
<P>class Person {<BR>public:<BR>&nbsp; virtual ~Person();</P> 
<P>&nbsp; virtual string name() const = 0;<BR>&nbsp; virtual string birthDate() const = 0;<BR>&nbsp; virtual string address() const = 0;<BR>&nbsp; virtual string nationality() const = 0;<BR>};</P> 
<P>Person类的用户必须通过Person的指针和引用来使用它,因为实例化一个包含纯虚函数的类是不可能的(但是,可以实例化Person的派生类----参见下文)。和句柄类的用户一样,协议类的用户只是在类的接口被修改的情况下才需要重新编译。</P> 
<P>当然,协议类的用户必然要有什么办法来创建新对象。这常常通过调用一个函数来实现,此函数扮演构造函数的角色,而这个构造函数所在的类即那个真正被实例化的隐藏在后的派生类。这种函数叫法挺多(如工厂函数(factory function),虚构造函数(virtual constructor)),但行为却一样:返回一个指针,此指针指向支持协议类接口(见条款M25)的动态分配对象。这样的函数象下面这样声明:</P> 
<P>// makePerson是支持Person接口的<BR>// 对象的"虚构造函数" ( "工厂函数")<BR>Person*<BR>&nbsp; makePerson(const string&amp; name,&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; // 用给定的参数初始化一个<BR>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; const Date&amp; birthday,&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; // 新的Person对象,然后<BR>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; const Address&amp; addr,&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; // 返回对象指针<BR>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; const Country&amp; country);&nbsp;&nbsp; </P> 
<P><BR>用户这样使用它:</P> 
<P>string name;<BR>Date dateOfBirth;<BR>Address address;<BR>Country nation;</P> 
<P>...</P> 
<P>// 创建一个支持Person接口的对象<BR>Person *pp = makePerson(name, dateOfBirth, address, nation);</P> 
<P>...</P> 
<P>cout&nbsp; &lt;&lt; pp-&gt;name()&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; // 通过Person接口使用对象<BR>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; &lt;&lt; " was born on "&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; <BR>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; &lt;&lt; pp-&gt;birthDate()<BR>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; &lt;&lt; " and now lives at "<BR>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; &lt;&lt; pp-&gt;address();</P> 
<P>...</P> 
<P>delete pp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; // 删除对象</P> 
<P>makePerson这类函数和它创建的对象所对应的协议类(对象支持这个协议类的接口)是紧密联系的,所以将它声明为协议类的静态成员是很好的习惯:</P> 
<P>class Person {<BR>public:<BR>&nbsp; ...&nbsp; &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;// 同上</P> 
<P>// makePerson现在是类的成员<BR>&nbsp; static Person * makePerson(const string&amp; name,<BR>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; const Date&amp; birthday,<BR>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; const Address&amp; addr,<BR>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; const Country&amp; country);</P> 
<P>这样就不会给全局名字空间(或任何其他名字空间)带来混乱,因为这种性质的函数会很多(参见条款28)。</P> 
<P>当然,在某个地方,支持协议类接口的某个具体类(concrete class)必然要被定义,真的构造函数也必然要被调用。它们都背后发生在实现文件中。例如,协议类可能会有一个派生的具体类RealPerson,它具体实现继承而来的虚函数:</P> 
<P>class RealPerson: public Person {<BR>public:<BR>&nbsp; RealPerson(const string&amp; name, const Date&amp; birthday,<BR>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; const Address&amp; addr, const Country&amp; country)<BR>&nbsp; :&nbsp; name_(name), birthday_(birthday),<BR>&nbsp;&nbsp;&nbsp;&nbsp; address_(addr), country_(country)<BR>&nbsp; {}</P> 
<P>&nbsp; virtual ~RealPerson() {}</P> 
<P>&nbsp; string name() const;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; // 函数的具体实现没有<BR>&nbsp; string birthDate() const;&nbsp;&nbsp;&nbsp;&nbsp; // 在这里给出,但它们<BR>&nbsp; string address() const;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; // 都很容易实现<BR>&nbsp; string nationality() const;&nbsp;&nbsp;&nbsp; </P> 
<P>private:<BR>&nbsp; string name_;<BR>&nbsp; Date birthday_;<BR>&nbsp; Address address_;<BR>&nbsp; Country country_;</P> 
<P>有了RealPerson,写Person::makePerson就是小菜一碟:</P> 
<P>Person * Person::makePerson(const string&amp; name,<BR>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; const Date&amp; birthday,<BR>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; const Address&amp; addr,<BR>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; const Country&amp; country)<BR>{<BR>&nbsp; return new RealPerson(name, birthday, addr, country);<BR>}</P> 
<P>实现协议类有两个最通用的机制,RealPerson展示了其中之一:先从协议类(Person)继承接口规范,然后实现接口中的函数。另一种实现协议类的机制涉及到多继承,这将是条款43的话题。</P> 
<P>是的,句柄类和协议类分离了接口和实现,从而降低了文件间编译的依赖性。"但,所有这些把戏会带来多少代价呢?",我知道你在等待罚单的到来。答案是计算机科学领域最常见的一句话:它在运行时会多耗点时间,也会多耗点内存。</P> 
<P>句柄类的情况下,成员函数必须通过(指向实现的)指针来获得对象数据。这样,每次访问的间接性就多一层。此外,计算每个对象所占用的内存大小时,还应该算上这个指针。还有,指针本身还要被初始化(在句柄类的构造函数内),以使之指向被动态分配的实现对象,所以,还要承担动态内存分配(以及后续的内存释放)所带来的开销 ---- 见条款10。</P> 
<P>对于协议类,每个函数都是虚函数,所有每次调用函数时必须承担间接跳转的开销(参见条款14和M24)。而且,每个从协议类派生而来的对象必然包含一个虚指针(参见条款14和M24)。这个指针可能会增加对象存储所需要的内存数量(具体取决于:对于对象的虚函数来说,此协议类是不是它们的唯一来源)。</P> 
<P>最后一点,句柄类和协议类都不大会使用内联函数。使用任何内联函数时都要访问实现细节,而设计句柄类和协议类的初衷正是为了避免这种情况。</P> 
<P>但如果仅仅因为句柄类和协议类会带来开销就把它们打入冷宫,那就大错特错。正如虚函数,你难道会不用它们吗?(如果回答不用,那你正在看一本不该看的书!)相反,要以发展的观点来运用这些技术。在开发阶段要尽量用句柄类和协议类来减少 "实现" 的改变对用户的负面影响。如果带来的速度和/或体积的增加程度远远大于类之间依赖性的减少程度,那么,当程序转化成产品时就用具体类来取代句柄类和协议类。希望有一天,会有工具来自动执行这类转换。</P> 
<P>有些人还喜欢混用句柄类、协议类和具体类,并且用得很熟练。这固然使得开发出来的软件系统运行高效、易于改进,但有一个很大的缺点:还是必须得想办法减少程序重新编译时消耗的时间。<br> 
</P> 
</DIV></div></div> 
 
</center></BODY></HTML> 

⌨️ 快捷键说明

复制代码 Ctrl + C
搜索代码 Ctrl + F
全屏模式 F11
切换主题 Ctrl + Shift + D
显示快捷键 ?
增大字号 Ctrl + =
减小字号 Ctrl + -