本文的代码很重要
一、让自己习惯C++
条款1,2,3:尽量用const ,enum和inline 而不用#define(尽量用编译器而不用预处理)
条款4 确定对象被使用前已先被初始化
构造函数对成员变量的初始化是在进入构造函数内之前!如果在构造函数内(会使用pass by value),是赋值。前者比较好(效率更高)!
- 总是使用成员初始值列
ABEntry::ABEntry(const string& name,
const string& address,
const list<PhoneNumber>& phones)
:theName(name),
theAddress(address),
thePhones(phones),
numTimesConsulted(0)
{
}
- 对于non-local static对象的初始化次序(函数内的static对象称为local static对象,因为对函数而言是local)
static对象:global对象、定义域namspace作用域内的对象、在classes内、在函数内、以及在file作用域内被声明为static的对象。
第三条解释:C++对定义于不同的编译单元内的non-local static对象的初始化次序无明确定义,可能会造成使用一个未初始化的对象。可以利用小小的设计解决,将每一个non-local static对象搬到自己的专属函数内(该对象在此函数内声明为static),这些函数返回一个reference指向它所含的对象,然后用户调用这些函数,而不直接指涉这些对象,这是Singleleton模式的一个常见实现手法。
class FileSystem
{
public:
inline FileSystem& tfs()//绝佳内联函数
{
static FileSystem fs;//定义并初始化
return fs;//返回
}
};
理论基础:C++保证,函数内的local static对象会在该函数被调用期间,首次遇上该对象定义式时被初始化。
二、构造、析构、赋值运算
条款5 了解C++默认编写并调用哪些函数
编译器会为空类声明一个defaut构造函数,一个copy构造函数,一个copy assignment操作符,一个析构函数。这些函数都是public且inline。一般会自己创建默认构造函数,编译器便不会再创建。
默认构造函数可被调用,无实参或者每个实参均有默认值,前面可以加explict,可防止隐式类型转换(比如int到class)。
copy构造函数被用来以同型对象初始化自我对象,很重要定义一个对象如何passed by value。
copy assignment操作符被用来从另一个同型对象中拷贝其值到自我对象。
(注意Name n = name是copy构造函数;
Name n; Name name; n = name;是copy assignment操作符)
//copy构造函数被用来以同型对象初始化自我对象,很重要定义一个对象如何passed by value
bool hasName(Name n) {}
Name name;
if (hasName(name)) {}
//name被复制到n体内,有Name的copy构造函数完成。相当于Name n = name;
//pass by value意味着调用copy构造函数。
//所以通常用引用传递用户自定义类型。
//初始化成员列表而不是放到构造函数内也是因为这个原因。
条款6:若不想使用编译器自动生成的函数(copy构造函数和copy assignment操作符),就该明确拒绝
将相应的成员函数声明为private并且不予实现,阻止编译器生成和用户调用他们,不定义是因为成员函数和友元函数有可能还会调用他们。如果不慎被调用会获得一个连接错误。例子标准程序库中的ios_base、basic_ios和sentry。
改进:将连接期错误移至编译期间,能更早侦测出错误,通过定义一个基类,缺点可能会导致多重继承
class Uncopyable()
{
protected:
Uncopyable() {}//允许派生类对象构造和析构
~Uncopyable() {}
private:
Uncopyable(const Uncopyable&);//阻止copying
Uncopyable& operator=(const Uncopyable&);
};
class HomeForSale :private Uncopyable//不再声明copy构造函数或copy assigment操作符
{
};
条款7:为多态基类(polymorphic base classes)声明virtual析构函数
- polymorphic base classes应该声明一个virtual析构函数。如果class带有任何virtaul函数,他就应该拥有一个virtual析构函数
- Classes的设计目的如果不是作为base classes使用,或不是为了具备polymorphically,就不该声明virtual析构函数
条款8:析构函数遇到异常时,要暂停程序
如果客户需要对某个操作函数运行期间抛出的异常做出反应,那么class应该提供一个普通函数(而非在析构函数中)执行该操作。
//释放DBConnection类
class DBConn
{
private:
DBConnection db;
bool closed;
void close()
{
db.close(); //供客户使用的新函数
closed = true;
}
~DBConn()
{
if (!closed)
{
try
{
db.close();
}
catch (...)
{
//制作运转记录,记下对close的调用失败
}
}
}
};
条款9:绝不在构造和析构过程中调用virtual函数
class Transaction {//所有交易的base class
public:
Transaction();
virtual void logTransaction() const = 0;//因类型而异的日志记录(在构造函数中执行)
};
Transaction::Transaction()//基类构造函数实现
{
...
logTransaction();//最后动作是记录这笔交易
}
class BuyTransaction :public Transaction {
public:
virtual void logTransaction() const;
};
class SellTransaction :public Transaction {
public:
virtual void logTransaction() const;
};
BuyTransaction b;//会执行基类中的virtual函数
派生类对象内的基类成分先构造,之后构造自身成分;即1.先执行基类构造函数,2.后执行派生类构造函数
比如,如果你的派生类构造函数中使用了virtual函数,会先执行基类的构造函数以及里面的virtual函数,而不执行派生类本身的virtual函数,即这类调用从不下降至derived classes。并且执行基类的构造函数以及里面的virtual函数时,派生类构造函数还未被执行,有一些变量还未被初始化,所以C++不会让执行派生类中的virtual函数。
条款10:令operator=返回一个reference to *this
令赋值操作符assignment operator返回一个reference to *this(为了连锁赋值x = y = z = 15;)
条款11:在operator=中处理“自我赋值”
不同的变量指涉同一对象,可能会造成在停止使用资源之前意外释放了它。
class Bitmap {
};
class Widget {
private:
Bitmap* pb;
public:
Widget& operator=(const Widget& rhs);
};
Widget& Widget::operator=(const Widget& rhs)
{
delete pb;//rhs和*this有可能是同一个对象,会删除rhs
pb = new Bitmap(*rhs.pb);//rhs's bitmap的副本
return *this;
}
//改进
Widget& Widget::operator=(const Widget& rhs)
{
if(this == &rhs) return *this;//相同测试
delete pb;//rhs和*this有可能是同一个对象,会删除rhs
pb = new Bitmap(*rhs.pb);//rhs's bitmap的副本
return *this;
}
- 确保当对象自我赋值时operator=有良好行为。技术包括比较“来源对象”和“目标对象”的地址、精心周到的语句顺序、以及copy-and-swap
- 确定任何函数如果操作多个对象时,而其中多个对象是同一个对象时(例如多个指针指向同一块内容),其行为仍然正确。
条款12:Copy all parts of an object
- Copying函数应该确保复制"对象内的所有成员变量”及“所有base class成分”
- 不要尝试以某个copying函数实现另一个copying函数。应该将共同机能放进第三个函数,并由两个coping函数共同调用(减少重复性代码)。
note:如果你要为派生类编写copying函数,要小心的复制其base class成分(往往是private),所以无法直接访问,要调用相应的base class函数获取base成员。
三、资源管理(将使用过的资源交还给系统)
条款13:Use objects to manage resources
把资源放进类(对象)中,利用对象的析构函数自动调用释放资源;
对于资源在heap-based的资源
思想,类似于利用对象的析构(自动)来释放资源。管理对象(managing object)例如智能指针shared_ptr
class Investment {
...//投资类型,继承体系中的root class
};
Investment* creatInvestment();//factory函数,返回指针,调用者要删除它
//利用creatAndDeleteInves函数建立和删除
void creatAndDeleteInves()
{
Investment* pInv = creatInvestment();//调用factory函数
...;//一些操作
delete pInv;//释放pInv所指对象
}
存在的问题:在一些操作中,可能会提前return、抛出异常等没有执行到delete pInv,则会造成资源的泄露。
改进:利用auto_ptr智能指针,避免creatAndDeleteInves函数潜在的资源泄露可能性
void creatAndDeleteInves()
{
auto_ptr<Investment> pInv(creatAndDeleteInves());//调用factory函数
...;//一些操作,经由auto_ptr的析构函数自动删除pInv
}
note:为了预防auto_ptr同时指向一个对象时,被删除多次,auto_ptr有个性质:若通过copy构造函数或者copy assignment操作符复制它们,它们会变成null,而复制所得的指针获得资源的唯一拥有权。总结auto_ptr并不太好,改进版是shared_ptr,引用计数型智慧指针(reference-counting smart pointer,RCSP),持续追踪共有多少对象指向某笔资源,并在无人指向它时自动删除该资源。
void creatAndDeleteInves()
{
...;
shared_ptr<Investment> pInv1(creatAndDeleteInves());//调用factory函数
shared_ptr<Investment> pInv2(pInv1);//利用copy构造函数,pInv1和pInv2指向同一对象
pInv1 = pInv2;//copy assignment操作符,无任何改变
//pInv1和pInv2被销毁
}
KJHwq2RAII:资源获得时机便是初始化时机
- 获得资源后立刻放进管理对象(比如智能指针)
- 管理对象运用析构函数确保资源被释放
条款14:在资源管理类中小心coping行为
并非所有资源都是heap-based,例如c的互斥器对象Mutex, shared_ptr不适用,要建立自己的资源管理类,在构造函数内获取资源,在析构函数内释放资源:
- 复制RAII对象必须一并复制它所管理的资源,所以资源的copying行为决定RAII对象的copying行为
- 普遍的RAII class copying行为是:抑制copying、施行引用计数法(reference counting)
条款15:在资源管理类中提供对原始资源的访问
许多API直接指涉资源,所以除非你永远不用它们,否则都会绕过资源管理对象直接访问原始资源。假设使用tr1::shared_ptr管理对象。
std::tr1::shared_ptr<Investment> pInv(createInvestment());
函数daysHeld的声明是这样的:
int daysHeld(const Investment *pi);
下面这种调用方式,肯定是错误的:
int days = daysHeld(pInv); //错误
因为函数需要的是指针,你传递是一个tr1::shared_ptr<Investment>对象。所以你需要一个函数将RAII对象转换为所内含的原始资源。有两种方法:隐式转换和显示转换。
(1)显式转换
tr1::shared_ptr和auto_ptr都提供了一个成员函数get返回内部的原始指针,这是显式转换。
int days = daysHeld(pInv.get()); //好的,没有问题
(2)隐式转换
tr1::shared_ptr和auto_ptr都重载了操作符operator->和operator*,这样就允许隐式转换到原始指针。举例:假设Investment类有个成员函数bool isTaxFree() const;那么下面的调用是OK的:
bool taxable1 = !(pInv->isTaxFree()); //好的,没有问题
bool taxable2 = !((*pInv).isTaxFree()); //好的,没有问题
现在的问题是,需要原始指针的地方(例如,函数形参),如何以智能指针代替。解决方法是:提供一个隐式转换函数。下面举个字体类的例子:
FontHandle getFont(); //取得字体句柄
void releaseFont(FontHandle fh); //释放句柄
class Font
{
public:
explicit Font(FontHandle fh) : f(fh){}//只允许显式转换
~Font()
{
releaseFont(f);
}
private:
FontHandle f;
};
如果C API处理的是FontHandle而不是Font对象,当然你可以像tr1::shared_ptr和auto_ptr那样提供一个get()函数:
FontHandle get() const { return f; } //显示转换函数
这样是可以的,但客户还是觉得麻烦,这时候定义一个隐式转换函数是必须的。
class Font
{
public:
...
operator FontHandle() const { return f; }
...
};
注意:假设你已经知道了隐式转换函数的用法。例如:必须定义为成员函数,不允许转换为数组和函数类型等。
完成了以上工作,对于下面这个函数的调用是OK的:
void changeFontSize(FontHandle f, int newSize);
Font f(getFont());
int newFontSize;
changeFontSize(f, newFontSize); //好的,Font隐式转换为FontHandle了
隐式类型转换也增加了一种风险。例如有以下代码:
Font f1(getFont());
FontHandle f2 = f1; //将Font错写成FontHandle了,编译仍然通过。
f1被隐式转换为FontHandle,这时f1和f2共同管理某个资源,f1被销毁,字体释放,这时候你可以想象f2的状态(原谅我这个词我不会说),再销毁f2,必然会造成运行错误。通常提供一个显示转换get函数是比较好的,因为它可以避免非故意的类型转换的错误,这种错误估计会耗费你很长的调试时间(我遇到过的情况)。
- APIs往往要求访问原始资源(raw resources),所以每一个RAII Class应该提供一个“”取得其所管理之资源“”的办法
- 对原始资源的访问可能经由显式转换或隐式转换。一般而言显式转换比较安全,但隐式转换对客户比较方便。
条款16:成对使用new和delete时要采取相同形式
string* str1 = new string;//一个string对象
string* str2 = new string[100];//100个string对象组成的数组
delete str1;
delete [] str2
对于typedef更易出错,要考虑到底该以哪种形式delete,eg:
typedef string AddressLines[4];//四行,每行是一个string
//AddressLines代表一个string数组
string* pal = new AddressLines;//返回一个string,就像new string[4]
delete [] pal;//容易出错
尽量嫑对数组形式做typedef动作,用vector代替。
条款17:以独立语句将newed对象置入智能指针
- 以独立语句将newed对象存储于智能指针内,如果不这样做,一旦异常被抛出,有可能导致难以察觉的资源泄露
processWidget(std::trl::shared_ptr<Widget>(new Widget), priority());
当编译器:执行“”new Widget“” -->调用prioruty-->trl::shared_otr构造函数次序时,如果对priority函数调用异常,则new Widget返回的指针会遗失,因为它尚未被置入shared_ptr内。
解决:分离语句
std::trl::shared_ptr<Widget>(new Widget);
processWidget(pw, priority());
4、设计与声明
条款18:让接口易被使用,不易被误用
Investment* createInvestment();
//改为
std::trl::shared_ptr<Investmen> createInvestment();
实质上强迫客户将返回值存储于一个shared_ptr内,防止忘记删除底部Investment对象(当它不再被使用时)。
shared_ptr提供的某个构造函数接受两个实参:一个是被管理的指针,另一个是引用次数变为0时将被调用的“”删除器。
//0转为null,getRidOfInvestment为自定义的删除器
shared_ptr<Investment> pInv(static_cast<Investment*> 0, getRidOfInvestment);
- 接口一致性、与内置类型的行为兼容
- trl::shared_ptr支持定制型删除器,可防范DLL问题,可被用来自动解除互斥锁。
条款19:设计class犹如涉及type
- 新type的对象如何被创建和删除?
- 对象的初始化和赋值的区别?
- 新type的对象怎么passed by value?copy构造函数
- 新type的合法值?利用约束条件,成员函数(特别是构造函数、赋值操作符)必须进行错误检查工作
- 新type需要配合某个继承图系吗
- 新type需要什么转换?如果要让类型T1被隐式转化为T2,要在T1内写一个类型转换函数(operator T2)或者在T2内写一个non-explicit-one-argument(可被单一实参调用)的构造函数。但是如果你只允许explicit构造函数存在,就得写出专门负责执行转换的函数。(条款15有隐式和显式转换的范例)
- 新type需要什么样的操作符和函数?某些是member函数,某些不是,见条款23,24,26
- 什么样的标准函数应该被驳回,必须声明为private。比如copy函数
- 谁该取用新type的成员?帮助判断为public、protected、private。或者哪一个classes、functions应该是friends
- 什么是新type的“未声明接口”?他对效率、异常安全性以及资源运用提供何种保证?
- 新type有多么一般化?是否需要定义成class template
- 是定义新type还是派生类
条款20:Prefer pass-by-reference-to-const替换pass-by-value
效率更高,放置切割
class Window;
class WindowWithScrollBars;
void printNameAndDisplay(const Window& w)
{
cout << w.name();
w.display();
}
WindowWithScrollBars wwsb;
printNameAndDisplay(wwsb);//传进来的窗口是什么类型,w就表现出那种类型
例外:对于内置类型,以及STL的迭代器和函数对象,pass-by-value更恰当
条款21:Don't try to return a reference when you must return an object
绝不要返回pointer或reference指向一个local stack对象;
绝不要返回reference指向一个heap-allocated对象;不知道何时delete
绝不要返回pointer或reference指向一个local stack对象而有可能同时需要多个这样的对象;因为只保留local static对象的现值。条款4有一个在单线程中合理返回reference指向一个local static对象提供了一份设计实例。
条款22:将成员变量声明为private
protected并不比public更具封装性
考虑如果有一个public成员变量被取消,所有使用它的客户码都会被破坏,同样如果取消一个protected成员变量被取消,所有derived class都会被破坏,因此protected也缺乏封装性。
条款23:Prefer non-member non-friend functions to member functions
#include <iostream>
using namespace std;
//利用member函数
class WebBrowser {
public:
void clearCache();
void clearHistory();
void removeCookies();
void clearEverything();
};
//利用non-member函数
void clearBrowser(WebBrowser& wb)
{
wb.clearCache();
wb.clearHistory();
wb.removeCookies();
}
member函数可以看到更多的数据,而non-member函数只能看到member成员函数,所以non-member函数封装性更好。
可以将non-member functions 和 class 放在同一个命名空间内,不同的功能放在不同的头文件中,但是均在一个namespace内,客户可以轻松扩展这一组便利函数,这也是C++标准程序库的组织方式,比如std内的vector、algorithm、list等头文件,不同的功能实现,降低了编译依存性。例如浏览器相关的功能实现:
//头文件webbrowser.h,针对class WebBrowser自身
//以及WebBrowser核心机能
namespace WebBrowserStuff
{
class WebBrowser {....};
...;//核心机能,例如所有客户都需要的non-member函数
}
//头文件webbrowserbookmarks.h
namespace WebBrowserStuff {
...;//与书签相关的便利函数
}
//头文件webbrowsercookies.h
namespace WebBrowserStuff {
...;//与cookie相关的便利函数
}
条款24:Declare non-member functions when type conversions should apply to all parameters
member函数的反面是non-member函数,不是friend函数,尽量避免friend函数。
如果你需要为某个函数的所有参数(包括被this指针所指的那个隐喻参数)进行类型转换,那么这个函数必须是个non-member函数。eg:
#include <iostream>
using namespace std;
class Rational {
public:
Rational(int numberator = 0,
int denuminator = 1);//构造函数刻意不为explicit;允许int-to-Rational隐式转换
int numerator() const;//分子分母的访问函数
int denominator() const;
const Rational operator*(const Rational& rhs) const;
private:
int m_num;
int m_den;
};
const Rational operator*(const Rational& lhs,
const Rational& rhs)//non-member函数
{
return Rational(lhs.numerator() * rhs.numerator(),
lhs.denominator() * rhs.denominator());
}
Rational oneEighth(1, 8);//1/8
Rational oneHalf(1, 2);
Rational result = oneHalf * oneEighth;//good
result = result * oneEighth;//good
//如果利用member operator
result = oneHalf * 2;//good,等价于result = oneHalf.operator*(2),2发生了隐式转换
result = 2 * oneHalf;//错误,result = 2.operator*(oneHalf)
//如果利用non-member函数
result = oneHalf * 2;//good
result = 2 * oneHalf;//good
条款25:考虑写出一个不抛出异常的swap函数
涉及到default swap、member swaps、non-member swaps、std::swap特化版本、以及对swap的调用
swap不只是STL的一部分,还是异常安全性编程的脊柱(条款29)
标准程序库的swap:基于类型T的copy构造函数和copy assignment操作符
namespace std {
template<typename T>
void swap(T& a, T& b)
{
T temp(a);
a = b;
b = temp;
}
}
评价:设计三个对象的复制,效率低,当涉及到指针指向的对象交换时,只用交换指针即可,无需再对对象进行操作!
改进利用“pimpl手法”(pointer to implementation):
#include <iostream>
#include <vector>
using namespace std;
class WidgetImpl {//针对Widget数据而设计的,细节不重要
public:
private:
int a, b, c;
vector<double> v;
};
class Widget {
public:
Widget(const Widget& rhs);
Widget& operator=(const Widget& rhs)
{
*pImpl = *(rhs.pImpl);//operator的一般性实现见条款10,11,12
}
void swap(Widget& other)
{
using std::swap;
swap(pImpl, other.pImpl);//利用std的swap置换指针
}
private:
WidgetImpl* pImpl;//只交换指针
};
//默认的swap不只复制三个Widgets还有WidgetImpl对象,效率极低
//需要自己编写特化版本,利用non-member swap调用member swap,整个结果:
namespace WidgetStuff
{
....;//模板化的WidgetImpl等等
template<typename T>
class Widget {//同前,内含swap成员函数
};
template<typename T>//non-member swap函数,并不属于std命名空间
void swap(Widget<T>& a, Widget<T>& b)
{
a.swap(b);
}
}
//调用
template<typename T>
void doSomething(T& obj1, T& obj2)
{
using std::swap;//令std::swap在此函数内可用
...;
swap(obj1, obj2);//为T型对象调用最佳swap版本
}
- 当std::swap对你的类型效率不高时,提供一个swap成员函数,并确定这个函数不抛出异常
- 如果你提供一个member swap,也该提供一个non-member swap用来调用前者。对于classes(而非templates),也请特化std::swap
- 调用swap时应针对std::swap使用using声明式,然后调用swap并且不带任何“命名空间资格修饰”。
- 为“用户定义类型”进行std templates全特化是好的,但千万不要尝试在std内加入某些对std而言全新的东西。
五、实现
条款26:尽可能延后变量定000义式的出现时间
特别是有些变量在判断语句之前定义,在判断语句之后使用是不合理的,因为有可能在判断语句块中抛出异常,使你承受析构成本,并且直接在构造时指定初值比通过default构造函数构造出一个对象后对它赋值效率好。
尽量使用做法B,除非n很大,对效率要求很高。因为做法B更易理解和维护。
条款27:尽量少做转型(casts)动作
转型风格通常有三种形式:
- C风格:(T) expression
- 函数风格:T(expression)
- C++还提供四种新式转型(c++style casts)
const_cast<T>(expression)//将对象的常量性移除!唯一由此能力的C++style转型操作符
dynamic_cast<T>(expression)//决定某对象是否归属继承体系中的某个类型。
reinterpret_cast<T>(expression)//不可移植。
static_cast<T>(expression)//强迫隐式类型转换,non-const-->const;void*-->typed*;int-->double
新式优点:在代码中易被辨别出来;转型细化,易被编译器诊断错误
条款28:避免返回handles指向对象内部成分
- 避免返回handles(包括references、指针、迭代器)指向对象内部。遵守这个条款可增加封装性,帮助const成员函数的行为像个const,并将发生“虚吊号码牌”(dangling handles)的可能性降至最低,虚吊:指向一个不存在的对象。
条款29:为“异常安全”而努力是值得的(strive for exception-safe code)
一个函数如果说是“异常安全”的,必须同时满足以下两个条件:1.不泄漏任何资源;2.不允许破坏数据。 我们先通过两个反面的例子开始。
第一个是造成资源泄漏的例子。一个类Type,内含一个互斥锁成员 Mutex mutex,以及一个成员函数void Func()。假设Func函数的实现如下所示:
void Type::Func()
{
Lock(&mutex);
DoSomething();
UnLock(&mutex);
}
首先是获得互斥锁,中间是做该做的事,最后释放互斥锁。从功能上来讲很完整,没任何问题。但从异常安全角度来说,它却不满足条件。因为一旦DoSomething()函数内部导致异常,UnLock(&mutex)将不会被执行,于是互斥器将永远不会被释放了。换句话说即造成了资源泄漏。
再来看第二个造成数据破坏的例子。这个例子是我们很熟悉的重载 ‘=’ 操作符的成员函数。依然假设一个类Type,其中一个成员是一个指向一块资源(假设类型为T)的指针。这时候我们一般就需要来自定义复制构造函数和重载复制操作符以及析构函数了。(绝大多数情况下,这三个成员总是要么同时存在,要么都不用定义,因为编译器默认定义了,即C++中所谓的 ”Rule of 3" 规则。这里不作详细介绍)。这里我们只考虑重载复制操作符的问题,其部分代码假设如下:
class Type
{
public:
....
Type& operator = (const Type &t)
{
if(this == &t)
return *this;
else
{
delete m_t;
m_t = new T(t->m_t);
return *this;
}
}
....
private:
T *m_t;
};
首先来判断是否是自我复制,如果是,则直接返回自己。如果不是,则安全释放当前指向的资源,再创建一块与被复制的对象资源一模一样的资源并指向它,最后返回复制好的对象。同样,抛开异常安全来看,没问题。但是考虑到异常安全性时,一旦“new T(t->m_t)"时抛出异常,m_t将指向一块已被删除的资源,并没有真正指向一块与被复制的对象一样的资源。也就是说,原对象的数据遭到破坏。
C++中’异常安全函数”提供了三种安全等级:
1. 基本承诺:如果异常被抛出,对象内的任何成员仍然能保持有效状态,没有数据的破坏及资源泄漏。但对象的现实状态是不可估计的,即不一定是调用前的状态,但至少保证符合对象正常的要求。
2. 强烈保证:如果异常被抛出,对象的状态保持不变。即如果调用成功,则完全成功;如果调用失败,则对象依然是调用前的状态。
3. 不抛异常保证:函数承诺不会抛出任何异常。一般内置类型的所有操作都有不抛异常的保证。
如果一个函数不能提供上述保证之一,则不具备异常安全性。
现在我们来一个个解决上面两个问题。
对于资源泄漏问题,解决方法很容易,即用对象来管理资源。RAII技术之前介绍过,这里就不再赘述。我们在函数中不直接对互斥锁mutex进行操作,而是用到一个管理互斥锁的对象MutexLock ml。函数的新实现如下:
void Type::Func()
{
MutexLock ml(&mutex);
DoSomething();
}
对象ml初始化后,自动对mutex上锁,然后做该做的事。最后我们不用负责释放互斥锁,因为ml的析构函数自动为我们释放了。这样,即时DoSomething()中抛出异常,ml也总是要析构的,就不用担心互斥锁不被正常释放的问题了。
对于第二个问题(数据破坏问题),一个经典的策略叫“copy and swap"。原则很简单:即先对原对象做出一个副本(copy),在副本上做必要的修改。如果出现任何异常,原对象依然能保证不变。如果修改成功,则通过不抛出任何异常的swap函数将副本和原对象进行交换(swap)。函数的新实现如下:
Type& Type::operator = (const Type &t)
{
Type tmp(t);
swap(m_t,tmp->m_t);
return *this;
}
先创建一个被复制对象t的副本tmp,此时原对象尚未有任何修改,这样即使申请资源时有异常抛出,也不会影响到原对象。如果创建成功,则通过swap函数对临时对象的资源和原对象资源进行交换,标准库的swap函数承诺不抛出异常的,这样原对象将成功变成对象 t 的复制版本。对于这个函数,我们可以认为它是”强烈保证“异常安全的。
当然,提供强烈保证并不是总是能够实现的。一个函数能够提供的异常安全性等级,也取决于它的实现。考虑以下例子:
如果f1和f2都提供了”强烈保证“,则显然Func函数是具有”强烈保证“的安全等级。但是如果f1或f2中有一个不能提供,则Func函数将不再具备”强烈保证“等级,而是取决于f1和f2中安全等级最低的那个。
void Func()
{
f1();
f2();
}
总结:
为了让代码具有更好的异常安全性,首先是”用对象来管理资源“,以避免资源的泄漏。其次,在异常安全性等级上,应该尽可能地往更高的等级上来限制。通过 copy-and-swap 方法往往可以实现”强烈保证“。但是我们也应该知道,”强烈保证“并不是对所有的情况都可实现,这取决于你在实现中用到的函数。函数提供的异常安全性的最高等级只能是你实现中调用的各个函数中异常安全性等级最低的那个
条款30:透彻了解inlining的里里外外
不要只因为function templates出现在头文件,就将它们声明为inline。
条款31:将文件的编译依存关系降至最低
对于C++类而言,如果它的头文件变了,那么所有这个类的对象所在的文件都要重编,但如果它的实现文件(cpp文件)变了,而头文件没有变(对外的接口不变),那么所有这个类的对象所在的文件都不会因之而重编。
因此,避免大量依赖性编译的解决方案就是:
在头文件中用class声明外来类,用指针或引用代替变量的声明;在cpp文件中包含外来类的头文件。
// ComplexClass.h
#include “SimpleClass2.h”
class SimpleClass1;
class ComplexClass
{
SimpleClass1* xx;//编译的时候知道指针的大小,所以不包含SimpleClass1.h也可可以通过编译
SimpleClass2 xxx;
};
// ComplexClass.cpp
#include “SimpleClass1.h”
void ComplexClass::Fun()
{
SimpleClass1->FunMethod();
}
六、继承与面向对象设计
继承link:public、protected、private、virtual、non-virtual
成员函数:virtual、non-virtual、pure virtual
virtual函数:接口必须被继承,分为纯虚函数和虚函数。
non-virtual函数:接口和实现必须被继承
条款32:确定你的public继承是is-a关系
正方形是矩形,企鹅是鸟
但是长方形能单独修改高度,正方形不能;而且企鹅不会飞,鸟会飞;有问题
public继承主张:能够施行与base class对象身上的每件事情,也可以施加于derived class对象身上!
解决:好的接口可以防止无效的代码通过编译,采取“在编译期间拒绝企鹅飞行”,而不是“在运行期检测飞行错误”
条款33:避免遮掩继承而来的名称(Avoid hiding inherited names)
只要函数名称相同,派生类成员函数就会遮掩基类成员函数,可以使用using声明式达到继承重载函数的目标。
#include <cstring>
using namespace std;
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 class 内名为mf1和mf3的所有东西在Derived作用域内都可见(并且public)
using Base::mf3;
void mf3();
void mf4();
};
derived classes内的名称会遮掩base classes内的名称。在public继承下从来没有人希望如此;
为了让被遮掩的名称再见天日,可使用using 声明式或转交函数(forwarding functions)。
条款34:区分接口继承(inheritance of interface)和实现继承(inheritance of implementation)
对应三种成员函数:non-virtual、impure virtual、pure virtual。
有时想只继承成员函数的接口(pure virtual),有时又想继承声明和实现但是想override所继承的实现(impure virtual),有时又想继承声明和实现但是不允许覆写任何东西(non-virtual)。
条款35:考虑virtual函数以外的其他选择(consider alternatives to virtual functions)
存疑
template<typename T>
class Set :public list<T>//将list应用于Set,错误做法,对于list为真的做法对于Set并不完全正确
{//比如list中可以有重复数值
};
条款36:绝不重新定义继承而来的non-virtual函数
条款37:绝不重新定义继承而来的缺省参数值
绝对不要重新定义一个继承而来的缺省参数值,因为缺省参数值都是静态绑定,而virtual函数-----你唯一应该覆写的东西----却是动态绑定
条款38:通过复合(composition)塑模出has-a或“根据某物实现出”
Model "has-a" or "is-implemented-in-terms-of"through composition
对象分为应用域和实现域
应用域:相当于你塑造的世界中的某些事物,比如人、汽车、一张张视频画面等
实现域:纯粹是实现细节上的人工制品,比如缓冲区、互斥器、查找树等。
当复合发生在应用域内的对象之间,表现出has-a关系(有一个);发生在实现域内则是表现出is-implementd-in-terms-of的关系(根据某物实现出)。
note:根据某物实现出和is-a不易区分,容易用错。例如如果要实现自己的Set集合(Set对象其实是个list对象),默认的set每个元素需要三个指针,空间大速度快,当你需要空间小对速度没要求时需要自己的Set:
设计一:is-a(错误)
template<typename T>
class Set :public list<T>//将list应用于Set,错误做法,对于list为真的做法对于Set并不完全正确
{//比如list中可以有重复数值
};
设计二:根据某物实现出(正确方案)
template<typename T>
class Set
{
public:
bool member(const T& item) const;
void insert(const T& item);
void remove(const T& item);
size_t size() const;
private:
list<T> rep;//用来表述Set的数据
};
条款39:明智而审慎的使用private继承use private inheritance judiciously
存疑
- Private意味着is-implemented-in-terms-of(根据某物实现出)。它通常比复合(composition)的级别低。但是当derived class需要访问protected base class的成员,或需要重新定义继承而来的virtual函数时,这么设计是合理的。
- 和复合不同,private继承可以改造empty base 最优化。这对致力于“对象尺寸最小化”的程序开发者而言,可能很重要。
条款40:明智而审慎的使用多重继承(MI)
存疑
- 多重继承逼单一继承复杂,可能导致新的歧义性,以及对virtual继承的需要
- virtual继承会增加大小、速度、初始化(及赋值)复杂度等等成本。如果virtual base classes不带任何数据,将是最具实用价值的情况
- 多重继承的确有正当用途。其中一个情景涉及“public继承某个Interface class”和“private继承某个协助实现的class”的两相结合。
七、模板与泛型编程 Templates and Generic Programming
条款41:了解隐式接口和编译期多态
隐式接口:在编译期,根据某个对象支持什么操作来隐式判断是什么对象。
面向对象编程世界总是以显式接口(explicit interfaces)和运行期多态(runtime polymorphism)即动态绑定解决问题
在Template及泛型编程的世界,与面向对象有根本上的不同,隐式接口(implicit interfaces)和编译期多态(compile-time polymorphism)更重要,因为templates事先并不知道具体的类型!
运行期多态和编译期多态之间的差异:类似于“哪一个重载函数该被调用,编译期”和“哪一个virtual函数该被绑定,运行期”。
条款42:了解typename的双重意义
从属名称:template内出现的名称依赖于某个template参数;
嵌套从属名称:从属名称在class内呈嵌套状;有可能导致解析(parsing)困难。
一般性规则:任何时候当你想要在template中指涉一个嵌套从属类型名称,就必须在紧邻它的前一个位置放上关键字typename,
typename只被用来验明嵌套从属类型名称,其他名称不该有它存在,eg:
template<typename C>
void f(const C& container,//并非嵌套于任何“取决于template参数”的东西内,不允许使用typename
typename C::iterator iter);//嵌套从属名称,一定要使用“typename”
例外:typename不可以出现在base class list内的嵌套从属类型名称之前,也不可在member initialization list(成员初始值)中作为base class修饰符。eg:
template<typename T>
class Derived : public Base<T>::Nested//base class list中不允许"typename"
{
public:
explicit Derived(int x)
:Base<T>::Nested(x)//mem.init.list中不允许typename
{
typename Base<T>::Nested temp;//嵌套从属名称,需要typename
}
};
请记住:
请使用关键字typename标识嵌套从属类型名称;但不得在base class lists(基类列)或者member initialion(成员初始列)内作为base class 修饰符。
条款43:学习处理模板化基类内的名称
当我们从Object Oriented C++跨进了template C++继承就不像以前那般畅行无阻了。
可在derived class templates内通过this-->“”指涉base class templates内的名称,或藉由一个明白写出的“base class资格修饰符”完成
class CompanyA
{
public:
void sendCleartext(const string& msg);
void sendEncrypted(const string& msg);
};
class CompanyB
{
public:
void sendCleartext(const string& msg);
void sendEncrypted(const string& msg);
};
class CompanyA
{
public:
void sendCleartext(const string& msg);
void sendEncrypted(const string& msg);
};
class MsgInfo{};//保存信息,以备将来产生信息
template<typename Company>
class MsgSender {
void sendClear(const MsgInfo& info)
{
string msg;
...;//根据info产生信息
Company c;
c.sendCleartext(msg);
}
void sendSecret(const MsgInfo& info)
{
string msg;
...;//根据info产生信息
Company c;
c.sendEncrypted(msg);
}
};
template<typename Company>
class LoggingMsgSender :public MsgSender<Company>
{
public:
void sendClearMsg(const MsgInfo& info)
{
...;// 将传送前的信息写到log
sendClear(info);//调用基类的函数,无法通过编译
...;// 将传送后的信息写到log
}
};
//改变,方法1
template<typename Company>
class LoggingMsgSender :public MsgSender<Company>
{
public:
void sendClearMsg(const MsgInfo& info)
{
...;// 将传送前的信息写到log
this->sendClear(info);//成立,假设sendClear将被继承
...;// 将传送后的信息写到log
}
};
//方法二
template<typename Company>
class LoggingMsgSender :public MsgSender<Company>
{
public:
void sendClearMsg(const MsgInfo& info)
{
using MsgSender<Company>::sendClear;//告诉编译器,请他假设sendClear位于base class内
...;// 将传送前的信息写到log
sendClear(info);//成立,假设sendClear将被继承
...;// 将传送后的信息写到log
}
};
条款44:将与参数无关的代码抽离templates
在template代码中,重复是隐晦的,毕竟只存在一份template源码,所以必须感受template被具现化多次时可能发生的重复
类似于,编写某个class和另一个class的某些部分相同,把共同部分搬移到新的class1t去,然后使用继承和复合,令原先的classes取用这共同特性。
非类型参数及template代码膨胀举例:
template<typename T,
size_t n>//n为非类型参数
class SquareMatrix {
public:
void invert();//求逆矩阵
};
//具现化两份invert,除了常量5和10不同,其他均相同,造成代码膨胀
SquareMatrix<double,5> sm1;
sm1.invert();
SquareMatrix<double, 10> sm2;
sm2.invert();
template<typename T>
class SquareMatrixBase {//存储矩阵大小和一个指针指向矩阵数值
protected:
SquareMatrixBase(size_t n, T* pMem)
:size(n),pData(pMem){}
void setDataPtr(T* ptr) { pData = ptr; }//重新赋值给pData
private:
size_t n;
T* pData;
};
//允许派生类决定内存分配方式,此处用数组,无需动态分配内存,但对象自身可能非常大
template<typename T, size_t n>
class SquareMatrix :private SquareMatrixBase<T> {
public:
SquareMatrix()//矩阵大小和数据指针给基类
:SquareMatrixBase<T>(n,data){}
private:
T data[n*n];
};
//此处把没一个矩阵的数据放进heap
template<typename T, size_t n>
class SquareMatrix :private SquareMatrixBase<T> {
public:
SquareMatrix()
:SquareMatrixBase<T>(n, 0),
pData(new T[n*n])
{
this->setDataPtr(pData.get());
}
private:
boost::scoped_array<T> pData;//条款13
};
请记住:
■ Templates生成多个classes和多个函数,所以任何template代码都不该与某个造成膨胀的template参数产生相依关系.
■ 因非类型模板参数而造成的代码膨胀,往往可消除,做法是以函数参数或class成员变量替换template参数.
■ 因类型参数而造成的代码膨胀,往往可降低,做法是让带有完全相同二进制表述的具现类型共享实现码.
条款45:运用成员函数模板接受所有兼容类型
STL容器的迭代器几乎总是智能指针,无疑的你不会奢望使用++将一个内置指针从linked list的某个节点移到另一个结点,但这在list::iterators身上办得到。
- 请使用member function templates生成“可接受所有兼容类型”的函数
- 如果你声明member templates 用于“泛化copy构造”或“泛化assignment操作”,你还是需要声明正常的copy构造函数和copy assignment操作符。
条款46:需要类型转换时请为模板定义非成员函数
当我们编写一个class template,而它所提供之“与此template相关的”函数支持“所有参数之隐式类型转换”时,请将那些函数定义为“class template内部的friend函数”。
条款47:请使用traits classes表现类型信息
条款48:认识template元编程Be aware of template metaprogramming
TMP是编写template-based C++程序并执行于编译期的过程;即以C++写成、执行于C++编译器内的程序。
八、定制new和delete
条款49:了解new-handler的行为
- set_new_handler允许客户指定一个函数,在内存分配无法获得满足时被调用
条款50:了解new和delete的合理替换时机
有许多理由需要写个自定义的new和delete,包括改善性能、对heap运用错误进行调试、收集heap使用信息
条款51:编写new和delete时需固守常规
- operator new 应该内含一个无穷循环,并在其中尝试分配内存,如果无法满足内存需求,就该调用new-handle。它也应该有能力处理0bytes申请。class专属版本则还应该处理“比正确大小更大的(错误)申请”
- operator delete应该在收到null指针时不做任何事情。class专属版本则还应该处理“比正确大小更大的(错误)申请”
条款52:写了placement new也要写placement delete
九、杂项讨论
条款53:不要轻忽编译器的警告
条款54:让自己熟悉包括TR1(C++ Technical Report 1 )在内的标准程序库
条款55:让自己熟悉Boost