[条款08:别让异常逃离析构函数]
问题:如果一个类的析构函数必须执行一个动作,而该动作可能会在失败时抛出异常,该怎么办?举个例子,假设使用过一个class负责数据库连接:
class DBConnection
{
public:
static DBConnection create(); //此函数返回DBConnection对象
void close(); //关闭联机;失败则抛出异常
};
一个较佳策略是创建一个用来管理DBConnection资源的DBConn类,DBConn类自己提供一个close函数,因而赋予客户一个机会得益处理“因该操作而发生的异常”。DBConn也可以追踪其所管理的DBConnection是否已被关闭,若没有被关闭,则由DBConn的析构函数关闭它。这可防止遗失数据连接。然而如果DBConnection析构函数调用close失败,则可使用“强制结束程序”或“吞下异常”的方法:
class DBConn
{
public:
DBConn();
~DBConn();
void close();
private:
DBConnection db;
bool closed;
};
DBConn::DBConn()
{
}
DBConn::~DBConn()
{
if(!closed)
{
try
{
db.close(); //关闭连接
}
catch(...) //如果关闭动作失败
{
写日志,记下对close的调用失败; //记录下来并结束程序
... //或者吞下异常;
}
}
}
void DBConn::close() //供客户使用的新函数
{
db.close();
closed = true;
}
如果某个操作可能在失败时抛出异常,而又存在某种需要必须处理该异常,那么这个异常必须来自析构函数以外的某个函数。因为析构函数抛出异常及时危险,总会带来“过早结束程序”或“发生不明确行为”的风险。
请牢记:
1、析构函数绝对不要抛出异常。如果一个被析构函数调用的函数可能抛出异常,析构函数应该捕获异常,然后吞下它们或结束程序。
2、如果客户需要对某个操作函数进行运行期间抛出的异常做出反应,那么class应该提供一个普通函数(而非在析构函数中)执行操作。
[条款09:绝不在构造和析构过程中调用virtual函数]
不该在构造函数和析构函数期间调用virtual函数,这一点是C++与jave/C#不同的地方之一。
假设有一个class继承体系,用来模拟股市交易如买进、卖出的订单等等。这样的交易一定要经过审计,所以每当创建一个交易对象,在审计日志中也需要创建一笔适当记录。
正确的做法是在基类Transaction内将logTransaction函数改为non-virtual,然后要求派生类构造函数传递必要信息给基类Transaction的构造函数,这样那个构造函数便可安全地调用non-virtual logTransaction。正确用法如下:
//绝不在构造和析构过程中调用virtual函数
//如有以下继承体系,希望每创建一个交易对象,都会有一笔日志记录
class Transaction
{
public:
Transaction()
{
logTransaction();
};
virtual void logTransaction() const = 0;
};
//继承类,需要实现logTransaction()
class BuyTransaction : public Transaction
{
public:
virtual void logTransaction() const;
};
//继承类,需要实现logTransaction()
class SellTransaction : public Transaction
{
public:
virtual void logTransaction() const;
};
//解决方案:logTransaction改为非虚,然后继承类构造函数传递必要的信息给基类构造函数
class Transaction
{
public:
explicit Transaction(const std::string& logInfo)
{
logTransaction();
}
void logTransaction(const std::string& logInfo) const; //非虚
};
class BuyTransaction : public Transaction
{
public:
BuyTransaction(parameters)
: Transaction(createLogString(parameters))
{}
private:
//静态的放置“初期未成熟的buytransaction对象内尚未初始化的成员变量”
static std::string createLogString(parameters);
};
注意示例BuyTransaction内的private static函数 createlogString的运用。比起在成员初值列内给予基类所需的数据,利用辅助函数创建一个值传递给基类的构造函数往往比较方便(也比较可读)。令此函数为static,也就不可能意外指向“初期未成熟的BuyTransaction对象内尚未初始化的成员变量”。这很重要,正是因为“那些成员变量处于未定义状态”,所以在基类构造和析构期间调用的virtual函数不可下降至派生类。
请牢记:
在构造和析构期间不要调用virtual函数,因为这类调用从不下降至派生类(比起当前执行构造函数和析构函数的那层)。
[条款10:令operator= 返回一个reference to *this]
关于赋值,可以写成连锁形式:
int x, y, z;
x = y = z = 15; //赋值连锁形式
赋值采用右结合律,故上述赋值被解析为:
x = (y = (z = 15));
为了实现连锁赋值,赋值操作符必须返回一个reference引用指向操作符的左侧实参。
下面示例是为classes实现赋值操作符时应该遵循的协议:
class Widget
{
public:
...
Widget& operator=(const Widget& rhs) //返回类型是个reference,指向当前对象
{
...
return *this; //返回左侧对象
}
...
};
这个协议不仅适用于以上的标准赋值形式,也适用于所有赋值相关运算,例如:
class Widget
{
public:
...
//这个协议适用于+=,-=,*=,等等
Widget& operator+=(const Widget& rhs) //返回类型是个reference,指向当前对象
{
...
return *this;
}
Widget& operator=(int rhs) //此函数也适用,即使此操作符的参数类型不符合协定
{
...
return *this;
}
...
};
这份协议被所有内置类型和标准程序库提供的类型如string,vector,complex,trl::shared_ptr或即将提供的类型共同遵守。
请牢记:
令赋值(assignment)操作符返回一个reference to *this
条款11:在operator= 中处理“自我赋值”
“自我赋值”发生在对象被赋值给自己时:
class Widget
{
...
};
Widget w;
...
w = w; //赋值给自己
operator=,不仅不具备“自我赋值安全性”,也不具备“异常安全性”。
让operator= 具备“异常安全性”往往自动获得“自我赋值安全性”的回报。因此越来越多的人对“自我赋值”的处理态度是不去管它,而把焦点放在实现“异常安全性”上。
确保代码不但“异常安全”而且“自我赋值安全”的一个替代方案是,使用所谓的copy and swap技术。此技术和“异常安全性”有密切关系,它是一个常见而够好的operator=撰写办法,其实现方式为:
class Widget
{
public:<br> ...
void swap(Widget& rhs); //交换*this和任rhs的数据
...
};
Widget& Widget::operator=(const Widget& rhs)
{
Widget temp(rhs); //为rhs数据制作一份复件(副本)
swap(temp); //将*this数据和上述复件的数据交换
return *this;
}
另外一种实现方式为:
Widget& Widget::operator=(Widget rhs) //rhs是被传对象的一份复件(副本),注意此处是值传递 pass by value
{
swap(rhs); //将*this数据和复件的数据交换
return *this;
}
上述实现方式因为:1、某类的copy assignment操作符可能被声明为“以by value方式接受实参”;2、以by value方式传递东西会造成一份复件/副本
此方式牺牲了清晰性,然而将拷贝动作从函数本体移至“函数参数构造阶段”却可令编译器有时生产更高效的代码
//在operator=中处理自我赋值
class Bitmap {};
//保存一个指针指向一块动态分配的bitmap
class Widget
{
public:
//问题的发生:
Widget& operator=(const Widget& rhs)
{
delete pb;
//如果rhs与this指向同一块内存则错误
pb = new Bitmap(*rhs.pb);
return *this;
}
//解决方法1:证同测试,
Widget& operator= (const Widget& rhs)
{
if (this == &rhs)
return *this;
delete pb;
//这里可能出现异常问题,如果new发生异常(不论内存不足还是copy构造函数异常)
//widget会持有一个指针指向一块被删除的bitmap.
pb = new Bitmap(*rhs.pb);
return *this;
}
//解决方法2:精心安排语句顺序来保证“异常安全”,防止解法1的问题
Widget& operator=(const Widget& rhs)
{
//记住原先指针,再构造一个副本,然后再删除,即删除在构造之后
Bitmap* porg = pb;
pb = new Bitmap(*rhs.pb);
delete porg;
return *this;
}
//解法3:2的一个替代方案,即copy and swap技术
void swap(Widget& rhs)
{
//交换数据
}
Widget& operator=(const Widget& rhs)
{
Widget temp(rhs);
swap(temp); //交换*this与temp的数据
return *this;
}
//3的另一个变型解法,依赖以下事实(1) 某类的拷贝赋值操作可能被声明为"by value"方式接受实参
//(2)以by value方式传递东西会造成另一份副本
Widget* operator=(Widget rhs) //这里利用by value构造一个副本
{
swap(rhs); //这里是将*this与副本数据互换,
return *this;
}
private:
Bitmap* pb; //指向一个从堆上分配的对象
};
请牢记:
1、确保当前对象自我赋值时operator= 有良好行为。其中技术包括比较“来源对象”和“目标对象”的地址、精心周到的语句顺心、以及copy-and-swap。
2、确定任何函数如果操作一个以上的对象,而其中多个对象是同一个对象时,其行为仍然正确。
[条款12:复制对象时勿忘其每一个部分]
设计良好的面向对象系统会将对象的内部封装起来,只留两个函数负责对象拷贝,即copy构造函数与copy assignment操作符。编译器会在必要的时候为类创建coping函数,并说明这些“编译器生成版”的行为:将被拷贝对象的所有成员变量都做一份拷贝。
任何时候,只要自己实现派生类的copying函数,则必须很小心的复制其基类成分。这些成分往往是private私有的,故无法直接访问它们,因此应该让派送类的coping函数调用相应的基类函数:
void logCall(const string& funcName);<br> class Customer
{
public:
Customer(const Customer& rhs);
Customer& operator=(const Customer& rhs);
private:
string name;
Date lastTranscation;
};
class PriorityCustomer : public Customer
{
public:
...
PriorityCustomer(const PriorityCustomer& rhs);
PriorityCustomer& operator=(const PriorityCustomer& rhs);
...
private:
int priority;
};
PriorityCustomer ::PriorityCustomer (const PriorityCustomer& rhs)
: Customer(rhs), //调用基类的copy构造函数
priority(rhs.priority)
{
logCall("PriorityCustomer copy constructor");
}
PriorityCustomer& PriorityCustomer ::operator = (const PriorityCustomer& rhs)
{
logCall("PriorityCustomer copy assignment constructor");
Customer::operator=(rhs); //对基类Customer成分进行复制动作
priority = rhs.priority;
return *this;
}
当编写一个copying函数,确保1、复制所有local成员变量,2、调用所有基类的适当的copying函数。
注意两个错误用法:
1、令copy assignment操作符调用copy构造函数是错误的,因为在这就像试图构造一个已存在的对象。
2、令copy构造函数调用copy assignment操作符同样是错误的。构造函数用来出事后对象,而assignment操作符只实行与已初始化的对象身上。对一个尚未构造好的对象赋值,就像在一个尚未初始化的对象身上做“z只对已初始化对象才有意义”的事意义。
消除copy构造函数与copy assignment操作符重复代码的做法是:建立一个新的成员函数给两者调用。这样的函数往往是private而且被命名为init。这个策略可以安全消除copy构造函数与copy assignment操作符之间的代码重复。
请牢记:
1、copying 函数应该确保复制“对象内的所有成员变量”及“所有基类成分”。
2、不要尝试以某个copying函数实现另一个copying函数。应该将共同机能放进第三个函数中,并由两个copying函数共同调用。
[条款13:以对象管理资源]
所谓资源就是,一旦使用了它,将来必须还给系统。C++最常使用的资源就是动态分配内存(如果分配了内存却不释放,会导致内存泄露),但内存只是必须要管理的众多资源之一。其他常见的资源还包括文件描述器(file descriptors)、互斥锁(mutex locks)、图形界面中的字型和笔刷、数据库连接、以及网络sockets。不论哪一种资源,重要的是,当不再使用它时,必须将它还给系统。
假设我们使用一个用来模拟投资行为(例如股票、债券等)的程序库,其中各式各样的投资类型继承自一个root class Investment:
//以对象管理资源
//基类
class Investment
{...}
//程序库通过一个工厂函数产生某个特定的inverstment对象,返回指针
Investment* createInvestment();
//这导致调用者需要对这个返回的指针进行删除
void f()
{
Investment* pInv = createInvestment();
//这里中间可能过早返回,或抛出异常,则都不会执行delete,也就会发生资源泄漏
...
delete pInv;
}
//解决方法1:使用auto_ptr管理对象资源
void f()
{
//这里是RAII,获得资源后立刻放进管理对象
std::auto_ptr<Investment> pInv(createInvestment);
//auto_ptr的复制行为
std::auto_ptr<Investment> pInv2(pInv); //pInv2指向对象,pInv=null
pInv = pInv2; //pInv指向对象,pInv2=null
}//离开函数的时候,调用auto_ptr的析构函数确保资源被释放
//解决方法2:使用tr1::shared_ptr管理对象资源
void f()
{
//这里是RAII,获得资源后立刻放进管理对象
std::tr1::shared_ptr<Investment> pInv(createInvestment);
//shared_ptr的复制行为
std::tr1::shared_ptr<Investment> pInv2(pInv); //pInv2, pInv指向同一对象
pInv = pInv2; //pInv,pInv2指向同一对象
}//离开函数的时候,调用shared_ptr的析构函数确保pInv,pInv2被释放
//以下行为虽然可以通过编译器,但是会有资源泄漏,不应该使用
std::auto_ptr<std::string> aps(new std::string[10]);
std::tr1::shared_ptr<std::string> api(new int[1024]);
请牢记:
1、为防止内存泄露,请使用RAII对象,它们在构造函数中获得资源并在析构函数中释放资源。
2、两个常被使用RAII class分别是trl1::shared_ptr和auto_ptr。trl1::shared_ptr通常是较佳选择,因为其copy行为比较直观。若选择auto_ptr,复制动作会使被复制物指向null。trl1::shared_ptr在头文件<memory>中
[条款14:在资源管理类中小心copying行为]
请牢记:
1、复制RAII对象必须一并复制它所管理的资源,所以资源的copying行为决定RAII对象的copying行为。
2、普遍常见的RAII class copying行为是:抑制copying、施行引用计数法。不过其他行为也可能被实现。
//资源管理类的copying行为
class Lock
{
public:
explicit Lock(Mutex* pm) : mutexPtr(pm)
{
lock(mutexPtr); //构造函数锁住资源
}
~Lock()
{
unlock(mutexPtr); //析构函数释放资源
}
};
//客户端的用法
Mutex m;
Lock ml(&m); //锁定,执行关键区域内的操作
...
//如果进行拷贝,可以发生以下情况:
Lock ml1(&m);
Lock ml2(ml1);
//情况1:禁止复制,参考条款6的做法,
//1声明为私有,不定义,如果复制发生链接错误
//2建一个不能拷贝的基类,并私有继承于它,这将错误移到编译期
class Lock : private Uncopyable
{
};
//情况2:引用计数法,例如shared_ptr的行为,但当资源变为0时,它的默认行为是
//删除所指物,而如果我们只是释放锁定,这种情况下可以利用shared_ptr中可以指定“删除器”
//的一个函数或函数对象,当引用计数为0时,调用此函数,这个参数对它的构造函数是可有可无的第二个参数
class Lock
{
public:
explicit Lock(Mutex* pm) : mutexPtr(pm, unlock) //unlock即为指定的删除器
{
lock(mutexPtr.get());
}
private:
std::tr1::shared_ptr<Mutex> mutexPtr; //用shared_ptr管理这个对象,并且指定删除器,自定义行为
};
//而且以上不再需要析够函数,因为析构函数会在引用计数为0时自动调用shared_ptr的删除器
//情况3:复制底部资源,进行深度拷贝,例如string类,
//情况4:转移底部所有权,如auto_ptr的行为,资源所有权从被复制物转移到目标物
[条款15:在资源管理类张提供对原始资源的访问]
请牢记:
1、APIs往往要求访问原始资源,所以每一个RAII class应该提供一个“取得其所管理之资源”的办法。
2、对原始资源的访问可能经由显式转换或隐式转换。一般而言显式转换比较安全,但隐式转换对客户比较方便。
//资源管理类中提供对原始资源的访问
class Font
{
public:
explicit Font(FontHandle fh) : f(fh)
{}
//可以提供两种访问原始资源的方式:
//1 显示转换函数,优点是安全,缺点是每次调用都需要访问该函数
FontHandle get() const
{
return f;
}
//2 隐式转换,重载转换操作符,优点不需显示调用,会隐式执行,缺点:容易出错
operator FontHandle() const
{
return f;
}
~Font()
{
releaseFont(f);
}
private:
FontHandle f; //管理的原始字体资源
};
Font f1(getFont());
FontHandle f2 = f1; //本来是想拷贝Font对象,却隐式转换f1为FontHandle,然后进行了复制
[条款16:成对使用new和delete时要使用相同的形式]
请牢记:
如果在new表达式中使用[],必须在相应的delete表达式中也使用[]。 new[] 对应 delete[]
如歌在new表达式中不适用[],一定不要在相应的delete表达式中使用[]。 new 对应 delete
当使用new时(即通过new动态生成一个对象),有两件事发生:第一,内存被分配出来(通过名为operator new 的函数);第二,针对此内存会有一个(或更多)构造函数被调用。
当使用delete时,也有两件事发生:针对此内存会有一个(或更多)析构函数被调用,然后内存才被释放(通过名为operator delete 的函数)。delete的最大问题在于:即将被删除的内存之内有究竟存有多少对象?(即将被删除的那个指针,所指的是单一对象或对象数组?)此问题的答案决定了又多少个析构函数必须被调用起来。
单一对象的内存布局不同于对象数组的内存布局:数组所用的内存包括“数组大小”记录,以便delete制定需要调用多少次析构函数。单一对象的内存则没有这笔记录。
delete[]认定指针指向一个数组,多次调用析构函数。因此切记 new和delete时要采取相同形式。
typedef std::string AddressLines[4]; //定义了一个AddressLines类型,执行string [4]
std::string* pal = new AddressLines; //这里分配的是数组,相当于new string[4]
delete pal; //错误,但是可能会发生
delete [] pal; //正确的形式,但与以上new不对称
[条款17:以独立语句将newed对象置入智能指针]
请牢记:
以独立语句将newed对象存储于(置入)智能指针内。如果不这样做,一旦异常被跑出来,有可能导致难以察觉的资源泄露。
请牢记:
以独立语句将newed对象存储于(置入)智能指针内。如果不这样做,一旦异常被跑出来,有可能导致难以察觉的资源泄露。
假设有个函数用来处理程序的优先权,另一个函数用来在某动态分配所得的Widget上进行某些带有优先权的处理:
int priority(); //处理程序优先权的函数<br>
void processWidget(std::tr1::shared_ptr<Widget> pw, int priority);//该函数在动态分配所得的Widget上进行某些带有优先权的处理。
调用:
processWidget(new Widget, priority()); //编译不过!该构造函数是explicit 无法隐式转换为shared_ptr
因此可以写成:
processWidget(std::tr1::shared_ptr<Widget>(new Widget), priority()); //可以编译通过,但是...可能泄露资源。
后果:一旦发生异常,可能资源泄露
原因:
在调用processWidget之前,编译器必须创建代码,执行三步:
(1)调用prority()
(2)执行"new Widget"
(3)调用 tr1"shared_ptr构造函数
但是c++调用顺序跟java和c#不同,不是以特定顺序完成。priority函数的调用有可能在第一、第二或者第三执行。当在第二位执行的情况下:
(1)执行"new Widget"
(2)调用prority()
(3)调用 tr1"shared_ptr构造函数
若调用prority时发生异常,则"new Widget"返回的指针将会遗失,这样会引发资源泄露。
解决方案:使用分离语句,分别写出(1)创建Widget,(2)将它置入一个智能指针内,然后再把那个智能指针传给processWidget:
std::tr1::shared_ptr<Widget> pw(new Widget); //在单独语句内以智能指针存储newed所得对象<br>
processWidget(pw, priority()); // 这个动作不会造成泄露
//以独立语句将newed对象置入智能指针
//有以下函数
int priority();
void processWidget(std::tr1::shared_ptr<Widget> pw, int priority);
//以下调用形式,不会通过编译,因为tr1::shared_ptr的构造函数是explicit函数
//虽然它接受一个widget的指针,但是不能进行隐式转换,
processWidget(new Widget, priority());
//所以如果使用以下强制转化,可以通过,但这可能有隐式的资源泄漏,在new与调用shared_ptr构造函数之间
//执行priority,如果它发生异常,则资源泄漏
processWidget(std::tr1::shared_ptr<Widget>(new Widget), priority());
//解决方案:
std::tr1::shared_ptr<Widget> pw(new Widget);
processWidget(pw, priority());
因为编译器对于”跨越语句的各项操作“没有重新排列的自由(只有在语句内编译器才拥有那个自由度)。
[33:避免掩盖继承而来的名称]
//避免掩盖继承而来的名称
//如以下类
class Base
{
private:
int x;
public:
virtual void mf1()=0;
virtual void mf1(int);
virtual void mf2();
void mf3();
void mf3(double);
};
class Derived : public Base
{
public:
virtual void mf1(); //这里会将基类的mf1,mf1(int)都隐藏掉,遮盖了名称
void mf3(); //这里也如mf1一样,将基类的两个mf3遮盖
void mf4();
};
Derived d;
int x;
d.mf1(); //Derived::mf1
d.mf1(x); //错误,因为继承类掩盖了基类的mf1(int)
d.mf2(); //Derived::mf2
d.mf3(); //Derived::mf3
d.mf3(x); //错误,掩盖了基类的mf3(int)
//为了避免名称被掩盖,1 public继承下可以使用using声明式
class Base
{
private:
int x;
public:
virtual void mf1()=0;
virtual void mf1(int);
virtual void mf2();
void mf3();
void mf3(double);
};
class Derived : public Base
{
public:
using Base::mf1; //让base类名为mf1和mf3的所有东西(函数了变量了)
using Base::mf3; //在Derived作用域内都可见(并且是public)
virtual void mf1();
void mf3();
void mf4();
};
d.mf1(); //Derived::mf1
d.mf1(x); //没问题,Base::mf1(int)
d.mf2(); //Derived::mf2
d.mf3(); //Derived::mf3
d.mf3(x); //没问题,Base::mf3(int)
//为了避免名称被掩盖,2 使用转交函数继承一个函数
class Base
{
private:
int x;
public:
virtual void mf1()=0;
virtual void mf1(int);
...
};
class Derived : private Base
{
public:
virtual void mf1()
{
Base::mf1(); //转交函数,暗自成为inline
}
};
d.mf1(); //Derived::mf1
d.mf1(x); //错误,名称被掩盖
[34:非纯虚函数的缺省实现提供]
//非纯虚函数的缺省实现提供
class Airport {...};
class Airplane
{
public:
virtual void fly(const Airport& destination);
...
};
void Airplane::fly(const Airport& destination)
{
缺省代码,将飞机飞向指定的目的地
}
class ModelA : public Airplane {...};
class ModelB : public Airplane {...};
//以上继承体系,A,B中fly的相同行为可以写入基类的fly,如果两者飞行方式相同,则不需重新定义fly,可以直接继承基类的实现。
//如果有区别可以调用基类的,不同的行为可以再在这个函数中写出来
//如果新增一个飞机C,且飞行方式不一样,为了防止忘记重新定义fly函数,即
//基类实现“提供缺省实现给derived classes”,但除非它们明确提出才能调用缺省实现,一种方法是切断“virtual”函数接口和“实现接口”之间的链接,如下
class Ariplane
{
public:
//声明为纯虚的保证子类必须被重写
virtual void fly(const Airplane& destination=0)=0;
...
protected: //定义为protected是因为只有继承类需要此函数,外部不应调用,也不关心
//将默认行为移至另外一个函数,使得子类必须显示调用该函数才能继承默认行为
void defaultFly(const Airport& destination);
};
void Airplane::defaultFly(const Airport& destination)
{
提供缺省行为
}
class ModelA : public Airplane
{
public:
virtual void fly(const Airport& destination)
{
defaultFly(destination);
}
};
class ModelB : public Airplane
{
public:
virtual void fly(const Airport& destination)
{
defaultFly(destination);
}
};
//这样新增的moduleC不可能意外继承fly的实现版本了,必须自己提供,且如果不继承
//默认行为则要自己写出来
class ModelC : public Airplane
{
public:
virtual void fly(const Airport& destination)
{
//不继承默认行为,自己实现
}
};
//但以上方法有人认为不应该以不同的方法分别提供接口和实现继承,则可以利用
//纯虚函数必须在derived classes中重新声明,但他们也可拥有自己的实现这个事实
//给基类的纯虚fly函数提供一个缺省实现
class Ariplane
{
public:
//声明为纯虚的保证子类必须被重写
virtual void fly(const Airplane& destination=0)=0;
...
};
void Airplane::fly(const Airport& destination)
{
提供缺省行为
}
class ModelA : public Airplane
{
public:
virtual void fly(const Airport& destination)
{
Airplane::fly(destination);
}
};
class ModelB : public Airplane
{
public:
virtual void fly(const Airport& destination)
{
Airplane::fly(destination);
}
};
//这样新增的moduleC不可能意外继承fly的实现版本了,必须自己提供,且如果不继承
//默认行为则要自己写出来
class ModelC : public Airplane
{
public:
virtual void fly(const Airport& destination)
{
//不继承默认行为,自己实现
}
};
[35:virtual函数替代方案,例子]
//virtual函数替代方案,例子
//游戏人物的基类,会有不同的继承类
class GameCharacter
{
public:
//返回人物健康指数,不是纯虚,说明是有一个计算健康指数的缺省算法
virtual int healthValue() const;
};
//方案1:NVI手法
class GameCharacter
{
public:
int healthValue() const
{
//可以做一些事前工作,如包括锁定互斥器,制造运转日志记录,验证classu
//约束条件,验证函数先决条件等等
...
int retVal = doHealthValue();
...
//调用结束后还可以做一些事后工作,包括互斥器接触锁定,验证函数的事后
//条件,再次验证类约束条件
return retVal;
}
private:
virtual int doHealthValue() const //继承类可以重定义它
{
...//缺省算法,计算健康指数
}
};
//2 成员函数指针实现stategy模式
class GameCharactor; //前置声明
//计算健康指数的缺省算法
int defaultHealthCalc(const GameCharacter& gc);
class GameCharacter
{
public:
//定义函数类型
typedef int (*HealthCalcFunc)(const GameCharacter&);
explicit GameCharacter(HealthCalcFunc hcf=defaultHealthCalc)
: HealthFunc(hcf)
{}
int healthValue() const
{
return healthFunc(*this);
}
private:
HealthCalcFunc heathFunc;
};
//GameCharacter可提供一个成员函数setHealthCalculator,
//用来替换当前的健康指数计算函数,则函数可以在运行时变更
//同一人物类型的不同实体可以有不同的健康计算函数
class EvilBadGuy : public GameCharacter
{
public:
explicit EvilBadGuy(HealthCalcFunc hcf=defaultHealthCalc)
: GameCharacter(hcf)
{...}
};
//不同的健康指数计算函数
int loseHealthQuickly(const GameCharacter&);
int loseHealthSlowly(const GameCharacter&);
//相同类型的人物搭配不同的健康计算方式
EvilBadGuy ebg1(loseHealthQuickly);
EvilBadGuy ebg2(loseHealthSlowly);
//方案3:tr1::function函数实现strategy模式
class GameCharactor; //前置声明
//计算健康指数的缺省算法
int defaultHealthCalc(const GameCharacter& gc);
class GameCharacter
{
public:
//代表函数是接受一个reference指向const GameCharacter,并返回int,而且这个
//函数对象可以持有任何与此前面式兼容的可调用物,即参数可被隐式转换为
//const GameCharacter&,返回值可被隐式转换为int,这个也是与方案2类的唯一
//区别,不是持有一个指针,而是持有一个tr1::function对象,相当于指向函
//数的泛化指针
typedef std::tr1::function<int (const GameCharacter&)> HealthCalcFunc;
explicit GameCharacter(HealthCalcFunc hcf=defaultHealthCalc)
: HealthFunc(hcf)
{}
int healthValue() const
{
return healthFunc(*this);
}
private:
HealthCalcFunc heathFunc;
};
//健康计算函数:返回是是non-int,但是可以隐式转换为int,可调用
short calcHealth(const GameCharacter&);
//函数对象
struct HealthCalculator
{
int operator() (const GameCharacter&) const
{...}
};
class GameLevel
{
public:
//成员函数,用于计算健康值,non-int返回类型
float health(const GameCharacter&) const;
};
//不同的人物类型,构造函数相同
class EvilBadGuy : public GameCharacter
{};
class EyeCanyCharacter : public GameCharacter
{};
EvilBadGuy ebg1(calHealth); //人物1,使用函数计算
EyeCandyCharacter ecc1(HealthCalculator()); //人物2,使用函数对象计算
//使用类的成员函数计算
GameLevel currentLevel;
EvilBadGuy ebg2(str::tr1::bind(&GameLevel::health, currentLevel, _1));
//方案4:古典的Strategy模式
class GameCharacter;
class HealthCalcGunc
{
public:
...
virtual int calc(const GameCharacter& gc) const
{}
};
class SlowHealthLoser : public HealthCalcFunc
{};
class FastHealthLoser : public HealthCalcFunc
{};
HealthCalcFunc defaultHealthCalc;
class GameCharacter
{
public:
explicit GameCharacter(HealthCalcFunc* phcf = &defaultHealthCalc)
: pHealthCalc(phcf)
{}
int healthValue() const
{
return pHealthCalc->calc(*this);
}
private:
HealthCalcFunc* pHealthCalc;
};
//不同的人物类型,构造函数相同
class EvilBadGuy : public GameCharacter
{};
class EyeCanyCharacter : public GameCharacter
{};
[37:不要重写定义继承而来的缺省参数值]
//不要重写定义继承而来的缺省参数值
//以下有缺省参数值调用时可不指定参数
class Shape
{
public:
enum ShapeColor {Red, Green, Blue};
virtual void draw(ShapeColor color = Red) const = 0;
};
class Rectangle : public Shape
{
public:
//这里赋予了不同的参数值,很糟糕,虽可以不带参数调用,却会造成不同
virtual void draw(ShapeColor color = Green) const;
};
class Circle : public Shape
{
public:
//客户端如果调用下面这个函数,有两种情况:1 以对象调用,需指名参数值,
//因为对象调用是静态绑定并不会从基类继承缺省参数值 2 以指针或reference
//调用,可以不指定参数值,因为动态绑定下这个函数会从其基类继承缺省参数值
virtual void draw(ShapeColor color) const;
};
//以下静态类型都是Shape*
Shape* ps; //动态:未确定,未指向任何对象
Shape* pc = new Circle; //动态类型是Circle,
Shape* pr = new Rectangle;//动态类型是Rectangle
//动态类型在程序执行过程中可以改变
ps = pc;
ps = pr;
pc->draw(Shape::Red) //Circle::Draw(Shape::Red)
pr->draw(Shape::Red) //Rectange::Draw(Shape::Red)
//因为Reactangle的draw有缺省参数值,所以这个调用正确,但是因为pr的静态值是
//Shape*,而缺省参数是静态调用的,所以这个的默认参数不是Rectangle中的Green,
//而是Shape中draw的Red,结果造成这个函数基类与继承类各出一半力,奇怪的行为
pr->draw();
//NVI手法实现缺省参数值,防止虚函数这种表现出异常的行为
class Shape
{
public:
enum ShapeColor {Red, Green, Blue};
//非虚调用一个虚函数完成人物,提供缺省参数
void draw(ShapeColor color = Red) const = 0
{
doDraw(color);
}
private:
//继承类需提供实现
virtual void doDraw(ShapeColor color) const = 0;
};
class Rectangle : public Shape
{
public:
//这里不指定缺省参数值
virtual void doDraw(ShapeColor color) const;
};
[39:明智而审慎的使用private继承]
//明智而审慎的使用private继承
class Timer
{
public:
explicit Timer(int tickFrequency);
//定时器每滴答一次,函数就自动被调用
virtual void onTick() const;
};
//Widget不是Timer,Timer只是帮助实现它,所以私有继承,函数onTick在Widget变成
//私有的,且把它放在私有域中,如果public则有可能调用阿
class Widget : private Timer
{
private:
virtual void onTick() const; //查看Widget的数据等等,
};
//使用复合实现以上方法
class Widget
{
private:
//使用一个私有的嵌套类,而这个类共有继承Timer,并实现onTick方法
class WidgetTimer : public Timer
{
public:
virtual void onTick() const;
};
WidgetTimer timer; //在包含一个嵌套类对象,则可以调用了呀
};
//空类例子
class Empty {}; //C++插入一个char成为一个字节的大小
//以下这个类不再是一个int大小,还包括empty的1字节,如果还有对齐,那就会有
//3个padding了,多一个int了,sizeof(HoldAnInt) > sizeof(int)
class HoldsAnInt
{
private:
int x;
Empty e;
};
//如果继承一个空类,则有空白基类最优化的情况发生EBO
//以下empty不占空间,sizeof(HoldAnInt) == sizeof(int)
class HoldsAnInt : private Empty
{
private:
int x;
};
[42:嵌套从属类型]
//typename的双重意义
//嵌套从属类型
template <typename C>
print2nd(const C& container)
{
//以下是一个嵌套从属名称,因为C是模板参数,并且const_iterator被嵌套于
//这个C中,所以下面需要使用typename,表明它是一个类型
typename C::const_iterator* x;
}
//typename只被用来验明嵌套从属类型名称,其他名称不该有它存在
template <typename C> //允许使用typename与class,此中情况下等同
void f(const C& container, //正常类型,不允许使用typename
typename C::iterator iter); //嵌套类型,需要使用typename
//特殊情况,typename不可以出现在基类列表内的嵌套从属类型名称之前,也不可在
//成员初始化列表中作为base class修饰符
template <typename T>
class Derived : public Base<T>::Nested //基类列表中不允许
{
public:
explicit Derived(int x)
: Base<T>::Nested(x) //成员初始化列表中不允许
{
typename Base<T>::Nested temp; //嵌套从属类型加typename
}
};
//STL中typename
template <typename IterT>
void workWithIterator(IterT iter)
{
//为嵌套从属类型使用一个类型声明符,value_type就代表这个类型
typedef typename std::iterator_traits<IterT>::value_type value_type;
value_type temp(*iter);
}
[49:了解new-handler的行为]
//了解new-handler的行为
//set_new_handler函数
namespace std {
typedef void (*new_handler)();
new_handler set_new_handler(new_handler p) throw(); //异常声明,表示该函数不抛出任何异常
}
//set_new_handler用法
//以下是operator new无法分配内存时,该被调用的函数
void outOfMem()
{
std::cerr << "Unable to satisfy request for memory\n";
std::abort();
}
int main()
{
std::set_new_handler(outOfMem); //安装内存处理函数
int *pBigDataArray = new int [100000000L]; //当无法分配时,调用outOfMem;
...
}
//提供类专属之new-handler,
class Widget
{
public:
//客户端需要提供一个new-handler函数专门是该类使用的,
static std::new_handler set_new_handler(std::new_handler p) throw();
static void* operator new(std::size_t size) throw(std::bad_alloc); //只抛出bad_alloc异常
private:
static std::new_handler currentHandler; //指向当前类使用的new-handler,保存它是为了恢复
};
std::new_handler Widget::currentHandler = 0; //初始化0
//客户端需要提供一个new-handler函数专门是该类使用的,该函数获得这个指针并存储起来,然后返回先前存储的指针。
//这也正式标准的set_new_handler的行为
static std::new_handler Widget::set_new_handler(std::new_handler p) throw();
{
std::new_handler oldHandler = currentHandler;
currentHandler = p;
return oldHandler;
}
//来管理Widget的new-handler,保证可以正常恢复原来的handler
class NewHandlerHolder
{
public:
explicit NewHandlerHolder(std::new_handler nh) //取得目前的new-handler
: handler(nh)
{}
~NewHandlerHolder()
{
std::set_new_handler(handler); //释放它,其实就是还原了handler
}
private:
std::new_handler handler; //记录下来
NewHandlerHolder(const NewHandlerHolder&); //阻止拷贝
NewHandlerHolder& operator=(const NewHandlerHolder&);
};
//实现了以上类,则就可以实现Widget's operator new,实现很简单
void *Widget::operator new(std::size_t size) throw(std::bad_alloc)
{
//安装Widget的new-handler,并且保存原来的handler,如果第一次调用,原来的是null
NewHandlerHolder h(std::set_new_handler(currentHandler));
return ::operator new(size); //分配内存或抛出异常,回复global new-handler(调用NewHandlerHolder的析构函数恢复)
}
//客户端可能的调用:
void outOfMem(); //为类声明的handler
Widget::set_new_handler(outOfMem); //设置outOfMem为Widget内存分配失败时调用的函数
Widget *pw1 = new Widget; //如果内存分配失败,调用outOfMem
std::string* ps = new std::string; //如果内存分配失败,调用global new-handler,如果有的话
Widget::set_new_handler(0); //设置Widget专属的为NULL
Wdiget* pw2 = new Widget; //如果内存分配失败,立刻抛出异常
//适用于每个类的方案
template <typename T>
class NewHandlerSupport
{
public:
static std::new_handler set_new_handler(std::new_handler p) throw();
static void* operator new(std::size_t size) throw(std::bad_alloc);
private:
static std::new_handler currentHandler;
};
template <typename T>
std::new_handler
NewHandlerSuppport<T>::set_new_handler(std::new_handler p) throw()
{
std::new_handler oldHandler = currentHandler;
currentHandler = p;
return oldHandler;
}
template <typename T>
void* NewHandlerSupport<T>::operator new(std::size_t size) throw(std::bad_alloc)
{
NewHandlerHolder h(std::set_new_handler(currentHandler));
return ::operator new(size);
}
//以下将每一个currentHandler初始化为NULL
template <typename T>
std::new_handler NewHandlerSupport<T>::currentHandler = 0;
//只要Widget继承自NewHandlerSupprt<Widget>就可以实现为该类提供一个专属的handler的功能了
class Widget : public NewHandlerSupport<Widget>
{
...
};
//nothrow new
class Widget {...};
Widget* pw1 = new Widget; //正常的,分配失败返回bad_alloc
if (pw1 == 0) ... //测试失败
Widget* pw2 = new (std::nothrow) Widget; // 如果分配失败,返回0
if (pw2 == 0) ... //测试可能成功.
[50:定制型operator new]
//定制型operator new
static const int signature = 0xDEADBEEF;
typedef unsigned char Byte;
void* operator new(std::size_t size) throw(std::bad_alloc)
{
using namespace std;
size_t realSize = size + 2 * sizeof(int); //增加大小,使其可以塞入两个signatures
void* pMem = malloc(realSize);
if (!pMem)
throw bad_alloc();
//将signature写入内存的最前段落与最后段落
*(static_cast<int*>(pMem)) = signature;
*(reinterpret_cast<int*>(static_cast<Byte*>(pMem) + realSize - sizeof(int))) = signature;
//返回指针,指向位于第一个signature之后的内存位置
return static_cast<Byte*>(pMem) + sizeof(int);
}
[51:编写new和delete时需固守常规]
//编写new和delete时需固守常规
void* operator new(std::size_t size) throw(std::bad_alloc)
{
using namespace std;
if (size == 0) //如果是0 byte, 则当成1byte
size = 1;
//循环尝试分配size bytes;
while (true)
{
尝试分配size bytes;
if (分配成功)
return (一个指针,指向分配得来的内存);
//分配失败,找出目前的new-handling函数,因为没有直接获得该值的函数,
//所以使现在的handler=null,利用其返回值返回以前的handler;
new_handler globalHandler = set_new_handler(0);
set_new_handler(globalHandler); //安装
//如果非空,则调用,失败则抛出异常
if (globalHandler)
(*globalHandler)();
else
throw std::bad_alloc();
}
}
class Base
{
public:
static void* operator new(std::size_t size) throw(std::bad_alloc);
static void* operator delete(void* rawMemory, std::size_t size) throw();
...
};
//假设类未声明operator new,则会继承基类的operator new
class Derived : public Base
{
...
};
Derived* p = new Derived; //调用基类的operator new,但基类专属的operator new并非用来对付上述这种大小的对象
//解决方案,operator new类版本
void* Base::operator new(std::size_t size) throw(sts::bad_alloc)
{
if (size != sizeof(Base)) //如果大小错误,则调用标准的operator new,这里省略了对0的判断,因为sizeof(类)不会返回0
return ::operator new(size);
...
}
//定制operator delete
void operator delete(void* rawMemory) throw()
{
if (rawMemory == 0) //如果被删除的是个null指针,则什么都不做
return;
以下归还rawMemory内存
}
//operator delete类版本
void Base::operator delete(void* rawMemory, std::size_t size) throw()
{
if (rawMemory == 0) //如果为null,什么都不做
return;
if (size != sizeof(Base)) //如果大小错误,调用标准版
{
::operator delete(rawMemory);
return;
}
现在,归还rawMemory所指内存
return;
}
[52:一般所指的placement new]
//一般所指的placement new
void* operator new(std::size_t, void* pMemory) throw();
//防止构造函数期间的内存泄漏
class Widget
{
public:
...
//一个placement new
static void* operator new(std::size_t size, std::ostream& logStream)
throw(std::bad_alloc);
//提供placement delete,防止operator new成功,构造函数失败时造成的内存泄漏
static void* operator delete(std::size_t size, std::ostream& logStream)
throw();
//operator new正常运行时调用的delete
static void* operator delete(void* pMemory, std::size_t size)
throw();
};
//以下语句如果引发Widget构造函数抛出异常,对应的placement delete会被调用,不会发生内存泄漏
Widget* pw = new (std::cerr) Widget;
//如果没有抛出异常,客户端有如下代码,则会调用正常的delete
delete pw;
//发生函数隐藏
class Base
{
public:
...
static void* operator new(std::size_t size, std::ostream& logStream)
throw (std::bad_alloc); //会掩盖正常的global形式
...
};
Base* pb = new Base; //错误!因为正常的operator new被掩盖
Base* pb = new (std::cerr) Base; //正确,调用Base的placement new
//继承类掩盖全局与基类的new
class Derived : public Base
{
public:
...
//重新声明正常形式的new
static void* operator new(std::size_t size) throw (std::bad_alloc);
...
};
Derived* pb = new (std::clog) Derived; //错误!因为被掩盖了
Derived* pb = new Derived; //正确,调用derived的new
//std中的operator new
void* operator new(std::size_t) throw(std::bad_alloc); //normal new
void* operator new(std::size_t, void*) throw(); //placement new
void* operator new(std::size_t, const std::nothrow_t&) throw(); //nothrow new
//解决名称掩盖
//提供一个基类内含所有的正常形式的new和delete,
class StandardNewDeleteForms
{
public:
//normal new/delete
static void* operator new(std::size_t size) throw(std::bad_alloc)
{
return ::operator new(size); //调用正常的new
}
static void* operator delete(void* pMemory) throw()
{
return ::operator delete(pMemory); //调用正常的delete
}
//placement new/delete
void* operator new(std::size_t size, void* ptr) throw()
{
return ::operator new(size, ptr);
}
void* operator delete(void* pMemory, void* ptr) throw()
{
return ::operator delete(pMemory, ptr);
}
//nothrow new/delete
void* operator new(std::size_t size, const std::nothrow_t& nt) throw()
{
return ::operator new(size, nt)
}
void* operator delete(void* pMemory, const std::nothrow_t& nt) throw()
{
return ::operator new(pMemroy);
}
};
//如果想以自定义形式扩充标准形式,则可利用继承机制及using声明式取得标准形式
class Widget : public StandardNewDeleteForms
{
public:
using StandardNewDeleteForms::operator new; //使这些形式可见
using StandardNewDeleteForms::operator delete;
//添加自定义的placement new/delete,这样就可以在这些函数中调用标准形式的了
static void* operator new(std::size_t size, std::ostream& logStream)
throw(std::bad_alloc);
static void* operator delete(std::size_t size, std::ostream& logStream)
throw();
};