适配器模式
定义
将一个类的接口,转换成客户期望的另一个接口。适配器让原本不兼容的类可以合作无间。
类图
代码示例
1 | class Duck { |
理解
适配器持有一个被适配者,适配器的接口和适配目标的接口是一样的,当调用适配目标的接口时,适配器转而调用被适配者的对应方法。也就是说适配器拥有一个被适配对象,就可以为所欲为!
将一个类的接口,转换成客户期望的另一个接口。适配器让原本不兼容的类可以合作无间。
1 | class Duck { |
适配器持有一个被适配者,适配器的接口和适配目标的接口是一样的,当调用适配目标的接口时,适配器转而调用被适配者的对应方法。也就是说适配器拥有一个被适配对象,就可以为所欲为!
将请求封装成对象,以便使用不同的请求、队列或者日志来参数化其他对象。命令模式支持可撤销操作。
1 | class Light { |
利用command接口,每个动作都继承这个接口成为一个命令对象,命令对象持有一个执行命令的实体,当该动作执行时,命令对象直接调用执行者的对应动作。有利于动作扩展,新动作只要继承command接口就行。
确保一个类只有一个实例,并提供一个全局访问点。
1 | class Singleton { |
相当于全局变量,只有一个实例,全局共享,只能通过函数来取得实例,将构造函数声明为私有,有必要的话,将拷贝构造和赋值也声明为私有。
工厂方法1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48class Pizza {
protected:
string name;
public:
string getName() {
return name;
}
};
class NYCheesePizza :public Pizza {
public:
NYCheesePizza(){
name = "NYCheesePizza";
}
};
class ChicagoCheesePizza :public Pizza {
public:
ChicagoCheesePizza() {
name = "ChicagoCheesePizza";
}
};
class PizzaStore {
public:
void orderPizza() {
Pizza* pizza = createPizza();
cout << "order " << pizza->getName() << endl;
}
virtual Pizza* createPizza() = 0;
};
class NYPizzaStore :public PizzaStore {
public:
Pizza* createPizza() {
Pizza* pizza = new NYCheesePizza();
return pizza;
}
};
class ChicagoPizzaStore :public PizzaStore {
public:
Pizza* createPizza() {
Pizza* pizza = new ChicagoCheesePizza();
return pizza;
}
};
抽象工厂可以看成是有多个工厂方法的一个工厂,生产一系列产品。
封装对象的创建,将对象实例化交给子类来决定,在基类中声明一个创建的接口,由子类来实现它。
动态地将责任附加到对象上。若要扩展功能,装饰者提供了比继承更有弹性的替代方案。
1 | class Beverage { |
使用接口,装饰者和被装饰者继承同一个接口,装饰者拥有一个被装饰者,当调用方法时,装饰者先执行自己新增的部分,再调用被装饰者的方法,这样就实现了扩展功能。
定义了对象之间的一对多依赖,这样一来,当一个对象改变状态时,它的所有依赖都会收到通知并自动更新。
1 | //被观察者 |
用户关注感兴趣的事件,当事件发生变化时,会自动收到通知从而改变自己的状态,而不是主动向事件询问状态。可以实现按键监听。
策略模式封装了算法族,分别封装起来,让它们之间可以互换,此模式让算法变化独立于使用算法的客户。
1 | class Character { |
把客户的行为抽象成一个接口,具体的行为继承这个接口并实现出自己特定的行为,客户不需要和具体的行为打交道,只需要和接口打交道,减少耦合,而且利于扩展,只需要扩展特定行为,不需要对客户代码做改变。
类和模板都支持接口和多态,对类而言,接口是显式的,以函数签名为中心,多态通过虚函数发生于运行期。
对模板参数而言,接口是隐式的,基于有效的表达式,多态通过模板具现化和函数重载解析发生于编译期。
作为模板参数,class和typename没有不同,可以互换。
当使用一个内嵌类型时,需要在前面加上typename,编译器才知道它是一个类型而不是成员或者函数。1
2
3
4
5template<typename T>
void Sample(T t){
typename std::iterator_traits<T>::value_type temp(T);
...
}
因为基类模板可能被特化不提供基类模板的某个函数,所以在派生类里面直接调用模板基类的函数无法通过编译,可以通过this指针和using声明或者基类作用域符号来告诉编译器假定该函数存在。
模板生成多个类和函数,所以任何模板都不该与某个造成膨胀的模板参数产生相依关系。
用函数参数或者class成员变量代替模板参数可以消除这种膨胀。
还可以让具有完全相同的二进制表述的类型共享实现码,也可以减小膨胀,如int和long在一些机器上是一样的,所有指针类型都是一样的。
泛化构造函数可以使一个类型隐式转换成另一个类型:1
2
3
4
5
6
7
8
9
10template<class T>
class shared_ptr{
public:
...
template<class Y>
shared_ptr(shared_ptr<Y> const&); //泛化构造函数
templa<class Y>
shared_ptr& operator=(shared_ptr<Y> const&); //泛化赋值函数
}
声明了泛化构造函数还是需要声明正常的构造函数和赋值函数。
当提供与模板相关的包含参数隐式转换的函数时,将函数定义为friend并在类内实现。
traits classes,特性萃取,使得类型相关信息在编译期可用,以template内嵌typedef和偏特化实现。
如何使用traits?
TMP可以将工作从运行期搬到编译期,从而实现更早的错误侦测和更高的执行效率。还可以被用来生成“基于政策选择组合”的客户定制代码,也可以用来避免生成对某些特殊类型并不合适的代码。1
2
3
4
5
6
7
8
9template<unsigned n>
class Factorial{
enum{value = n * Factorial<n-1>::value};
}
template<>
class Factorial<0>{
enum{value = 1};
}
set_new_handler允许客户制定一个函数,当内存分配失败时调用这个函数进行处理。
Nothrow new只适用于内存分配,如果构造函数出现问题还是可能抛出异常。
有许多理由需要写个自定义的new和delete,包括改善性能、对heap运用错误进行调试、收集heap调试信息等。
operator new应该内含一个无穷循环,并在其中尝试分配内存,如果它无法满足内存要求,就该调用new_handler。它也应该有能力处理0bytes申请,class专属版本还应该处理“比正确大小更大的申请”。
operator delete应该在收到null指针时不做任何事情,class专属版本还应该处理“比正确大小更大的申请”。
placement new只有placement delete能够delete,所以没有声明placement delete使用placement new的话会出现内存泄漏。
当在类内声明placement new和placement delete时会遮盖标准的new和delete,一种做法是将所有标准的new/delete放到一个基类里,然后要自定义new/delete的类继承这个基类,使用using声明获取标准形式。
C++标准库的主要机能由STL、iostream、locales组成,并包含C99标准程序库。
TR1添加了智能指针、一般化函数指针(function)、hash-based容器、正则表达式以及另外10个组件的支持。
TR1自身只是一个规范,为获得TR1提供的好处,需要一个实物,一个好的来源是Boost。
Boost是一个社群,致力于免费、源码开放、同僚复审的C++程序库开发。Boost在C++标准化过程中扮演深具影响力的角色。
Boost提供了许多TR1组件实现品,以及其他许多程序库。
这样做可以增加程序的清晰度并改善程序效率。如果程序异常提前退出而变量定义在前,则该变量不会被使用但是还是付出了构造和析构的成本,因此将变量定义延后直到使用变量之前,甚至延后到能给予初值之前,这样可以避免无意义的default构造函数。
尽量避免转型,特别是避免dynamic_cast,其运行速度很慢,如果某个设计需要转型动作,试着找一些替代做法,如virtual函数,如果转型是必要的,将其隐藏于某个函数里。
使用C++新式转型不要用旧式转型,前者很容易识别出来。
避免返回handles(指针,引用,迭代器)指向对象内部,一是提升封装性,如果指向private成员,则用户可以修改这些成员,private就没用了,二是帮助const成员函数的行为像一个const,并降低空悬的可能性。
异常安全函数即使发生异常也不会泄漏资源或允许任何数据结构被破坏,这样的函数分为3种可能,基本型(若异常被抛出,程序内任何事物仍然处于有效状态下,有可能发生改变)、强烈型(要么全变,要么不变)、不抛异常型。
强烈保证往往能够以copy and swap实现出来,但不一定对所有函数都有实现意义,例如copy的成本太高。
一个函数提供的异常安全性最高是它所调用的各个函数的安全性的最弱的一个。
支持“编译依存性最小化”的一般构想是:相依于声明式,不要相依于定义式,两个手段是Handle class(pimpl)和Interface class(抽象类)。
程序库头文件应该以“完全且仅有声明式”的形式存在,这种做法不论是否涉及templates都适用。
public继承意味着is-a,对基类适用的所有操作对派生类也适用,因为每个派生类对象也是一个基类对象。
派生类内的名称会遮掩基类内的名称,可以适用using声明来让基类的名称可见,或者使用转交函数,转交函数显式调用基类的函数。
接口继承和实现继承不同,在public继承下,派生类总是继承基类的接口。
纯虚函数只具体指定接口继承,派生类必须自己实现。
虚函数指定接口继承和缺省实现继承,派生类可以选择实现,可以用纯虚函数来实现,基类的纯虚函数定义缺省行为,在派生类的虚函数实现中显式调用基类的纯虚函数。
非虚函数实现接口继承和强制性实现继承,派生类不应该重新定义。
虚函数的替代方案,第一个是NVI(Non-Virtual Interface)手法,是一种Template Method设计模式,定义一个非虚的public函数调用private的虚函数,这个非虚函数成为外覆器,这么做的好处是在虚函数调用前后,还可以做其他的处理,如互斥锁上锁,制造日志等,只用virtual函数无法做到这些。
第二个是所谓的Strategy模式,用函数指针代替虚函数,创建对象时传递相应的函数指针,这样的好处是对于同一类型可以有不同的表现方法,传递的函数不同表现则不同,有更大的弹性,若对象的表现在运行期会改变,也可以提供一个setMethod函数方便地进行改变,但是这样的函数若需要访问类的私有成员就不得不声明为友元,降低了类的封装性。
还可以用function来实现Strategy模式,function可以保存的是所有可以调用的对象,包括函数指针,仿函数,成员函数等,而且也允许兼容,例如返回类型或者参数类型可以隐式转换成所需的类型,这样的函数也可以放进function里面。
一种古典的Strategy模式是将体系内的virtual函数替换为另一个继承体系中的virtual函数。
由于静态绑定的原因,当用基类指针调用non-virtual函数时永远是调用基类的,因此不要重新定义继承来的non-virtual函数。
缺省参数值是静态绑定,而虚函数是动态绑定,当缺省参数不同时,基类指针只会调用基类的缺省参数不会用派生类的。
public继承是is-a,而在类里定义另一个类的对象则是has-a或者is-implemented-in-terms-of a(由a实现出)。
private继承是一种is-implemented-in-terms-of,private继承的派生类对象不会被转为基类对象,类似于复合,根据某物实现出,尽可能使用复合而不是private继承,当需要访问protected成员和virtual函数时,使用private继承。
private继承可以利用empty base最优化:1
2
3
4
5
6class Empty{};
class HoldInt{
private:
int x;
Empty e;
}
上面这个复合的例子,空的类的size并不是0,编译器往往会放入一个char,size往往是1,因此HoldInt的size加上对齐达到了8,而使用private继承就不会有这种问题:1
2
3
4
5class Empty{};
class HoldInt : private Empty{
private:
int x;
}
这时HoldInt的size就为一个int的大小。
多重继承比单继承复杂,可能导致二义性,即两个基类中有同名函数,不知道该调用哪一个,此外如果一个基类在继承路径上出现多次还可能需要虚继承。
虚继承会增加大小、速度、初始化复杂度等成本,如果虚基类不带任何数据是最具有实用性的情况。
多重继承的一个用途是,其中一个以public继承某个interface,另一个以private继承某个实现。
将动态分配的资源放进一个资源管理类中,构造函数中获得资源(RAII),析构函数保证了这些资源的释放。
常用的有shared_ptr和auto_ptr,前者常用,后者复制时会把被复制的指针置为null,一块内存只能有一个auto_ptr指向它。
考虑下面这么一个管理互斥锁的类:1
2
3
4
5
6
7
8
9class Lock{
public:
explicit Lock(Mutex* pm): mutexPtr(pm){
lock(mutexPtr);//获得资源
}
~Lock(){
unlock(mutexPtr);//释放资源
}
}
当Lock类复制时,大多数时候可以有两种选择,第一个是禁止复制:1
2
3
4class Lock: private Uncopyable{
public:
...
}
第二种是用引用计数法来管理复制,复制时,引用加1,当引用为0时,释放资源,使用shared_ptr还可以定制自己的删除器,因为有时候并不希望释放资源,像Lock,析构的时候不释放资源,只是解锁。1
2
3
4
5
6
7
8
9class Lock{
public:
explicit Lock(Mutex* pm): mutexPtr(pm, unlock)//指定删除器unlock
{
lock(mutexPtr.get());
}
private:
shared_ptr<Mutex> mutexPtr;//不再需要析构函数
}
有些API需要对原始资源进行访问而不是资源管理类,故资源管理类应该提供办法对原始资源进行访问。
有显示转换和隐式转换两种办法。显示转换专门定义一个返回原始资源的函数,例如shared_ptr提供get函数返回原始指针。
隐式转换则是直接将资源管理类转换成原始资源类型,如下:1
2
3
4
5
6
7
8
9
10class Font{
public:
...
//隐式转换,Font转换为FontHandle
operator FontHandle() const{
return f;
}
private:
FontHandle f;
}
隐式转换对用户来说很方便,但可能会产生一些问题,显示转换比较安全。
new和delete搭配,new[]和delete[]搭配,最好不要对数组做typedef。
以单独一条语句将new出来的对象放到智能指针中去。如果不这么做,一旦有异常抛出,可能会导致难以察觉的资源泄漏。
让接口正确使用的方法包括接口的一致性,与内置类型的行为兼容。
阻止误用的方法包括建立新类型、限制类型上的操作,束缚对象值,以及消除客户的资源管理责任。
shared_ptr支持定制删除器,可以防范DLL问题(即在一个DLL里创建在另一个DLL里释放),可以用来包装互斥锁,实现自动解锁。
设计class就是设计一个新type,要考虑方方面面的问题。
对一个类对象传值意味着复制,会调用拷贝构造函数和析构函数,造成了不必要的时间消耗,而传引用则不会调用拷贝构造函数,更高效,如果函数对传入的参数不进行改变,应该加上const。
对于内置类型和STL的迭代器和函数对象,传值比较适合。
不要返回对一个局部对象的引用,在函数结束后该对象被释放,引用变成不合法的。
同样不要在函数内返回一个newed对象的引用,因为delete得不到保障。
也不要返回对一个static对象的引用,如果同时需要多个这样的对象,会出问题。
指针同理。
为了封装性,将成员变量声明为private,这可以赋予客户端访问数据的一致性、可以细微划分成员的访问控制(可读可写)、允许约束条件获得保证,并提供class作者充分的实现弹性。
protect的封装性不比public好,因为派生类可以访问protect成员。
这样做可以增加封装性、包裹弹性和机能扩充性。
如果一个函数需要对所有参数进行类型转换,它必须是non-member的,因此调用端不能进行类型转换。如下:1
2
3
4
5
6
7
8
9
10class Rational{
public:
Rational(int numerator = 0, int denominator = 1); //无explicit,允许隐式转换
const Rational operator*(const Rational& rhs) const;
...
}
Rational onehalf = Rational(1, 2);
Rational result;
result = onehalf * 2; //正确,2进行了隐式转换
result = 2 * onehalf; //错误,相当于2.operator*(onehalf),2不是Rational类型!
解决方法就是将该函数声明为non-meber的:1
2
3
4
5
6const Rational operator*(const Rational& lhs, const Rational& rhs) const;
Rational onehalf = Rational(1, 2);
Rational result;
result = onehalf * 2; //正确,2进行了隐式转换
result = 2 * onehalf; //正确!
当std的swap对一个类型效率不高时,提供一个swap成员函数,且这个成员函数不应该抛出异常,往往这个类承担一种指针的角色,指向真正的实现类(pimpl),swap只需要互换指针而不需要互换数据。
如果提供一个成员函数,也该提供一个非成员函数来调用成员函数,对于class,也请特化std的swap。
调用swap时先声明using std::swap然后再调用,且调用不包含任何的命名空间的修饰。
不要在std里加入新东西。1
2
3
4
5
6
7
8
9
10
11
12
13
14
15namespace WidgetStuff{
class Widget{
private:
WidgetImpl* pImpl;
public:
...
void swap(Widget& other){
using std::swap;
swap(pImpl, other.pImpl);
}
}
void swap(Widget& a, Widget& b){
a.swap(b);//调用成员函数
}
}