TOP

读书笔记 effective c++ Item 31 把文件之间的编译依赖降到最低(一)
2017-10-13 10:06:40 】 浏览:10151
Tags:读书 笔记 effective Item 文件 之间 编译 依赖 降到 最低

 

1. 牵一发而动全身

现在开始进入你的C++程序,你对你的类实现做了一个很小的改动。注意,不是接口,只是实现,而且是private部分。然后你需要rebuild你的程序,计算着这个build应该几秒钟就足够了。毕竟,只修改了一个类。你点击了build 或者输入了make( 或者其他方式),你被惊到了,然后羞愧难当,因为你意识到整个世界都被重新编译和重新链接了!当这些发生时你不觉的感到愤恨么?

2. 编译依赖是如何发生的

问题出在C++并不擅长将接口从实现中分离出来。类定义不仅指定了类的接口也同时指定了许多类的细节。举个例子:

 1 class Person {
 2 public:
 3 Person(const std::string& name, const Date& birthday,
 4 const Address& addr);
 5 std::string name() const;
 6 std::string birthDate() const;
 7 std::string address() const;
 8 ...
 9 private:
10 std::string theName; // implementation detail
11 Date theBirthDate; // implementation detail
12 
13 Address theAddress;              // implementation detail
14 
15 };

 

 

这里,类Person的实现需要使用一些类的定义,也就是string,Date,和Address,如果类Person对这些类的定义没有访问权,那么Person不会被编译通过。这些定义通过使用#include指令来提供,所以在定义Person类的文件中,你可能会发现像下面这样的代码:

1 #include <string>
2 
3 #include "date.h"
4 
5 #include "address.h"

 

不幸的是,定义Person类的文件和上面列出的头文件之间建立了编译依赖。任何一个头文件被修改,或者这些头文件依赖的文件被修改,包含Person类的文件就必须要重新编译,使用Person的任何文件也必须要重新编译。这样的级联编译依赖会对一个工程造成无尽的伤痛。

 

3. 尝试将类的实现分离出来

你可能想知道为什么C++坚持将类的实现细节放在类定义中。举个例子,你为什么不能这么定义Person类,将指定类的实现细节单独分离开来。

 1 namespace std {
 2 class string; // forward declaration (an incorrect
 3 } // one — see below)
 4 class Date; // forward declaration
 5 class Address; // forward declaration
 6 class Person {
 7 public:
 8 Person(const std::string& name, const Date& birthday,
 9 const Address& addr);
10 std::string name() const;
11 std::string birthDate() const;
12 std::string address() const;
13 ...
14 };

 

如果这是可能的 ,Person的用户只有在类的接口被修改的时候才必须要重新编译。

 

这个想法有两个问题。首先,string不是类,它是一个typedef(basic_string<char>的typedef)。因此,对string的前置声明是不正确的。合适的前置声明实质上更加复杂,因为它涉及到了额外的模板。然而这没关系,因为你不应该尝试对标准库的某些部分进行手动声明。相反,简单的使用合适的#include来达到目的。标准头文件看上去不像是编译的瓶颈,特别是你的编译环境允许你利用预编译头文件。如果标准头文件的解析真的是一个问题,你可能需要修改你的接口设计来避免使用标准库的某些部分(使用标准库的某些部分需要使用不受欢迎的#includes)。

 

对每件事情进行前置声明的第二个难点(并且是更加明显的)是需要处理如下问题:在编译过程中编译器需要知道对象的大小。考虑:

1 int main()
2 {
3 int x;                      // define an int
4 
5 Person p( params ); // define a Person
6 
7 
8 ...
9 }

 

当编译器看到x的定义时,它们知道必须要为一个int分配足够的空间。这没问题。编译器知道一个int有多大。当编译器看到p的定义时,它们知道必须要为一个Person分配足够的空间,但是他们如何知道一个Person对象有多大呢?唯一的方法就是通过查看类定义,但是对于一个类的定义来说,如果将其实现细节忽略掉是合法的,编译器如何知道需要分配多少空间呢?

这种问题不会出现在像Smalltalk 和Java这样的语言中,因为当在这些语言中定义一个对象时,编译器只为指向对象的指针分配足够的空间。对于上面的代码,它们会像下面这样进行处理:

1 int main()
2 {
3 int x;         // define an int
4 
5 Person *p; // define a pointer to a Person
6 ...
7 }

 

这当然是合法的C++代码,所以你可以自己玩“将对象细节隐藏在指针后面”的游戏。对于Person来说,一种实现方式就是将其分成两个类,一个只提供接口,另一个实现接口。如果实现类的被命名为PersonImpl,Person将会被定义如下:

 1 #include <string> // standard library components
 2 // shouldn’t be forward-declared
 3 
 4 #include <memory> // for tr1::shared_ptr; see below
 5 
 6 class PersonImpl; // forward decl of Person impl. class
 7 
 8 class Date;             // forward decls of classes used in
 9 
10  
11 
12 class Address;                                                                      // Person interface
13 
14 class Person {                                                                      
15 
16 public:                                                                                
17 
18 Person(const std::string& name, const Date& birthday,       
19 
20 const Address& addr);                                                        
21 
22 std::string name() const;                                                    
23 
24 std::string birthDate() const;                                              
25 
26 std::string address() const;                                                 
27 
28 ...                                                                                        
29 
30 private:                                                                               // ptr to i  
		
读书笔记 effective c++ Item 31 把文件之间的编译依赖降到最低(一) https://www.cppentry.com/bencandy.php?fid=49&id=158516

首页 上一页 1 2 3 4 下一页 尾页 1/4/4
】【打印繁体】【投稿】【收藏】 【推荐】【举报】【评论】 【关闭】 【返回顶部
上一篇运算符重载 与 sort() 下一篇bzoj4771 -- dfs序+倍增+主席树