本篇文章是继续第一篇笔记的续。
17.Factory Method模式
一个高级设计通常要求基于一个现有对象类型来创建一个“适当”类型的对象。例如,我们可能拥有一个指向某种类型的Employee对象的指针或引用,现在需要为该类型的Employee生成一个适当的HRInfo对象,如图:
一般情况下我们会采用一下两种不好的方式:
使用一个“类型编码”和switch class Employee { public: enum Type{SALARY,HOURLY,TEMP}; Type type(){return type_;} //… private: Type type_; //… }; HRInfo *genInfo(const Employee *e) { switch(e.type_) case SALARY: case HOURLY: return new StdInfo(e);break; case TEMP:return new TempInfo(e);break; default:return 0; //未知类型 } |
使用dynamic_cast来询问信息 HRInfo *genInfo(const Employee *e) { if(const Salary *s=dynamic_cast return new StdInfo(s); else if(const Hourly *h=dynamic_cast return new StdInfo(s); else if(const Temp *t=dynamic_cast return new TempInfo(s); else return 0; } |
这两种实现的主要缺点在于,它们与所从Employee和HRInfo派生下来的具体类型相耦合,当我们对代码进行维护时,如果不对两个类十分清楚或者相对应的生成关系改变了而我们却忽略它们的改变,我们都要手工进行大量代码编辑。而这样就很容易导致错误产生。
现在我们来分析一下问题的关键在那:把Employee到HRInfo类型的映射怎样处理?用书上的话说,谁最清楚Temp employee需要何种HRInfo对象?理所当然,是Temp employee自己。好了,既然已经知道了关键所在方法也就出来了:
class Employee
{
public:
//…
virtual HRInfo *genInfo() const =0; //factory method
//…
};
class Temp:public Employee
{
public:
//…
TempInfo *genInfo()const
{return new TempInfo(*this);}
//…
};
Factory Method 本质在于,基类提供一个虚函数“挂钩”,用于生产适当的“产品”。而且每一个派生类可以重写集成的虚函数,为自己产生适当的产品。实际上,我们具有了使用一个类型对象来产生另一个未知类型对象的能力。使用Factory Method模式通常意味着一个高级设计需要基于一个对象的确切类型产生另个“适当”的对象,这样的需要往往发生于存在多个平行或几乎平行的类层次结构的情况下。
18.协变返回类型
一般来说,一个重写的函数必须与被他重写的函数具有相同的返回类型。但是对于协变返回类型(covariant return type)来说情形有所放松。
简单来说,如果一个函数的返回值是一个类,那么它被重写了以后的返回值可以使该类的派生类。如下:
class Shape{
public:
virtual Shape* clone() const=0;
};
class Cricle:public Shape
{
public:
Cricle *clone() const; //返回值类型为Cricle *,与原来Shape*有派生关系
}
19.禁止复制
通过访问限制修饰符public/private/protected可以用于高级约束技术,指明一个类可以被怎样使用。
class NoCopy{
public:
NoCopy(int);
private:
NoCopy(const NoCopy &); //复制构造函数
NoCopy &operator = (const NoCopy &); //赋值构造函数
};
如果没有将上面的两个构造函数声明为私有,编译器就会偷偷地将它们声明为共有的、内联成员函数!
注意:特意把复制构造函数和赋值构造函数显示的声明为私有成员,不仅可以限制编译器自己暗中添加构造函数,而且还可以防止程序中对这两个成员函数的直接调用!!!
20.禁止或强制使用堆heap分配
禁止使用堆分配,是为了保证对象的析构函数一定会得到调用。维护对本体对象(body object)的引用计数的句柄对象(handle object)就属于这种对象。具有自动存储区的类的局部对象,其析构函数会被自动地调用(通过exit/abort发生的非正常的程序终止除外),具有静态存储区的类对象也会自动调用析构函数,但是对分配的对象必须显示的销毁。
禁止堆分配:
class NoHeap{
public:
//…
protected: //声明为protected是因为基类的析构或构造函数可能会隐式调用
void *operator new(size_t) {return 0;}
void operator delete(void *) {}
private:
//禁止分配数组
void *operator new[](size_t){return 0;}
void operator delete[](void *);
}
在上面的代码中,size_t参数将被自动初始化为NoHeap对象的大小(以字为单位);void *则会被编译器自动设置为“将被delete的那个对象”的地址。
想要鼓励使用对分配,只要把new/delete声明为public,然后修改为:
NoHeap *operator new(size_t){return new(size_t);}
void operator delate(void*){delete this;}
21.定位new(placement new)
(可先查看http://www.parashift.com/c++-faq-lite/dtors.html#faq-11.10)
placement new最简单的用法就是把一个对象放到一个指定的内存位置,它位于全局名字空间std中。
void *operator new(size_t,void *p) throw()
{return 0;}
placement new 的实现忽略了表示“大小”的实参:size_t,直接返回其第二个实参。placement new允许我们在一个特定位置“放置”对象,起作用有点像调用构造函数。
class SPort{…..}; //表示一个串口
const int comLoc=0x00400000; //表示一个串口的空间位置
//….
void *comAddr=reinterpret_cast
SPort *com1=new (comAddr) SPort; //在comLoc位置创建一个SPort对象,size_t会自动初始化为SPort对象的大小(以字节为单位)
上面这个代码同时也表示了怎样定义一个串口。
placement new是函数operator new的一个版本,它并不实际分配任何存储区,仅仅返回一个(可能)指向已经分配好空间的指针。正因为调用placement new并没有分配空间,所以不要对其进行delete操作,这一点很重要!我们可以通过显示的调用类的析构函数来销毁对象。
我们也可以在特定位置空间上创建对象数组:
const int numComs=4;
//…
SPort *comPort=new (comAddr) SPort[numComs]; //在comAddr位置创建数组
最后要销毁:
int i=numComs;
while(i)
{
comPort[—i].~SPort();
}
实例:
向一个简单的、固定大小的缓冲区附加新值的代码:
string *sbuf=new string[BUFSIZ]; //调用string的默认构造函数
int size=0;
void append(string buf[],int &size,const string &val)
{buf[size++]=val; } //这里调用赋值构造函数,这样就多调用了string默认构造函数
可以采用placement new,来避免下面这些默认的初始化代码的执行:
const size_t n=sizeof(string)* BUFSIZE;
string *subf=static_cast
int size =0;
具体代码如下:
int size=0;
void append(string buf[],int &size,const string &val)
{
new (&buf[size++]) string(val); //placement new,调用的是复制构造函数
}
上面两个append()函数同时也体现了”复制”构造函数和”赋值”构造函数的区别!!!
通常使用placement new要做一些清理工作:
void cleanupBuf(string buf[],int size)
{
while(size)
{
buf[—size].~string(); //销毁已初始化的元素
::operator delete(buf); //释放存储区
}
}
这一技术被广泛的应用于大多数标准库的实现。
22.数组分配
c++程序员都知道分配和归还内存时保持数组和非数组形式的操作符的匹配:
T *aT=new T;
delete aT;
T *aryT=new T[12];
delete [] aryT;
另外,如果你自定义了非数组的new和delete,最好也同时定义new[]/delete[]。形式如下:
void *operator new(size_t) throw(bad_alloc)
{
return ::operator new(size_t);
}
void *operator new[](size_t) throw(bad_alloc)
{
return ::operator new[] (size_t);
}
void operator delete(void *) throw();
{
return ::operator delete aT;
}
void operator delete[](void *) throw();
{
return ::operator delete [] aT;
}
通过new表达式隐式调用array new时,编译器常常会略微增加一些内存请求:
aryT=new T[5]; //请求内存量为5*sizeof(T)+delta字节
所请求的额外空间用于运行期间内存管理器(runtime memory manager)记录关于数组的一些信息(包括分配元素个数、每个元素的大小等)对于以后回收内存是必不可少的。不过,编译器未必为每一个数组分配都会请求额外的内存空间,并且对于不同的数组分配而言,额外请求的内存空间大小也会发生变化。
23.异常安全公理
编写异常安全的程序或库有点像欧几里德几何学中证明定理:以尽可能小的一组公理为起点来证明一些简单的定理,然后在使用这些辅助定理去证明后续更复杂更有意义的定理。
处理异常安全问题于之类似:从异常安全的组件开始构建异常安全的代码。不过,简单的将一组异常安全的组件或函数调用组合起来,并不能保证所得结果就是类型安全的。
公理1. 异常是同步的
异常是同步的并且只发生在函数调用的边界。因此一些基本的操作是不会产生异常的。荣耀译者对“同步”的理解是:如果程序在某点抛出了一个异常,那么在该异常被处理之前程序是不会继续执行的。
公理2.对象的销毁是安全的
对于这一公理是建立在众多c++社群的共识上的。
公理3.交换操作不会抛出异常
同样这一公理是建立在众多c++社群的共识上的。
24.异常安全函数
编写异常安全的代码时,最困难的地方不在于抛出或捕获异常,而是在“抛出”和“捕获”之间我们应该怎么做,来尽量减少由于异常的抛出而带来的副作用。举例如下:
按照异常安全公理,delete是不会抛出异常的。
不推荐方式 | 推荐方式 |
void Button::setAction(const Action *newAction) { delete action_;//先改变状态 action_=newAction->clone(); //然后再做,但是可能会抛出异常,并且Button的状态已经改变了,损坏了Button的状态 } |
void Button::setAction(const Action *newAction) { Action *temp=newAction->clone(); //先做了再说,如果有异常,就会停止执行,但是Button的状态还是没有改变 delete action_;//然后再改变状态 action_=temp; } |
所以这有一条建议性的规则:首先做任何可能会抛出异常的事情(但不会改变对象的重要状态),然后再使用不会抛出异常的操作结束。
有必要一提的是:编写正确的异常处理代码其实很少使用try语句!只要可能,尽量少的使用try语句块。主要在这些地方使用它们:确实希望检查一个传递的异常的类型,为的是对他做一些事情。在实践中,这些地方通常是代码和第三方的库之间、以及代码和操作系统之间的模块分解处。