一,类与类之间的关系:继承(Inheritance)、复合(Composition)、委托(Delegation)。
二,复合:表示 is-a ,该设计思想可以参照C语言的 struct 。
1. 例子:源自STL中queue的源代码。
1 template <class T, class Sequence = deque<T> > 2 class queue { 3 ... 4 protected: 5 Sequence c; // 底层容器 6 public: 7 // 以下完全利用 c 的操作函數完成 8 bool empty() const { return c.empty(); } 9 size_type size() const { return c.size(); } 10 reference front() { return c.front(); } 11 reference back() { return c.back(); } 12 // deque 是兩端可進出,queue 是末端進前端出(先進先出) 13 void push(const value_type& x) { c.push_back(x); } 14 void pop() { c.pop_front(); } 15 };
其中第1行代码 class Sequence = deque<T> 表示 Sequence 默认是 deque<T> 类型。
以上的 queue 类也等价于把 deque<T> 替换进来。
template <class T> class queue { ... protected: deque<T> c; // 底层容器 public: // 以下完全利用 c 的操作函数完成 bool empty() const { return c.empty(); } size_type size() const { return c.size(); } reference front() { return c.front(); } reference back() { return c.back(); } // void push(const value_type& x) { c.push_back(x); } void pop() { c.pop_front(); } };
2. 对于这种类与类之间的关系,表示了 GoF 23 种设计模式中的 Adapter 适配器模式——类对象结构型模式。如下图表示:
3. 其中,queue 是容器,它改装了 deque——也就是重写了一些操作的方法名。比如原来 deque 的 push_back()改写成了 push();pop_front()改写成了pop()。
4. 复合(Composition)关系的 UML 表示法,如下图:
尤其重要的是表示类与类之间的“复合”关系。
5. 从内存的角度,复合关系在内存中的分布是这样的,如下图所示:
6. 构造与析构函数的执行流程
6.1 构造函数由内而外:Container 的构造函数首先调用 component 的 default 构造函数,然后才执行自己。
Container::Container(...) : Component() {...} ;
构造函数后面的单引号“:”表示委托构造函数语法,同时也是构造函数初始值列表。
上面那段代码中红体字部分,表示是由编译器加入的,如果不满足程序员应用的新情况,程序员可以自行修改或者加入参数。
6.2 析构函数由外而内:Container 的析构函数首先执行自己,然后才调用 Component 的析构函数。
Container::~Container(...) {... ~Component() };
上面那段代码中红体字部分,表示是由编译器加入的。
三,委托:Composition by reference
1. Composition by reference 不能根据现实情况——用指针“组合”到一起——表示为 by pointer。是因为学术界不说 pointer 而是 reference。该课程也从来不提 by pointer。
2. UML表示如下图:
其中,空心的表示内部用指针而不是实体来“组合”,所以有点虚,用空心的菱形表示。
3. 例子 String.h 代码如下
1 // file String.hpp 2 class StringRep; 3 class String { 4 public: 5 String(); 6 String(const char* s); 7 String(const String& s); 8 String &operator=(const String& s); 9 ~String(); 10 . . . . 11 private: 12 StringRep* rep; // pimpl 13 };
代码第12行:pimpl 表示 pointer to implementation 。pimpl 是一种设计模式。“左边的类”是接口,“右边的类”是具体实现。此方法也称“编译防火墙”
文件 String.cpp 代码如下
// file String.cpp #include "String.hpp" namespace { class StringRep { friend class String; StringRep(const char* s); ~StringRep(); int count; char* rep; }; } String::String(){ ... } ...
“委托”关系类之间的生命周期不同步:因为类与类之间的“组合”方式是 reference ,所以声明周期不同步。
4. String 只是对外的接口,当需要任何实现时都会去调用 StringRep 这个具体的实现类的操作。这种设计模式是 GoF 23 当中的 Bridge 桥接模式,也称 Handle and Body 模式。
该设计模式有利于“实现方法”的切换,很具有弹性。这样的方法也叫“编译防火墙”左边只编译一次,右边可以变换实现方式。
5. String类 和 StringRep类 的画图表示如下图
三个对象共享一个数据空间:Hello。此时的reference counting,也就是 n 的值是 3。
6. 当有其中某个对象需要改变数据时,程序会分配一个新的数据空间给它让他去修改,这样的设计方式也称作“copy on write”。
四,继承:表示 is-a。
1. 继承与虚函数搭配最强有力。
2. UML图表示如下
其中空心的三角形表示继承,且父类与子类一定要保持上下位置。
(表示继承的空心三角形)
(放块 T 表示该类是模版类型)
对应UML的代码如下
struct _List_node_base { _List_node_base* _M_next; _List_node_base* _M_prev; }; template<typename _Tp> struct _List_node : public _List_node_base { _Tp _M_data; };
在C++中,struct 同样具有继承的功能。
3. 继承关系的UML图表示如下图
4. 从内存分配的角度看继承关系
5. bass class 的 dtor(析构函数)必须是 virtual function,否则会出现编译错误:undefined behavior。
6. 构造与析构函数的执行流程
6.1 构造由内而外:Derived(派生类)的构造函数首先调用 Base 的 default 构造函数,然后才执行自己。
Derived::Derived(...) : Base() {...};
上面那段代码中红体字部分,表示是由编译器加入的,如果不满足程序员应用的新情况,程序员可以自行修改或者加入参数。
6.2 析构由外而内:Derived 的析构函数首先执行自己,然后才调用 Base 的析构函数。
Derived::~Derived(...) { ... ~Base() };
上面那段代码中红体字部分,表示是由编译器加入的。
7. 继承权限有三种(public、private、protected)public在C++中最常用,并且Java语言只有 public 继承权限。
五,Inheritance with virtual functions
1. 大部分的继承,都是继承的函数调用权。(因为数据都是放在 private 区)
2. non-virtual 函数 : 不希望 derived class(子类)重新定义(override)覆盖它。
virtual 函数 : 希望 derived class 重新定义(override)它,且对它已有的默认定义进行覆盖。
pure-virtual 函数 : 希望 derived class 一定要重新定义它,且目前对它函数本身无任何默认定义。
代码以及示例如下图:
3. 虚函数 :可以完成子类 override 的操作。类似 PHP mvc 框架中 IndexController 继承 Controller 之后,在 IndexController::init() 中定义的“前操作”。
4. virtual functions 的作用:延缓父类中的动作到子类中去写出来。是设计模式 Template Method 的基础。
比如下面这段节选自 MFC 中的代码。用到的设计模式是GoF 23 中 Template Method (模版方法)——类行为型模式。
4.1 UML图如下
4.2 Application framework 代码(MFC官方定义的基础类)如下
CDocument::OnFileOpen() { ... Serialize(); // 读取不同类型文件的方法暂时未知,所以交由子类去实现 ... }
4.3 Application 代码(程序员应用MFC基础类,继承之后的代码实现)如下
class CMyDoc::public CDocument { virtual Serialize() { // users do something } };
4.4 在主程序中,对于用户自定义类的用法如下
main()
{
CMyDoc myDoc;
…
myDoc.OnFileOpen();
}
4.5 程序执行流程,如下图
第一步:初始化 myDoc 类。
第二步:调用 myDoc.OnFileOpen() 方法,编译器会自动寻径到父类(CDocument)中的OnFileOpen() 方法。
第三步:编译器发现 serialize() 方法是虚函数,所以会自动向“下”寻径(CMyDoc),去用子类的 serialize() 来覆盖父类中的 serialize()。
4.6 具体的代码实现如下图
六,Inheritance + Composition 关系下的构造和析构
1.情况一:复合发生在子类中
1.1 UML图如下
1.2 内存分布如下图
1.3 代码实现
class Component { public: Component() { std::cout << "component" << std::endl; } }; class Base { public: Base() { std::cout << "base"<< std::endl; } }; class Derived : public Base { public: Derived() { std::cout << "derived" << std::endl; } private: Component _component; }; int main() { Derived d; return 0; }
1.4 运行结果,如下图
1.5 结论:在 inheritance + composition 设计模式下,当复合发生在子类时,编译器的执行流程是 base->component->derived。
Derived::Derived(…): Base(),Component() { … }; // 红色字体表示编译器自动生成的
2.情况二:复合发生在父类中
2.1 UML,如下图
2.2 内存分布,如下图
2.3 代码实现
class Component { public: Component() { std::cout << "component" << std::endl; } }; class Base { public: Base() { std::cout << "base"<< std::endl; } private: Component _component; }; class Derived : public Base { public: Derived() { std::cout << "derived" << std::endl; } }; int main() { Derived d; return 0; }
2.4 运行结果
2.5 结论:在 inheritance + composition 设计模式下,当复合发生在父类时,编译器的执行流程是 component->base->derived。
Derived::~Derived(…){ … ~Component(), ~Base() }; // 红色表示编译器自动生成的
至此,类与类之间的三种关系已经交待完毕,下面是实际应用的部分。
七,Delegation(委托) + Inheritance(继承)
1. 这种设计模式在 UI 程序设计中最常用到,功能也是最强大的。
2. GoF 23 observer 观察者模式
2.1实例代码 Subject类
class Subject { int m_value; vector<Observer*> m_views; public: void attach(Observer* obs) { m_views.push_back(obs); } void set_val(int value) { m_value = value; notify(); } void notify() { for (int i = 0; i < m_views.size(); ++i) m_views[i]->update(this, m_value); } };
Observer类
class Observer { public: virtual void update(Subject* sub, int value) = 0; };
2.2. UML图如下
2.3. 窗口(window)就是 observer 类。
2.4 实际代码如下图
2.5 对 2.4 中代码的解释
2.5.1 注册的动作
void attach(Observer* obs)
把观察者注册到本体(被观察者)中。
2.5.2 通知观察者的动作
void notify()
通知观察者,进行更新数据。
以上就是OOD(面向对象设计)
至此,已经学习了 Adapter、Handle-Body、Template Method、Observer四中设计模式。还将学习Composition、Prototype两种设计模式。
八,委托相关设计
1. 设计一个文件系统。采用 GoF 23 当中的 Composite(组成)模式——对象结构型模式。
其中 primitive adj.基本的 表示文件
composite n.合金 表示容器
1.1 Composite 设计模式目的:容器中既能放置自己(composite),又能放置 primitive。
1.2 UML
1.3 代码
Component类
class Component { int value; public: Component(int val) { value = val; } virtual void add( Component* ) { } };
primitive类
class Primitive: public Component { public: Primitive(int val): Component(val) {} };
composite类
class Composite: public Component { vector <Component*> c; public: Composite(int val): Component(val) { } void add(Component* elem) { c.push_back(elem); } … };
1.4 在C++的容器中(比如vector)只能存储每一个占用内存大小都是一样的类型,所以composite类中容器存储的是指针,尤其对于用户自定义的类型,更要存储指针。
2. 设计一个框架。采用 GoF 23 当中的 Prototype(原型) 模式——对象创建型模式。
2.1 Prototype 设计模式的目的:在不可能知道子类名称的时候,创建子类对象。
C++原型模式中子类创建了一个自己,就是所谓的原型
该设计模式突破了C++不知道类名而无法创建该类对象的束缚。
比如如下php代码可以动态地创建一个类。
$obj = new $className();
2.2 UML
2.2.1 UML图中,带有下划线的表示是静态成员。
(成员函数、成员变量)标识符:类型名
“-”负号表示 private
“#”井号表示protected
2.3 代码
Image.h
enum imageType { LSAT, SPOT }; class Image { public: virtual void draw() = 0; static Image *findAndClone(imageType); protected: virtual imageType returnType() = 0; virtual Image *clone() = 0; // 每一个声明过的Image的子类,注册它自己的原型 static void addPrototype(Image *image) { _prototypes[_nextSlot++] = image; } private: // addPrototype()函数在这里存储每一个注册过的原型 static Image *_prototypes[10]; static int _nextSlot; }; Image *Image::_prototypes[]; int Image::_nextSlot;
Image.cpp
// 客户端需要客户端调用这个public static方法,当它需要Image的子类的实例时 Image *Image::findAndClone(imageType type) { for (int i = 0; i < _nextSlot; i++) if (_prototypes[i]->returnType() == type) return _prototypes[i]->clone(); }
LandSatImagine.h
class LandSatImage : public Image { public: imageType returnType() { return LSAT; } void draw() { cout << "LandSatImage::draw " << _id << endl; } // 当clone()函数被调用时,它调用带有一个无用参数的构造函数 Image *clone() { return new LandSatImage(1); } protected: // 这个构造函数只能被clone()函数调用 LandSatImage(int dummy) { _id = _count++; } private: // 原理:初始化Image子类将会引起注册过的子类原型的默认构造函数被调用 static LandSatImage _landSatImage; // 只有private static数据成员被初始化后,才会被调用 LandSatImage() { addPrototype(this); } // 名词性的“state”是每一个实例的机制(Nominal "state" per instance mechanism) int _id; static int _count; }; // 注册子类原型 LandSatImage LandSatImage::_landSatImage; // 初始化“state”是每一个实例的机制(Initialize the "state" per instance mechanism) int LandSatImage::_count = 1;
SpotImage.h
class SpotImage : public Image { public: imageType returnType() { return SPOT; } void draw() { cout << "SpotImage::draw " << _id << endl; } Image *clone() { return new SpotImage(1); } protected: SpotImage(int dummy) { _id = _count++; } private: SpotImage() { addPrototype(this); } static SpotImage _spotImage; int _id; static int _count; }; SpotImage SpotImage::_spotImage; int SpotImage::_count = 1;
main.cpp
// 模仿创建请求的流 (Simulated stream of creation requests) const int NUM_IMAGES = 8; imageType input[NUM_IMAGES] = { LSAT, LSAT, LSAT, SPOT, LSAT, SPOT, SPOT, LSAT }; int main() { Image *images[NUM_IMAGES]; // 给予一个image类型,找到与之对应的原型,然后返回一个clone对象 for (int i = 0; i < NUM_IMAGES; i++) images[i] = Image::findAndClone(input[i]); // 示例:正确的image对像被clone for (i = 0; i < NUM_IMAGES; i++) images[i]->draw(); // 释放动态内存 for (i = 0; i < NUM_IMAGES; i++) delete images[i]; }
其中 LSAT 就是 LastSatImage。
3. clone() 等于 拷贝。