C++编程实用技巧34将文件间的编译依赖性降至最低.docx
- 文档编号:2824457
- 上传时间:2022-11-15
- 格式:DOCX
- 页数:16
- 大小:26.19KB
C++编程实用技巧34将文件间的编译依赖性降至最低.docx
《C++编程实用技巧34将文件间的编译依赖性降至最低.docx》由会员分享,可在线阅读,更多相关《C++编程实用技巧34将文件间的编译依赖性降至最低.docx(16页珍藏版)》请在冰豆网上搜索。
C++编程实用技巧34将文件间的编译依赖性降至最低
C++编程实用技巧34:
将文件间的编译依赖性降至最低
假设某一天你打开自己的C++程序代码,然后对某个类的实现做了小小的改动。
提醒你,改动的不是接口,而是类的实现,也就是说,只是细节部分。
然后你准备重新生成程序,心想,编译和链接应该只会花几秒种。
毕竟,只是改动了一个类嘛!
于是你点击了一下"Rebuild",或输入make(或其它类似命令)。
然而,等待你的是惊愕,接着是痛苦。
因为你发现,整个世界都在被重新编译、重新链接!
当这一切发生时,你难道仅仅只是愤怒吗?
问题发生的原因在于,在将接口从实现分离这方面,C++做得不是很出色。
尤其是,C++的类定义中不仅包含接口规范,还有不少实现细节。
例如:
1.classPerson{
2.public:
3. Person(conststring&name,constDate&birthday,
4. constAddress&addr,constCountry&country);
5. virtual~Person();
6. ... //简化起见,省略了拷贝构造
7. //函数和赋值运算符函数
8. stringname()const;
9. stringbirthDate()const;
10. stringaddress()const;
11. stringnationality()const;
12.private:
13. stringname_; //实现细节
14. DatebirthDate_; //实现细节
15. Addressaddress_; //实现细节
16. Countrycitizenship_; //实现细节
17.};
复制代码
这很难称得上是一个很高明的设计,虽然它展示了一种很有趣的命名方式:
当私有数据和公有函数都想用某个名字来标识时,让前者带一个尾部下划线就可以区别了。
这里要注意到的重要一点是,Person的实现用到了一些类,即string,Date,Address和Country;Person要想被编译,就得让编译器能够访问得到这些类的定义。
这样的定义一般是通过#include指令来提供的,所以在定义Person类的文件头部,可以看到象下面这样的语句:
1.#include
2.#include"date.h"
3.#include"address.h"
4.#include"country.h"
复制代码
遗憾的是,这样一来,定义Person的文件和这些头文件之间就建立了编译依赖关系。
所以如果任一个辅助类(即string,Date,Address和Country)改变了它的实现,或任一个辅助类所依赖的类改变了实现,包含Person类的文件以及任何使用了Person类的文件就必须重新编译。
对于Person类的用户来说,这实在是令人讨厌,因为这种情况用户绝对是束手无策。
那么,你一定会奇怪为什么C++一定要将一个类的实现细节放在类的定义中。
例如,为什么不能象下面这样定义Person,使得类的实现细节与之分开呢?
1.classstring; //"概念上"提前声明string类型
2. //详见技巧49
3.classDate; //提前声明
4.classAddress; //提前声明
5.classCountry; //提前声明
6.classPerson{
7.public:
8. Person(conststring&name,constDate&birthday,
9. constAddress&addr,constCountry&country);
10. virtual~Person();
11. ... //拷贝构造函数,operator=
12. stringname()const;
13. stringbirthDate()const;
14. stringaddress()const;
15. stringnationality()const;
16.};
复制代码
如果这种方法可行的话,那么除非类的接口改变,否则Person的用户就不需要重新编译。
大系统的开发过程中,在开始类的具体实现之前,接口往往基本趋于固定,所以这种接口和实现的分离将大大节省重新编译和链接所花的时间。
可惜的是,现实总是和理想相抵触,看看下面你就会认同这一点:
1.intmain()
2.{
3. intx; //定义一个int
4. Personp(...); //定义一个Person
5. //(为简化省略参数)
6. ...
7.}
复制代码
当看到x的定义时,编译器知道必须为它分配一个int大小的内存。
这没问题,每个编译器都知道一个int有多大。
然而,当看到p的定义时,编译器虽然知道必须为它分配一个Person大小的内存,但怎么知道一个Person对象有多大呢?
唯一的途径是借助类的定义,但如果类的定义可以合法地省略实现细节,编译器怎么知道该分配多大的内存呢?
原则上说,这个问题不难解决。
有些语言如Smalltalk,Eiffel和Java每天都在处理这个问题。
它们的做法是,当定义一个对象时,只分配足够容纳这个对象的一个指针的空间。
也就是说,对应于上面的代码,他们就象这样做:
1.intmain()
2.{
3. intx; //定义一个int
4. Person*p; //定义一个Person指针
5.
6. ...
7.}
复制代码
你可能以前就碰到过这样的代码,因为它实际上是合法的C++语句。
这证明,程序员完全可以自己来做到"将一个对象的实现隐藏在指针身后"。
下面具体介绍怎么采用这一技术来实现Person接口和实现的分离。
首先,在声明Person类的头文件中只放下面的东西:
1.//编译器还是要知道这些类型名,
2.//因为Person的构造函数要用到它们
3.classstring; //对标准string来说这样做不对,
4. //原因参见技巧49
5.classDate;
6.classAddress;
7.classCountry;
8.//类PersonImpl将包含Person对象的实
9.//现细节,此处只是类名的提前声明
10.classPersonImpl;
11.classPerson{
12.public:
13. Person(conststring&name,constDate&birthday,
14. constAddress&addr,constCountry&country);
15. virtual~Person();
16. ... //拷贝构造函数,operator=
17. stringname()const;
18. stringbirthDate()const;
19. stringaddress()const;
20. stringnationality()const;
21.private:
22. PersonImpl*impl; //指向具体的实现类
23.};
复制代码
现在Person的用户程序完全和string,date,address,country以及person的实现细节分家了。
那些类可以随意修改,而Person的用户却落得个自得其乐,不闻不问。
更确切的说,它们可以不需要重新编译。
另外,因为看不到Person的实现细节,用户不可能写出依赖这些细节的代码。
这是真正的接口和实现的分离。
分离的关键在于,"对类定义的依赖"被"对类声明的依赖"取代了。
所以,为了降低编译依赖性,我们只要知道这么一条就足够了:
只要有可能,尽量让头文件不要依赖于别的文件;如果不可能,就借助于类的声明,不要依靠类的定义。
其它一切方法都源于这一简单的设计思想。
下面就是这一思想直接深化后的含义:
?
如果可以使用对象的引用和指针,就要避免使用对象本身。
定义某个类型的引用和指针只会涉及到这个类型的声明。
定义此类型的对象则需要类型定义的参与。
?
尽可能使用类的声明,而不使用类的定义。
因为在声明一个函数时,如果用到某个类,是绝对不需要这个类的定义的,即使函数是通过传值来传递和返回这个类:
1. classDate; //类的声明
2. DatereturnADate(); //正确----不需要Date的定义
3. voidtakeADate(Dated);
复制代码
当然,传值通常不是个好主意(见技巧22),但出于什么原因不得不这样做时,千万不要还引起不必要的编译依赖性。
如果你对returnADate和takeADate的声明在编译时不需要Date的定义感到惊讶,那么请跟我一起看看下文。
其实,它没看上去那么神秘,因为任何人来调用那些函数,这些人会使得Date的定义可见。
"噢"我知道你在想,"为什么要劳神去声明一个没有人调用的函数呢?
"不对!
不是没有人去调用,而是,并非每个人都会去调用。
例如,假设有一个包含数百个函数声明的库(可能要涉及到多个名字空间----参见技巧28),不可能每个用户都去调用其中的每一个函数。
将提供类定义(通过#include指令)的任务从你的函数声明头文件转交给包含函数调用的用户文件,就可以消除用户对类型定义的依赖,而这种依赖本来是不必要的、是人为造成的。
?
不要在头文件中再(通过#include指令)包含其它头文件,除非缺少了它们就不能编译。
相反,要一个一个地声明所需要的类,让使用这个头文件的用户自己(通过#include指令)去包含其它的头文件,以使用户代码最终得以通过编译。
一些用户会抱怨这样做对他们来说很不方便,但实际上你为他们避免了许多你曾饱受的痛苦。
事实上,这种技术很受推崇,并被运用到C++标准库(参见技巧49)中;头文件
Person类仅仅用一个指针来指向某个不确定的实现,这样的类常常被称为句炳类(Handleclass)或信封类(Envelopeclass)。
(对于它们所指向的类来说,前一种情况下对应的叫法是主体类(Bodyclass);后一种情况下
- 配套讲稿:
如PPT文件的首页显示word图标,表示该PPT已包含配套word讲稿。双击word图标可打开word文档。
- 特殊限制:
部分文档作品中含有的国旗、国徽等图片,仅作为作品整体效果示例展示,禁止商用。设计者仅对作品中独创性部分享有著作权。
- 关 键 词:
- C+ 编程 实用技巧 34 文件 编译 依赖性 降至 最低