C箴言:最小化文件之间的编译依赖

C箴言:最小化文件之间的编译依赖,第1张

C箴言:最小化文件之间的编译依赖,第2张

你进入你的程序,对一个类的实现做细微的改变。请注意,它不是一个类的接口,它只是一个实现,只是一些私有的东西。然后你重建这个程序。预计该任务只需几秒钟。毕竟只改了一个班。你在Build上点击或者输入make(或者其他等价行为),然后你就懵了,然后抑郁了,就像你突然意识到整个世界都被重新编译连接了一样!发生这种事你不讨厌吗?

问题是C没有做好从实现中剥离接口的工作。一个类定义不仅指定了一个类的接口,而且有相当数量的实现细节。例如:

class Person {
public:
Person(常量std::string& name,常量日期&生日,常量地址& addr);
STD::string name()const;
STD::string birth date()const;
STD::string address()const;
...

private:
STD::string theName;//实现细节
Date the birthdate;//实现细节
Address地址;//实现细节
};
在这里,如果不访问Person的实现所使用的类,也就是string、Date和Address的定义,Person这个类是无法编译的。这样的定义通常是通过#include指令提供的,因此在定义Person类的文件中,您可能会发现类似这样的内容:

# include
# include " date . h "
# include " address . h "
不幸的是,这在定义Person的文件和这些头文件之间建立了编译依赖关系。如果这些头文件中的一些发生了变化,或者这些头文件所依赖的文件发生了变化,那么包含Person类的文件必须重新编译,就像使用Person的文件一样。这样的级联编译依赖给项目带来了无数的麻烦。

也许你想知道为什么C坚持把一个类的实现细节放在类定义里。比如为什么不能这样定义Person,单独指定这个类的实现细节?

命名空间std {
类字符串;//转发声明(一个不正确的
} // one -见下文)

上课日期;//转发声明
类地址;//向前声明

class Person {
public:
Person(常量std::string& name,常量日期&生日,常量地址& addr);
STD::string name()const;
STD::string birth date()const;
STD::string address()const;
...
};
如果这样可行,只有当类的接口发生变化时,Person的客户才必须重新编译。

这个想法有两个问题。第一,string不是类,它是typedef(对于basic_string)。因此,字符串的前向声明是不正确的。的正确前向声明要复杂得多,因为它包括附加模板。但是,这没有关系,因为您不应该试图手动声明标准库的各个部分。相反,只要使用适当的#includes,让它去做就行了。标准头文件不太可能成为编译的瓶颈,尤其是当您的构建环境允许您使用预编译头文件时。如果解析标准头文件真的成了问题。也许你需要改变你的界面设计来避免使用导致不受欢迎的#includes的标准库部件。

第二个(也是更重要的)困难是,所有声明为forward的东西都必须让编译器在编译时知道其对象的大小。考虑:

int main()
{
int x;//定义int

人p(参数);//定义一个人
...
}
当编译器看到x的定义时,他们知道必须分配足够的空空间(通常在堆栈上)来保存一个int。没问题。每个编译器都知道int有多大。编译器看到p的定义,就知道必须给一个人分配足够多的空房间,但是怎么猜Person对象有多大呢?他们获取这些信息的方式是参考这个类的定义,但是如果一个省略了实现细节的类定义是合法的,编译器怎么知道要分配多少空空间呢?这个问题在Smalltalk和Java等语言中不会出现,因为在这些语言中,定义一个类时,编译器只为一个指向对象的指针分配足够的空空间。也就是说,它们处理上述代码,就好像这些代码是这样写的:

int main()
{
int x;//定义int

人* p;//定义一个指向人员的指针
...
}
当然,这是合法的C,你也可以自己玩这个“把类的实现藏在指针后面”的游戏。对Person这样做的一种方法是将它分成两个类,一个只提供一个接口,另一个实现这个接口。如果实现类名是PersonImpl,Person可以这样定义:

#include //标准库组件
//不应向前声明

# include//for tr1::shared _ ptr;见下文

类PersonImpl//转发人员实现的decl。class
上课日期;//转发中使用的类的decls

班级地址;// Person接口
类Person {
public:
Person(常量std::string& name,常量日期&生日,常量地址& addr);
STD::string name()const;
STD::string birth date()const;
STD::string address()const;
...

private: // ptr到实现;
STD::tr1::shared _ ptr pImpl;
};// std::tr1::shared_ptr
这样,主类(Person)除了一个指向其实现类的指针(这里是一个TR1: tr1::shared_ptr ——见第13项)之外,不包含其他数据成员。这样的设计经常被说成是使用了pimpl习语(指向实现的指针)。在这样的类中,那个指针的名字往往是pImpl,就像上面那个一样。

通过这种设计,人的客户与日期、地址和人的细节分离。这些类的实现可以随意更改,但是Person的客户不必重新编译。此外,因为看不到Person实现的细节,客户不太可能写出在某种程度上依赖于它的东西。/td >

这种分离的关键是用对声明的依赖代替对定义的依赖。这就是最小化编译依赖的本质:只要能实现,就让你的头文件独立自足;如果没有,依赖于其他文件中的声明而不是定义。其他一切都来自这个简单的设计策略。所以:

当对象的引用和指针可以完成时,避免使用对象。只能通过类型的声明来定义对该类型的引用或指针。对象类型的定义必须存在。

只要有可能,用对类声明的依赖替换对类定义的依赖。请注意,当您声明一个使用类的函数时,绝对不需要有该类的定义,即使该函数通过值传递或返回该类:

上课日期;//类声明
Date today();//fine-no definition
void clear appointments(日期d);// of Date是必需的
当然,传递一个值通常不是一个好主意,但是如果你发现自己因为某种原因在使用它,你仍然不能证明引入不必要的编译依赖是正当的。

宣布今天和取消约会而不宣布日期的能力可能会让你吃惊,但它并不像看起来那样不寻常。如果有人调用这些函数,必须在调用之前看到日期的定义。何必去声明没人调用的函数呢?你想知道吗?很简单。不是没人叫他们,而是不是每个人都要叫他们。如果你有一个有很多函数声明的库,不太可能每个客户都会调用每个函数。通过将提供类定义的责任从声明函数的头文件转移到包含客户函数调用的文件,您消除了客户对他们并不真正需要的类型的依赖。

位律师回复
DABAN RP主题是一个优秀的主题,极致后台体验,无插件,集成会员系统
白度搜_经验知识百科全书 » C箴言:最小化文件之间的编译依赖

0条评论

发表评论

提供最优质的资源集合

立即查看 了解详情