概述
绝缘与封装的过程类似,目的是要消除不必要的编译时的耦合的过程。
绝缘 是个物理过程,它的逻辑相似物便是 封装。
涉及到的技术:
- 私有基类
- 嵌入数据成员
- 私有成员函数
- 保护成员函数
- 枚举类型
- 编译器产生函数
- 包含指令
- 私有成员数据
- 默认参数
何为绝缘
从一个简单的例子看绝缘。
两个Stack类的接口部分完全一致,也都使用private对成员变量进行了封装,符合面向对象类的设计思想。不同之处在于右侧的类中使用了StackLink的指针对成员变量进行了再次的封装。
这里左侧的Stack可以称为封装,但非绝缘
这里右侧的Stack可以称为封装,并绝缘
因为当左侧类中对成员变量进行修改时(例如将 int* 修改为double*),依赖于它的其他组件也必须被重新编译,但右侧的类则不会发生这样的情况。在它看来StackLink 只是一个指针,针对StackLink内容的修改不会影响 Stack内存布局的变化,所以并不需要重新编译。
当然并不是说要把所有的一个类中基本类型都换成指针。
绝缘要解决的本质问题是“防止一些修改对客户程序产生影响” 这里的客户程序是指使用你开发的库的程序。
换句话说绝缘更主要是针对接口部分的设计,避免因一处小的修改而造成程序需要大规模的编译。
编译时的耦合(非绝缘的产生)
一, 继承(IsA)和编译时的耦合
一个类派生自另一个类,即使是私有派生,可能也没法让他与客户程序绝缘。
例如图中,如果Base 发生变化,即使只是添加注释,不可避免的所有的 Client程序也会受到影响,并被重新编译。
二, 分层(HasA/HoldsA)和编译时的耦合
HasA: 当一个类在其定义中嵌入了另一个用户自定义的类型。
此时,B是嵌入在类A中。
class A
{
private:
B m_B;
public:
A();
}
这时,在编译类A时,它是需要清楚的知道类B的定义的,也就是说类B的内存结构会对类A产生影响。类A的头文件中,要包含类B的头文件。
HoldsA : 当一个类只拥有另一个类的指针。
class A
{
private:
C *m_C;
public:
A();
}
此时,类A只有类C的地址,A并不需要知道类C的具体构造,也就不依赖于类C的物理布局。 此时 A只需要知道类C的声明即可。
三, 内联和编译时的耦合
声明为 inline 的函数,在头文件中定义并实现该函数。
内联会造成如下的一些问题:
1) 使用该组件的任何程序员都可以看见其实现
2) 改变内联函数的实现会迫使定义内联函数的组件和使用该组件的所有程序被重新编译。
3) 将一个函数改为内联函数或将内联函数改为普通函数都会造成使用该组件的所有程序被重新编译。
四, 私有成员和编译时的耦合
私有成员实现了对程序逻辑上的封装,但并不一定在物理上是绝缘的。 上面Stack的例子可知。
五, 保护成员和编译时的耦合
类中的保护成员其实面对的是两种客户,继承类和普通用户类。如果保护成员是非绝缘的,那么在修改后会造成三方面的影响。
1) 包含该类的组件需要重新编译
2) 改类的子类需要重新编译
3) 使用该类子类的客户程序需要重新编译。
六, 编译器产生的成员函数和编译时的耦合
类中一些基本的成员函数有时是由系统自动生成的,例如:一些拷贝构造函数,运算符函数等。当我们需要自定义这些系统生成的函数时,会造成相关组件的重新编译。
七, 包含命令(#include)和编译时的耦合
包含不必要的头文件也可能造成非绝缘。
八, 默认参数和编译时的耦合
关于默认参数,参考如下程序
class A
{
public void func(double x = 0.0, int y = 1)
}
其中,x, y 都使用了默认值,当对默认值进行修改时,会造成客户程序的重新编译。
九, 枚举类型和编译时的耦合
枚举类型, typedfe,宏,非成员 const变量,这些项一般都会被定义在头文件中。
在小型工程中,这些组件会被定义在同一个头文件中类似于 sysdef.h 或 global.h 等等。但当工程不断加大时,对于这种头文件的修改或添加新内容会造成整个系统的重新编译,编译的代价也会越来越大。
部分绝缘技术
并不是每个组件都应该绝缘的,绝缘并不是一个要么全有,要么全无的命题。针对某些接口或某些细节的绝缘处理会减小客户程序重新编译的概率。
1) 消除私有继承
关于私有继承: 私有继承的访问权限
它的目的是子类即可以使用基类的成员函数功能,同时又保护基类的成员不会通过子类被外部访问。这也是使用私有继承的优点。
程序示例:使用 car -> wheel & engine 有逻辑关系可以更好的表现私有继承的作用。但这里只是示例,其实engien和wheel 是不应该定义在同一个组件中,它们应该有自己分别的头文件和实现。
(这里car 是通过私有继承使用了wheel 和 engine的方法。)
main.h
#include <iostream>
#ifndef _MAIN_H_
#define _MAIN_H_
using namespace std;
class engine
{
public :
void start() {
cout << "engine->start" << endl;}
void move() {
cout << "engine->move" << endl;}
void stop() {
cout << "engin