一直都没有系统地看过一次Effective C++,最近好好地完整地读了一遍,收获颇丰,把读书笔记分享给大家看看,与大家交流交流~
里面抄了书上不少的内容,也有一些个人的理解,可能有误,希望大家能及时指出~
1. 让自己习惯C++
条款01:视C++为一个语言联邦,C、OO C++,Template C++, STL
条款02:尽量以const, enum, inline替换#define
普通常量都用const
不想被获取到地址的常量用enum hack(对enum访问地址是不合法的)
宏都用inline,糟糕宏中的典型
#define
MAX(a, b) f((a) > (b) ? (a) : (b)) //当MAX(++a,
b)时就出问题了
|
条款03:尽可能用const
*前const是对象常量,*后const是指针常量
返回const,如
class R{...};
const R operator*(const R& lhs,
const R& rhs);
可避免
R a,b,c;
(a*b) = c;
//如果返回的不是const,编译是能通过的
这样的失误
两个成员函数只是常量性的不同(即函数定义前后有无const)是可以重载的
const成员函数需要改变成员赋值时,可用mutable
const与non-const成员函数若实现是等价的,令non-const成员函数调用const成员函数,方法const_cast<T2>(static_cast<const
T1&>(*this)...)
条款04:确定对象被使用前已先被初始化
赋值操作不如初始化操作
成员变量是const或者reference时必须要在初始化列表中初始化
non-local
static对象的初始化,主要指的是多文件的情况,想某头文件中的某变量在别的头文件或CPP中也被使用。最直观的方法是用extern。如下代码
class A {
public:
...
void print () const ;
...
};
extern A
a;
|
但这里的问题是我在别的文件调用a时,我不能保证a已经被初始化了(都怪糟糕的编译器),于是就有以下方法
class A {
public:
...
void print () const ;
...
};
A& a () {
static A
tmpa ;
return tmpa ;
}
|
要调用时只需要用a()就可以了,调用的是local变量的引用,而且保证有初始化。
2. 构造/析构/赋值运算
条款05:了解C++默默编写并调用哪些函数
类若用户没有定义会默认有copy构造函数、copy
assignment操作符、析构函数,这种copy就是二进制的copy。而如果有引用或者是const成员变量时,以上的函数若需要只能自己定义,特别是copy
assignment操作符,因为reference是不能指向不同对象的,而const也是不能被改变的。
条款06:若不想使用编译器自动生成的函数,就该明确拒绝
不想类拥有copy构造与copy
assignment操作符,可以把这些函数显式地定义在private中。另一个方案,就是定义一个uncopyable的类,这一个类就是把这些函数定义在private中,需要定义不能被copy的类时可以直接继承。
条款07:为多态基类声明virtual析构函数
这样做的原因是,用父类指针指向子类时,如果没有在父类定义的virtual析构,不能保证指向的子类能正确的被销毁,可能调用的还是父类的析构。
另外,virtual函数的定义会增加类本身的大小,因为实现virtual是靠维护vptr(virtual
table pointer)做到的,因此不是什么情况都用virtual。只在要实现多态的基类用virtual。
条款08:别让异常逃离析构函数
C++不喜欢析构函数吐出异常。原因是当C++遇到两个以上的异常同时存在时不是结束执行就是导致不明确行为。而出现两个以上的异常在容器成员要析构时要吐出异常的话可能就是一系列的异常,于是就出现了C++不允许的情况了。(个人理解,其实感觉还是没说清)
这里举的例子是一个负责数据库连接的类
class DBConnection {
public:
...
static DBConnection
create ();
void close ();
};
|
为了保证用户记得调用DBConnection对象中的close(),很自然的想法就是创建一个管理这个类的类,并在析构中调用close()。
class DBConn{
public:
...
~DBConn () {
db .close();
}
private:
DBConnection
db ;
};
|
但这里的问题是万一close()吐出异常怎么破?
方法一:直接abort()
~DBConn() {
try { db.close();}
catch (...) {
std ::abort();
}
}
|
方法二:吞下异常,当什么事儿也没发生
~ DBConn() {
try { db .close ();}
catch (...) {
}
}
|
class DBConn{
public:
...
void close () {
db .close();
closed = true ;
}
~DBConn () {
if (! closed) {
try { db.close();}
catch (...) {
...
}
}
}
private:
DBConnection
db ;
bool closed ;
};
|
从以上可知,要尽量把析构中可能出现的异常的部分转移到别的成员函数中,把异常扼杀在别的成员函数中,而不是析构函数中。
条款09:绝不在构造和析构过程中调用virtual函数
这里应该针对的是子类没有定义构造函数,直接继承父类构造函数的情况,原因是子类在继承父类的构造函数后其调用的顺序是先调用父类的,再调用子类的,然后这里的问题是,如果是先调用父类的话,子类中的成员变量是被编译器认为是没有定义的(包括virtual函数的定义),因此是调用不了的。同样的析构函数在被调用时,是先把子类的成员变量空间都释放了,然后再调用父类的,此时对于父类而言,子类中的定义都是不可知的(包括virtual函数的定义),因此也并没有调用理想中的函数。
直接在构造或者析构中放virtual是比较明显的错误,但是当在virtual函数之外再定义一个非virtual的函数,这样的错误就很隐蔽了,不可不防。
条款10:令operator=返回一个reference to *this。这是个约定,认了吧。
条款11:在operator=中处理“自我赋值”
问题代码是这样的
class Bitmap {};
class Widget {
...
Widget& Widget ::operator=(const Widget& rhs);
...
private:
Bitmap* pb ;
};
Widget& Widget ::operator=(const Widget& rhs) {
delete pb ;
pb = new Bitmap(*rhs.pb );
return *this;
}
|
方法一:证同测试
Widget& Widget ::operator=(const Widget& rhs) {
if (this == & rhs) return * this;
delete pb ;
pb = new Bitmap(*rhs.pb );
return *this;
}
|
这里就涉及到另一个问题,“异常安全”,如果pb在new的时候出异常了,pb指向的数据就没了,这时候数据就不完整了,异常无法处理。
方法二:调整顺序
Widget& Widget ::operator=(const Widget& rhs) {
Bitmap* pOrig = pb;
pb = new Bitmap(*rhs.pb );
delete pOrig ;
return *this;
}
|
这样可能不如方法一效率来得高,但至少安全。
方法三:copy and swap
Widget& Widget ::operator=(const Widget& rhs) {
Widget
temp (rhs);
swap(temp );
return *this;
}
|
条款12:复制对象时勿忘其每一个成分
这里容易出问题的是子类继承父类时,如果重新定义了构造函数,然后又没有考虑到父类的构造函数(因为父类构造函数可能会存在一些私有变量的初始化),这样的后果是,对象会调用父类的无参构造函数,如果无参构造函数是你想要的那还能接受,但是如果无参构造参数里面出了问题(比如说忘了给某私有变量赋值,那样就黑了)。
另外,copy赋值与copy构造如果代码相同,不应该互相调用,应该借助别的成员函数(如init())来完成。
3. 资源管理
条款13:以对象管理资源
给指针动态分配空间,容易因为过程中的某一步return或continue而错过了自以为会delete的代码。而通过对象来管理这样分配的空间会更合适,因为对象可以在退出代码块时通过析构来进行delete。于是就推荐使用RAII对象(Resource
Acquisition Is Initialization)。
常用的有std::auto_ptr和std::tr1::shared_ptr,前者保证使用对象的只有一个,因此复制会造成被复制指针变为NULL,保证对象只会引用一次,后者则是带计数的智能指针,比较常用。但两者都有问题,实现的是delete而不是delete
[]。实现这个可以用Boost里的boost::scoped_array与boost::shared_array。
条款14:在资源管理类中小心copying行为
这里主要是针对RAII对象,因为RAII对象是资源管理的脊柱(书是这么写的。)。设计好自己的RAII对象的COPYING行为,一般是禁止复制或者是引用计数。还有一些就是复制底部资源以及转移底部资源的拥有权(如auto_ptr)。
条款15:在资源管理类中提供对原始资源的访问
RAII对象把资源保护得好好的,但是有些函数却需要直接用到RAII对象所保护的对象,如auto_ptr里的指针是A* a,但是我函数f(A*
a)要接收的参数类型是A*,那样是不可能把auto_ptr传递给函数f的,这时候就需要一个获取原始资源的接口了。这种访问可能经由显式转换(即通过get()成员函数)或者是隐式转换(即定义operator
A*() const{return a;})。
显式转换比较安全,因为它将“非故意的类型转换”的可能性最小化了。但使用不太方便,因为看着太长太烦了。
隐式转换比较方便,但问题代码如下:
class my_ptr{
private:
A* pa ;
...
public:
...
operator A *() const {
return pa;
}
...
};
A* getA ();
my_ptr
ptr1(getA ());
...
A* p_tmp = ptr1 ;
|
这个操作的问题是,当ptr1被销毁后,p_tmp就会悬空(dangle),程序会产生core
dump。因此不太安全。
条款16:成对使用new和delete时要采取相同形式
这里主要强调的是new时用的[],delete别忘了[]。没有的当然就不用加了。因为数组的new是带数组长度的。
条款17:以独立语句将已new的对象置入智能指针
问题代码如下:
class A{};
int func();
void process(std::tr1 ::shared_ptr<A> pa , int );
|
当如下调用时
process (std :: tr1:: shared_ptr <A >( new A ), func()); |
这里的问题是不知道编译器的处理顺序(这个问题只能怪编译器,特别是各种不同标准的C++编译器。),理想顺序应该是先new
A,然后把它传给shared_ptr,然后再执行func(),最后再赋给process。或者是先执行func(),然后再new
A,然后blabla。这样都没有问题。
但,如果,赋给shared_ptr不是紧接着new A后,而是new A后,先执行func(),再传new
A的值给shared_ptr,那就可能会出问题,因为不保证func()不会出错。
改进代码:
std::tr1 ::shared_ptr<A> pa (new A );
process(pa , func());
|
4. 设计与声明
条款18:让接口容易被正确使用,不易被误用
一方面是促进正确使用,这里提到的主要是接口的一致性,以及与内置类型的行为兼容。
另一方面就是防止误用。提到的几个方法
a) 建立新类型:
先看
class Date {
public:
Date(int month, int day, int year);
...
};
|
这里要正确使用很容易,要误用也很容易,一个不小心把month与day的顺序搞错了,就出问题了。于是就有个方案是建立新类型Month, Day,
Year。对应的类就应该这么写
class Date {
public:
Date(const Month& month , const Day& day , const Year& year );
...
};
|
这样定义变量时就应该是类似
Date
d(Month (1), Day(2 ), Year( 2013));
|
明了而又不容易犯错,而且Month、Day、Year还可慢慢写,不断完善。
b) 限制类型上的操作。
这里的一个典型就是返回const,这样可以避免出现if(a*b=c)这种错误。
c) 束缚对象值。
这里说的例子还是日期,月份只有12个,别的都是错的,为了防止用户出错,我们可以限制它的值,于是Month类可以这么写:
class Month {
public:
static Month
Jan () { return Month( 1);}
static Month
Feb () { return Month( 2);}
...
static Month
Dec () { return Month( 12);}
...
private:
explicit Month (int m);
...
};
|
d) 消除客户的资源管理责任
这里作者强烈推荐使用Boost的tr1::shared_ptr,虽然笨重,但能有效避免错误的发生。
条款19:设计class犹如设计type
书中列的一系列注意问题,没细讲,都列出来吧
a) 新type的对象应该如何被创建和销毁
b) 对象的初始化和对象的赋值该有什么差别?
c) 新type的对象如果被passed by value,会怎样?
d) 什么是新type的“合法值”
e) 你的新type需要配合某个继承图系吗?
f) 你的新type需要什么样的转换?
g) 什么样的操作符和函数对此新type而言是合理的?
h) 什么样的标准函数应该驳回?
i) 谁该取用新type的成员
j) 什么是新type的“未声明接口”?
k) 你的新type有多么一般化?
l) 你真的需要一个新type吗?
条款20:宁以pass-by-reference-to-const替换pass-by-value
但是如果传递的是基础类型、STL的迭代器和函数对象,还是建议pass-by-value
条款21:必须返回对象时,别妄想返回其reference
就是注意返回的东西是在stack还是在heap,就算是返回static也是有可能出错的,因为reference引用的可能是同一个static变量,当进行==比较时就没法比了。
条款22:将成员变量声明为private
首先是代码的一致性(调用public成员时不用考虑是成员还是函数)。
其次封装性,都写成函数进行访问可以提供以后修改访问方法的可能性,而不影响使用方法。另外,public影响的是所有使用者,而protected影响的是所有继承者,都影响巨大,所以都不建议声明成员变量。
条款23:宁以non-member、non-friend替换member函数
书中展开讨论的是这个情况,当你有一个类这么写的:
class A {
public:
...
void func1 ();
void func2 ();
void func3 ();
...
};
|
然后因为有个经常使用的操作需要顺序的使用三个成员函数,所以就想写一个便利的函数。这里有个选择,是写成member函数,还是non-member
non-friend函数。也就是
class A {
public:
...
void func1 ();
void func2 ();
void func3 ();
void funcAll ();
...
};
|
与
void funcAllA(A& a ) {
a.func1 ();
a.func2 ();
a.func3 ();
}
|
之间的选择。作者的意思是后者好。
而这个作者也有一套关于封装性的解释,作者通过计算能够访问对象内数据的函数数量,大概计算封装性。原则就是越多函数能访问,封装性越低。
其实就我个人理解,这里特意强调non-member、non-friend其实要强调的就是这种函数是不能访问类内的private区的。
那么这条款的逻辑应该是可以这么认为的,当要写的函数并不需要直接调用private区的变量时,尽量写成non-member、non-friend函数。
当然C++是不会阻止大家把函数都写成member函数的,java/c#使用者不用担心。只是针对以上情况,C++的写法一般是写在namespace里面(其实我觉得是不是可以写成static的member函数呢?)
作者顺带谈了下namespace与class的区别
namespace是可以分好几个文件写的,不受约束,而且可扩充,当然它不是一个可能实例化的存在。
class的声明必须得一起写,要扩充也只能继承,但问题是,不是所有的类都设计用于继承的。不过class是一个可以实例化的,就是数据是有自己的私有空间的,可能带着周围跑的。
条款24:若所有参数皆需类型转换,请为此采用non-member函数
这里提出的例子是对Rational的operator*的实现应该是member函数还是non-member。
member函数时
result = oneHalf * 2 ; //OK
result = 2 * oneHalf; //错误
|
这里暗含一个条件就是构造函数是允许隐式类型转换(不带explicit),不允许的话两个就过不了。而如果用non-member就一点问题也没有了。
这个operator*的特点就是,两个变量其实都有类型转换的需要,如果是写成member函数,那么左操作符就不能进行类型转换了。而non-member函数就能满足这一需求了。
条款25:考虑写出一个不抛出异常的swap函数
首先要明白,swap函数用处很大,在之前的条款11中就用于处理自我赋值可能性上,而在条款29(往后看吧)将会说到与异常安全性编程相关的用法。总之很重要,同时很复杂。
std中的swap是基于copy构造与copy赋值的,典型实现如下
namespace std {
template<typename T>
void swap (T& a, T& b ) {
T
temp (a);
a = b;
b = temp;
}
}
|
但当自己定义的类,有更高效的swap方法时,如成员变量中的数据包含指针,copy赋值或copy构造时进行的操作是对指针指向的内容进行完全的拷贝(很合理),但是放时swap里面时就要进行这样多次的指针指向内容的拷贝,再进行交换,而事实上,更好的方法是直接进行指针的交换就可以,不需要通过copy赋值与构造。(常适用于pimpl手法实现的class,pimpl,pointer
to implementation)
因此这里就引进了特化的方法。这里要访问到类内数据(private),就得是member或者是friend了,而对于swap这么一个特殊的函数(可用于异常安全性编程,本人猜想),则倾向于先定义一个member的swap,然后再定义一个non-member的swap。STL内也是这么实现的,代码如下
class A {
public:
...
void swap (A& other) {
using std:: swap;
swap (pImpl, other.pImpl );
}
...
private:
AImpl* pImpl ; //实现类
};
namespace std {
template<> //全特化版本
void swap <A>( A& l, A & r) {
l .swap( r);
}
}
|
以上是针对A是class的情况,而如果A是class
template时就不一样了,对应的swap函数就是偏特化的,大概形式如下
namespace std {
template<typename T> //偏特化版本
void swap < A< T> >(A <T>& l, A<T >& r) {
l .swap( r);
}
}
|
但这样是错的,C++允许对class
template进行偏特化(即class定义前约束为template<typename T>),而不允许对class
template进行偏特化。解决方法是写一个swap的重载版本,如下
namespace std {
template<typename T>
void swap (A< T>& l, A <T>& r) {
l .swap( r);
}
}
|
到这里,问题应该说是算解决了,但std是个很特殊的命名空间,里面的东西你可以用,可以特化,但写这个std的人们是不想我们去改里面的东西,甚至是重载也不行(虽然是可以编译通过)。作为乖孩子,我们只有在自己的小空间里满足自己的小要求了,也就是把这个重载写在自己的命名空间里面。
namespace MyWorld {
template<typename T>
void swap (A< T>& l, A <T>& r) {
l .swap( r);
}
}
|
到最后,就是要注意最终使用时,编译器会调用哪一个的问题了,容易不清楚的是以下情况
template<typename T>
void doSomething(T& a , T& b) {
using std ::swap;
...
swap(a , b);
...
}
|
这里的关键在于using std::swap。
试想如果没有的话,可能就只能找到你自己的小空间里的swap函数了,然而T本身是不受约束的,你自己的命名空间内的swap是不够用的。
再想想,如果特定约束为std::swap(a,
b),则问题就是只能找到std内对应的swap版本,却找不到你自己定义的更高效的实现版本。
总的来说就是,使用前swap时,先using
std::swap,然后使用时swap不带命名空间约束。
另外,关于不抛出异常的swap问题只是针对member函数而言的,在条款29会讲。
此条款较长,需要总结下:
1. 当std::swap对自定义类型效率不高时,就提供一个swap成员函数,并确保不抛出异常。
2.
提供与swap的member函数相对应的non-member函数版本,对于class提供全特化版本std::swap,对于class
template提供非std空间的重载。
3. 使用前swap时,先using
std::swap,然后使用时swap不带命名空间约束。
5. 实现
条款26:尽可能延后变量定义式的出现时间
主要是延后到要用时才定义,延后到你愿意赋初值时才定义。
另外有个问题就是应该把变量定义在循环体内还是体外,这得看情况。
copy赋值开销低于构造+析构开销,且效率要求高,则建议定义在循环体外,否则定义在循环体内(主要考虑便于管理)。
条款27:尽量少做转型动作
C style转型:
1. (T)expression
2. T(expresstion)
C++ style转型:
1. const_cast,把const转到non-const
2. dynamic_cast,转成子类,可用于判断是否有归属关系,但特别慢
3. reinterpret_cast,低级的转型,比如pointer to int转型为int
4. static_cast,强迫隐式转换。把non-const转成各位。
注意:
1. 转型不会改变待转型对象的值,只是产生一个转型后的副本。
2. 尽量避免使用dynamic_cast,使用前一定要三思。
3. 转型动作尽量隐藏起来,不要让使用者进行转型。
4. 使用C++ style转型,因为分得更细,且容易辨认(特别是要查找代码时)。
条款28:避免返回handles指向对象内部成分
handles指的是reference、pointer、iterator之类的,就是可能有悬空可能存在的东西。因为
1. 返回handles的话就会有handles所指向对象比handles本身提前销毁的可能性。
2. 而且返回handles会降低封装性。
条款29:为“异常安全”而努力是值得的
异常安全两条件,就是在出现异常时:
1. 不泄漏任何资源
2. 不允许数据败坏
异常安全函数要提供以下三个保证之一:
1. 基本保证:出现异常仍能保持在一个有效的状态。
2. 强烈保证:出现异常程序状态不改变
3.
不抛保证:保证不抛出异常,这与带着空白的异常明细的函数不一样,带着空白异常明细的函数是指一旦抛出异常,将是严重错误。
int doSomething() throw(); //
空白异常明细
|
其中:
1. 不抛保证是最好的,但明显不好实现。
2. 强烈保证一般通过copy and
swap都能实现(这就是为什么swap函数要保证不出异常,方法在条款11中提到过),但
a)
有时候需要考虑到有没有这么实现的意义,因为这么实现是有效率上的牺牲的。
b)
另外,不是所有函数都能实现的,特别是有嵌套的保证需求的时候。比如,要实现强烈保证,函数内调用的函数也同样需要强烈保证,而就算保证了调用的函数都能做到强烈保证,其调用带来的状态改变也有可能是不能复原的。
3. “异常安全保证”服从木桶原理,决定异常安全的关键在于最薄弱的“异常安全保证”
条款30:透彻了解inlining的里里外外
inline是这样的:
1. inline函数的调用,是对函数本体的调用,是函数的展开,使用不当会造成代码膨胀。
2. 大多数C++程序的inline函数都放在头文件,inlining发生在编译期。
3. inline函数只代表“函数本体”,并没有“函数实质”,是没有函数地址的。
要注意到:
1.
构造函数与析构函数往往不适合inline。因为这两个函数都包含了很多隐式的调用,而这些调用付出的代价是值得考虑的。可能会有代码膨胀的情况。
2. inline函数无法随着程序库升级而升级。因为大多数都发生在编译期,升级意味着重新编译。
3. 大部分调试器是不能在inline函数设断点的。因为inline函数没有地址。
因此
1. 大多数inlining限制在小型、被频繁调用的函数身上。
2. 另外,对function
templates的inline也要慎重,保证其所有实现的函数都应该inlined后再加inline。
6. 继承与面向对象设计
条款31:将文件间的编译依存关系降至最低
这个问题产生是源于希望编译时影响的范围尽量小,编译效率更高,维护成本更低,这一需求。
实现这个目标首先第一个想到的就是,声明与定义的分离,用户的使用只依赖声明,而不依赖定义(也就是具体实现)。
但C++的Class的定义式却不仅仅只有接口,还有实现细目(这里指实现接口需要的私有成员)。而有时候我们需要修改的通常是接口的实现方法,而这一修改可能需要添加私有变量,但这个私有变量对用户是不应该可见的。但这一修改却放在了定义式的头文件中,从而造成了,使用这一头文件的所有代码的重新编译。
于是就有了pimpl(pointer to
implementation)的方法。用pimpl把实现细节隐藏起来,在头文件中只需要一个声明就可以,而这个poniter则作为private成员变量供调用。
这里会有个有意思的地方,为什么用的是指针,而不是具体对象呢?这就要问编译器了,因为编译器在定义变量时是需要预先知道变量的空间大小的,而如果只给一个声明而没有定义的话是不知道大小的,而指针的大小是固定的,所以可以定义指针(即使只提供了一个声明)。
这样把实现细节隐藏了,那么实现方法的改变就不会引起别的部分代码的重新编译了。而且头文件中只提供了impl类的声明,而基本的实现都不会让用户看见,也增加了封装性。
结构应该如下:
class AImpl;
class A {
public:
...
private:
std::tr1 ::shared_ptr<AImpl> pImpl ;
};
|
这一种类也叫handle class
另一种实现方法就是用带factory函数的interface
class。就是把接口都写成纯虚的,实现都在子类中,通过factory函数或者是virtual构造函数来产生实例。
声明文件时这么写
class A {
...
static std ::tr1:: shared_ptr<A >
create (...);
...
};
|
定义实现的文件这么写
class RealA: public A {
public:
...
private:
...
};
std::tr1 ::shared_ptr<A> A ::create(...) {
return std ::tr1:: shared_ptr<A >(new RealA (...));
}
|
以上说的为了去耦合而使用的方法不可避免地会带上一些性能上的牺牲,但作者建议是发展过程中使用以上方法,当以上方法在速度与/或大小上的影响比耦合更大时,再写成具体对象来替换以上方法。
条款32:确定你的public继承塑模出is-a关系
public继承意味着is-a关系。确定关系再继承。
条款33:避免遮掩继承而来的名称
这个问题来源是变量的有效区域引起的,而引入到继承,就是子类与基类同名函数的关系问题了。
1. 当子类不重载基类的函数的时候,基类的public函数是能够被继承的。
2. 当子类重载基类的函数的时候,基类所有同名函数均不会被继承,即被遮掩了。
既然是继承,那就是is-a关系了,没有基类的被重载函数一般情况是不合适的。
解决方法一:使用using,继承所有版本的重载
class Base {
private:
int x;
public:
virtual void mf1() ;
void mf1(int );
void mf3();
void mf3(double );
};
class Derived: public Base {
public:
using Base ::mf1;
using Base ::mf3;
virtual void mf1();
void mf3();
...
};
|
解决方法二:转交函数(forwarding
function),只继承个别版本。有时候我们只需要继承重载函数中的个别版本,如以上的mf1的无参版本.
class Derived: public Base {
public:
virtual void mf1()
{ Base ::mf1(); }
...
};
|
关于继承结合templates的情况,将在条款43提及。
条款34:区分接口继承和实现继承
这里个人认为可以说是pure virtual、impure
virtual、non-virtual成员函数在继承中的表现的讨论。
pure
virtual,只能继承接口。若被继承,则必须提供实现方案,基类也可以实现默认方案(函数名同名),只是不会被继承,要调用必须得inline调用(类似Base::func()这样的形式)。
impure
virtual,继承接口与缺省实现。基类提供默认实现方案,可被子类继承,也可被重写,继承时要搞清楚是否需要继承缺省实现。
non-virtual,继承接口与强制性实现,不能被重写,可以被重载。
总的来说,就是接口是一定会被继承的(至少接口名),实现方法怎么继承就看实际。
条款35:考虑virtual函数以外的其他选择
这条款谈了两种设计模式,鼓励我们多思考的。
以游戏中的人物设计继承体系为例子,不同的人物有不同的计算健康指数的方法,就叫healthValue函数吧,很自然的就会想到设计一个基类,把healthValue函数设计为virtual的用于继承。
此条款提供了两种不同的思路
1. Template
Method模式,由Non-Virtual Interface手法实现。
具体到以上的例子就是大概如下:
class GameCharacter {
public:
int healthValue () const {
...
int retVal = doHealthValue();
...
return retVal;
}
private:
virtual int doHealthValue () const {
...
}
};
|
其实最先提的把healthValue设计为virtual的方法也是Template
Method模式,而这里就是保留healthValue为public,而实现为private
virtual(当然这里是可以protected的),这样的好处在于其中的前后“...”(省略号),这部分可以进行一些类似检查、调整的操作,保证doHealthValue()在一个适当的场景下调用。而且子类也可以继承实现private
virtual成员函数。
2.
Strategy模式,这一模式令实现方法是个变量,就算是同一个对象在不同的时段也可以有不同的实现方法。但这里都有个约束,就是对私有成员变量的访问限制。
a) Function
Pointers实现,此种实现手法的约束是只能是函数,而且形式受函数的签名(参数数量,参数类型,返回类型)的约束。
b)
tr1::function实现,摆脱了a)的约束,支持隐式类型转换,还支持函数对象或者是成员函数(通过std::tr1::bind实现)
c) 古典实现,其实就是对象实体是一类,而实现方法是另一类。
条款36:绝不重新定义继承而来的non-virtual函数
继承non-virtual函数的后果是,最终函数的实现效果不由声明时的类型决定,而是由使用时用的指针或者引用类型决定。简单些用代码表达如下:
class A {
public:
void f(){
cout << "A" << endl;
}
};
class B: public A {
public:
void f() {
cout << "B" << endl;
}
};
int main() {
B t;
B* pb = & t;
A* pa = & t;
pa->f (); //调用A的f()
pb->f (); //调用B的f()
return 0;
}
|
这个其实是不符合non-virtual成员函数的不变性特点的。也不能体现出B的特异性(因为t被调用f时的效果不一定来自B)
因此,绝不重新定义。
其实这一条款的另一层含义就是只重新定义virtual成员函数
条款37:绝不重新定义继承而来的缺省参数值
先说两个名词,动态绑定(又称前期绑定,early binding)与静态绑定(又称后期绑定,late
binding)。
另外,对应的,静态类型就是在声明时所采用的类型,动态类型就是目前所指对象的类型。
virtual函数就是动态绑定的,而non-virtual函数就是静态绑定的。
而这条款讨论的是更细的一层,“继承带有缺省参数值的virtual函数”。(因为条款36说过了,绝不继承non-virtual函数)
这里的问题是C++编译器对缺省参数是静态绑定的(出于效率考虑),virtual函数却是动态绑定的。因为这样的设定就会可能会导致调用的不一致(当用到多态时),看以下代码:
class A {
public:
virtual void f(char c = ‘A‘){
cout << "this
is A: " ;
cout << c << endl;
}
};
class B: public A {
public:
virtual void f(char c = ‘B‘) {
cout << "this
is B: " ;
cout << c << endl;
}
};
int main() {
A* p = new B;
p->f (); //调用B的f(),缺省参数来自A,结果是
//this
is B: A
return 0;
}
|
因为p的静态类型是A*,所以缺省是来自A,而动态类型是B,所以f()调用来自B。
解决方法就是选择virtual函数的替代设计方案,如NVI(non-virtual
interface)。
条款38:通过复合(composition)塑模出has-a或is-implemented-in-terms-of
主要是介绍除之前public继承的is-a关系外的has-a与is-implemented-in-terms-of关系。
has-a对应的是应用域,如World中的persons,
cars等等,这些通常是被访问查询而存在。
is-implemented-in-terms-of对应的是实现域,如对象中的buffers,
mutexes, search trees等等,这些通常是为实现某些功能而存在的东西。
条款39:明智而审慎地使用private继承
首先private继承意味的是is-implemented-in-terms-of
其次特殊情况才用private继承做到is-implemented-in-terms-of的关系,一般都用复合(composition,条款38)实现。
原因这里提了两个:
1.
private继承不能阻止在virtual函数在再一次被继承后的再一次被重写。如Base为要private继承的基类,而Base有virtual函数f(),private继承后为Derived,当Derived被继承时,f()还是可以重写的。
2.
private继承可能会增加编译依存关系。因为一般可以通过只在class内包含一个仅仅是声明而没有实现的类型的指针,实现对用户是不可见的方式(可以是在别的cpp文件中public继承)去替代private继承。这就涉及到条款31提到的编译依存性最小化的问题了。
特殊情况就是需要访问基类的protected成员时又或者是需要重新定义继承而来的virtual函数时。
还有一种情况就是需要对象尺寸最小化时。当一个类里面没有non-static的数据时,C++编译器认为对象都应该有非零大小,因此,当用包含的方式(当为对象中的成员变量)时,没有non-static的数据仍然会被分配空间(至少char的大小,虽然没有意义),而如果是private继承就不会增加空间开销的。当然这种基类就是一般只有一些typedef或者non-virtual的函数,没有任何可能带来空间花销的成员。
条款40:明智而审慎地使用多重继承
使用多重继承就要考虑歧义的问题(成员变量或者成员函数的重名)。
最简单的情况的解决方案是显式的调用(诸如item.Base::f()的形式)。
复杂一点的,就可能会出现“钻石型多重继承”,以File为例:
class File {...}
class InputFile: public File {...}
class OutputFile: public File {...}
class IOFile: public InputFile , public OutputFile {...}
|
这里的问题是,当File有个filename时,InputFile与OutputFile都是有的,那么IOFile继承后就会复制两次,就有两个filename,这在逻辑上是不合适的。解决方案就是用virtual继承:
class File {...}
class InputFile: virtual public File {...}
class OutputFile: virtual public File {...}
class IOFile: public InputFile , public OutputFile {...}
|
这样InputFile与OutputFile共享的数据就会在IOFile中只保留一份了。
但是virtual继承并不常用,因为:
1. virtual继承会增加空间与时间的成本。
2. virtual继承会非常复杂(编写成本),因为无论是间接还是直接地继承到的virtual base
class都必须承担这些bases的初始化工作,无论是多少层的继承都是。针对这一特性,可以让class实现类似java的final功能,这就不是这一条款涉及的内容了,这里只贴代码跟说明吧:
template<typename T> class MakeFinally {
private: //构造函数与析造函数都在private,只有友员可以访问
MakeFinally(){};
~MakeFinally (){};
friend T;
};
//
MyClass是MakeFinally<MyClass>的友员,能实现初始化
class MyClass: public virtual MakeFinally <MyClass> {};
//
D不是MakeFinally<MyClass>的友员,不能访问private,而D继承的
//
是virtual base
class,必须得访问MakeFinally<MyClass>中的构造
//
因此,不能通过,
class D: public MyClass{};
int main() {
MyClass var1;
D var2; //
到这里就会出错,注释掉不会报错,因为定义为空,
//
被编译器忽略了
}
|
对于virtual bases的建议就是
1. 非必要不要使用virtual bases。
2. 实在要用,就尽量避免在virtual bases里面放数据。
最后总结就是,能用单一继承尽量使用单一继承,而多继承在审慎考虑过后也要大胆使用,如之前提到的is-a与is-implemented-in-terms-of两个关系分别与两个base
class相关时,只要审慎考虑过了再使用就可以了。
7. 模板与泛型编程
条款41:了解隐式接口和编译期多态
以几行代码为例:
template<typename T>
void doProcessing(T& w ){
if (w. size() > 10 && w != something ) {
T
temp (w);
w .normalize();
temp .swap( w);
}
}
|
隐式接口,就是例子中类型T的变量w使用到的所有相关的函数。就是要求使用时调用的类型T必须具备的接口。
编译期多态,就是通过不同的template参数(T)导致不同的调用结果,而这些发现在编译期。
需要注意templates与classes的区别,classes的接口是显式的,多态是通过virtual函数实现的,发生在运行期。
条款42:了解typename的双重意义
在声明template参数时,class与typename是可以互换的。
在调用template嵌套的从属类型名称时,就只能是typename,但不能在base class
lists(基类列表)或member initialization list(成员初值表)内使用。简单的示例代码如下:
template<typename C>
void print( const C& c) {
//
没有typename是通不过编译的
typename C ::const_iterator
iter(c.begin ());
...
}
|
条款43:学习处理模板化基类内的名称
(其实我觉得这一条款翻译为“学习访问模板化基类内的名称”(Know how to access names
in templatized base classes)更合适一些。)
C++编译时,如果继承的是模板化的基类,那么像普通的基类继承那样直接调用基类的函数是不合法的。本条款说到的原因是,模板化的基类是可以被特化的(可以参考条款33),而特化后的基类是可以不具备某一函数的,而这一函数也许就是你继承时需要调用的。把书中的代码敲一下看看吧:
class CompanyA {
public:
...
//
发送明文
void sendCleartext (const std ::string
msg);
//
发送密文
void sendEncrypted (const std ::string
msg);
...
};
class CompanyB {
public:
...
void sendCleartext (const std ::string
msg);
void sendEncrypted (const std ::string
msg);
...
};
template<typename Company>
class MsgSender {
public:
...
void sendClear (const std ::string
msg) {
Company
c ;
c .sendCleartext(msg);
}
void sendSecret (const std ::string
msg)
{...}
};
|
到这里都没有问题,当要继承MsgSender的模板化基类时就出问题了
template<typename Company>
class DerivedMsgSender: public MsgSender <Company> {
public:
...
void sendClearMsg (const std ::string& msg) {
sendClear(msg); //这样是编译不过的!
}
};
|
因为MsgSender是能被特化的,而特化的版本是允许不存在某些接口的。如我们特化一个CompanyZ的版本如下:
class CompanyZ {...};
template<>
class MsgSender<CompanyZ> {
public:
...
//
只能传密文,不能传明文
void sendSecret (const std ::string& msg)
{...}
...
};
|
这样是合法的,没有了sendClear那么在DerivedMsgSender中就不能用了,所以在C++的编译中,就不允许这样的调用。解决方法有三:
1. 函数前加this->
template <typename Company >
class DerivedMsgSender : public MsgSender < Company > {
public :
...
void sendClearMsg ( const std :: string& msg ) {
this->sendClear ( msg);
}
};
|
2. 使用using
template<typename Company>
class DerivedMsgSender: public MsgSender <Company> {
public:
using MsgSender <Company>::sendClear;
...
void sendClearMsg (const std ::string& msg) {
sendClear (msg);
}
};
|
3. 显式调用base class
template<typename Company>
class DerivedMsgSender: public MsgSender <Company> {
public:
...
void sendClearMsg (const std ::string& msg) {
MsgSender<Company>::sendClear(msg );
}
};
|
条款44:将与参数无关的代码抽离templates
(其实这条款没有太多的实际代码参考,将懂不懂吧,直接把最后的总结抄了下来)
这条款要提醒的是注意templates可能带来的代码膨胀。
非类型模板参数造成的代码膨胀(如template<typename T, int
n>中的n),往往可消除,做法是以函数参数或者class成员变量替换template参数。
类型参数造成的代码膨胀,往往可降低,做法是让带有完全相同的二进制表述的具现类型共享实现码。
条款45:运用成员函数模板接受所有兼容类型
这条款讲的是学会使用成员函数模板(member function
templates),用于兼容可兼容的类型。问题来源是:
class Top{...};
class Middle: public Top {...};
class Bottom: public Mid {...};
Top* pt1 = new Middle;
Top* pt2 = new Bottom;
const Top* pct2 = pt1 ;
|
这样的操作对于指针是很自然的,也是很方便的,只是直接用的指针实在太不安全了,应该让智能指针也能有这样的能力。类似以下的效果:
SmartPtr<Top > pt1 = SmartPtr<Middle >(new Middle );
SmartPtr<Top > pt2 = SmartPtr<Bottom >(new Bottom );
SmartPtr<const Top> pct2 = pt1;
|
其实认真观察以上的操作,以上都是copy构造(声明时使用=调用的是copy构造)。于是就有了以下的解决方案。
template<typename T>
class SmartPtr {
public:
template<typename U> //
成员函数模板
SmartPtr(const SmartPtr<U >& other)
:heldPtr(other.get ()) {...}
T* get () const { return heldPtr :}
private:
T* heldPtr ;
};
|
以上就是成员函数模板,就是泛化了的成员函数。
这里需要注意到,泛化的copy构造其实在U==T时就与正常的copy构造在实质上是一样的。
但是事实上,如果你没有定义正常的copy构造(没有泛化的),编译器依然会默默地生成正常的copy构造。
所以,如果想所有的copy构造都在自己的掌控,还是要进行正常的定义。
同样的,对于编译器自动提供的copy assignment也是有相同的注意点。
看看tr1中的shared_ptr的定义摘要就明白了。
template<class T>
class shared_ptr {
public:
//
copy构造
shared_ptr(shared_ptr const& r );
// 泛化 copy构造
template<class Y>
shared_ptr(shared_ptr <Y> const & r);
//
copy assignment
shared_ptr& operator=(shared_ptr const& r );
// 泛化 copy
assignment
template<class Y>
shared_ptr& operator=(shared_ptr <Y> const & r);
};
|
条款46:需要类型转换时请为模板定义非成员函数
(其实这条款的翻译我觉得也有问题,原文是Define non-member functions inside
templates when type conversions are
desired,个人觉得应该这么翻译:当非成员函数模板需要类型转换时把函数定义在模板类中。具体解释看下文吧)
以operator*函数的模板化为例(有理数的模板化类Rational<T>)。
首先,由条款24可知,应该把operator*写成non-member
function。
template<typename T>
class Rational {
public:
Rational(const T& num = 0;
const T& den = 1);
const T
num () const ;
const T
den () const ;
...
};
template<typename T>
const Rational<T> operator* (const Rational <T>& lhs,
const Rational<T>& rhs )
{...}
Rational<int > oneHalf(1, 2);
Rational<int > result = oneHalf * 2; //编译不过
|
编译不过的原因是,operator*右侧的参数(2)转化不到Rational<int>上,参数(2)要转化成Rational<int>上,先要知道T=int,这两层推导已经超出了编译器的能力范围了。
于是,问题就变成了,如何让operator*能把T=int告诉编译器,并且还是一个non-member
function(条款24)。
这里我们注意到oneHalf要推出T=int是只需要一步,而当operator*是在template
class内部时,template的参数类型是不需要推导的(在声明时已经知道了)。利用这一点把函数放进template class内就省去了template
function的类型(T)推导过程了。
但写进class内了如何做到non-member
function(还是条款24)呢?解决方法就是前面加个friend。代码如下:
template<typename T>
const Rational<T> doMultiply (const Rational & lhs,
const Rational& rhs);
template<typename T>
class Rational {
public:
...
friend const Rational <T> operator* (const Rational<T>& lhs ,
const Rational<T>& rhs )
{
return doMultiply(lhs, rhs );
}
};
|
这写法很新鲜吧,记住就好。
另外,用doMultiply函数去封装是因为写进class内意味着inline,这样是可能带来代码膨胀的,为了避免这种情况出现,一般都会在外面封装一个实现函数(这里这么用其实是为了提醒大家使用时要注意inline的问题而已,就这个例子来说,代码展开了也只是一行,封装意义不大)。
说到这里应该明白我不同意此条款的翻译的原因吧。
条款47:请使用traits classes表现类型信息
书中以为迭代器提供的advance函数为例展开说明。
先说迭代器的5个种类:
1. input,如istream
2. output,如ostream
3. forward,std没有实现的,意思就是单向链表
4. bidirectional,如list
5. random access,如vector、deque、指针
于是就有了这么一堆tag的定义
struct input_iterator_tag {};
struct output_iterator_tag {};
struct forward_iterator_tag: public input_iterator_tag {};
struct bidirectional_iterator_tag: public forward_iterator_tag {};
struct random_access_iterator_tag: public bidirectional_iterator_tag {};
|
到这里的时候大概会有些疑问:
1.
为什么不是使用类似enum或者是const的文件去定义这些tag,而是这么郑重地定义成struct?
2. 以上为什么会有继承关系?或者更进一步问,为什么需要继承关系?
3. 为什么是struct?
先说说第3个问题的个人理解,因为这只涉及有类型信息,没有不可被用户所知的私有信息,因此struct合适。
至于第1、第2个问题先不具体回答,但首先我们明确的一点是这些classes是为类型区分服务的。
然后,我们明确这个advance函数的设计目标:
1. 利用不同迭代器的类型的优势(如random access的迭代器)
2. 能兼容内置的迭代器类型(如指针)
这里就需要一个能够得到类型信息的方法,也就是我们要提到的traits。
由以上分析可知,traits
class内包含信息的方法是不可取的(内置类型做不到),因此就必然用templates的方法实现并为传统类型提供特化版本(不太明白的话,往下看就明白了)。当然一般也是struct的:
template <typename IterT >
struct iterator_traits {
typedef typename IterT:: iterator_category
iterator_category ;
...
};
template<typename IterT> //对指针类型的特化,认真学习下
struct iterator_traits<IterT*> {
typedef random_access_iterator_tag
iterator_category ;
...
};
|
然后还得迭代器的配合。
template<...>
class deque {
public:
class iterator {
public:
typedef random_access_iterator_tag
iterator_category;
...
};
...
};
template<...>
class list {
public:
class iterator {
public:
typedef bidirectional_iterator_tag
iterator_category;
...
};
...
};
|
接着就有了advance函数的初始样子
template <typename IterT , typename DistT>
void advance (IterT & iter , DistT
d ) {
if ( typeid (typename std:: iterator_traits <IterT >:: iterator_category )
== typeid ( std:: random_access_iterator_tag ))
...
}
|
实际上,这个advance函数其实是有缺点的(条款48深入解释)。而事实上用if来进行类型上的判断也不是最好的方案,因为if判断发生在运行期。这里提供的方案是利用函数重载的特性,因为重载函数的选取就已经包括了函数参数类型的判断并且发生在编译期。实现方案如下
template<typename IterT, typename DistT>
void doAdvance(IterT& iter , DistT
d,
std::random_access_iterator_tag) {
iter += d ;
}
template<typename IterT, typename DistT>
void doAdvance(IterT& iter , DistT
d,
std::bidirectional_iterator_tag) {
if (d >= 0 ) { while (d --) ++ iter}
else {while (d++) --iter;}
}
template<typename IterT, typename DistT>
void doAdvance(IterT& iter , DistT
d,
std::input_iterator_tag) {
if (d < 0 ) {
throw std:: out_of_range("Negative
distance" );
}
while (d--) ++iter;
}
template<typename IterT, typename DistT>
void advance(IterT& iter , DistT
d) {
doAdvance(
iter , d,
typename std:: iterator_traits<IterT >::iterator_category
);
//
书中版本为
//
doAdvance(
// iter,
d,
// typename
std::iterator_traits<IterT>::iterator_category()
//
);
//
个人觉得有误。也未查证英文原版
}
|
到此,就可以解答我们开始的两个问题了。
1.
因为如果用enum或者const这种方式,相应的类型判断就应该用if,而if判断只能在运行期,不是最好方案,而用class的形式就可以借助重载函数的选择过程中进行类型判断的这一特性了,并且这一切都发生在编译期。
2.
这个继承关系无疑是正确的,都是is-a关系。除了明确关系外,这样做其实是有好处的,细心的同学可能会发现这里没有实现std::forward_iterator_tag版本的重载函数,但事实上只需要std::input_iterator_tag的重载函数就足够了,因为两者是is-a关系。
总结:
关于traits class的设计与实现:
1. 确认相关类型信息,如上例所需要的category。
2. 为该信息选择一个名称,如上例的iterator_category。
3. 让相应的类型具备相同名称的信息,如上例提到的typedef random_access_tag
iterator_category。
4. 为需要兼容的类型提供特化版本,如上例对指针的特化。
关于traits class的使用:
1. 建立一组重载函数或函数模板(如doAdvance),彼此间差异只在于各自的traits参数。
2. 建立一个控制函数或函数模板(如advance),调用上述函数,并传递traits提供的参数。
最后提一下,std除了提供iterator_traits外,还提供了char_traits和numeric_limits等这样保存类型相关信息的traits。而TR1也补充了不少,如is_fundamental<T>(判断T是否为内置类型),is_array<T>,is_base_of<T1,T2>等50个以上的traits
classes。
条款48:认识template元编程(Be aware of template
metaprogramming)
条款47的traits技术其实就是TMP的一个应用,先解释下条款47留下来的那个问题吧。
template <typename IterT , typename DistT>
void advance (IterT & iter , DistT
d ) {
if ( typeid (typename std:: iterator_traits <IterT >:: iterator_category )
== typeid ( std:: random_access_iterator_tag ))
...
}
|
代码是这样的:
std::list <int>:: iterator
iter;
...
advance(iter , 10 );
|
具现的代码是这样的:
void advance(std::list <int>:: iterator& iter , int d) {
if (typeid(typename std:: iterator_traits< std ::list< int>::iterator >::iterator_category)
== typeid (std:: random_access_iterator_tag))
{...}
else
{...}
}
|
这个代码在运行结果上是没有问题的,但缺点就是,这个具现出来的代码里,存在废话。因为当参数类型(std ::list < int>:: iterator)确定以后,if里面的判断其实是固定的,就是说,这个函数,的if或者是else里面那部分代码实际上是永远都不会被调用的。这是一种浪费。用条款47中的方案也就是TMP的方案就没有这个问题了。
解释完了这个问题,我们继续领略TMP的魅力吧。
TMP的循环都是通过递归完成的。
直接上代码:计算阶乘
template<unsigned n>
struct Factorial {
enum { value = n * Factorial<n -1>:: value };
};
template<>
struct Factorial<0> {
enum {value = 1 };
};
int main() {
std::cout << Factorial<5>::value ; //
打印:120
std::cout << Factorial<10>::value ; //
打印:3628800
}
|
我感觉挺酷的。
然后书中就列了几个TMP的应用例子,没有代码实现(失望。得自己去找找看了),我就抄一下吧:
1. 确保量度单位正确。关键字:编译期错误侦测。
2. 优化矩阵运算。关键字:expression templates。
3. 可以生成客户定制的设计模式(Strategy, Observer,
Visitor等等)实现品。关键字:policy-based design之TMP-based技术。
总结:
TMP很酷,不过很不直观,而且资料还很少,虽不能成为主流,但不可缺少。
8. 定制new和delete
条款49:了解new_handler的行为
new_handler就是当new不能满足需求(如申请不到内存)时调用的函数。
先看new_handler在<new>中的声明:
namespace std {
typedef void (* new_handler)();
new_handler
set_new_handler (new_handler
p) throw();
}
|
new_handler是个typedef,是个函数指针,指向的是无参数无返回的函数。
set_new_handler是用于指定new不能满足要求时该被调用的函数,其返回值是个指针,指向set_new_handler调用前正在执行的(马上就要被调换的)那个new_handler函数。
一个简单的使用例子:
#include
<iostream>
#include
<new> //
set_new_handler
#include
<cstdlib> //
abort
using namespace std;
void outOfMem() {
cerr << "out
of mem" << endl;
abort();
}
int main() {
set_new_handler(outOfMem );
int* data = new int [10000000000000L];
return 0;
}
|
设计良好的new_handler必须做以下事情(直接抄的,实现方法没说):
1. 让更多内存可被使用。使operator new内的下一次内存分配动作可能成功。
2.
安装另一个new_handler。若目前的new_handler无法取得更多可用内存,而知道存在别的new_handler有此能力,就可以安装那个new_handler以替换自己。
3.
卸除new_handler,就是把null指针传给set_new_handler。一量没有安装任何的new_handler,operator
new会在内存分配不成功时抛异常。
4. 抛出bad_alloc(或者派生自bad_alloc)的异常。
5. 不返回,通常调用abort或exit。
这里没涉及new_handler的例子,往后说。而针对一个可能的要求——为特定的class提供特定的new_handlers——提供了一个参考方案。
先看方案1:
Widget的声明
class Widget {
public:
static std ::new_handler
set_new_handler(std::new_handler
p ) throw ();
static void* operator new(size_t
size) throw ( bad_alloc);
private:
static std ::new_handler
currentHandler;
};
|
这里用的都是static,为什么呢?
首先我们需要明确的就是我们需要currentHandler存当前的new_handler,而这个是所有Widget对象共享的(不是独有的),所以需要static。static成员变量必须得在外内被定义(const的整型可以在成员内部定义),如下:
std::new_handler
Widget ::currentHandler = 0;
|
而Widget::set_new_handler函数是在还没有new之前被外部调用的,所以写成static也可以理解(写成外部函数就修改不了private变量currentHandler)。代码如下:
std::new_handler
Widget ::set_new_handler(std::new_handler
p ) throw () {
std::new_handler
oldHandler = currentHandler;
currentHandler = p ;
return oldHandler ;
}
|
输入输出跟全局的set_new_handler的效果是一样的,不过要注意到这过程并没有调用全局的set_new_handler,只是做一个简单的状态存储而已。
最后就是重头戏new了,这个函数为什么也是static呢?这是要被全局调用的,而且如果不是static,对象还不存在,又如何调用new呢?所以static也可以理解,但可能一些事多的同学(如我)可能会发现,在实际操作时,我们不加static的声明,其实效果也是一样的!为什么呢?其实是这样的,在C++的标准里面说到:
Any allocation function for a
class
T
is a static member (even if not
explicitly declared static
). 也就是说无论写不写static,new都必须保证是能在全局调用的。
疑问解决完了,就学习下实现吧:
class NewHandlerHolder {
public:
explicit NewHandlerHolder (std:: new_handler
nh):handler (nh){}
~NewHandlerHolder () { std::set_new_handler (handler);}
public:
std::new_handler
handler ;
//
阻止copying,见条款14
NewHandlerHolder (const NewHandlerHolder & nh){}
NewHandlerHolder & operator =(const NewHandlerHolder & nh){}
};
void* Widget ::operator new(std ::size_t
size) throw(std::bad_alloc ) {
NewHandlerHolder h (std:: set_new_handler(currentHandler ));
return ::operator new(size );
}
|
这里的精华我觉得是NewHandlerHolder的使用,利用了临时对象在栈中的特性,就lock对象一样,在出栈时自动调用析构函数,还原之前的状态。
以下便是大概的调用过程了。
void outOfMem();
Widget::set_new_handler (outOfMem);
Widget* pw1 = new Widget;
std::string * ps = new std::string ;
Widget::set_new_handler (0);
Widget* pw2 = new Widget;
|
对Widget这个类来说,支持设置自己的new_handler的功能算是实现好了,很明显这样的代码是可以复用的,怎么复用呢?这段代码有个核心问题就是需要同样的class(不是对象)共享相同的currentHandler,很自然就会想到使用base
classes的templates。
于是看看升级版:
template<typename T>
class NewHandlerSupport {
public:
static std ::new_handler
set_new_handler(std::new_handler
p ) throw ();
static voie * operator new(std:: size_t
size) throw(std ::bad_alloc);
private:
static std ::new_handler
currentHandler;
};
template<typename T>
std::new_handler
NewHandlerSupport<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 );
}
template<typename T>
std::new_handler
NewHandlerSupport <T>:: currentHandler = 0;
|
要为Widget添加set_new_handler支持能力只需要
class Widget: public NewHandlerSupport <Widget> {
... //
这样就不需要再声明set_new_handler和operator new
};
|
到这里顺带提一下nothrow,就是在内存分配失败时返回null,使用如下:
class Widget {...}
Widget* pw1 = new Widget; //如果分配失败抛出bad_alloc
if ( pw1 == 0) ... //这个测试一定失败
Widget* pw2 = new (std::nothrow ) Widget; //如果分配失败返回0
if ( pw2 == 0) ... //这个测试可能成功
|
用了nothrow为什么说是可能成功呢?这里的成功是指测试能达到想要的效果,这里的意思就是分配失败了,但pw2可能并不等于0。因为nothrow只作用于给Widget分配内存时起作用,而当Widget进行自己的构造函数时所调用的东西(比如说进行一个可能会失败的new操作)就不是nothrow所管的事情的。这里的建议是忘记nothrow吧,它是为了照顾老使用者而产生的东西。
条款50:了解new和delete的合理替换时机
这条款主要说了一些要重写new和delete的场合。列一下:
1. 检测运用错误
2. 收集动态内存使用统计信息
3. 增加分配和归还的速度
4. 降低缺省内存管理器带来的空间额外开销
5. 弥补缺省分配器中的非最佳齐位(alignment)
6. 将相关对象成簇集中
7. 为了获取非传统行为
个人总结:
1. 更省的空间
2. 更省的时间
3. 更全的信息
4. 为了做到某些神奇的行为
条款51:编写new和delete时需固守常规
明显,这条款是讲规则的。
1. operator new能处理0 byte的请求。即使要求0
byte,也得返回一个合法指针。最简单的莫过于类似于这样的if(size == 0)size=1;这样的处理方法。
2. operator
new应该内含一个无穷循环,如while(true){do something}之类的,其中的do
something就是尝试分配内存,如果它无法满足内存需求,就该调用new_handler。
3. operator
new能处理“比正确大小更大的(错误)申请”。其实这个问题的来源是子类是可以继承父类的operator
new的,而如果没有注意处理的话,子类可以调用的是父类的operator new,这里一个简单的解决方案就是加一个判断if (size !=
sizeof(Base)) { 调用标准的new } 大概这样的形式。另外提醒下sizeof的返回不可能为0的(条款39),因此当size为0(0
byte的请求)时,就一定会交给大括号内的内容,这里就是标准的new去处理了。
4. operator
delete应该在收到null指针时不做任何事,保证删除null指针永远安全。
5. operator
delete同样的也要能处理“比正确大小更大的(错误)申请”。简单的方案也如3提到的那样加一个判断。顺便也提一下,当base
class不提供virtual析构时,在operator delete时可能就会传递一个错误的size,导致delete失败,因为base
class就别忘了提供virtual析构。
条款52:写了placement new也要写placement delete
这一条款主要讲定制非正常operator new(也就是这里的placement
new)时要注意的东西。
先认识下正常的operator new。
Widget* pw = new Widget;
|
这里面包含两个过程,一个是用operator
new分配内存,一个是Widget的default构造函数。
当分配到了内存,但是Widget的构造函数出错时,那么就需要做到分配的内存取消掉,并恢复原样。在这里,这个任务就交给了正常operator
new对应的operator
delete(注意是对应的)。到这里我们需要认识下这些正常的东西的函数签名式:
//
正常的operator new
void* operator new (std:: size_t) throw(std ::bad_alloc);
//
正常的global作用域的operator delete
void operator delete(void* rawMemory) throw();
//
正常的class作用域的operator delete
void operator delete(void* rawMemory, std ::size_t
size) throw();
|
然后我们再认识下非正常的operator new,也就是我们要讨论的placement
new。
先说placement new最早也是很有用的一个版本:
void* operator new (std:: size_t
size, void* pMemory ) throw ();
|
这一版本已经纳入了C++标准程序库了,在<new>中,负责在vector未使用的空间上创建对象。而这一目的也导致了placement
new这一名称的出现:在特点位置上new。这也是placement new多数所指的版本:唯一额外参数是个void*的new。
而这里讨论的主要是那个比较小众的版本:带任意额外参数的new。我就继续抄书中的代码了(做过整合的)。
class Widget {
...
//
带输出日志的new
static void* operator new(std::size_t
size , std:: ostream& logStream )
throw(std ::bad_alloc);
//
正常的class作用域的operator delete
static void operator delete(void * pMemory, std::size_t
size )
throw();
//
与带输出日志的new对应的delete
static void operator delete(void * pMemory, std::ostream & logStream)
throw();
...
};
|
这个operator new的调用就应该是这样的:
Widget* pw = new (std::cerr ) Widget; //
是不是有点像条款49中nothrow的调用?
|
而在这样调用operator new产生的对象,往后使用operator delete如下
delete pw;
|
调用的就是正常的operator delete了,因为placement
delete只会伴随的placement new出现异常时才会调用。
这里我们特别强调对应关系啊,因为如果调用了placement
new,出错时候如果没有对应版本的delete的话,程序是不知道如何delete的!而且这些都是发生在运行期的!
另外,还有一个问题,就是函数名被掩盖的问题。因为所有(不同版本)的operator
new重写都会掩盖global版本和继承而得的operator new!就像刚才写的Widget,没有写正常的new版本,然后
Widget* pw = new Widget;
// 错误
Widget * pw = new (std ::cerr ) Widget ;
// 正确
|
同样的子类继承父类后,重写了new,那么父类的new也是会被掩盖的。
在解决这个问题之前,我们先了解下global域中的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
|
这里的忠告是,如果不是为了阻止class使用以上的operator
new,请确保它们在你所生成的任何定制型operator new之外还能用。还要提供对应的operator delete。
书中提供了这一问题的一个简单解决方案,建立一个base
class,内含所有正常形式的new和delete,如下
class StandardNewDeleteForms {
public:
//
normal new/delete
static void* operator new(std::size_t
size ) throw (std:: bad_alloc)
{return ::operator new(size);}
static void operator delete(void * pMemory) throw()
{::operator delete(pMemory);}
//
placement new/delete
static void* operator new(std::size_t
size , void * ptr) throw()
{return ::operator new(size, ptr );}
static void operator delete(void * pMemory, void* ptr) throw()
{::operator delete(pMemory, ptr );}
//
nothrow new/delete
static void* operator new(std::size_t
size , const std::nothrow_t & nt)
throw()
{return ::new(size, nt);}
static void operator delete(void * pMemory, const std ::nothrow_t&)
throw()
{::operator delete(pMemory);}
};
|
然后利用继承和using用上这些函数:
class Widget: public StandardNewDeleteForms {
public:
using StandardNewDeleteForms ::operator new;
using StandardNewDeleteForms ::operator delete;
static void* operator new(std::size_t
size , std:: ostream& logStream )
throw(std ::bad_alloc);
static void operator delete(void * pMemory, std::ostream & logStream)
throw();
...
};
|
总结起来就是:
1. 写了placement new就得写对应的placement delete。
2. 别无意识(非故意)地让placement new 与
placement delete 掩盖了它们的正常版本。
9. 杂项讨论
这一部分以提醒介绍为主,没有太多的代码。作者说很重要。不过我是看过就算了。把各条款的总结抄一下吧。
条款53:不要轻忽编译器的警告
1. 严肃对待编译器发出的警告信息,努力争取无任何警告。
2. 不要过度依赖编译器的报警能力,因为编译器是有可能变的。
条款54:让自己熟悉包括TR1在内的标准程序库
1.
C++标准程序库的主要机能由STL、iostreams、locales组成。并包含C99标准程序库。
2.
TR1添加了智能指针、一般化函数指针、hash-based容器、正则表达式以及另外10个组件的支持。
3. TR1自身只是一份规范。一个好的实物来源是Boost
条款55:让自己熟悉Boost
1. Boost是一个社群,网址http://boost.org
2. Boost提供许多TR1组件实现品,以及其他许多程序库。