EMACS & 程序 编程点滴...
C++编程
TOPC++对象基础知识
- 在空类的对象中,其对象大小为1bit
- Static 的变量不会增加对象的大小
- 存在virtual函数时,对象大小增加4bit
TOP多态
TOP虚函数
- 虚拟函数表包含此类及其父类的所有虚拟函数的地址。如果它没有重载父类的虚拟函数,vtable中对应表项指向其父类的此函数。反之,指向重载后的此函数。
- 析构函数的工作方式是:最底层的派生类(most derived class)的析构函数最先被调用,然后调用每一个基类的析构函数。多态基类应该声明虚析构函数。如果一个类有任何虚函数,它就应该有一个虚析构函数。
TOP有虚函数时的C++对象模型
- 子类中不重载父类中的虚函数
- 子类的虚函数表中,先存放基类的虚函数,在存放子类自己的虚函数。

- 子类中重载父类中的虚函数
- 当子类重载了父类的虚函数,则编译器会将子类虚函数表中对应的父类的虚函数替换成子类的函数。

- 多重继承

- 虚拟继承
- 虚拟继承的子类,有单独的虚函数表, 另外也单独保存一份父类的虚函数表,两部分之间用一个四个字节的0x00000000来作为分界。 如果子类没有自己的虚函数,那么子类就不会有虚函数表,但是子类数据和父类数据之间,还是需要0x0来间隔。

TOP总结
- 对于基类,如果有虚函数,那么先存放虚函数表指针,然后存放自己的数据成员;如果没有虚函数,那么直接存放数据成员。
- 对于单一继承的类对象,先存放父类的数据拷贝(包括虚函数表指针),然后是本类的数据。
- 虚函数表中,先存放父类的虚函数,再存放子类的虚函数
- 如果重载了父类的某些虚函数,那么新的虚函数将虚函数表中父类的这些虚函数覆盖。
- 对于多重继承,先存放第一个父类的数据拷贝,在存放第二个父类的数据拷贝,一次类推,最后存放自己的数据成员。其中每一个父类拷贝都包含一个虚函数表指针。如果子类重载了某个父类的某个虚函数,那么该将该父类虚函数表的函数覆盖。另外,子类自己的虚函数,存储于第一个父类的虚函数表后边部分。
- 当对象的虚函数被调用是,编译器去查询对象的虚函数表,找到该函数,然后调用。
TOPprivate的纯虚函数
一般看到虚函数(virtual修饰的函数)都认为是用来重载,实现C++多态机制的。如果给其加上private修饰符,怎样在子类中使用呢?首先,我们要明确几个概念:
- 访问限定符(public, private, protected)是控制类成员对外部的可见性,不限制多态行为。换一句话说,实 现多态的函数有怎样的访问限定符与多态性本身无关
- 虚函数以外的函数是在编译器确定的,而虚函数是在运行期确定的(多态的基本)
- private以外的函数里,虚函数被子类重载(overload), 非虚函数被子类重写覆盖(overwrite)
要用private函数实现多态,那么肯定该函数的指针仍然位于vtbl中,只不过该函数的多态行为需要通过所在基类内部的其他 非虚函数来反映。
比如:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
class Base { public: void test() { foo(); } private: virtual void foo() { printf("base\n"); } }; class Derived : public Base { private: virtual void foo() { printf("derived\n"); } }; int main(int, char*[]) { std::auto_ptr<Base> base(new Derived()); base->test(); } |
代码执行后显示“derived”,而不是“base”。这说明在子类中的虚函数重载了基类中的私有虚函数。
类成员函数在不同限定符下的表征可以参考下表:
| 函数 | 表征 |
|---|---|
| 1.普通的私有成员函数 | 实现内容不可被更改 |
| 2.虚拟私有成员函数 | 实现内容可以被子类修改(在基类中缺省实现) |
| 3.虚拟保护成员函数 | 实现内容可以被子类修改(子类中拥有不同实现) |
如果子类中不重载虚函数,2与3的都缺省使用基类中的实现。如果重载的话,2将产生新方法,而3的重载实现中可以调用基类 中的实现。 该方法经常在设计模式被应用(Template Method)。
TOP模板
TOP厂模板
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 |
template <class ManufacturedType, typename ClassIDKey=std::string> class GenericFactory { typedef std::auto_ptr<ManufacturedType> (*BaseCreateFn)(); typedef std::map<ClassIDKey, BaseCreateFn> FnRegistry; FnRegistry registry; GenericFactory(); GenericFactory(const GenericFactory&); // Not implemented GenericFactory &operator=(const GenericFactory&); // Not implemented public: static GenericFactory &instance(); void RegCreateFn(const ClassIDKey &, BaseCreateFn); std::auto_ptr<ManufacturedType> Create(const ClassIDKey &className) const; }; template <class AncestorType, class ManufacturedType, typename ClassIDKey=std::string> class RegisterInFactory { public: static std::auto_ptr<AncestorType> CreateInstance() { return std::auto_ptr<AncestorType>(new ManufacturedType); } RegisterInFactory(const ClassIDKey &id) { GenericFactory<AncestorType>::instance().RegCreateFn(id, CreateInstance); } }; RegisterInFactory<Base, Base> registerMe(“Base”); |
TOP持久化一个对象
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 |
class Base { private: // … 一些数据,与本主题无关 … virtual std::string classID() const { return "Base"; } protected: // 当派生类装入自身后,应该调用该函数的父类实现。 virtual void do_read( std::istream& ); // 当派生类保存完自身后,应该调用该函数的父类实现。 virtual void do_write( std::ostream& ) const; public: // … 需要实现的一些虚拟函数,与本主题无关… // Streaming functions. void read( std::istream& ); void write( std::ostream& ) const; virtual ~Base(); }; // 流处理过程中的几个帮助函数。注意它们并非友元! std::ostream& operator << ( std::ostream& o, const Base& b) { b.write(o); } std::istream& operator >> ( std::istream& o, Base& b) { b.read(o); } void Base::write( std::ostream& o ) const { o << classID() << std::endl; do_write(o); } std::auto_ptr<Base> loadBase( std::istream& inFile ) { std::string className; std::getline( inFile, className ); std::auto_ptr<Base> newBase = genericFactory<Base>::instance().create(className); if( newBase.get() != 0 ) { inFile >> *newBase; } return newBase; } |
TOPThunk(成员函数作为回调函数)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 |
#pragma pack(push, 1) template <class T> class CAuxThunk { BYTE m_mov; // mov ecx, %pThis DWORD m_this; // BYTE m_jmp; // jmp func DWORD m_relproc; // relative jmp public: typedef void (T::*TMFP)(); void InitThunk(TMFP method, const T* pThis) { union { DWORD func; TMFP method; } addr; addr.method = method; m_mov = 0xB9; m_this = (DWORD)pThis; m_jmp = 0xE9; m_relproc = addr.func - (DWORD)(this+1); FlushInstructionCache(GetCurrentProcess(), this, sizeof(*this)); } FARPROC GetThunk() const { _ASSERTE(m_mov == 0xB9); return (FARPROC)this; } }; template <class T> class CAuxStdThunk { BYTE m_mov; // mov eax, %pThis DWORD m_this; // DWORD m_xchg_push; // xchg eax, [esp] : push eax BYTE m_jmp; // jmp func DWORD m_relproc; // relative jmp public: typedef void (__stdcall T::*TMFP)(); void InitThunk(TMFP method, const T* pThis) { union { DWORD func; TMFP method; } addr; addr.method = method; m_mov = 0xB8; m_this = (DWORD)pThis; m_xchg_push = 0x50240487; m_jmp = 0xE9; m_relproc = addr.func - (DWORD)(this+1); FlushInstructionCache(GetCurrentProcess(), this, sizeof(*this)); } FARPROC GetThunk() const { _ASSERTE(m_mov == 0xB8); return (FARPROC)this; } }; #pragma pack(pop) // CAuxThunk //TimerProc为成员函数 InitThunk((TMFP)TimerProc, this); //使用这个函数 ::SetTimer(NULL, 0, timeout, (TIMERPROC)pTimeout->GetThunk()); |
TOPSTL
TOPSTL容器选择
| 容器名称 | 适用范围 |
|---|---|
| vector | 元素访问速度最快,类似于数组 |
| deque | 注重开头,尾部的插入 |
| list | 注重中间的插入 |
| hash (table,map,set) | 侧重于单纯的存储,取出操作。平均复杂度很好,最坏复杂度很大 |
| map,set | 查找复杂度是对数级 |
STL容器选择
TOP各种智能指针的比较
| 所有权转移 | 数组扩展 | |
|---|---|---|
| std::auto_ptr | ○ (可拷贝,复制) | × |
| boost::scoped_ptr | × | × |
| boost::scoped_array | × | ○ |
| boost::share_ptr | ○ (可拷贝,复制,使用引用计数管理对象) | × |
| boost::share_array | ○ | ○ |
| boost::weak_ptr | 避免share_ptr的循环引用,使用“弱引用” |
TOPEffective STL 规则
- 仔细选择你的容器:string,vector,deque是线性容器,即元素连续存储;list是一个链表;因此如果需要频繁的在容器中间插入删除元素的话,list更合适,如果频繁的在尾部插入删除元素的话vector更合适。
- 小心对“容器无关代码”的幻想:写出完全的容器无关代码是很困难的。
- 使容器里对象的拷贝操作轻量而正确:对于拷贝代价很高的对象,建立指针容器效率更高,但指针容器必须显式的释放容器里的对象,因此智能指针容器可能较好。容器比起数组的一个效率上的进步是,不用初始时就建立指定数量的对象。
- 尽量使用区间成员函数代替它们的单元素兄弟:用一个成员函数代替循环,更好理解。例如assign函数可以将一个区间赋值给另一个区间,优于单元素拷贝时的循环。
- 用empty来代替检查size()是否为0:empty是常数时间的操作,size方法对于list是线性时间。
- 当使用new得指针的容器时,记得在销毁容器前delete那些指针:对于容纳对象指针的容器,在销毁容器前要释放容器内指针指向的对象。
- 永不建立auto_ptr的容器:auto_ptr会导致对象所有权发生转移,所以当autoptr的容器应用于某些算法(例如sort)时某些指针会变为空,导致异常。
- 在删除选项中仔细选择:当要去除一个连续容器的元素时,用erase-remove(或remove_if)惯用法;非连续容器用erase或者remove(remove_if)成员函数;
- 注意分配器的协定和约束:
- 理解自定义分配器的正确用法:
- 对STL容器线程安全性的期待现实一些:stl容器本身未考虑线程互斥等问题,因此如果是多线的,需要添加互斥锁。
- 尽量使用vector和string来代替动态分配的数组:动态数组需要手动释放内存,vector和string可以自己管理自己的内存(当vector和string销毁时,他们内部的元素也自动释放),且具有动态增长的特性。
- 使用reserve来避免不必要的重新分配:当容器预先分配的内存已经满时,就需要重分配,重分配时需要申请新空间 ,拷贝旧容器的内容到新容器,释放旧空间,因此代价是很高的。因此尽量使用reserve来为容器预留空间,避免过多的内存重分配。尤其是当容器里的元素个数可以预期时,更要在创建容器时使用reserve来预留空间。
- 小心string实现的多样性:
- 如何将vector和string的数据传给遗留的API:对于数组参数,可以将vector首元素地址传给该参数;对于char*类型的参数,可以将string通过c_str成员函数转为char*。
- 使用“交换技巧”来修整过剩容量:vector<Contestant>(contestants).swap(contestants)
- 避免使用vector<bool>:vector<bool>不是一个容器,bool值被压缩了,只占用一个bit,因此容纳的并不是一个bool。
- 了解相等和等价的区别:有很多stl算法可以传递参数指定比较规则,通常比较规则指定的是等价关系。
- 为指针的关联容器指定比较类型:指针容器容纳的是指针,因此默认的比较规则是比较指针的值,这就需要将指针指向对象的比较方法作为比较规则。
- 永远让比较函数对相等的值返回false:在set中插入元素时会比较该值和容器中其他元素的值,如果对相等的值返回true,会导致将相同的元素又一次插入容器,破坏了容器。
- 避免原地修改set和multiset的键:set中元素保持自有序,如果原地修改容器的值,新值不在原来的位置,会破坏容器的有序性。
- 考虑用有序vector代替关联容器:有序的vector可以使用性能高的查找算法,效率上高于使用关联容器。
- 当关乎效率时应该在map::operator[]和map-insert之间仔细选择:如果你要更新已存在的map元素,operator[]更好,但如果你要增加一个新元素,insert则有优势,因为operator[]需要先查找元素,如果不存在再插入。
- 熟悉非标准散列容器:hash_set、hash_multiset、hash_map和hash_multimap。
- 尽量用iterator代替const_iterator,reverse_iterator和const_reverse_iterator:const_iterator到iterator的转换是困难的,而有些容器的成员函数只接受非const迭代器。
- 用distance和advance把const_iterator转化成iterator:可以定一个iterator,并让它前进到距离它和const_iterator一样远的位置。
- 了解如何通过reverse_iterator的base得到iterator:注意reverse_iterator的base()函数产生的iterator和reverse_iterator之间的距离。
- 需要一个一个字符输入时考虑使用istreambuf_iterator:istreambuf_iterator一个字符一个字符读取,不忽略空格,而istream_iterator忽略空格符。
- 确保目标区间足够大:确保目标区间足够大,以保证算法作用于该容器时指针不会指向不存在的元素,或者使用插入迭代器。
- 了解你的排序选择:partial_sort——鉴别出容器中若干个最好的并对它们排序;nth_element——只选出20个最好的,但不排序;partical_sort、sort、nth_element都是不稳定的,stable_sort是稳定的;算法sort、stable_sort、partial_sort和nth_element需要随机访问迭代器,所以它们可能只能用于vector、string、 deque和数组,对标准关联容器排序元素没有意义,因为这样的容器使用它们的比较函数来在任何时候保持有序。唯一我们可能会但不能使用sort、stable_sort、partial_sort或nth_element的容器是list,list通过提供sort成员函数做了一些补偿。算法的复杂度:1. partition,2. stable_partition, 3. nth_element,4. partial_sort,5. sort,6. stable_sort。
- 如果你真的想删除东西的话就在类似remove的算法后接上erase:remove并没有真正删除元素,它只是将那些不被删除的元素移动到开头,并返回指向不被删除的最后一个元素的下一个位置,因此在调用remove方法后要调用erase方法。
- 提防在指针的容器上使用类似remove的算法:在指针容器上直接使用remove方法会造成内存泄露。
- 注意哪个算法需要有序区间:很多排序算法需要随机访问迭代器。搜索算法binary_search、lower_bound、upper_bound和equal_range(参见条款45)需要有序区间,因为它们使用二分法查找来搜索值。
- 通过mismatch或lexicographical比较实现简单的忽略大小写字符串比较:
- 了解copy_if的正确实现:
- 用accumulate或for_each来统计区间:
- 把仿函数类设计为用于值传递:C和C++都不允许你真的把函数作为参数传递给其他函数。取而代之的是,你必须传指针给函数。值传递意味着:第一,你的函数对象应该很小。否则它们的拷贝会很昂贵。第二,你的函数对象必须单态(也就是,非多态)——它们不能用虚函数。那是因为派生类对象以值传递代入基类类型的参数会造成切割问题。的确有办法让大的和/或多态的函数对象仍然被允许。带着你要放进你的仿函数类的数据和/或多态,把它们移到另一个类中。然后给你的仿函数一个指向这个新类的指针。
- 用纯函数做判断式:
- 使仿函数类可适配:从unary_function和binary_function产生的类继承产生你的仿函数,从而避免在函数用作算法的比较规则时被迫使用ptr_fun等配接器。
- 了解使用ptr_fun、mem_fun和mem_fun_ref的原因:ptr_fun用于普通函数做算法规则且使用了某些适配器(如not1)时;mem_fun用于对象指针容器,比较规则是类成员函数;mem_fun_ref用于对象容器,比较规则是类成员函数。
- 确定less<T>表示operator<:
- 尽量用算法调用代替手写循环:算法看起来简洁,且可能有更高的效率。
- 尽量用成员函数代替同名的算法:成员函数一般都是为该容器量身打造的,因此可能具有更高的效率。
- 注意count、find、binary_search、lower_bound、upper_bound和equal_range的区别:
- 考虑使用函数对象代替函数作算法的参数:把STL函数对象传递给算法所产生的代码一般比传递真的函数高效。仿函数的operator()是一个内联函数,而将普通函数传给算法时,是通过函数指针实现的调用,非内联函数。
- 避免产生只写代码:一条语句里如果函数调用次数过多,让人很难读懂,应该尽量避免。
- 总是#include适当的头文件:不同的平台,一些头文件可能包括不同的其他头文件。无论何时你使用了一个头文件中的任意组件,就要确定提供了相应的#include指示,就算你的开发平台允许你不用它也能通过编译。当你发现移植到一个不同的平台时这么做可以减少压力,你的勤奋将因而得到回报。
TOP设计模式相关
TOP单件(singleton)模式
单件模式通过static成员函数创建对象实例,而不能在外部通过new来在堆中创建,或者是在栈中声明。为了达到这一要求,需要将构造函数设置为私有的。同样,对象的拷贝构造和operator=也应当是私有的。
- 析构函数也可以作为私有的来声明,在两种情况下有效
- 该类作为Helper类,只提供static函数,就像使用全局函数一样使用,并不需要生成其实例。
- 避免内存泄露,显示的要求用户调用销毁接口来释放对象实例(类中与create对应同样应该提供类似destroy的static接口函数)
下面是一些常用到的宏
1 2 3 4 5 6 |
#define SEALCONSTRUCT(type) \ private: \ explicit type(void); \ type(type const&); \ ~type(void); \ type& operator=(type const&); |
1 2 3 4 |
#define SEALCOPY(type) \ private: \ type(type const&); \ type& operator=(type const&); |
1 2 3 |
#define SEALASSIGN(type) \ private: \ type& operator=(type const&); |
TOPOthers
TOP防止内存泄露1
熟悉Symbian OS系统的朋友大概都应该清楚CleanupStack的作用。简而言之CleanupStack是一个用C++实现的防止内存泄露的 FrameWork。有了它,资源受限的嵌入式系统就不用担心内存泄露的问题了。也许有人会说,不是有垃圾收集GC,RTTI,智能指针等高 级东东专门对付内存泄露问题吗。可是在嵌入式编程领域,这些东东太高极了,以至于编译器不能很好的支持,或者代价太 大,不适合嵌入式领域。CleanupStack具有以下的成员。
1 2 3 4 5 6 7 8 9 10 11 |
class CleanupStack { static void PushL( TAny* aPtr ); static void PushL( CBase* aPtr ); static void PushL( TCleanupItem anItem ); static void Pop(); static void Pop( TInt aCount ); static void PopAndDestroy(); static void PopAndDestroy( TInt aCount ); …… } |
- CleanupStack::PushL() : 将实例放入CleanupStack中,当TRAP宏捕捉到程序异常时,放入CleanupStack中的实例将被自动清除。
- CleanupStack::Pop() : 因为对同一指针双重的delete是严禁的,所以但不要CleanupStack保护的时候,通过该函数将对 象实例取出。
- CleanupStack::PopAndDestroy() : 通过C++的重载机制,自动调用CleanupStack::PushL(),将实例放入CleanupStack中。 删除的时候通过该函数将实例删除。
使用CleanupStack的例子如下:
1 2 3 4 |
CClass* o1 = new( ELeave ) CClass(); ……(A) CleanupStack::PushL( o1 ); ……(B) CClass* o2 = new( ELeave ) CClass(); ……(C) CleanupStack::Pop( o1 ); ……(D) |
在(C)行,由于内存不足可能产生异常。但是在(B)行,o1已经放入CleanupStack中,所以当异常发生时,肯定被某处的 TRAP捕获,最终正确释放。而如果程序到达(D)行,则意味着没有发生意外,那么执行CleanupStack::Pop(),避免了多重删 除的问题。
在堆中创建类实例的时候,有两种发生例外的可能:
- 内存不足,类实例自己的内存空间得不到保障。
- 构造函数中发生的例外。
如果是1)倒好办,反正内存没有被分配,可如果是2)呢? 返回的指针是NULL。这样的内存泄露怎么处理呢?
Symbian OS中通过以下的规则来处理这个问题。
- 类本身的构造函数中不写例外处理过程(不必实现他的细节)
- 例外处理被写到用户级的构造函数ConstructL()中 (两级构造)
- 类成员中增加NewL(),NewLC()两个工厂函数,并按一定顺序调用
以下是一个按以上规则定义的类代码例子:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 |
class CClass : public CBase { public: static CClass* NewL(); static CClass* NewLC(); ~Class(); private: CClass(); void ConstructL(); …… } static CClass* CClass::NewLC() { CClass* self = new( ELeave ) CClass(); CleanupStack::PushL( self ); self->ConstructL(); return self; } static CClass* CClass::NewL() { CClass* self = CClass::NewLC(); CleanupStack::Pop( self ); return self; } |
TOP二进制位插入
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 |
class IntSetBitVec { private: enum { BITSPERWORD = 32, SHIFT = 5, MASK = 0x1F }; int n, hi, *x; void set(int i) { x[i>>SHIFT] |= (1<<(i & MASK)); } void clr(int i) { x[i>>SHIFT] &= ~(1<<(i & MASK)); } int test(int i) { return x[i>>SHIFT] & (1<<(i & MASK)); } public: IntSetBitVec(int maxelements, int maxval) { hi = maxval; x = new int[1 + hi/BITSPERWORD]; for (int i = 0; i < hi; i++) clr(i); n = 0; } int size() { return n; } void insert(int t) { if (test(t)) return; set(t); n++; } void report(int *v) { int j=0; for (int i = 0; i < hi; i++) if (test(i)) v[j++] = i; } }; |
TOP优先队列
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 |
template<class T> class priqueue { private: int n, maxsize; T *x; void swap(int i, int j) { T t = x[i]; x[i] = x[j]; x[j] = t; } public: priqueue(int m) { maxsize = m; x = new T[maxsize+1]; n = 0; } void insert(T t) { int i, p; x[++n] = t; for (i = n; i > 1 && x[p=i/2] > x[i]; i = p) swap(p, i); } T extractmin() { int i, c; T t = x[1]; x[1] = x[n--]; for (i = 1; (c=2*i) <= n; i = c) { if (c+1<=n && x[c+1]<x[c]) c++; if (x[i] <= x[c]) break; swap(c, i); } return t; } }; |
TOPEffective C++ 规则
TOP让自己习惯C++
TOP条款01:视C++为一个语言联邦
今天的C++已经是个多重范型编程语言(multiparadigm programming language),一个同时支持过程形式 (procedural) 、 面向对象形式 (object-oriented) 、 函数形式 (functional) 、泛型形式(generic) 、元编程形式(metaprogramming)的语言。 最简单的方法是将C++视为一个由相关次语言(sublanguage)组成的联邦而非单一语言。幸运的是,次语言总共有四个:
- C(C part of C++):按照C的高效编程方式就行了,比如对于内置类型来说用值传递(pass by value)比用引用传递(pass-by-reference)高效;
- Object-Oriented C++。 包括: class (包括构造函数和析构函数) 、 封装 (encapsulation) 、继承(inheritance) 、多态(polymorphism) 、virtual 函数(动态绑定) 、... ... 等等。
- Template C++。这是C++ 的泛型编程(generic programming)部分。
- STL。STL 是个template 程序库,但它是非常特殊的一个。它对容器(containers) 、迭代器(iterators) 、算法(algorithms)以及函数对象(function objects)的规约有极佳的紧密配合与协调,然而templates 及程序库也可以其他想法建置出来。
TOP条款02:尽量以const,enum,inline替换#define
这个条款或许可以改为“宁可用编译器替换预处理器”。通常C++要求你对所使用的任何东西提供一个定义式,但如果它是个class专属常量又是static且为整数类型(integral type,例如int,char,bool),则可特殊处理。只要不取它们的地址,你可以声明并使用它们而无需提供定义式。 例:
1 2 3 4 5 6 |
class GamePlayerf {} private: static const int NumTurns = 5 ; //常量声明式 int scores[NumTurns] ; //使用该常量 ... }; |
上面的是NumTurns的声明式而非定义式。如果你要取这个class专属常量的地址,你就必须另外提供定义式如下:
const int GamePlayer::NumTurns; //NumTurns的定义;
上面的这个式子应该 放入一个实现文件而非头文件! 由于它已经在声明时获得了初值,定义时不可以再设初值。
旧式编译器也许不支持上述语法,他们不允许static成员在其声明式上获得初值。此外,所谓的“in-class初值设定”也只允许对整数常量进行。如果你的编译器不支持上述语法,你可以将初值放在定义式:
例:
1 2 3 4 5 |
class CostEstimatef private: static const double FudgeFactor; //static class 常量声明,位于头文件内 ... }; |
const double CostEstimate::FudgeFactor = 1.35; //static class 常量定义,位于实现文件中
一个例外是当你在class编译期间需要一个class常量值, 例如上面的GamePlayer::scores的数组声明式中(编译器坚持必须在编译期间知道数组的大小) 。这时候万一你的编译器不允许 “static整数型class常量” 完成 “in class 初值设定” ,可改为所谓的 “the enum hack” 补偿做法。其理论基础是: “一个属于枚举类型(enumerated type)的数值可权充int被使用” ,于是GamePlayer可定义如下:
1 2 3 4 5 6 |
class GamePlayerf private: enum { NumTurns = 5 } ; int scores[NumTurns] ; ... }; |
TOP条款03:尽可能使用const
STL 迭代器是以指针为根据塑模出来的,所以迭代器的作用就像个T* 指针。声明迭代器为const就像声明指针为const一样(即声明一个T* const 指针) 。
例:
1 2 3 4 |
std::vector<int> vec ; ... const std::vector<int>::iterator iter=vec.begin() ; // iter 的作用像个T* const ++iter ; //错误!iter 是const |
如果你希望迭代器所指的东西不可被改动(即希望STL 模拟一个const T* 指针),你需要的是const iterator :
1 2 3 |
std::vector<int>::const iterator cIter=vec.begin() ; *cIter = 10 ; // 错误!cIter 是const ++cIter ; // 正确。 |
令函数返回一个常量值,往往可以降低因客户错误而造成的意外,而又不至于放弃安全性和高效性。
例:
1 2 3 4 5 6 |
class Rational { ... } ; const Rational operator* ( const Rational& lhs, const Rational& rhs ) ; Rational a, b, c ; ... (a * b) = c ; //错误!operator* 返回常量值 |
TOP条款04:确定对象被使用前已先被初始化
读取未初始化的值会导致不明确的行为。 通常如果你使用C part of C++(见条款01)而且初始化可能招致运行期成本,那么就不保证发生初始化。一旦进入non-C parts of C++,规则有些变化。这就是为什么array (来自C part of C++)不保证其内容被初始化,而vector(来自STL part of C++)却有此保 证。
C++规定,对象的成员变量的初始化动作发生在进入构造函数本体之前。所以,相对于在构造函数本体里为成员变量赋值,一个更好的办法是在member initialization list(成员初值列)里初始化成员变量。如果成员变量是const或references,它们就一定需要初值(所以只能放在成员初值列中),不能被赋值。
C++有着十分固定的“成员初始化次序”:base classes更早于derived classes被初始化,而class的成员变量总是以其声明次序(类定义中的次序)被初始化。即使它们在成员初值列中以不同的次序出现(很不幸那是合法的),也不会有任何影响。
一旦你已经很小心地将“内置型成员变量”明确地加以初始化,而且也确保你的构造函数运用“成员初值列”初始化baseclasses和成员变量,那就只剩唯一一件事需要操心 —“不同编译单元内定义之non-local static对象”的初始化次序(P30-P33) 。
- 为内置型对象进行手工初始化,因为C++不保证初始化它们。
- 构造函数最好使用成员初值列(member initialization list),而不要在构造函数本体内使用赋值操作(assignment) 。初值列列出的成员变量,其排列次序应该(不是必须)和它们在class中的声明次序相同。
- 为免除“跨编译单元之初始化次序”问题,请以local static对象替换non-local static对象。
TOP构造/析构/赋值运算
TOP条款05:了解C++ 默默编写并调用哪些函数
如果你自己没声明,编译器就会为它声明(编译器版本)一个default 构造函数、一个copy 构造函数、一个copy assignment 操作符和一个析构函数。因此,如果你写下:
1 |
class Empty {} ; |
这就好像你写下这样的代码:
1 2 3 4 5 6 7 |
class Empty { public: Empty() { ... } // default 构造函数 Empty(const Empty& rhs) { ... } // copy 构造函数 ~Empty() { ... } // 析构函数 Empty& operator=(const Empty& rhs) { ... } // copy assignment 操作符 }; |
惟有当这些函数被需要(被调用),它们才会被编译器创建出来。
所有这些函数都是public 且inline (见条款30) 。注意,编译器产生的析构函数是个non-virtual(见条款07),除非这个class 的base class 自身声明有virtual 析构函数。
如果某个base classes 将copy assignment 操作符声明为private,编译器将拒绝为其derived classes 生成一个copy assignment 操作符。
TOP条款06:若不想使用编译器自动生成的函数,就该明确拒绝
所有编译器产生的函数都是public。为阻止这些函数被创建出来,你得自行声明它们。为使产生的类不支持copying,你可以将copy构造函数或copy assignment操作符声明 为private。
- 为驳回编译器自动(暗自)提供的机能,可将相应的成员函数声明为private并且不予实现。 使用像Uncopyable这样的base class(见P39)也是一种做法。
TOP条款07:为多态基类声明virtual析构函数
C++明白指出,当derived class对象经由一个base class指针被删除,而该base class带着一个non-virtual析构函数,其结果未有定义|实际执行时通常发生的是对象的derived成分没被销毁。
任何class只要带有virtual函数都几乎确定应该也有一个virtual析构函数。如果class不含virtual函数,通常表示它并不意图被用做一个base class。 当class不企图被当做base class,令其析构函数为virtual往往是个馊主意,因为这样会增加其对象的大小。
如果你希望让一个类成为抽象基类,你应该为它声明一个pure virtual析构函数。
例:
1 2 3 4 |
class AWOV { public: virtual ~AWOV() = 0; }; |
这个class有一个pure class函数,所以它是个抽象class,又由于它有个virtual析构函数,所以你不需要担心析构函数的问题。然而这里有个窍门:你必须为这个pure virtual析构函数提供一份定义:
1 |
AWOV::~AWOV() {} //pure virtual析构函数的定义 |
析构函数的运作方式是,最深层派生(most derived)的那个class其析构函数最先被调用,然后是其每一个base class 的析构函数被调用。编译器会在AWOV的derived classes的析构函数中创建一个对~AWOV的调用动作,所以你必须为这个函数提供一份定义。如果不这样做,连接器会发出抱怨。
“给base classes一个virtual析构函数” ,这个规则只适用于polymorphic(带多态性质的)base classes身上。这种base classes的设计目的是为了用来“通过base class接口处理derived class对象”。
并非所有base classes的设计目的都是为了多态用途。例如标准string和STL容器都不被设计作为base classes使用,更别提多态了。某些classes的设计目的是作为base classes使用,但不是为了多态用途。
- polymorphic (带多态性质的)base classes应该声明一个virtual析构函数。 如果class带有任何virtual函数,它就应该拥有一个virtual析构函数。
- Classes的设计目的如果不是作为base classes使用, 或不是为了具备多态性 (polymorphically),就不该声明virtual析构函数。
TOP条款08:别让异常逃离析构函数
C++并不禁止析构函数吐出异常,但它不鼓励你这样做。如果某个操作可能在失败时抛出异常,而又存在某种需要必须处理该异常,那么这个异常必须来自析构函数以外的某个函数。
- 析构函数绝对不要吐出异常。如果一个被析构函数调用的函数可能抛出异常,析构函数应该捕捉任何异常,然后吞下它们(不传播)或结束程序。
- 如果客户需要对某个操作函数运行期间抛出的异常做出反应,那么class应该提供一个普通函数(而非在析构函数中)执行该操作。
TOP条款09:绝不在构造和析构过程中调用virtual函数
Base class构造期间virtual函数绝不会下降到derived classes阶层。取而代之的是,对象的作为就像隶属base类型一样。也就是说,在base classes构造期间,virtual函数不是virtual函数。
根本的原因是,在derived class对象的base class构造期间,对象的类型是base class而不是derived class。
相同道理也适用于析构函数。
- 在构造和析构期间不要调用virtual函数,因为这类调用从不下降至derived class(比起当前执行构造函数和析构函数的那层) 。
TOP条款10:令operator=返回一个reference to *this
赋值可以写成连锁形式:
1 |
x = y = z = 15 ; |
赋值采用右结合律:
1 |
x = (y = (z = 15)) ; |
为了实现“连锁赋值” ,赋值操作符必须返回一个reference指向操作符的左侧实参。这是你为classes实现赋值操作符时应该遵循的协议:
1 2 3 4 5 6 7 8 9 10 |
class Widget { public: ... Widget & operator=(const Widget& rhs) //返回类型是个reference, 指向当前对象 { ... return * this ; //返回左侧对象 } ... }; |
这个协议不仅适用于以上的标准赋值形式,也适用于所有赋值相关运算,如“+=、-、*”等。
- 令赋值(assignment)操作符返回一个reference to *this。
TOP条款11:在operator=中处理“自我赋值”
“自我赋值”发生在对象被赋值给自己时。例如,假设你建立一个class用来保存一个指针指向一块动态分配的位图(bitmap):
1 2 3 4 5 6 |
class Bitmap { ... }; class Widget { ... private: Bitmap* pb; //指针,指向一个从heap分配而得的对象 }; |
下面是operator=实现代码,其自我赋值出现时并不安全(它也不具备异常安全性) 。
1 2 3 4 5 6 |
Widget& Widget::operator=(const Widget& rhs) //一份不安全的operator=实现版本 { delete pb; //停止使用当前的bitmap, pb = new Bitmap(*rhs.pb); //使用rhs的bitmap副本(复件) 。 return *this; //见条款10。 } |
欲阻止这种错误,传统做法是借由operator=最前面的一个 “证同测试(identity test) ”达到“自我赋值”的检验目的:
1 2 3 4 5 6 7 |
Widget& Widget::operator=(const Widget& rhs) //一份不安全的operator=实现版本 { if( this == &rhs ) return *this; //证同测试(identity test) 。 delete pb; pb = new Bitmap(*rhs.pb); return *this; } |
这个版本仍然存在异常方面的麻烦。如果“new Bitmap”导致异常,Widget最终会持有一个指针指向一块被删除的Bitmap。令人高兴的是,让operator=具备“异常安全性”往往自动获得“自我赋值安全”的回报。本条款只要你注意“许多时候一群精心安排的语句就可以导出异常安全(以及自我赋值安全)的代码” ,这就够了。例如以下代码,我们只需注意在复制pb所指东西之前别删除pb:
1 2 3 4 5 6 7 |
Widget& Widget::operator=(const Widget& rhs) { Bitmap* pOrig = pb; //记住原先的pb pb = new Bitmap(*rhs.pb); //令pb指向*pb的一个复件(副本) delete pOrig; //删除原先的pb return *this; } |
在operator=函数内手工排列语句(确保代码不但“异常安全”而且“自我赋值安全” )的一个替代方案是,使用所谓的copy and swap技术:
1 2 3 4 5 6 7 8 9 10 11 12 |
class Widget { ... void swap(Widget& rhs); //交换*this和rhs的数据;详见条款29 ... }; Widget& Widget::operator=(const Widget& rhs) { Widget temp(rhs); //为rhs数据制作一份复制(副本) swap(temp); //将*this数据和上述复件的数据交换。 return *this; } |
- 确保当对象自我赋值时operator=有良好行为。 其中技术包括比较 “来源对象” 和 “目标对象” 的地址、精心周到的语句顺序、以及copy-and-swap。
- 确保任何函数如果操作一个以上的对象,而其中多个对象是同一个对象时,其行为仍然正确。
TOP条款12:复制对象时勿忘其每一个成分
设计良好之面向对象系统(OO-systems)会将对象的内部封装起来,只留两个函数负责对象拷贝(复制),那便是带着适切名称的copy构造函数和copy assignment操作符,我称它们为copying函函函数数数。
任何时候只要你承担起 “为derived class撰写copying函数 “的重责大任,必须很小心地也复制其base class成分。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 |
class Customer { ... }; class PriorityCustomer: public Customer { public: PriorityCustomer(const PriorityCustomer& rhs); PriorityCustomer& operator=(const PriorityCustomer& rhs); ... private: int priority; }; PriorityCustomer::PriorityCustomer(const PriorityCustomer& rhs) : Custormer(rhs), priority(rhs.priority) {} PriorityCustomer& PriorityCustomer::operator=(const PriorityCustomer& rhs) { Customer::operator=(rhs); priority = rhs.priority; return *this; } |
当你编写一个copying函数,请确保(1)复制所有local成员变量,(2)调用所有baseclasses内的适当的copying函数。
- Copying函数应该确保复制 “对象内的所有成员变量” 及 “所有base class成分”。
- 不要尝试以某个copying函数实现另一个copying函数。应该将共同机能放进第三个函数中(通常叫init),并由两个copying函数共同调用。
TOP条款13:以对象管理资源
善于使用智能指针,为了防止资源泄漏,请使用RAII对象,他们在构造函数中获得资源并在析构函数中释放资源。
两个常用的RAII classes是tr1::shared_ptr和auto_ptr。前者往往是较佳选择(通过引用计数器实现),后者不支持指针的拷贝和复制,复制动作会使它(被复制物)指向null。
类似的Boost库中的boost::scoped_array和boost::shared_array类也可以提供类似功能
1 2 3 4 5 6 7 8 9 10 |
void test_fun() { std::auto_ptr<Child> aptr_c(new Child("Child 1")); Child *ptr = new Child("Child 2"); aptr_c.get()->func(); // 尽量使用显示转换,防止不合适的隐式转换 (*aptr_c).func(); // 使用隐式转换,可以增强可读性 std::auto_ptr<Child> aptr_c2 = aptr_c; // will set aptr_c to null // aptr_c->func(); failed. aptr_c is null. delete ptr; } |
智能指针auto_ptr可以在生命期结束时自动销毁返回资源。尽量使用智能指针保存factory函数返回的指针。
此外,利用栈对象自动销毁调用析构函数的特性,可以将需要释放的资源根据作用域封装在栈对象中,此栈对象即为一个资源管理对象,对应类为资源管理类。
TOP条款14:在资源管理类中小心coping行为
禁止复制。许多时候允许资源管理对象被复制是不合理的。应该将coping行为声明为private。
对底层资源使用“引用计数器法”。
tr1::shared_ptr是一个绝好的实现手段,这个类在vs2005的库中还没有被加入,vs2008的c++标准库包含了这个类,当然tr1的大部分类实现来自于boost,这个也不例外。
1 2 3 4 5 6 7 8 9 10 11 12 13 |
class Lock{ public: explicit Lock(Mutex* pm) : mutexPtr(pm) { lock(mutexPtr.get()); } /* 我们并没有忘记析构函数,默认的析构函数会自动调用每个非static类变量的析构函数 当mutexPtr 的引用计数为0就会调用智能指针的删除器 */ private: std::tr1::shared_ptr<Mutex> mutexPtr; }; |
TOP条款15:析构函数里对指针成员调用delete
删除空指针是安全的,因此在析构函数里可以简单的delete类的指针成员,而不用担心他们是否被new过。
标准C++规定,delete空指针是完全合法的,不会引发任何问题。
TOP资源管理
TOP条款16:成对使用new和delete时要采取相同形式
当你使用new(也就是通过new动态生成一个对象),有两件事发生。第一,内存被分配出来(通过名为operator new的函数,见条款49和条款51) 。第二,针对此内存会有一个(或更多) 构造函数被调用。当你使用delete,也有两件事发生:针对此内存会有一个(或更多)析构函数被调用,然后内存才被释放。delete的最大问题在于:即将被删除的内存之内究竟存有多少对象?
单一对象的内存布局一般而言不同于数组的内存布局。数组所用的内存通常还包括“数组大小”的记录,以便delete知道需要调用多少次析构函数。单一对象的内存则没有这笔记录。
如果你使用delete时加上中中中括括括号号号(((方方方括括括号号号))),delete便认定指针指向一个数组,否则它便认定指针指向单一对象:
1 2 3 4 5 |
std::string* stringPtr1 = new std::string; std::string* stringPtr2 = new std::string[100]; ... delete stringPtr1; //删除一个对象 delete [] stringPtr2; //删除一个由对象组成的数组 |
如果你调用new时使用[],你必须在对应调用delete时也使用[]。如果你调用new时没有使用[],那么也不该在对应调用delete时使用[]。
这个规则对于喜欢使用typedef的人也很重要。考虑下么这个typedef:
1 2 3 4 5 6 |
typedef std::string AddressLines[4]; std::string* pal = new AddressLines; //注意, “new AddressLines” 返回一个string*, //就像 “new string[4]” 一样。 ... delete pal; //行为未有定义! delete [] pal; //很好。 |
为避免此类错误,最好尽量不要对数组形式做typedef动作。
- 如果你调用new时使用[],你必须在对应调用delete时也使用[]。 如果你调用new时没有使用[],那么也不该在对应调用delete时使用[]。
TOP设计与声明
TOP条款19:设计class犹如设计type
C++就像在其他OOP(面向对象编程)语言一样,当你定义一个新class,也就定义了一个新type。你并不只是class设计者,还是type设计者。
那么,如何设计高效的classes呢?首先你必须了解你面对的问题。几乎每一个class都要求你面对一下提问,而你的回答往往导致你的设计规范:
- 新type的对象应该如何被创建和销毁?
- 对象的初始化和对象的赋值该有什么样的差别?
- 新type的对象如果被passed by value,意味着什么?
- 什么是新type的“合法值” ?
- 你的新type需要配合某个继承图系(inheritance graph)吗?
- 你的新type需要什么样的转换?
- 什么样的操作符和函数对此新type而言是合理的?
- 什么样的标准函数应该驳回?
- 该谁取用新type的成员?
- 什么是新type的“未声明接口” (undeclared interface)?
- 你的新type有多么一般化?
- 你真的需要一个新type吗?
TOP条款20:宁以pass-by-reference-to-const替换pass-by-value
Pass-by-reference-to-const的效率高得多,没有任何构造函数或析构函数被调用,因为没有任何新对象被创建。
以by reference方式传递参数也可以避免slicing (对象切割) 问题。 当一个derived class对象以by value 方式传递并被视为一个base class对象,base class的copy构造函数会被调用,而“造成此对象的行为像个derived class 对象”的那些特化性质全被切割掉了,仅仅留下一个base class对象。
references往往以指针实现出来,因此pass by reference通常意味真正传递的是指针。因此如果你有个对象属于内置类型(例如int),pass by value往往比pass by reference的效率高些。这个忠告也适用于STL的迭代器和函数对象。
并不是小型types都是pass-by-value的合格候选人,对象小并不就意味其copy构造函数不昂贵。即使小型对象拥有并不昂贵的copy构造函数,还是可能有效率上的争议。某些编译器对待 “内置类型” 和 “用户自定义类型” 的态度截然不同,纵使两者拥有相同的底层表述(underlying representation) 。
“小型的用户自定义类型不必然成为pass-by-value优良候选人”的另一个理由是,作为一个用户自定义类型,其大小容易有所变化。
一般而言,你可以合理假设“pass-by-value并不昂贵”的唯一对象就是内置类型和STL的迭代器和函数对象。
- 尽量以pass-by-reference-to-const替换pass-by-value。 前者通常比较高效, 并可避免切割问题(slicing problems) 。
- 以上规则并不适用于内置类型,以及STL的迭代器和函数对象。 对它们而言,pass-by-value往往比较适当。
TOP条款22:将成员变量声明为private
将成员变量声明为private。protected不比public更具有封装性。因为更改了protect变量将会影响虽有的derived class
TOP条款23:宁以non-member、non-friend替换member函数(尤其是需要满足交换律的operator)
C++不是纯面向对象语言,不是所有的函数都定义在类中。
你不需要强行将所有函数定义在class内,因为这会造成class庞大的体积,而不同的用户又可能对不同的方法感兴趣。适当的拆分是有好处的。
将所有的便利函数(uitlity funcation)放在多个头文件中,但隶属于同一个命名空间是C++标准库的组织方式。
因为在意封装而让函数成为class的non-member函数不意味它不可以是另一个class的member,它可以是某个工具类的static member函数。这让纯面向对象思维的java程序员感觉比较习惯。
请记住:
- 宁可拿non-member non-friend函数替换member函数。
- 这样做可以增加封装性、包裹弹性(packaging flexibility)和机能扩充性
TOP条款24:若所有参数皆需类型转换,请为此采用non-member函数
此条款一般用于实现operator时,为了满足交换律和调用隐式类型转换将operator在类外实现。是否声明为friend看需要。
TOP实现
TOP条款26:尽可能延后变量定义式的出现时间
只要你定义了一个变量而其类型带有一个构造函数或析构函数,那么当程序的控制流(control °ow)到达这个变量定义式时,你便得承受构造成本;当这个变量离开其作用域时,你便得承受析构成本。
你不只应该延后变量的定义,直到非得使用该变量的前一刻为止,甚至应该尝试延后这份定义直到能够给它初值实参为止。如果这样,不仅能够避免构造(和析构)非必要对象,还可以避免无意义的default构造行为。
“但循环怎么办? ” 如果变量只在循环内使用,那么把它定义于循环外并在每次循环迭代时赋值给它比较好,还是该把它定义于循环内?
1 2 3 4 5 6 7 |
//方法A:定义于循环内 Widget w; for(int i = 0; i < n; ++ i ) { w = 取决于i的某个值; ... } |
1 2 3 4 5 6 |
//方法B:定义于循环外 for(int i = 0; i < n; ++ i ) { Widget w(取决于i的某个值) ; ... } |
如果classes的一个赋值成本低于一组构造+析构成本,做法A大体而言比较高效。否则做法B或许较好。此外,做法A 造成名称w的作用域(覆盖整个循环)比做法B更大,有时会对程序的可理解性和易维护性造成冲突。因此除非(1) 你知道赋值成本比 “构造+析构”成本低,(2) 你正在处理代码中效率高度敏感(performance-sensitive) 的部分,否则你应该使用做法B。
- 尽可能延后变量定义式的出现。 这样做可增加程序的清晰度并改善程序效率。
TOP条款28:避免返回handles指向对象内部成分
References、指针和迭代器都是所谓的handles(号码牌,用来取得某个对象) 。返回一个“代表对象内部数据”的handle,随之而来的便是“降低对象封装性”的风险。
对象的“内部”是指它的成员变量和不被公开使用的成员函数(也就是被声明为protected或private者),因此也要留心不要返回它们的handles。
返回handles时可能导致dangling handles(空悬的号码牌):这种handles所指东西(的所属对象) 不复存在。这种 “不复存在的对象” 最常见的来源就是函数返回值。这里唯一的关键是,有个handle被传出去了,一旦如此你就是暴露在“handle比其所指对象更长寿”的风险下。
这并不意味着你绝对不可以让成员函数返回handle。有时候你必须那么做。例如operator[]就允许你 “摘采” strings和vectors的个别元素, 而这些operator[]就是返回references指向“容器内的数据” (见条款3),那些数据会随着容器被销毁而销毁。尽管如此,这样的函数毕竟是例外,不是常态。
- 避免返回handles(包括references、指针和迭代器)指向对象内部。 遵守这个条款可增加封装性,帮助const成员函数的行为像个const,并将发生“虚吊号码牌” (danglinghandles)的可能性降至最低。
TOP条款30:透彻了解inlining的里里外外
Inline只是对编译器的一个申请,不是强制命令。
Inline函数通常一定被置于头文件内,因为大多数建置环境(build environments) 在编译过程中进行inlining,而为了将一个“函数调用”替换为“被调用函数的本体” ,编译器必须知道那个函数长什么样子。
Templates通常也被置于头文件内,因为它一旦被使用,编译器为了将它具现化,需要知道它长什么样子。 (某些建置环境可以再连接期才执行template具现化。 )
大部分编译器拒绝将太过复杂(例如带有循环或递归)的函数inlining,而所有对virtual函数的调用(除非是最平淡无奇的)也都会使inlining落空。因为virtual意味“等待,直到运行期才确定调用哪个函数” ,而inline意味 “执行前,先将调用动作替换为被调用函数的本体”.
如果程序要取某个inline函数的地址,编译器通常必须为此函数生成一个outlined函数本体。毕竟编译器不可能提出一个指针指向并不存在的函数。与此并提的是,编译器通常不对“通过函数指针而进行的调用”实施inlining,这意味对inline 函数的调用有可能被inlined,也可能不被inlined,取决于该调用的实施方式。
1 2 3 4 5 |
inline void f() { ... } //假设编译器有意愿inline “对f的调用” void (*pf) () = f; //pf指向f ... f(); //这个调用将被inlined,因为它是一个正常调用。 pf(); //这个调用或许不被inlined,因为它通过函数指针达成。 |
实际上构造函数和析构函数往往是inlining的糟糕候选人,因为编译器在编译期间会安插一些代码到程序中。
程序库设计者必须评估“将函数声明为inline”的冲击:inline函数无法随着程序库的升级而升级。一旦一个inline函数被修改,所有用到此函数的程序都必须被重新编译。但对于non-inline函数,一旦它有任何修改,只需重新连接就好。
若从纯粹实用观点出发,有一个事实比其他因素更重要:大部分调试器面对inline函数都束手无策。毕竟你如何在一个并不存在的函数内设立断点呢?
- 将大多数inlining限制在小型、 被频繁调用的函数身上。 这可使日后的调试过程和二进制升级(binary upgradability)更容易,也可使潜在的代码膨胀问题最小化,使程序的速度提升机会最大化。
- 不要只因为function templates出现在头文件,就将它们声明为inline。
TOP条款31:将文件间的编译依赖性降至最低
- 如果可以使用对象的引用和指针,就要避免使用对象本身。定义某个类型的引用和指针只会涉及到这个类型的声明,定义此类型的对象则需要类型定义的参与。
- 尽可能使用类的声明,而不使用类的定义。因为在声明一个函数时,如果用到某个类,是绝对不需要这个类的定义的,即使函数是通过传值来传递和返回这个类。
- 不要在头文件中再包含其它头文件,除非缺少了它们就不能编译。相反,要一个一个地声明所需要的类,让使用这个头文件的用户自己去包含其它的头文件。
- 最后一点,句柄类和协议类都不大会使用类联函数。使用任何内联函数时都要访问实现细节,而设计句柄类和协议类的初衷正是为了避免这种情况。
TOP继承与面向对象设计
TOP条款33:避免遮掩继承而来的名称
C++的名称遮掩规则(name-hiding rules)所做的唯一事情就是:遮遮遮掩掩掩名名名称称称。对于继承来说,derive class作用域被嵌套在base class作用域内。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
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(); void mf3(); void mf4(); ... }; |
以作用域为基础的 “名称遮掩规则” 并没有改变, 因此base class内所有名为mf1和mf3的函数都被derived class内的mf1和mf3函数遮掩掉了。
1 2 3 4 5 6 7 8 |
Derived d; int x; ... d.mf1(); //没问题,调用Derived::mf1 d.mf1(x); //错误!因为Derived:mf1遮掩了Base::mf1 d.mf2(); //没问题,调用Base::mf2 d.mf3(); //没问题,调用Derived::mf3 d.mf3(x); //错误!因为Derived::mf3遮掩了Base::mf3 |
如你所见,上述规则都适用,即使base classes和derived classes内的函数有不同的参数类型也适用,而且不论函数是virtual或non-virtual一体适用。
不幸的是你通常会想继承重载函数。实际上如果你正在使用public继承而又不继承那些重载函数,就是违反base和derived classes之间的is-a关系,而条款32说过is-a是public继承的基石。
你可以使用using声明式达成目标:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 |
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的所有东西 using Base::mf3; //在Derived作用域内都可见(并且public) virtual void mf1(); void mf3(); void mf4(); ... }; |
现在,继承机制将一如往昔地运作:
1 2 3 4 5 6 7 8 |
Derived d; int x; ... d.mf1(); //仍然没问题,调用Derived::mf1 d.mf1(x); //现在没问题了,调用Derived::mf1 d.mf2(); //仍然没问题,调用Base::mf2 d.mf3(); //没问题,调用Derived::mf3 d.mf3(x); //现在没问题了,调用Derived::mf3 |
这意味如果你继承base classes并加上重载函数,而你又希望重新定义或覆写(推翻)其中一部分,那么你必须为那些原本会被遮掩的每个名称引人一个using声明式,否则某些你希望继承的名称会被遮掩。
- derived classes内的名称会遮掩base classes内的名称。在public继承下从来没有人希望如此。
- 为了让被遮掩的名称再见天日,可使用using声明式或转交函数(forwarding func-tions) 。
TOP条款34:区分接口继承和实现继承
pure virtual函数、impure virtual函数、non-virtual函数之间的差异,使得你得以精确指定你想要derived class继承的东西:只继承接口(pure virtual),或是继承接口和一份缺省实现(virtual),或是继承接口和一份强制实现(non-virtual)。
pure virtual函数只具体指定接口继承
impure virtual函数具体指定接口继承及实现继承
non-virtual 函数具体指定接口继承以及强制性实现继承,non-virtual函数会为该class建立起一个不变性,凌驾其特异性。你最好绝不重新定义继承而来的non-virtual函数。
否则如:
1 2 3 4 5 6 7 8 9 10 11 12 |
class D { public: void mf(); } class D : public B { / *… */ }; D x; D *pD = &x;; B *pB = &x; pD->mf(); pB->mf(); |
这两个调用将调用不同的mf,因为mf是静态绑定的,而不像virtual是动态绑定的。
就是因为同样的道理,一个derived class绝不应该重新定义一个继承而来的non-virtual析构函数。
TOP条款35:考虑virtual函数以外的其他选择
Non-Virtual Interface手法
一个有趣的思想流派主张virtual函数应该总是被实现为private。这一基本设计,也就是“令客户通过public non-virtual函数间接调用private virtual函数”,称为non-virtual interface手法。是template method设计模式的一个特例。Effective C++的作者把这个non-virtual函数成为virtual函数的wrapper。
NVI函数的一个优点是可以在执行virtual函数的特化行为之前和之后,做一些公共的初始化和清理工作。如果让客户直接调用virtual函数就没有任何好办法做这些事。
有些事情看似比较怪异,你在derived class中实现一个derived class从不调用的virtual函数。但这并不存在矛盾。重新定义表示某些事情如何被完成,而调用他们表示何时被完成,base class只是保留了“函数合适被调用的”权利。一开始这些听起来有些诡异,但是C++这种derived class”可重新定义被继承来的private virtual函数“的规则完全合情合理。
另外一种情况,使用Strategy模式
使用函数指针代替virtual函数,这样是Strategy模式的一个简单应用。这样做的优点是同一个类型的实体可以有不同的运算实现方式(实体作为参数传入)。但这样做,指针指向的函数只能访问public内容,如果要访问private内容只能降低封装。
便利设施:借由boost和tr1::funcation实现Strategy模式会得到高于函数指针的弹性(flexibility)。
TOP条款36:绝不重新定义继承而来的non-virtual 函数
举例说明:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
class B { public: void mf(); ... }; class D: public B { public: void mf(); ... }; D x ; B* pB = &x ; //获得一个指针指向x D* pD = &x ; //获得另一个指针指向x pB->mf() ; // 调用B::mf pD->mf() ; // 调用D::mf |
你可能会相当惊讶。毕竟两者都通过对象x 调用成员函数mf。造成这种两面行为的原因是,non-virtual 函数如B::mf 和D::mf 都是静态绑定(statically bound) 。这意味着通过pB 调用的non-virtual 函数永远是B 所定义的版本,即使pB 指向一个类型为“B 派生之class”的对象。
但另一方面,virtual 函数却是动态绑定(dynamically bound) 。如果mf 是个virtual 函数,不论是通过pB 或pD 调用mf,都会导致调用D::mf,因为pB 和pD 真正指的都是一个类型为D 的对象。
TOP条款37:绝不重新定义继承而来的缺省参数值
既然重新定义一个继承而来的non-virtual 函数永远是错误的(见条款36),所以我们将本条款局限于“继承一个带有缺省参数值的virtual 函数”。
对象的所谓静态类型(static type),就是它在程序中被声明时所采用的类型。
1 2 3 4 5 6 7 8 9 10 11 12 13 |
class Shape { public: enum ShapeColor f Red, Green, Blue g; virtual void draw( ShapeColor color = Red ) const = 0 ; ... }; class Rectangle : public Shape { public: //注意,赋予不同的缺省参数值。 这真糟糕! virtual void draw( ShapeColor color = Green ) const ; ... }; |
现在考虑这些指针:
1 2 |
Shape* ps ; //静态类型为Shape* Shape* pr = new Rectangle ; //静态类型为Shape* |
本例中ps 和pr 都被声明为pointer-to-Shape 类型,所以它们都以它为静态类型。不论它们真正指向什么,他们的静态类型都是Shape*。
对象的所谓动态类型 (dynamic type) 则是指 “目前所指对象的类型”。 也就是说,动态类型可以表现出一个对象将会有什么行为。以上例而言,pr 的动态类型是Rectangle*,ps没有动态类型,因为它尚未指向任何对象。
virtual 函数系动态绑定而来,意思是调用一个virtual 函数时,究竟调用哪一份函数实现代码,取决于发出调用的那个对象的动态类型:
1 |
pr->draw(Shape::Red) ; //调用Rectangle::draw(Shape::Red) |
virtual 函数是动态绑定,而缺省参数值却是静态绑定的。是你可能会在“调用一个定义于derived class 内的virtual 函数” 的同时,却使用base class 为它所指定的缺省参数值:
1 |
pr->draw( ) ; //调用Rectangle::draw(Shape::Red)! |
- 绝对不要重新定义一个继承而来的缺省参数值,因为缺省参数值都是静态绑定,而virtual 函数||你唯一应该覆写的东西||却是动态绑定。
TOP条款38:通过复合塑模出has-a 或“根据某物实现出”
复合(composition)是类型之间的一种关系,当某种类型的对象内含它种类型的对象,便是这种关系。
1 2 3 4 5 6 7 8 9 10 |
class Address { ... } ; class PhoneNumber { ... } ; class Person { public: ... private: Address address ; PhoneNumber voiceNumber ; }; |
本例之中Person对象由Address, PhoneNumber构成。 在程序员之间复合 (composition)这个术语有许多同义词,包括layering(分层),containment(内含),aggregation(聚合)和embedding(内嵌) 。
条款32曾说, “public继承”带有is-a (是一种)的意义。复合意味has-a (有一个)或is-implemented-in-terms-of(根据某物实现出)。
程序中的对象其实相当于你所塑造的世界中的某些事物,例如人、汽车、一张张视频画面等等。这样的对象属于应用域(application domain)部分。其他对象则纯粹是实现细节上的人工制品,像是缓冲区(bu®er) 、互斥器(mutexes) 、查找树(search trees)等等。这些对象相当于你的软件的实现域(implementation domain) 。当复合发生于应用域内的对象之间,表现出has-a 的关系;当它发生于实现域内则是表现is-implemented-in-terms-of的关系。
TOP条款39:明智而审慎地使用private继承
Private 继承意味着 is-implemented-in-terms of(根据某物实现出)。它通常比复合(组合)的级别低。但是,当derived class要重新定义继承而来的virtual函数时或者要访问protected base class的成员时,这种设计是合理的。
和复合不同,private继承可以造成empty base的最优化。这对致力于“对象尺寸最小化”的程序开发者(特别是嵌入式程序员)而言,可能很重要。
TOP条款40:明智而审慎地使用多重继承
使用virtual 继承的那些classes 所产生的对象往往比使用non-virtual 继承的兄弟们体积大,访问virtual base classes 的成员变量时,也比访问non-virtual base classes 的成员变量速度慢。总之,你得为virtual 继承付出代价。
支持“virtual base classes 初始化”的规则比起non-virtual bases 的情况远为复杂且不直观。virtual base 的初始化责任是由继承体系中的最底层(most derived)class 负责,中间层的classes 对其virtual bases 的初始化都将被屏蔽。也就是说当一个新的derived class加入到继承体系的底层时,它必须承担其virtual bases(不论直接或间接)的初始化责任。
在产生一个新的derived class 对象时,所有virtual bases 的构造函数总是先于所有non-virtual bases 的构造函数被调用。
我对virtual base classes(亦相当于对virtual 继承)的忠告是:
- 非必要不使用virtual bases 。
- 如果你必须使用virtual base classes,尽可能避免在其中放置数据。
如果你有个单一继承的设计方案,而它大约等价于一个多重继承设计方案,那么单一继承设计方案几乎一定比较受欢迎。
- 多重继承比单一继承复杂。 它可能导致新的歧义性,以及对virtual 继承的需要。
- virtual 继承会增加大小、速度、初始化(及赋值)复杂度等等成本。如果virtual base classes 不带任何数据,将是最具实用价值的情况。
- 多重继承的确有正当用途。其中一个情节涉及“public 继承某个Interface class”和 “private 继承某个协助实现的class” 的两相组合。
TOP模版和泛型编程
TOP条款42:了解typename的双重意义
声明template参数时,不论使用关键字class或typename,意义完全相同。下面两个template声明式完全相同:
1 2 |
template<class T> class Widget; // 使用 “class” template<typename T> class Widget; // 使用 “typename” |
template内出现的名称如果相依于某个template参数,称之为从属名称(dependentnames) 。如果从属名称在class 内呈嵌套状,我们称它为嵌套从属名称(nested depen-dent name) 。
嵌套从属名称有可能导致解析(parsing)困难。
1 2 3 4 5 6 |
template<typename C> void print2nd(const C& container) { C::const iterator* x; ... } |
看起来好像我们声明x为一个local变量,它是个指针,指向一个C::const iterator。但它之所以被那么认为, 只因为我们 “已经知道” C::const iterator是个类型。 如果C::const iterator不是个类型呢?如果C有个static成员变量而碰巧被命名为const iterator,或如果x碰巧是个global变量名称呢?那样的话上述代码就不再是声明一个local变量,而是一个相乘动作:C::const iterator乘以x。
C++有个规则可以解析(resolve)此一歧义状态:如果解析器在template中遭遇一个嵌套从属名称,它便假设这名称不是个类型,除非你告诉它是。所以缺省情况下嵌套从属名称不是类型。 此规则有个例外, 稍后我会提到。 所以下面的C++代码不是有效的:
1 2 3 4 5 6 |
template<typename C> //这是无效的C++代码 void print2nd(const C& container) { if( container.size() >= 2 ) f C::const iterator iter(container.begin()); //这个名称被假设为非类型 ... |
iter声明式只有在C::const iterator是个类型时才合理,但我们并没有告诉C++说它是,于是C++假设它不是。所以我们必须告诉C++说C::const iterator是个类型。只要紧邻它之前放置关键字typename即可:
1 2 3 4 5 6 7 8 |
template<typename C> //这是合法的C++代码 void print2nd(const C& container) { if( container.size() >= 2 ) { typename C::const iterator iter(container.begin()); ... } } |
typename只被用来验明嵌套从属类型名称;其他名称不该用它。
“typename必须作为嵌套从属类型名称的前缀词”这一规则的例外是,typename不可以出现在base classes list内的嵌套从属类型名称之前,也不可在member initializationlist(成员初值列)中作为base class修饰符。
1 2 3 4 5 6 7 8 9 10 11 12 |
template<typename T> class Derived: public Base<Y>::Nested { //base class list中不允许 “typename”。 public: explicit Derived(int x) : Base<T>::Nested(x) //mem.init.list中不允许 “typename”。 { typename Base<T>::Nested temp; //嵌套从属类型名称, //作为一个base class修饰符需加上typename。 ... } ... }; |
- 声明template参数时,前缀关键字class和typename可互换。
- 请使用关键字typename标识嵌套从属类型名称;但不得在base class lists(基类列)或member initialization list(成员初值列)内以它作为base class修饰符。
TOP条款44:将与参数无关的代码抽离templates
在大多数平台上,所有指针类型都有相同的二进制表述,因此凡templates持有指针者(例如list<int*>,list<const int*>等等)往往应该对每一个成员函数使用唯一一份底层实现。这很具代表性地意味,如果你实现某些成员函数而它们操作强型指针(stronglytyped pointers,即T*),你应该令它们调用另一个操作无类型指针(untyped pointers,即void*)的函数,由后者完成实际工作。
- Templates生成多个classes和多个函数,所以任何template代码都不该与某个造成膨胀的template参数产生相依关系。
- 因非类型模版参数(non-type template parameters)而造成的代码膨胀,往往可消除,做法是以函数参数或class成员变量替换template参数。
- 因类型参数 (type parameters)而造成的代码膨胀, 往往可降低, 做法是让带有完全相同二进制表述(binary representations)的具现类型(instantiation types)共享实现码。
TOPMore Effective C++ 规则
TOP基础议题
TOP条款1:区分指针和引用
仔细区别pointers和references 指针定义不一定非得初始化,能置NULL;引用只是变量别名,必须初始化并且初始化后不能更改。 应用技巧:防止默认拷贝构造.
以下情况应该使用引用->
- 当知道要指代某个对象并且不会再指代其他的东西
- 当实现某些操作符时,如果这些操作符在语义上的要求使得指针不可行。(如operator[])
以下情况应该使用指针->
- 当知道要指代有可能什么也不指向
- 当有可能在不同时候指向不同的对象,即更改指针的指向
TOP条款2:优先考虑C++风格的类型转换
最好使用C++转型操作符 static_cast, 不涉及继承机制的型别执行转型动作 const_cast, 更改表达式中的常量性(constness)或变易性(volatileness) dynamic_cast, 继承体系中的型别转换,不能应用在缺乏虚函数的型别上,具有类型检查功能,比其他关键字安全 reinterpret_cast,与编译平台息息相关,不具有移植性。最常用途是函数指针型别转换。
- static_cast: 类似于C风格的类型转换
- const_cast: 去掉一个对象的const属性
- dynamic_cast: 针对一个继承体系做向下或者横向的安全转换
- reinterpret_cast:在函数指针之间进行类型转换(几乎是不可移植的)
TOP条款3:决不要把多态用于数组
绝不要以多态(polymorphically)方式处理数组 因为对数组的访问是通过指针位移来实现,必须准确知道每一个对象的大小,而继承后编译器没法确定对象大小(一般继承对象比base对象大)
由于派生类对象一般要比基类对象大,所以针对多态使用指针运算很可能会出问题。而因为数组操作几乎总是要涉及指针运算,所以数组和多态也不能一起使用。当delete [] array时就会出问题。
TOP条款4:避免不必要的默认构造函数
非必要不提供 default constructor 不提供会有一定的使用限制,比如想在heap构建数组对象时;提供了无意义的会影响classes效率,根据事实需要定义。
通常的类都应该有一个默认构造函数,否则
- 很难创建一个栈上的对象数组(TheClass classArray[10])
- 无法作为许多基于模板的容器类的类型参数使用,因为模板内部需要创建关于模板参数类型的数组(同①)。
- 导致没有默认构造函数的虚基类要求所有它的派生类都必须知道、理解虚基类构造函数的参数的含义并且提供这些参数,无论继承层次有多远。
但是没有当足够信息时去完全初始化一个对象会使得其他成员函数变得复杂,因为必须检测变量是否真的有意义。提供无意义的默认构造函数也会影响类的运行效率。
TOP操作符
TOP条款5:小心用户自定义的转换函数
对定制的[型别转换函数]保持警觉 存在数据遗失的型别转换 出现意想不到的结果不容易察觉. string.c_str() = char* 构造函数导入 explicit 关键字
编译器可能会在你根本不希望、想不到的时候调用一个隐式类型转换函数。如:
1 2 3 4 5 6 7 |
class Rational{ public: …… operator double() const; }; Rational r(1,2); //r = 1/2 cout << r; //Rational has not operator<< |
此时编译器并不会报错,而是自作主张找到operator double使得整个调用成功。 解决方法:
1 2 3 4 5 |
class Rational{ public: …… double asDouble() const; }; |
则必须显式调用。正如STL的string的成员函数c_str。
例子①可以通过不声明类型转换运算符来避免,但下面这个更夸张的例子②:
1 2 3 4 5 6 7 8 9 10 11 12 13 |
template<class T> class Array { public: Array(int lowBound, int highBound); Array(int size); //单个参数的构造函数,maybe evil T& operator[](int index); ...... }; bool operator==(const Array<int>& lhs, const Array<int>& rhs) { Array<int> a(10); Array<int> b(10); ...... if (a==b[i]) {...} //a should have been a[i] }; |
a的下标打掉了,但编译器并不会报错,而是会生成以下代码:
if (a==static_cas<Array<int> >(b[i])) {...}来全调用成功(注意这里最后两个>符号之间有个空格,why?)。这句代码以b[i]生成了一个临时数组。
单个参数的构造函数的问题只能通过explict关键字来克服。
1 2 3 4 5 6 7 |
template<class T> class Array { public: ...... explict Array(int size); T& operator[](int index); ...... }; |
此外还可以通过一种代理类的技术来重新构造类:
1 2 3 4 5 6 7 8 9 10 11 12 13 |
template <class T> class Array { public: class ArraySize { public: ArraySize(int numElements): theSize(numElements) {} int size() const {return theSize;} private: int theSize; }; Array(int lowBound, int highBound); Array(ArraySize size); //注意参数的不同 ...... }; |
Array<int> a(10)仍然能够通过,会将int转化为ArraySize。但a==b[i]则不能通过,因为为了使得调用成功,会需要一个Array<int>在==的右侧,于是要先将int(即b[i])转换成ArraySize,再将ArraySize转换为Array<int>,但第二个转换是不允许的。
建议:允许编译器进行隐式类型转换通常是弊大于利的,所以除非确实需要,否则不要提供类型转换函数。
TOP条款6:区分自增运算符和自减运算符的前缀形式与后缀形式
区别increment/decrement操作符的前置(prefix)和后置(postfix) 这个就不说了,按照标准来,不要乱来,特别是违反直觉的那种。比如 i++++(用const返回值能避免);
1 2 3 4 5 6 7 8 9 10 |
class UPInt{ public: UPInt& operator++(); //++i const UPInt operator++(int); //i++,postfix UPInt& operator--(); //--i const UPInt operator--(int); //i--,postfix UPInt& operator+=(int); }; |
后缀形式调用时,编译器会悄悄地传递一个0作为参数。而这个int参数实际上是不起作用的,仅仅是为了区分前缀与后缀。后缀形式必须在内部创建一个临时对象返回。后缀形式的const是为了防止链式后缀(如i++++)
TOP条款7:不要重载"&&"、"||"和","
千万不要重载&&,||和,操作符 就是非必要不要重载操作符
因为operator&&(expression1, expression2)与operator||(expression1, expression2)均无法保证expression1和expression2之中哪个先被求值(实际上都被求值),因此无法使用C、C++的“短路求值法”,因此它是从左到右求值的。同样,如果重载逗号运算符,也无法保证从左到右的顺序求值。
TOP条款8:理解new和delete在不同情形下的含义
了解各种不同意义的new 和 delete new :分两步:先用operator new纯粹分配内存,然后在调用构造函数,为分配的内存设定初始值。 delete:道理同上,先析构,然后调用 operator delete 将以上步骤拆开,有时候想在分配好的内存上调用constructor时: placement new. e.g. widget* constructWidgetInBuffer(void* buffer, int widgetsize) { return new(buffer) Widget(widgetsize); }
① operator new ->其唯一职责是分配内存,它对构造函数一无所知。这就是operator new与new最本质的区别。
operator new会返回一个未经处理的指针,而new操作符会将这个指针转换为一个对象。当编译器看到语句:
string *ps = new string("Memory Management")时,它会生成如下的代码:
1 2 3 4 5 6 |
//这里是operator new void *memory = operator new(sizeof(string)); //这段代码只有编译器可调用,实质为构造函数 call string::string("Memory Management") on *memory; //转换指针类型 string *ps = static_cas<string*>(memory); |
小结:如果你仅仅想分配内存,就调用operator new,它不会调用构造函数。如果想定制在堆对象被建立时的内存分配过程,应该写一个自己的operator new函数,然后使用new操作符,new会调用你定制的operator new。
② placement new ->在一块已经有指针指向的内存里建立一个对象:
1 2 3 |
class Widget {...} Widget *constructWidgetInBuffer(void *buffer, int widgetSize) {return new(buffer) Widget(widgetSize);} |
小结:如果想在一块已经有指针指向的内存里建立一个对象,用placement new。
③与①相对应,operator delete仅仅负责释放内存,而delete操作符会调用operator delete,然后再调用析构函数。当编译器看到语句:delete ps;时,它会生成如下的代码:
1 2 |
ps->~string(); //调用析构函数 operator delete(ps); //释放内存 |
小结:如果只想处理原始的、未被初始化的内存,应该完全绕过new和delete操作符,而是通过直接调用operator new获得内存和operator delete释放内存。
1 2 3 |
void *buffer = operator new(50*sizeof(char)); ... operator delete(buffer); |
所以如果使用placement new在内存中创建对象,应该避免对这块内存使用delete操作符,因为delete是通过调用operator delete来释放内存,但这块内存最初不是由operator new分配的。此时应该显式释放对象(通常放在对象的析构函数中,即显式调用析构函数)。
④ operator new[]与operator delete[]对应。
1 2 |
string *ps = new string[10]; delete []ps; |
TOP异常
TOP条款9:使用析构函数防止资源泄漏
利用destructors避免泄露内存 为了防止程序在delete object前掉了而内存泄露,可以使用智能指针:在指针对象离开其scope就自动析构。
通过智能指针auto_prt将清除代码放入其析构函数里。核心部分如下:
1 2 3 4 5 6 7 8 9 |
template<class T> class auto_ptr { public: auto_ptr(T *p=0): ptr(p) {} ~auto_ptr() {delete ptr;} private: T *ptr; }; |
(因为auto_ptr的析构函数使用的是用于删除单个对象的delete,所以它不能用于指向对象数组的指针。这时个用vector代替array可能是更好的选择。)
auto_ptr背后的思想可以用于所有防止在异常被抛出时动态分配的资源泄漏问题,如下面的GUI程序:
1 2 3 4 5 6 7 8 9 10 11 12 13 |
class WindowHandle { public: WindowHandle(WINDOW_HANDLE handle): w(handle) {} ~WindowHandle() {destroyWindow(w);} operator WINDOW_HANDLE() {return w;} //* private: WINDOW_HANDLE w; //以下明确禁止掉赋值和拷贝,Item28有更flexible的实现 WindowHandle(const WindowHandle&); WindowHandle& operator=(const WindowHandle&); }; |
TOP条款10: 防止构造函数里的资源泄漏
在constructors内阻止资源泄露 构造函数内的异常处理,有资源分配的那种,这个时候使用auto_ptr会使程序阅读性强很多... 所以构造函数当中分配的资源最好自己内部能够析构。
C++只销毁构造完全的对象,即其构造函数被完全执行的对象。所以如果构造函数运行的过程中抛出了一个异常,那对象的析构函数将不会被调用。
先看非静态成员:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 |
class BookEntry { public: BookEntry(const string& name, const string& address = "", const string& imageFileName = "", const string& audioClipFileName = ""); ~BookEntry(); ... private: string theName; string theAddress; list<PhoneNumber> thePhones; Image *theImage; AudioClip *theAudioClip; void cleanup(); //公共清除语句 }; void BookEntry::cleanup() {//为了防止构造函数与析构函数中的清除语句重复而提取成一个公共函数 delete theImage; delete theAudioClip; } BookEntry::BookEntry(const string& name, const string& address = "", const string& imageFileName = "", const string& audioClipFileName = "") :theName(name), theAddress(address), theImage(0), theAudioClip(0) { try { ...//给theImage和theAudioClip设置 } catch(...) { cleanup(); throw; //让调用者得知异常 } } BookEntry::~BookEntry() { cleanup(); } |
但是如果让theImage和theAudioClip都成为常量指针:
1 2 3 4 5 6 7 8 |
class BookEntry { public: ... private: ... Image * const theImage; AudioClip * const theAudioClip; }; |
则只能通过成员初始化列表对其进行初始化。于是不能再在构造函数中使用try, catch块来解决这个问题了。这时可使用auto_ptr来帮忙:
1 2 3 4 5 6 7 8 9 |
class BookEntry { public: ... private: ... const auto_ptr<Image> theImage; const auto_ptr<AudioClip> theAudioClip; //注意const与前面位置的不同 }; |
现在的初始化形式为:
1 2 3 4 5 6 7 8 9 10 |
BookEntry::BookEntry(const string& name, const string& address = "", const string& imageFileName = "", const string& audioClipFileName = "") :theName(name), theAddress(address), theImage(imageFileName != "" ? new Image(imageFileName) : 0), //注意这里new对象来赋值初始化的用法 theAudioClip(audioClipFileName != "" ? new AudioClip(audioClipFileName : 0) {} //构造函数与析构函数变得极为简单 BookEntry::~BookEntry() {} |
小结:如果把那些声明为指针的类成员替换成它们相应的auto_ptr对象(thy:在Effective C++ 3rd中已经进一步建议用std::tr1::shared_ptr),在发生异常的时候构造函数就可以避免资源泄漏,而且免去了在析构函数中手工释放资源的必要。使用它们不仅使程序让人更容易理解,而且使程序在发生异常的时候更为健壮。
TOP条款11:阻止异常传递到析构函数以外
禁止异常(exceptions)流程descriptors之外 析构函数在有可能出现异常的情况下,要保证其能捕捉到,比如try cathch。否则将导致程序调用terminate。
阻止异常传递到析构函数以外以两个很好的理由。首先,在异常传递进行到堆栈解开(stack-unwinding)的过程中,防止terminate被调用,从而防止程序被当掉。第二它能帮助确保析构函数总能完成我们希望它做的所有事情,比如后面可能的endTransaction。
1 2 3 4 5 6 7 8 |
Session::~Session() { try { logDestruction(this); } catch(...) {} endTransaction(); //如果有的话 } |
表面上看catch块什么也没做,但它阻止了logDestruction抛出的异常传递到Session的析构函数外面。
TOP条款12:理解抛出异常与传递参数或者调用虚函数之间的不同
了解[抛出异常],[传递参数],[调用一个虚函数]之间的差异 对象当做异常抛出时是发生拷贝的(静态型别的)...by value 方式有两个副本.所以抛出异常肯定比传递参数慢.. 抛出是采用first fit. 调用虚函数是 best fit。
TOP传递方式不同
① 你调用函数时,程序的控制权最终还会返回到函数的调用处(除非函数没有正常返回),但是当你抛出一个异常时,控制权永远不会回到抛出异常的地方。所以不论通过传值捕获异常还是通过引用捕获(不能通过指针捕获这个异常,因为类型不匹配)都将进行拷贝操作,也就说传递到catch子句中的是一份拷贝。
当异常对象被拷贝时,拷贝操作是由对象的拷贝构造函数完成的。该拷贝构造函数是对象的静态类型(static type)所对应类的拷贝构造函数,而不是对象的动态类型(dynamic type)对应类的拷贝构造函数。即不再有多态性:
1 2 3 4 5 6 7 8 9 |
class Widget { ... }; class SpecialWidget: public Widget { ... }; void passAndThrowWidget() { SpecialWidget localSpecialWidget; ... Widget& rw = localSpecialWidget; //rw引用SpecialWidget throw rw; //它抛出一个类型为Widget的异常,而非SpecialWidget } |
② 比较下面两个catch块:
1 2 3 4 5 6 7 8 9 10 |
catch (Widget& w) // 捕获Widget异常 { ... // 处理异常 throw; // 重新抛出异常,让它 } // 继续传递 catch (Widget& w) // 捕获Widget异常 { ... // 处理异常 throw w; // 传递被捕获异常的 } // 拷贝 |
这两个catch块的差别在于第一个catch块中重新抛出的是当前捕获的异常,而第二个catch块中重新抛出的是当前捕获异常的一个新的拷贝。第一个块中重新抛出的是当前异常(current exception),无论它是什么类型。特别是如果这个异常开始就是做为SpecialWidget类型抛出的,那么第一个块中传递出去的还是SpecialWidget异常,即使w的静态类型(static type)是Widget。这是因为重新抛出异常时没有进行拷贝操作。第二个catch块重新抛出的是新异常,类型总是Widget,因为w的静态类型(static type)是Widget。所以一般情况下应该使用throw;效率也更高。
③ 比较下面三个catch块:
1 2 3 |
catch (Widget w) ... //通过传值捕获异常,会两次创建被抛出对象的拷贝 catch (Widget& w) ... //通过传递引用捕获,会一次创建被抛出对象的拷贝 catch (const Widget& w) ... //通过传递指向const的引用,会一次创建被抛出对象的拷贝 |
通过指针抛出异常与通过指针传递参数是相同的。不论哪种方法都是一个指针的拷贝被传递。只要记住不要抛出一个指向局部对象的指针,因为当异常离开局部变量的生存空间时,该局部变量已经被释放。Catch子句将获得一个指向已经不存在的对象的指针。
TOP类型匹配的过程不同
如标准库中double sqrt(double); // from <cmath> or <math.h> 我们能这样计算一个整数的平方根,如下所示:
1 2 |
int i; double sqrtOfi = sqrt(i); |
但如下的try...catch却不能如你相像地工作:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
void f(int value) { try { if (someFunction()) { //如果 someFunction()返回真,抛出一个整形值 throw value; ... } } catch (double d) { //只处理double类型的异常,前面throw的int值永远不会在这里被catch ... } ... } |
不过在catch子句中进行异常匹配时可以进行两种类型转换。第一种是继承类与基类间的转换。一个用来捕获基类的catch子句也可以处理派生类类型的异常:
1 2 3 4 5 6 7 8 9 10 11 12 |
catch (runtime_error) ... // can catch errors of type catch (runtime_error&) ... // runtime_error, catch (const runtime_error&) ... // range_error, or // overflow_error catch (runtime_error*) ... // can catch errors of type catch (const runtime_error*) ... // runtime_error*, // range_error*, or // overflow_error* (range_error和overflow_error继承自runtime_error,而runtime_error又继承自exception) 第二种是允许从一个类型化指针(typed pointer)转变成无类型指针(untyped pointer),所以带有const void* 指针的catch子句能捕获任何类型的指针类型异常: catch (const void*) ... //捕获任何指针类型异常 |
TOP处理哲学不同
异常处理的策略是”最先匹配first fit“,而虚函数是”最优匹配best fit“(被调用的函数是属于离调用此函数的对象的动态类型最相近的类的)。
1 2 3 4 5 6 7 8 9 10 |
try { ... } catch (logic_error& ex) { // 这个catch块 将捕获 ... // 所有的logic_error } // 异常, 包括它的派生类 catch (invalid_argument& ex) { // 这个块永远不会被执行 ... //因为所有的invalid_argument异常 都被上面的catch子句捕获。 } |
(invalid_argument继承自logic_error) 因此:不要把处理基类异常的catch子句放在处理派生类异常的catch子句的前面。
1 2 3 4 5 6 7 8 9 |
try { ... } catch (invalid_argument& ex) { //处理 invalid_argument ... //异常 } catch (logic_error& ex) { //处理所有其它的 ... //logic_errors异常 } |
TOP条款13:通过引用捕获异常
以 by reference 方式捕捉 exceptions 捕捉方式分三种 by pointer: pointer 的scope难以把握,global或static型.不方便 catch by pointer 与四大标准异常有冲突: bad_alloc:operator new分配内存失败时 bad_cast:dynamic_cast针对引用操作失败 bad_typeid:dynamic_cast针对空指针 bad_exception:用于unexpected异常 by value 需要构造两次,还会有切割问题 by reference
捕获异常有三种方式:
TOP通过指针(by pointer)
在理论上这种方法的实现对于这个过程来说是效率最高的。因为在传递异常信息时,只有采用通过指针抛出异常的方法才能够做到不拷贝对象
1 2 3 4 5 6 7 8 9 |
void doSomething() { try { someFunction(); // 抛出一个 exception* } catch (exception *ex) { // 捕获 exception*; ... // 没有对象被拷贝 } } |
但为了能让程序正常运行,程序员定义异常对象时必须确保当程序控制权离开抛出指针的函数后,对象还能够继续生存,即全局与静态对象。
另一种抛出指针的方法是在建立一个堆对象(new heap object),但这会引发另一个问题:没办法知道异常对象该不该删除。通过指针捕获异常也不符合C++语言本身的规范。四个标准的异常――bad_alloc,bad_cast,bad_typeid和bad_exception都不是指向对象的指针,所以你必须通过值或引用来捕获它们。
TOP通过传值(by value)
可以消除①所带来的问题,但是当它们被抛出时系统将对异常对象拷贝两次。而且它会产生slicing problem,即派生类的异常对象被做为基类异常对象捕获时,那它的派生类行为就被切掉了(sliced off)。这样的sliced对象实际上是一个基类对象:它们没有派生类的数据成员,而且当调用它们的虚拟函数时,系统解析后调用的是基类对象的函数。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
void someFunction() // 抛出一个 validation异常 { ... if (a validation 测试失败) { throw Validation_error(); } ... } void doSomething() { try { someFunction(); //抛出 validation异常 } catch (exception ex) { //捕获所有标准异常类或它的派生类 cerr << ex.what(); //调用 exception::what(),而不是Validation_error::what() ... } } |
TOP通过引用(by reference)
通过引用捕获异常能使你避开上述所有问题。不象通过指针捕获异常,这种方法不会有对象删除的问题而且也能捕获标准异常类型。也不象通过值捕获异常,这种方法没有slicing problem,而且异常对象只被拷贝一次。
TOP条款14:审慎地使用异常规格
明智运用 exception specifications 在模板中不要使用exception specifications A fun 调用了 B fun时,B没有 exception specifications,A也不要设定,注意回调函数里... 处理系统可能抛出的exception(非预期的)
TOP条款15:理解异常处理所付出的代价
了解异常处理(exception handing)的成本 使用try时空开销将增加一般情况下的5%到10%,exception specification也和try有类似的开销,抛出异常退出函数的速度是正常退出函数速度的千分之一
三大开销分别为:
- 为了在运行时处理异常,程序要记录大量的信息。即使你没有使用try,throw或catch关键字,你同样得付出一些代价。
- 来自于try块,这还是假设程序没有抛出异常,这里讨论的只是在程序里使用try块的开销。
- 编译器为异常规格生成的代码与它们为try块生成的代码一样多,所以一个异常规格一般花掉与tyr块一样多的系统开销。
现在我们来到了问题的核心部分,看看抛出异常的开销。事实上我们不用太关心这个问题,因为异常是很少见的。80-20规则告诉我们这样的事件不会对整个程序的性能造成太大的影响。与一个正常的函数返回相比,通过抛出异常从函数里返回可能会慢三个数量级。这个开销很大。但是仅仅当你抛出异常时才会有这个开销,一般不会发生。但是如果你用异常表示一个比较普遍的状况,例如完成对数据结构的遍历或结束一个循环,那你必须重新予以考虑。
所以:只要可能尽量就采用不支持异常的方法编译程序,把使用try块和异常规格限制在你确实需要它们的地方,并且只有在确为异常的情况下(exceptional)才抛出异常。如果你在性能上仍旧有问题,总体评估一下你的软件以决定异常支持是否是一个起作用的因素。
TOP效率
TOP条款16:记住80-20准则
谨记80 - 20 法则 80%的程序资源用在20%的代码上,好的算法跟设计是性能的关键
80-20准则说的是大约20%的代码使用了80%的程序资源;大约20%的代码耗用了大约80%的运行时间;大约20%的代码使用了80%的内存;大约20%的代码执行80%的磁盘访问;80%的维护投入于大约20%的代码上;通过无数台机器、操作系统和应用程序上的实验这条准则已经被再三地验证过。80-20准则不只是一条好记的惯用语,它更是一条有关系统性能的指导方针,它有着广泛的适用性和坚实的实验基础。
软件整体的性能通常取决于程序中的一小部分代码。
正确的方法是用profiler程序识别出令人讨厌的程序的20%部分。不是所有的工作都让profiler去做。你想让它去直接地测量你感兴趣的资源。请记住profiler仅能够告诉你在某一次运行(或某几次运行)时一个程序运行情况,所以如果你用不具有代表性的输入数据profile一个程序,那你所进行的profile也没有代表型。相反这样做很可能导致你去优化不常用的软件行为,而在软件的常用领域,则对软件整体的效率起相反作用(即效率下降)。
要防止这种不正确的结果,最好的方法是用尽可能多的数据profile你的软件。
TOP条款17:考虑使用延迟计算
考虑使用 lazy evaluation (缓式评估) 比喻很生动,有些动作只在用之前来实现,挺有意思,可以深深考虑一下 mutable:可以被任何成员函数修改
即时计算(eager evaluation)是在函数被调用时计算。而提前计算(over-eager evaluation)是预计某个计算会被频繁调用时就可以设计一个数据结构高效地处理这些计算需求,这样可以降低每次计算需求的开销。
以下程序使用了标准模板库(STL)里的map对象作为本地缓存,这样就不需要每次从数据库中查询。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 |
int findCubicleNumber(const string& employeeName) { // 定义静态map,存储 (employee name, cubicle number) // pairs. 这个 map 是local cache。 typedef map<string, int> CubicleMap; static CubicleMap cubes; // try to find an entry for employeeName in the cache; // the STL iterator "it" will then point to the found // entry, if there is one (see Item 35 for details) CubicleMap::iterator it = cubes.find(employeeName); // "it"'s value will be cubes.end() if no entry was // found (this is standard STL behavior). If this is // the case, consult the database for the cubicle // number, then add it to the cache if (it == cubes.end()) { int cubicle = the result of looking up employeeName's cubicle number in the database; cubes[employeeName] = cubicle; // add the pair // (employeeName, cubicle) // to the cache return cubicle; } else { // "it" points to the correct cache entry, which is a // (employee name, cubicle number) pair. We want only // the second component of this pair, and the member // "second" will give it to us return (*it).second; } } |
(有一个代码细节需要解释一下,最后一个语句返回的是(*it).second,而不是常用的it->second。为什么?答案是这是为了遵守STL的规则。简单地说,iterator是一个对象,不是指针,所以不能保证”->”被正确应用到它上面。但是STL明确要求”.”和”*”在iterator上是合法的,所以(*it).second在语法上虽然比较繁琐,但是保证能运行。)
TOP条款18:分期摊还预期的计算开销
分期摊还预期的计算成本 缓存(caching) 内存分配策略 用好 lazy evaluation 和 eager evaluation...
TOP条款19:了解临时对象的来源
了解临时对象的来源 常量引用的参数隐身转换产生临时对象,函数返回值也可以。。 c++防止为 no-const reference 参数产生临时对象。否则临时对象值可能会被修改.. 这里要加强洞察。
在C++中真正的临时对象是看不见的,它们不出现在你的源代码中。一个无命名的非堆(non-heap)对象就是临时对象。这种未命名的对象通常在两种条件下产生:为了使函数成功调用而进行隐式类型转换和函数返回对象时。
仅当通过传值(by value)方式传递对象或传递常量引用(reference-to-const)参数时,才会发生这些类型转换。当传递一个非常量引用(reference-to-non-const)参数对象,就不会发生。
在任何时候只要见到常量引用(reference-to-const)参数,就存在建立临时对象而绑定在参数上的可能性。在任何时候只要见到函数返回对象,就会有一个临时对象被建立(以后被释放)。
TOP条款20:协助编译器实现返回值优化
协助完成【返回值优化(RVO)】 尽量使用内联函数(inline) 返回值尽量用少用或不用局部变量或者临时变量。。 e.g. return a+b;//ok c = a+b;return c;
相信我:一些函数(operator*也在其中)必须要返回对象。这就是它们的运行方法。不要与其对抗,你不会赢的。 你所应该关心的是把你的努力引导到寻找减少返回对象的开销上来,而不是去消除对象本身。
诀窍就是返回带有参数的构造函数而不是直接返回对象:
1 2 3 4 5 6 |
const Rational operator*(const Rational& lhs, const Rational& rhs) { return Rational(lhs.numerator() * rhs.numerator(), lhs.denominator() * rhs.denominator()); } |
通过这个表达式建立一个临时的Rational对象,
1 |
Rational(lhs.numerator() * rhs.numerator(), lhs.denominator() * rhs.denominator()); |
函数的返回值正是这人上临时对象的拷贝。C++规则允许编译器优化超出生存周期的临时对象(temporary objects out of existence)。
1 |
Rational c = a * b; // 在这里调用operator* |
编译器就会被允许消除在operator*内的临时变量和operator*返回的临时变量。它们能在为目标c分配的内存里构造return表达式定义的对象。如果你的编译器这样去做,调用operator*的临时对象的开销就是零:没有建立临时对象。
TOP条款21:通过函数重载避免隐式类型转换
利用重载技术(overload)避免隐式型别转换(implicit conversions) 这个很容易理解:e.g. T a = b + 10;强制将10转换成了T类型 如果 const T operator +(T lhs,int rhs);就可以避免隐式强制转换
1 2 3 4 5 6 7 8 9 10 11 |
class UPInt { // unlimited precision public: // integers 类 UPInt(); UPInt(int value); ... }; //有关为什么返回值是const的解释,参见Effective C++ 条款21 const UPInt operator+(const UPInt& lhs, const UPInt& rhs); UPInt upi1, upi2; ... UPInt upi3 = upi1 + upi2; |
现在考虑下面这些语句:
1 2 |
upi3 = upi1 + 10; upi3 = 10 + upi2; |
这些语句也能够成功运行。方法是通过建立临时对象把整形数10转换为UPInts。还有一种方法可以成功进行operator的混合类型调用,它将消除隐式类型转换的需要。如果我们想要把UPInt和int对象相加,通过声明如下几个函数达到这个目的,每一个函数有不同的参数类型集。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
const UPInt operator+(const UPInt& lhs, // add UPInt const UPInt& rhs); // and UPInt const UPInt operator+(const UPInt& lhs, // add UPInt int rhs); // and int const UPInt operator+(int lhs, // add int and const UPInt& rhs); // UPInt UPInt upi1, upi2; ... UPInt upi3 = upi1 + upi2; // 正确,没有由upi1 或 upi2 // 生成的临时对象 upi3 = upi1 + 10; // 正确, 没有由upi1 or 10 // 生成的临时对象 upi3 = 10 + upi2; //正确, 没有由10 or upi2 //生成的临时对象。 |
一旦你开始用函数重载来消除类型转换,你就有可能这样声明函数,把自己陷入危险之中:
1 |
const UPInt operator+(int lhs, int rhs); // 错误! |
在C++中有一条规则是每一个重载的operator必须带有一个用户定义类型(user-defined type)的参数。
不过,必须谨记80-20规则(参见条款16)。没有必要实现大量的重载函数,除非你有理由确信程序使用重载函数以后其整体效率会有显著的提高。
TOP条款22:考虑使用op=来取代单独的op运算符
考虑以操作符复合形式( OP= )取代其独身形式(OP) 比如 T result= a+b+c+d; -> T result = a; result += b; result += c; result += d; 后者不用临时对象,前者需要3个;前者维护性要稍微强一点...
1 2 3 4 5 6 7 8 9 10 11 |
template<class T> const T operator+(const T& lhs, const T& rhs) { return T(lhs) += rhs; // 参见下面的讨论 } template<class T> const T operator-(const T& lhs, const T& rhs) { return T(lhs) -= rhs; // 参见下面的讨论 } |
第一、总的来说operator的赋值形式比其单独形式效率更高,因为单独形式要返回一个新对象,从而在临时对象的构造和释放上有一些开销。operator的赋值形式把结果写到左边的参数里,因此不需要生成临时对象来容纳operator的返回值。
第二、提供operator的赋值形式以及其标准形式,可以允许类的客户端在便利与效率上做出折衷选择。也就是说,客户端可以决定是这样编写:
1 2 3 4 5 6 7 8 9 |
Rational a, b, c, d, result; ... result = a + b + c + d; // 可能用了3个临时对象,每个operator+ 调用使用1个 还是这样编写: result = a; //不用临时对象 result += b; // 不用临时对象 result += c; //不用临时对象 result += d; //不用临时对象 |
前者比较容易编写、debug和维护,并且在80%的时间里它的性能是可以被接受的。后者具有更高的效率,估计这对于汇编语言程序员来说会更直观一些。
最后一点,涉及到operator单独形式的实现。由于历史的原因,无名字的对象比有名字的对象更容易清除,因此当我们面对在命名对象和临时对象间进行选择时,用临时对象更好一些。它使你耗费的开销不会比命名的对象更多,特别是使用老编译器时,它的耗费会更少。
TOP条款23:考虑使用其他等价的程序库
考虑使用其他程序库 实现同样的功能,根据需要使用不同库函数去实现带来不一样的效果。 就像iostream跟stdio的区别
TOP条款24:理解虚函数、多重继承、虚基类以及RTTI所带来的开销
了解virtual functions/multiple inheritance/virtual base classes/runtime type identification的成本 vtbl应该放哪:(暴力法)需要vtbl就产生一个副本(勘探式做法)产生在第一个non_inline,non_pure虚函数定义式的目标文件。 如果都是虚函数都是inline,(幸好现在编译器通常都忽略虚函数的inline) virtual base classes(虚拟基类) ,多重继承 RTTI 更多参考《Inside the C++ Object Model》
当调用一个虚拟函数时,被执行的代码必须与调用函数的对象的动态类型相一致;指向对象的指针或引用的类型是不重要的。编译器如何能够高效地提供这种行为呢?大多数编译器是使用virtual table和virtual table pointers。virtual table和virtual table pointers通常被分别地称为vtbl和vptr。
虚函数所需的第一个开销:你必须为每个包含虚函数的类的virtual talbe留出空间。
虚函数所需的第二个开销是:在每个包含虚函数的类的对象里,你必须为它的对象付出一定的开销来存放一个额外的指针。
为了找到一个虚函数的地址编译器生成的代码会做如下这些事情:
- 通过对象的vptr找到类的vtbl。找到对应vtbl内的指向被调用函数的指针。
- 调用第二步找到的的指针所指向的函数。这与非虚函数的调用在效率上相差无几。
而虚函数在运行时刻的真正开销与内联函数有关。实际上虚函数不能是内联的。这是因为“内联”是指“在编译期间用被调用的函数体本身来代替函数调用的指令,”但是虚函数的“虚”是指“直到运行时才能知道要调用的是哪一个函数。”如果编译器在某个函数的调用点不知道具体是哪个函数被调用,你就能理解为什么它不会内联该函数的调用。
这是虚函数所需的第三个开销:你实际上必须放弃内联函数。(当通过对象调用的虚函数时,它可以被内联,但是大多数虚函数是通过对象的指针或引用被调用的,这种调用不能被内联。因为这种调用是标准的调用方式,所以虚函数实际上不能被内联。)实际上现在的编译器一般总是忽略虚函数的的inline指令。
多继承的引入,使得事情将会变得更加复杂。
TOP技术
TOP条款25:使构造函数和非成员函数具有虚函数的行为
将constructor 和non-member-functions虚化 虚化 deconstructor很容易理解。 vitrual constructor/ virtual copy constructor,编译器本身不支持,无法被正在虚化 但是通过子类中返回父类对象指针的非虚方法实现virtual constructor行为功能(产生不同型别对象) 和 先写出来用于完成实际任务的虚函数,然后写一个非虚函数专门用来调用该虚函数实现 virtual copy constructor 值得借鉴。 Non-Member Functions行为虚化,同上,无法真正被虚化。实现行为跟实现virtual copy constructor一样。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 |
class NLComponent { //用于 newsletter components public: // 的抽象基类 ... //包含只少一个纯虚函数 }; class TextBlock: public NLComponent { public: ... // 不包含纯虚函数 }; class Graphic: public NLComponent { public: ... // 不包含纯虚函数 }; class NewsLetter { // 一个 newsletter 对象 public: // 由NLComponent 对象 ... // 的链表组成 private: list<NLComponent*> components; }; |
其构造函数如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
class NewsLetter { public: NewsLetter(istream& str); ... private: // 为建立下一个NLComponent对象从str读取数据, // 建立component 并返回一个指针。 static NLComponent * readComponent(istream& str); ... }; NewsLetter::NewsLetter(istream& str) { while (str) { // 把readComponent返回的指针添加到components链表的最后, // "push_back" 一个链表的成员函数,用来在链表最后进行插入操作。 components.push_back(readComponent(str)); } } |
readComponent所做的工作是:它根据所读取的数据建立了一个新对象,或是TextBlock或是Graphic。因为它能建立新对象,它的行为与构造函数相似,而且因为它能建立不同类型的对象,我们称它为虚拟构造函数。虚拟构造函数是指能够根据输入给它的数据的不同而建立不同类型的对象。
还有一种特殊种类的虚拟构造函数――虚拟拷贝构造函数,也有着广泛的用途。虚拟拷贝构造函数能返回一个指针,指向调用该函数的对象的新拷贝:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
class NLComponent { public: // declaration of virtual copy constructor virtual NLComponent * clone() const = 0; ... }; class TextBlock: public NLComponent { public: virtual TextBlock * clone() const // virtual copy { return new TextBlock(*this); } // constructor ... }; class Graphic: public NLComponent { public: virtual Graphic * clone() const // virtual copy { return new Graphic(*this); } // constructor ... }; |
类的虚拟拷贝构造函数只是调用它们真正的拷贝构造函数。因此”拷贝”的含义与真正的拷贝构造函数相同(浅拷贝或深拷贝、是否引用计数)。注意上述代码的实现利用了最近才被采纳的较宽松的虚拟函数返回值类型规则。被派生类重定义的虚拟函数不用必须与基类的虚拟函数具有一样的返回类型。
NLComponent有了虚拷贝构造函数以后,要为NewLetter实现一个的(常规的)拷贝构造函数变得很容易:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 |
class NewsLetter { public: NewsLetter(const NewsLetter& rhs); ... private: list<NLComponent*> components; }; NewsLetter::NewsLetter(const NewsLetter& rhs) { // 遍历整个rhs链表,使用每个元素的虚拟拷贝构造函数 // 把元素拷贝进这个对象的component链表。 // 有关下面代码如何运行的详细情况,请参见条款35. for (list<NLComponent*>::const_iterator it = rhs.components.begin(); it != rhs.components.end(); ++it) { // "it" 指向rhs.components的当前元素,调用元素的clone函数, // 得到该元素的一个拷贝,并把该拷贝放到 //这个对象的component链表的尾端。 components.push_back((*it)->clone()); } } |
让非成员函数具有虚函数的行为
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 |
class NLComponent { public: virtual ostream& print(ostream& s) const = 0; ... }; class TextBlock: public NLComponent { public: virtual ostream& print(ostream& s) const; ... }; class Graphic: public NLComponent { public: virtual ostream& print(ostream& s) const; ... }; inline ostream& operator<<(ostream& s, const NLComponent& c) { return c.print(s); } |
具有虚拟行为的非成员函数很简单。你编写一个虚拟函数来完成工作,然后再写一个非虚拟函数,它什么也不做只是调用这个虚拟函数。为了避免这个句法花招引起函数调用开销,你可以内联这个非虚函数。
TOP条款26:限制类对象的个数
限制某个class所能产生的对象个数 0 将构造private化 1 单键模式实现(计数器、指针都能实现) n 静态计数器来实现控制 using可以用来将private继承中基类的函数导出到子类,具体实现方法细读书本.
TOP条款27:要求或禁止对象分配在堆上
TOP要求在堆中建立对象
最直接的方法是让析构函数成为private,让构造函数成为public。通过引入一个专用的伪析构函数,用来访问真正的析构函数。客户端调用伪析构函数释放他们建立的对象。
TOP判断对象是否在堆中
如果想利用一个在很多系统上存在的事实,程序的地址空间被做为线性地址管理,程序的栈从地址空间的顶部向下扩展,堆则从底部向上扩展,来判断某个特定的地址是否在堆中:
1 2 3 4 5 6 |
// 不正确的尝试,来判断一个地址是否在堆中 bool onHeap(const void *address) { char onTheStack; // 局部栈变量 return address < &onTheStack; } |
到目前为止,这种逻辑很正确,但是不够深入。最根本的问题是对象可以被分配在三个地方,而不是两个。是的,栈和堆能够容纳对象,但是我们忘了静态对象。它们的位置是依据系统而定的,但是在很多栈和堆相向扩展的系统里,它们位于堆的底端。onHeap不能工作的原因立刻变得很清楚了,不能辨别堆对象与静态对象的区别。
令人伤心的是不仅没有一种可移植的方法来判断对象是否在堆上,而且连能在多数时间正常工作的“准可移植”的方法也没有。(http://www.aristeia.com/BookErrata/M27Comments.html)
其实研究对象是否在堆中这个问题,一个可能的原因是你想知道对象是否能在其上安全调用delete。幸运的是“判断是否能够删除一个指针”比“判断一个指针指向的事物是否在堆上”要容易一些。
我们希望这些函数提供这些功能时能够不污染全局命名空间,没有额外的开销,没有正确性问题。幸运的是C++使用一种抽象混合(mixin)基类满足了我们的需要。
抽象基类是不能被实例化的基类,也就是至少具有一个纯虚函数的基类。mixin(mix in)类提供某一特定的功能,并可以与其继承类提供的其它功能相兼容)。这种类几乎都是抽象类。因此我们能够使用抽象混合(mixin)基类给派生类提供判断指针指向的内存是否由operator new分配的能力。该类如下所示:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
class HeapTracked { // 混合类; 跟踪 public: // 从operator new返回的ptr class MissingAddress{}; // 异常类,见下面代码 virtual ~HeapTracked() = 0; static void *operator new(size_t size); static void operator delete(void *ptr); bool isOnHeap() const; private: typedef const void* RawAddress; static list<RawAddress> addresses; }; |
下面是其实现:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 |
// mandatory definition of static class member list<RawAddress> HeapTracked::addresses; // HeapTracked的析构函数是纯虚函数,使得该类变为抽象类。 // (参见Effective C++条款14). 然而析构函数必须被定义, //所以我们做了一个空定义。. HeapTracked::~HeapTracked() {} void * HeapTracked::operator new(size_t size) { void *memPtr = ::operator new(size); // 获得内存 addresses.push_front(memPtr); // 把地址放到list的前端 return memPtr; } void HeapTracked::operator delete(void *ptr) { //得到一个 "iterator",用来识别list元素包含的ptr; //有关细节参见条款35 list<RawAddress>::iterator it = find(addresses.begin(), addresses.end(), ptr); if (it != addresses.end()) { // 如果发现一个元素 addresses.erase(it); //则删除该元素 ::operator delete(ptr); // 释放内存 } else { // 否则 throw MissingAddress(); // ptr就不是用operator new } // 分配的,所以抛出一个异常 } bool HeapTracked::isOnHeap() const { // 得到一个指针,指向*this占据的内存空间的起始处, // 有关细节参见下面的讨论 const void *rawAddress = dynamic_cast<const void*>(this); // 在operator new返回的地址list中查到指针 list<RawAddress>::iterator it = find(addresses.begin(), addresses.end(), rawAddress); return it != addresses.end(); // 返回it是否被找到 } |
代码还是很一目了然。只有一个地方可能让你感到困惑,就是这个语句(在isOnHeap函数中)
1 |
const void *rawAddress = dynamic_cast<const void*>(this); |
因为带有多继承或虚基类的对象会有几个地址,这导致编写全局函数isSafeToDelete会很复杂。这个问题在isOnHeap中仍然会遇到,但是因为isOnHeap仅仅用于HeapTracked对象中,我们能使用dynamic_cast操作符的一种特殊的特性来消除这个问题。只需简单地放入dynamic_cast,把一个指针dynamic_cast成void*类型(或const void*或volatile void* 。。。。。),生成的指针指向“原指针指向对象内存”的开始处。如果你的编译器支持dynamic_cast 操作符,这个技巧是完全可移植的。
使用这个类,即使是最初级的程序员也可以在类中加入跟踪堆中指针的功能。他们所需要做的就是让他们的类从HeapTracked继承下来。例如我们想判断Assert对象指针指向的是否是堆对象:
1 2 3 4 5 |
class Asset: public HeapTracked { private: UPNumber value; ... }; |
我们能够这样查询Assert*指针,如下所示:
1 2 3 4 5 6 7 8 9 |
void inventoryAsset(const Asset *ap) { if (ap->isOnHeap()) { ap is a heap-based asset — inventory it as such; } else { ap is a non-heap-based asset — record it that way; } } |
TOP禁止对象分配在堆上
通常对象的建立这样三种情况:对象被直接实例化;对象做为派生类的基类被实例化;对象被嵌入到其它对象内。以下分别说明:
TOP禁止用户直接实例化对象:
可以这样编写:
1 2 3 4 5 6 |
class UPNumber { private: static void *operator new(size_t size); static void operator delete(void *ptr); ... }; |
现在客户端仅仅可以做允许它们做的事情:
1 2 3 4 |
UPNumber n1; // okay static UPNumber n2; // also okay UPNumber *p = new UPNumber; // error! attempt to call // private operator new |
如果你也想禁止UPNumber堆对象数组,可以把operator new[]和operator delete[](参见条款8)也声明为private。
把operator new声明为private同样会阻碍UPNumber对象做为一个位于堆中的派生类对象的基类被实例化。因为如果operator new和operator delete没有在派生类中被声明为public,它们就会被继承下来,继承了基类private函数的类,如下所示:
1 2 3 4 5 6 7 8 9 10 11 12 13 |
class UPNumber { ... }; // 同上 class NonNegativeUPNumber: //假设这个类 public UPNumber { //没有声明operator new ... }; NonNegativeUPNumber n1; // 正确 static NonNegativeUPNumber n2; // 也正确 NonNegativeUPNumber *p = // 错误! 试图调用 new NonNegativeUPNumber; // private operator new |
同样,UPNumber的operator new是private这一点,不会对分配包含做为成员的UPNumber对象的对象产生任何影响:
1 2 3 4 5 6 7 8 9 10 11 12 13 |
class Asset { public: Asset(int initValue); ... private: UPNumber value; }; Asset *pa = new Asset(100); // 正确, 调用 // Asset::operator new 或 // ::operator new, 不是 // UPNumber::operator new |
TOP条款28:智能(smart)指针
当你使用智能指针替代C++的内建指针(dumb pointer),你就能控制下面这些方面的指针的行为:
构造和析构(Construction and destruction)、拷贝和赋值(Copying and assignment)、解引用(Dereferencing)
大多数智能指针模板看起来都象这样:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 |
template<class T> //灵巧指针对象模板 class SmartPtr { public: SmartPtr(T* realPtr = 0); // 建立一个灵巧指针 // 指向dumb pointer所指的 // 对象。未初始化的指针 // 缺省值为0(null) SmartPtr(const SmartPtr& rhs); // 拷贝一个灵巧指针 ~SmartPtr(); // 释放灵巧指针 // make an assignment to a smart ptr SmartPtr& operator=(const SmartPtr& rhs); T* operator->() const; // dereference一个灵巧指针 // 以访问所指对象的成员 T& operator*() const; // dereference 灵巧指针 private: T *pointee; // 灵巧指针所指的对象 }; |
(当面对异常时,让对象自己开始(constructor)和结束(destructor)日志记录比显示地调用函数可以使的程序更健壮、更容易。)
TOP智能指针的构造、赋值和析构
如果一个智能指针拥有它所指向的对象,当它被销毁时必须负责删除这个对象。
auto_ptr模板的实现大致如下:
1 2 3 4 5 6 7 8 9 10 |
template<class T> class auto_ptr { public: auto_ptr(T *ptr = 0): pointee(ptr) {} ~auto_ptr() { delete pointee; } ... private: T *pointee; }; |
auto_ptr采用了更为灵活的解决方案:当auto_ptr被拷贝或赋值时,同时转移对象的所有权给新的auto_ptr。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 |
template<class T> class auto_ptr { public: ... auto_ptr(auto_ptr<T>& rhs); // 拷贝构造函数 auto_ptr<T>& // 赋值 operator=(auto_ptr<T>& rhs); // 操作符 ... }; template<class T> auto_ptr<T>::auto_ptr(auto_ptr<T>& rhs) { pointee = rhs.pointee; // 把*pointee的所有权 // 传递到 *this rhs.pointee = 0; // rhs不再拥有 } // 任何东西 template<class T> auto_ptr<T>& auto_ptr<T>::operator=(auto_ptr<T>& rhs) { if (this == &rhs) // 如果这个对象自我赋值 return *this; // 什么也不要做 delete pointee; // 删除现在拥有的对象 pointee = rhs.pointee; // 把*pointee的所有权 rhs.pointee = 0; // 从 rhs 传递到 *this return *this; } |
注意赋值操作符在接受新对象的所有权以前必须删除原来拥有的对象。如果不这样做,原来拥有的对象将永远不会被删除。记住:除了auto_ptr对象,不会有其它人拥有auto_ptr所指向的对象。
因为当调用auto_ptr的拷贝构造函数时,对象的所有权被传递出去,所以通过传值方式传递auto_ptr对象是一个很糟糕的方法。即:
1 2 3 4 5 6 7 8 9 10 11 |
// 这个函数通常会导致灾难发生 void printTreeNode(ostream& s, auto_ptr<TreeNode> p) { s << *p; } int main() { auto_ptr<TreeNode> ptn(new TreeNode); ... printTreeNode(cout, ptn); //通过传值方式传递auto_ptr ... } |
通过const引用传递(Pass-by-reference-to-const)才是合适的做法:
1 2 3 4 |
// 这个函数的行为更直观一些 void printTreeNode(ostream& s, const auto_ptr<TreeNode>& p) { s << *p; } |
TOP实现解引用操作符
1 2 3 4 5 6 7 |
template<class T> T& SmartPtr<T>::operator*() const { perform "smart pointer" processing; return *pointee; } |
注意返回类型是一个引用。如果返回对象,尽管编译器允许这么做,这也将会导致灾难性后果。必须时刻牢记:pointee不用必须指向T类型对象;它也可以指向T的派生类对象。如果在这种情况下operator*函数返回的是T类型对象而不是派生类对象的引用,你的函数实际上返回的是一个错误类型的对象!(这是一个slicing问题,参见Effective C++条款22和本书条款13)。
1 2 3 4 5 6 7 |
template<class T> T* SmartPtr<T>::operator->() const { perform "smart pointer" processing; return pointee; } |
TOP测试智能指针是否为空
为了达到这个目的,传统上采取的是转换为void* 类型:
1 2 3 4 5 6 7 |
template<class T> class SmartPtr { public: ... operator void*(); // 如果灵巧指针为null, ... // 返回0, 否则返回 }; // 非0。 |
它有一个缺点:允许灵巧指针与完全不同的类型之间进行比较:
1 2 3 4 5 |
SmartPtr<Apple> pa; SmartPtr<Orange> po; ... if (pa == po) ... // 这也能够被成功编译! |
即使在SmartPtr<Apple> 和 SmartPtr<Orange>之间没有operator= 函数,也能够编译,因为灵巧指针被隐式地转换为void*指针。
有一种折衷之策可以提供合理的测试空值的语法形式,同时把不同类型的灵巧指针之间进行比较的可能性降到最低。这就是在智能指针类中重载operator!,当且仅当灵巧指针是一个空指针时,operator!返回true:
1 2 3 4 5 6 7 |
template<class T> class SmartPtr { public: ... bool operator!() const; // 当且仅当灵巧指针是 ... // 空值,返回true。 }; |
客户端程序如下所示:
1 2 3 4 5 6 7 8 9 |
SmartPtr<TreeNode> ptn; ... if (!ptn) { // 正确 ... // ptn 是空值 } else { ... // ptn不是空值 } |
但是这样就不正确了:
1 2 |
if (ptn == 0) ... // 仍然错误 if (ptn) ... // 也是错误的 |
仅在这种情况下会存在不同类型之间进行比较:
1 2 3 4 |
SmartPtr<Apple> pa; SmartPtr<Orange> po; ... if (!pa == !po) ... // 仍然能够编译 |
幸好程序员不会经常这样编写代码。
TOP把智能指针转变成dumb指针
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 |
void normalize(Tuple *pt); // 把*pt 放入 // 函数中; 注意使用的 // 是dumb指针 template<class T> // 同上 class DBPtr { public: ... operator T*() { return pointee; } ... }; DBPtr<Tuple> pt; ... normalize(pt); // 能够运行 并且这个函数也消除了测试空值的问题: if (pt == 0) ... // 正确, 把pt转变成 // Tuple* if (pt) ... // 同上 if (!pt) ... // 同上 (reprise) |
然而,它也有类型转换函数所具有的缺点(几乎总是这样,看条款5)。它使得客户端能够很容易地直接访问dumb指针,绕过“类指针(pointer-like)”对象所提供的“智能”特性。
提供到dumb指针的隐式类型转换的灵巧指针类也暴露了一个非常讨厌的bug。考虑这个代码:
1 2 3 |
DBPtr<Tuple> pt = new Tuple; ... delete pt; |
这段代码应该不能被编译,pt不是指针,它是一个对象,你不能删除一个对象。只有指针才能被删除,对么?
当然对了。但是回想一下条款5:编译器使用隐式类型转换来尽可能使函数调用成功,再回想一下条款8:使用delete会调用析构函数和operator delete,两者都是函数。编译器欲使在delete语句里的两个函数成功调用,就把pt隐式转换为Tuple*,然后删除它。这样做必然会破坏你的程序。
如果pt拥有它指向的对象,对象就会被删除两次,一次在调用delete时,第二次在pt的析构函数被调用时。如果pt不拥有对象,而是其他人拥有,拥有者可以删除pt,但是如果pt指向对象的拥有者不是删除pt的人,有删除权的拥有者以后还会再次删除该对象。不论是前者还是后者都会导致一个对象被删除两次,这样做会产生不能预料的后果。
所以底线很简单:除非有一个让人非常信服的理由,否则绝对不要提供转换到dumb指针的隐式类型转换操作符。
TOP智能指针和继承类到基类的类型转换
1 2 3 4 5 6 7 |
void displayAndPlay(const MusicProduct* pmp, int numTimes) { for (int i = 1; i <= numTimes; ++i) { pmp->displayTitle(); pmp->play(); } } |
这个函数能够这样使用:
1 2 3 4 5 |
Cassette *funMusic = new Cassette("Alapalooza"); CD *nightmareMusic = new CD("Disco Hits of the 70s"); displayAndPlay(funMusic, 10); displayAndPlay(nightmareMusic, 0); |
这并没有什么值得惊讶的东西,但是当我们用智能指针替代dumb指针,会发生什么呢:
1 2 3 4 5 6 7 8 |
void displayAndPlay(const SmartPtr<MusicProduct>& pmp, int numTimes); SmartPtr<Cassette> funMusic(new Cassette("Alapalooza")); SmartPtr<CD> nightmareMusic(new CD("Disco Hits of the 70s")); displayAndPlay(funMusic, 10); // 错误! displayAndPlay(nightmareMusic, 0); // 错误! |
幸运的是,有办法避开这种限制,这种方法的核心思想(不是实际操作)很简单:对于可以进行隐式转换的每个智能指针类都提供一个隐式类型转换操作符。但这种方法有两个缺点。第一,你必须手工特化(specialize)SmartPtr类,但是这种做法在很大程度上也就破坏了模板的通用性。第二,你可能需要添加许多类型转换符,因为你指向的对象可以位于继承层次中很深的位置,你必须为直接或间接继承的每一个基类提供一个类型转换符。
所幸C++最近的语言扩展,让我们能够做到这一点,这个扩展能声明(非虚)成员函数模板(通常就叫成员模板(member template)),你能使用它来生成智能指针类型转换函数,如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
template<class T> // 模板类,指向T的 class SmartPtr { // 智能指针 public: SmartPtr(T* realPtr = 0); T* operator->() const; T& operator*() const; template<class newType> // 模板成员函数 operator SmartPtr<newType>() // 为了实现隐式类型转换. { return SmartPtr<newType>(pointee); } ... }; |
假设编译器有一个指向T对象的智能指针,它要把这个对象转换成指向“T的基类”的智能指针。编译器首先检查SmartPtr<T>类的定义,看其有没有声明相应的类型转换符,结果它没有声明。(这不意味着在上面的模板没有声明类型转换符)编译器然后检查是否存在一个成员函数模板,可以被实例化,用来进行它所期望的类型转换。它发现了一个这样的模板(带有形式类型参数newType),所以它把newType绑定到T的基类类型上,来实例化模板。
这种方法可以成功地用于任何合法的指针类型转换。如果你有dumb指针T1*和另一种dumb指针T2*,当且仅当你能隐式地把T1*转换为T2*时,你就能够隐式地把指向T1的智能指针类型转换为指向T2的智能指针类型。
但是:如何能够让智能指针在基于继承的类型转换时的行为与dumb指针一样呢?答案很简单:不可能。正如Daniel Edelson所说,智能指针固然灵巧,但不是指针。最好的方法是使用成员模板生成类型转换函数,在会产生二义性结果的地方使用casts。
TOP智能指针和const
可以建立const和non-const对象和指针的四种不同组合:
1 2 3 4 5 6 7 8 |
SmartPtr<CD> p; // non-const 对象 // non-const 指针 SmartPtr<const CD> p; // const 对象, // non-const 指针 const SmartPtr<CD> p = &goodCD; // non-const 对象 // const指针 const SmartPtr<const CD> p = &goodCD; // const 对象 // const 指针 |
但是美中不足的是,如下代码:
1 2 |
CD *pCD = new CD("Famous Movie Themes"); const CD * pConstCD = pCD; |
在dumb指针身上都是正确的,但是如果我们试图把这种方法用在灵巧指针上,情况立刻变了:
1 2 |
SmartPtr<CD> pCD = new CD("Famous Movie Themes"); SmartPtr<const CD> pConstCD = pCD; // 正确么? |
因为SmartPtr<CD> 与SmartPtr <const CD>是完全不同的类型。在编译器看来,它们是毫不相关的,所以没有理由相信它们是赋值兼容的。
如果你使用的编译器支持成员模板,就可以利用前面所说的技巧自动生成你需要的隐式类型转换操作。
TOP条款29:引用计数
这个技巧有两个常用动机。第一个是简化跟踪堆中的对象的过程。第二个动机是由于一个简单的常识。如果很多对象有相同的值,将这个值存储多次是很无聊的。
TOP引用计数的基类
引用计数不只适用于字符串,只要是多个对象具有相同值的类理论上说都可以使用引用计数。
第一步是构建一个基类RCObject,任何需要引用计数的类都必须从它继承。RCObject封装了引用计数功能,以及增加和减少引用计数的函数。它还包含了当这个值不再被用到时(也就是引用计数为0时)销毁对象的代码。最后,它包含了一个字段以跟踪这个值对象是否可共享,并提供查询这个值和将它设为false的函数。不需将可共享标志设为true的函数,因为所有的值对象默认都是可共享的。一旦一个对象变成了不可共享,将没有办法使它再次成为可共享。
RCObject的定义如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
class RCObject { public: RCObject(); RCObject(const RCObject& rhs); RCObject& operator=(const RCObject& rhs); virtual ~RCObject() = 0; void addReference(); void removeReference(); void markUnshareable(); bool isShareable() const; bool isShared() const; private: int refCount; bool shareable; }; |
注意这里的虚析构函数,它明确表明这个类是被设计了作基类使用的。同时要注意这个析构函数是纯虚的,它明确表明这个类只能作基类使用。
RCOject的实现代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 |
RCObject::RCObject() : refCount(0), shareable(true) {} RCObject::RCObject(const RCObject&) : refCount(0), shareable(true) {} >RCObject& RCObject::operator=(const RCObject&) { return *this; } RCObject::~RCObject() {} // virtual dtors must always // be implemented, even if // they are pure virtual // and do nothing (see also // Item M33 and Item E14) void RCObject::addReference() { ++refCount; } void RCObject::removeReference() { if (--refCount == 0) delete this; } void RCObject::markUnshareable() { shareable = false; } bool RCObject::isShareable() const { return shareable; } bool RCObject::isShared() const { return refCount > 1; } |
在两个构造函数中都将refCount设成了0。实际上REObjects的创建者会把refCount设为1。
另外拷贝构造函数也将refCount设成了0,而不管被拷贝的RCObject对象的refCount的值。这是因为我们正在创建新对象以代表这个值,而这个新的对象总是未被共享的,并且只被它的创建者引用。
注意,创建者将必须负责将refCount设为正确的值。
RCObject的赋值运算看起来完全是颠覆性的:它不做任何事情。
为了使用新写的引用计数基类,将StringValue修改为从RCObject继承而得到引用计数功能:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
class String { private: struct StringValue: public RCObject { char *data; StringValue(const char *initValue); ~StringValue(); }; ... }; String::StringValue::StringValue(const char *initValue) { data = new char[strlen(initValue) + 1]; strcpy(data, initValue); } String::StringValue::~StringValue() { delete [] data; } |
这个版本的StringValue和前面的几乎一样,唯一改变的就是StringValue的成员函数不再处理refCount字段。RCObject现在接管了这个工作。
TOP引用计数操作的自动化
供引用计数对象使用的智能指针模板:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
// template class for smart pointers-to-T objects. T must // support the RCObject interface, typically by inheriting // from RCObject template<class T> class RCPtr { public: RCPtr(T* realPtr = 0); RCPtr(const RCPtr& rhs); ~RCPtr(); RCPtr& operator=(const RCPtr& rhs); T* operator->() const; // see Item 28 T& operator*() const; // see Item 28 private: T *pointee; // dumb pointer this // object is emulating void init(); // common initialization }; |
这个模板可以让智能指针对象控制在构造、赋值、析构时控制所发生的事情。它有两个构造函数:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 |
template<class T> RCPtr<T>::RCPtr(T* realPtr): pointee(realPtr) { init(); } template<class T> RCPtr<T>::RCPtr(const RCPtr& rhs): pointee(rhs.pointee) { init(); } template<class T> void RCPtr<T>::init() { if (pointee == 0) { // if the dumb pointer is return; // null, so is the smart one } if (pointee->isShareable() == false) { // if the value pointee = new T(*pointee); // isn't shareable, } // copy it pointee->addReference(); // note that there is now a } // new reference to the value |
因为init()中的pointee = new T(pointee)会创建一个新的T对象,并用拷贝构造函数进行了初始化。所以我们必须在StringValue中增加这样的一个构造函数:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
class String { private: struct StringValue: public RCObject { StringValue(const StringValue& rhs); ... }; ... }; String::StringValue::StringValue(const StringValue& rhs) { data = new char[strlen(rhs.data) + 1]; strcpy(data, rhs.data); } |
RCPtr<T>不仅仅假设T类型有深拷贝的构造函数,它还要求T从RCObject继承,或至少提供了RCObject的所提供的函数。
RCPtr<T>的最后一个假设是它所指向的对象类型为T。这似乎是显然的。毕竟pointee的类型被申明为T。但pointee可能实际上指向T的一个派生类。我们可以提供使用虚拷贝构造函数(见Item25)来实现这一点。
用这种方式实现了RCPtr的构造函数后,类的其它函数实现得很轻快。赋值运算很简洁明了:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
template<class T> RCPtr<T>& RCPtr<T>::operator=(const RCPtr& rhs) { if (pointee != rhs.pointee) { // skip assignments // where the value // doesn't change if (pointee) { pointee->removeReference(); // remove reference to } // current value pointee = rhs.pointee; // point to new value init(); // if possible, share it } // else make own copy return *this; } |
析构函数很容易。当一个RCPtr被销毁时,它只是简单地将它对引用计数对象的引用移除:
1 2 3 4 5 |
template<class T> RCPtr<T>::~RCPtr() { if (pointee) pointee->removeReference(); } |
最后,RCPtr的模拟指针的操作就是在Item28中看到的智能指针的部分:
1 2 3 4 |
template<class T> T* RCPtr<T>::operator->() const { return pointee; } template<class T> T& RCPtr<T>::operator*() const { return *pointee; } |
TOP合在一起
终于可以这些零散的代码放在一起,使用可重用的RCObject和RCPtr类构建带引用计数的String类。
类的定义是:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 |
template<class T> // template class for smart class RCPtr { // pointers-to-T objects; T public: // must inherit from RCObject RCPtr(T* realPtr = 0); RCPtr(const RCPtr& rhs); ~RCPtr(); RCPtr& operator=(const RCPtr& rhs); T* operator->() const; T& operator*() const; private: T *pointee; void init(); }; class RCObject { // base class for reference- public: // counted objects void addReference(); void removeReference(); void markUnshareable(); bool isShareable() const; bool isShared() const; protected: RCObject(); RCObject(const RCObject& rhs); RCObject& operator=(const RCObject& rhs); virtual ~RCObject() = 0; private: int refCount; bool shareable; }; class String { // class to be used by public: // application developers String(const char *value = ""); const char& operator[](int index) const; char& operator[](int index); private: // class representing string values struct StringValue: public RCObject { char *data; StringValue(const char *initValue); StringValue(const StringValue& rhs); void init(const char *initValue); ~StringValue(); }; RCPtr<StringValue> value; }; |
这里有一个重大的不同:这个String类的公有接口和本条款开始处我们使用的版本不同。拷贝构造函数在哪里?赋值运算在哪里?析构函数在哪里?这儿明显有问题。实际上,没问题。它工作得很好。如果你没看出为什么,prepare yourself for a C++ epiphany。
String对象仍然可以被拷贝,并且,这个拷贝可以正确处理后台被引用计数的StringValue对象,但String类不需要写下哪怕一行代码。因为编译器为String自动生成的拷贝构造函数将自动调用其RCPtr成员的拷贝构造函数,而这个拷贝构造函数完成所有的对StringValue对象的必要操作,包括它的引用计数。RCPtr是一个智能指针,所以这是它的本职工作。它同样处理赋值和析构,所以String类同样不需要写出这些函数。
下面是RCObject的实现:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 |
RCObject::RCObject() : refCount(0), shareable(true) {} RCObject::RCObject(const RCObject&) : refCount(0), shareable(true) {} RCObject& RCObject::operator=(const RCObject&) { return *this; } RCObject::~RCObject() {} void RCObject::addReference() { ++refCount; } void RCObject::removeReference() { if (--refCount == 0) delete this; } void RCObject::markUnshareable() { shareable = false; } bool RCObject::isShareable() const { return shareable; } bool RCObject::isShared() const { return refCount > 1; } |
下面是RCPtr的实现:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 |
template<class T> void RCPtr<T>::init() { if (pointee == 0) return; if (pointee->isShareable() == false) { pointee = new T(*pointee); } pointee->addReference(); } template<class T> RCPtr<T>::RCPtr(T* realPtr) : pointee(realPtr) { init(); } template<class T> RCPtr<T>::RCPtr(const RCPtr& rhs) : pointee(rhs.pointee) { init(); } template<class T> RCPtr<T>::~RCPtr() { if (pointee)pointee->removeReference(); } template<class T> RCPtr<T>& RCPtr<T>::operator=(const RCPtr& rhs) { if (pointee != rhs.pointee) { if (pointee) pointee->removeReference(); pointee = rhs.pointee; init(); } return *this; } template<class T> T* RCPtr<T>::operator->() const { return pointee; } template<class T> T& RCPtr<T>::operator*() const { return *pointee; } |
下面是String::StringValue的实现:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
void String::StringValue::init(const char *initValue) { data = new char[strlen(initValue) + 1]; strcpy(data, initValue); } String::StringValue::StringValue(const char *initValue) { init(initValue); } String::StringValue::StringValue(const StringValue& rhs) { init(rhs.data); } String::StringValue::~StringValue() { delete [] data; } |
最后是String,它的实现是:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
String::String(const char *initValue) : value(new StringValue(initValue)) {} const char& String::operator[](int index) const { return value->data[index]; } char& String::operator[](int index) { if (value->isShared()) { value = new StringValue(value->data); } value->markUnshareable(); return value->data[index]; } |
TOP为既有类添加引用计数
到现在为止,我们所讨论的都假设我们能够访问有关类的源码。但如果我们想让一个位于不能被修改的程序库中的类获得引用计数的好处呢?
可以套用这句格言:计算机科学中的绝大部分问题都可以通过增加一个中间层次来解决。
增加一个新类CountHolder以处理引用计数,它从RCObject继承。我们让CountHolder包含一个指针指向Widget。然后用等价的智能指针RCIPter模板替代RCPtr模板,它知道CountHolder类的存在。(名字中的“i”表示间接“indirect”。)
RCIPtr的实现如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 |
template<class T> class RCIPtr { public: RCIPtr(T* realPtr = 0); RCIPtr(const RCIPtr& rhs); ~RCIPtr(); RCIPtr& operator=(const RCIPtr& rhs); const T* operator->() const; // see below for an T* operator->(); // explanation of why const T& operator*() const; // these functions are T& operator*(); // declared this way private: struct CountHolder: public RCObject { ~CountHolder() { delete pointee; } T *pointee; }; CountHolder *counter; void init(); void makeCopy(); // see below }; template<class T> void RCIPtr<T>::init() { if (counter->isShareable() == false) { T *oldValue = counter->pointee; counter = new CountHolder; counter->pointee = new T(*oldValue); } counter->addReference(); } template<class T> RCIPtr<T>::RCIPtr(T* realPtr) : counter(new CountHolder) { counter->pointee = realPtr; init(); } template<class T> RCIPtr<T>::RCIPtr(const RCIPtr& rhs) : counter(rhs.counter) { init(); } template<class T> RCIPtr<T>::~RCIPtr() { counter->removeReference(); } template<class T> RCIPtr<T>& RCIPtr<T>::operator=(const RCIPtr& rhs) { if (counter != rhs.counter) { counter->removeReference(); counter = rhs.counter; init(); } return *this; } template<class T> // implement the copy void RCIPtr<T>::makeCopy() // part of copy-on- { // write (COW) if (counter->isShared()) { T *oldValue = counter->pointee; counter->removeReference(); counter = new CountHolder; counter->pointee = new T(*oldValue); counter->addReference(); } } template<class T> // const access; const T* RCIPtr<T>::operator->() const // no COW needed { return counter->pointee; } template<class T> // non-const T* RCIPtr<T>::operator->() // access; COW { makeCopy(); return counter->pointee; } // needed template<class T> // const access; const T& RCIPtr<T>::operator*() const // no COW needed { return *(counter->pointee); } template<class T> // non-const T& RCIPtr<T>::operator*() // access; do the { makeCopy(); return *(counter->pointee); } // COW thing |
RCIPtr与RCPtr只两处不同。第一,RCPtr对象直接指向值对象,而RCIptr对象通过中间层的CountHolder对象指向值对象。第二,RCIPtr重载了operator->和operator*,当有对被指向的对象的非const的操作时,写时拷贝自动被执行。
有了RCIPtr,很容易实现RCWidget,因为RCWidget的每个函数都是将调用传递给RCIPtr以操作Widget对象。举个例子,如果Widget是这样的:
1 2 3 4 5 6 7 8 9 |
class Widget { public: Widget(int size); Widget(const Widget& rhs); ~Widget(); Widget& operator=(const Widget& rhs); void doThis(); int showThat() const; }; |
那么RCWidget将被定义为这样:
1 2 3 4 5 6 7 8 |
class RCWidget { public: RCWidget(int size): value(new Widget(size)) {} void doThis() { value->doThis(); } int showThat() const { return value->showThat(); } private: RCIPtr<Widget> value; }; |
注意RCWidget的构造函数是怎么用它被传入的参数调用Widget的构造函数的;RCWidget的doThis怎么调用Widget的doThis函数的;以及RCWidget的showThat怎么返回Widget的showThat的返回值的。同样要注意RCWidget没有申明拷贝构造函数和赋值操作函数,也没有析构函数。
TOP小结
引用计数通常假设对象共享相同的值。如果假设不成立的话,引用计数将比通常的方法使用更多的内存和执行更多的代码。另一方面,如果你的对象确实有具体相同值的趋势,那么引用计数将同时节省时间和空间。
总之,引用计数在下列情况下对提高效率很有用:
- 少量的值被大量的对象共享。这样的共享通常通过调用赋值操作和拷贝构造而发生。对象/值的比例越高,越是适宜使用引用计数。
- 对象的值的创建和销毁代价很高昂,或它们占用大量的内存。即使这样,如果不是多个对象共享相同的值,引用计数仍然帮不了你任何东西。
使用profiler或其它工具来分析才能确认这些条件是否满足。
即使上面的条件满足了,使用引用计数仍然可能是不合适的。有些数据结构(如有向图)将导致自我引用或环状结构。这样的数据结构可能导致孤立的自引用对象,它没有被别人使用,而其引用计数又绝不会降到零。因为这个不被外部对象用到的结构中的每个对象都被该结构内部的至少一个对象所引用。
让我们用最后一个问题结束讨论。当RCObject::removeReference减少对象的引用计数时,它检查新值是否为0。如果是,removeReference通过调用delete this销毁对象。这个操作只在对象是通过调用new生成的时才安全,所以我们需要一些方法以确保RCObject只能用这种方法产生。
此处,我们用约定的方法来解决。RCObject被设计为只作被引用计数的值对象的基类使用,而这些值对象应该只通过智能指针RCPtr引用。此外,值对象应该只能由值会共享的对象来实例化;它们不能被按通常的方法使用。
于是,我们可以指定一组满足这个要求的类,并确保只有这些类能创建RCObject对象,从而限制RCObject只能在堆上创建。
TOP条款30:代理类
TOP条款31:基于多个对象的虚函数
TOP杂项
TOP条款32:在将来时态下开发程序
作为软件开发人员,我们也许知道得不够多,但我们知道万物都会变化。我们没必要知道什么将发生变化,这么变化又怎么发生,以什么时候发生,在哪里发生,但我们知道:万物总会变化。
好的软件能够适应变化。它提供新的特性,适应到新的平台,满足新的需求,处理新的输入。这种软件的灵活性、健壮性、可靠性不是来自于运气。它是那些遵照了现在的要求并关注了将来可能的开发人员设计和实现出来的。这样的优雅地各应变更的软件是那些在未来时态下开发程序的人写出来的。
新的函数将被加入到函数库中,新的重载将发生,于是要注意那些潜在的函数调用的二义性;新的类将会加入继承层次,现在的派生类将会是以后的基类;将会编制新的应用软件,函数将在新的运行环境下被调用,它们应该被写得在新平台上运行正确;程序的维护人员通常不是原来编写它们的人,因此应该被设计得易于被别人理解、维护和扩充。
一种好的做法是:用C++语言自己来表达设计上的约束条件,而不是用注释或文档。
例如,如果一个类被设计得不能被继承,不要只是在其头文件中加个注释,用C++的方法来阻止继承;Item M26显示了这个技巧。如果一个类需要其实例全部创建在堆中,不要只是对用户说了这么一句,用Item M27的方法来强迫这一点。如果拷贝构造和赋值对一个类是没有意义的,通过申明它们为私有来阻止这些操作(见Item E27)。C++提供了强大的功能、灵活度和表达力。用语言提供的这些特性来强迫程序符合设计。
避免按照需要随意更改虚函数的声明(“demand-paged”)。应该判断一个函数的含意,以及它被派生类重定义的话是否有意义。如果是有意义的,申明它为虚,即使没有人立即重定义它。如果不是的话,申明它为非虚,并且不要在以后为了便于某人而更改其声明;确保更改是对整个类的运行环境和类所表示的抽象是有意义的(见Item E36)。
处理每个类的赋值和拷贝构造函数,即使“从没人这样做过”。他们现在没有这么做并不意味着他们以后不这么做(见Item E18)。如果这些函数是难以实现的,那么申明它们为私有。这样,不会有人误调编译器提供的默认版本而做错事(这在默认赋值和拷贝构造函数上经常发生,见Item E11)。
努力提供这样的类,它们的操作和函数有自然的语法和直观的语义。和内建数据类型的行为保持一致:拿不定主意时,仿照int来做。
记住:只要是能被人做的,就有人这么做(WQ:莫菲法则)。他们会抛异常;会用自己给自己赋值;在没有赋初值前就使用对象;给对象赋了值而没有使用;会赋过大的值、过小的值或空值。一般而言,只要能编译通过,就有人会这么做。所以,要使得自己的类易于被正确使用而难以误用。要承认用户可能犯错误,所以要将你的类设计得可以防止、检测或修正这些错误(例子见Item M33和Item E46)。
努力于可移植的代码。写可移植的代码并不比不可移植的代码难太多,只有在性能极其重要时采用不可移植的结构才是可取的(见Item M16)。
将你的代码设计成:当需要变化时,影响是局部的。尽可能地封装;将实现细节申明为私有(例子见Item E20)。只要可能,使用无名的命名空间和文件内的静态对象或函数(见Item E31)。避免导致虚基类的设计,因为这种类需要每个派生类都直接初始化它--即使是那些间接派生类(见Item M4和Item E43)。避免需要RTTI的设计,它需要if...then...else型的瀑布结构(再次参见Item M31,然后看Item E39上的好方法)。每次,类的继承层次变了,每组if...then...else语句都需要更新,如果你忘掉了一个,你不会从编译器得到任何告警。
当然,现在时态的思维是必须的。未来时态的考虑只是简单地增加了一些额外约束:
- 提供完备的类(见Item E18),即使某些部分现在还没有被使用。如果有了新的需求,你不用回过头去改它们。
- 将你的接口设计得便于常见操作并防止常见错误(见Item E46)。使得类容易正确使用而不易用错。例如,阻止拷贝构造和赋值操作,如果它们对这个类没有意义的话(见Item E27)。防止部分赋值(见Item M33)。
- 如果没有限制你不能通用化你的代码,那么通用化它。例如,如果在写树的遍历算法,考虑将它通用得可以处理任何有向不循环图。
未来时态的考虑增加了你的代码的可重用性、可维护性、健壮性,已及在环境发生改变时易于修改。它必须与进行时态的约束条件进行取舍。太多的程序员们只关注于现在的需要,然而这么做牺牲的是软件的长期生存能力。要与众不同一点,做一个离经叛道者,在未来时态下开发程序。
TOP条款33:将非尾端类设计为抽象类
TOP条款34:理解如何在同一程序中混合使用C++和C
在同一程序里混合使用C++和C之前,请确保你的C++编译器和C编译器兼容。
确认兼容后,还有四个要考虑的问题:名称改编(name mangling),静态变量初始化,内存动态分配,数据结构兼容性。
TOP名称改编
即C++编译器给程序的每个函数换一个独一无二的名字。在C中,这个过程是不需要的,因为没有函数重载,但几乎所有C++程序都有函数重名(例如,流库就申明了几个版本的operator<<和operator>>)。重载不兼容于绝大部分链接程序,因为链接程序通常无法分辨同名的函数。名变换是对链接程序的妥协;链接程序通常坚持函数名必须独一无二。
如果只在C++范围内,名称改编不会影响你。如果你你有一个函数叫drawline而编译器将它变换为xyzzy,你总使用名字drawLine,不会注意到背后的obj文件引用的是xyzzy的。
如果drawLine位于C运行库中,那情况就不同了。你的C++源文件包含的头文件中申明为:
1 |
void drawLine(int x1, int y1, int x2, int y2); |
代码体中通常也是调用drawLine。每个这样的调用都被编译器转换为调用名变换后的函数,所以写下的是
1 2 3 |
drawLine(a, b, c, d); // call to unmangled function name // obj文件中调用的是: xyzzy(a, b, c, d); // call to mangled function mame |
但如果drawLine是一个C函数,obj文件(或者是动态链接库之类的文件)中包含的编译后的drawLine函数仍然叫drawLine;没有名变换动作。当你试图将obj文件链接为程序时,将得到一个错误,因为链接程序在寻找一个叫xyzzy的函数,而没有这样的函数存在。
要解决这个问题,你需要一种方法来告诉C++编译器不要在这个函数上进行名变换。如果你调用一个名字为drawLine的C函数,它实际上就叫drawLine,你的obj文件应该包含这样的一个引用,而不是引用进行了名变换的版本。
要禁止名称改编,使用C++的extern "C"指示:
1 2 3 4 |
// declare a function called drawLine; don't mangle // its name extern "C" void drawLine(int x1, int y1, int x2, int y2); |
extern "C"可以对一组函数生效,只要将它们放入一对大括号中:
1 2 3 4 5 6 7 |
extern "C" { // disable name mangling for // all the following functions void drawLine(int x1, int y1, int x2, int y2); void twiddleBits(unsigned char bits); void simulate(int iterations); ... } |
当用C++编译时,你应该加extern 'C',但用C编译时,不应该这样。通过只在C++编译器下定义的宏__cplusplus,你可以将头文件组织得这样:
1 2 3 4 5 6 7 8 9 10 |
#ifdef __cplusplus extern "C" { #endif void drawLine(int x1, int y1, int x2, int y2); void twiddleBits(unsigned char bits); void simulate(int iterations); ... #ifdef __cplusplus } #endif |
TOP静态变量的初始化
对C++来说,在main执行前和执行后都可能有大量代码被执行。尤其是静态的类对象和定义在全局的、某个命名空间中的或文件体中的类对象的构造函数通常在main被执行前就被调用。这个过程称为静态初始化(参见Item E47)。这和我们对C++和C程序的通常认识相反,我们一直把main当作程序的入口。同样,通过静态初始化产生的对象也要在静态析构过程中调用其析构函数;这个过程通常发生在main结束运行之后。
为了解决main()应该首先被调用,而对象又需要在main()执行前被构造的两难问题,许多编译器在main()的最开始处插入了一个特别的函数,由它来负责静态初始化。同样地,编译器在main()结束处插入了一个函数来析构静态对象。产生的代码通常看起来象这样:
1 2 3 4 5 6 7 8 |
int main(int argc, char *argv[]) { performStaticInitialization(); // generated by the // implementation the statements you put in main go here; performStaticDestruction(); // generated by the // implementation } |
所以如果有可能,就尽量用C++来写main函数。只要将C写的main()改名为realMain(),然后用C++版本的main()调用realMain():
1 2 3 4 5 6 |
extern "C" // implement this int realMain(int argc, char *argv[]); // function in C int main(int argc, char *argv[]) // write this in C++ { return realMain(argc, argv); } |
这么做时,最好加上注释来解释原因。
如果不能用C++写main(),那就有麻烦了,因为没有其它办法确保静态对象的构造和析构函数被调用了。一般都是由编译器来具体实现。
TOP动态内存分配
C++部分使用new和delete(参见Item M8),C部分使用malloc(或其变形)和free。只要new分配的内存使用delete释放,malloc分配的内存用free释放,那么就没问题。用free释放new分配的内存或用delete释放malloc分配的内存,其行为没有定义。那么,唯一要记住的就是:将你的new和delete与mallco和free进行严格的隔离。
看一下这个粗糙(但很方便)的strdup函数,它并不在C和C++标准(运行库)中,却很常见:
1 2 |
char * strdup(const char *ps); // return a copy of the // string pointed to by ps |
要想没有内存泄漏,strdup的调用着必须释放在strdup()中分配的内存。但这内存怎么释放?用delete?用free?如果你调用的strdup来自于C函数库中,那么是后者。如果它是用C++写的,那么恐怕是前者。
所以要减少这种可移植性问题,就要尽可能避免调用那些既不在标准运行库中(参见Item E49和Item M35)也不在大多数计算机平台下有固定形式的函数。
TOP数据结构的兼容性
C了解普通指针,所以想让你的C++和C编译器生产兼容的输出,两种语言间的函数可以安全地交换指向对象的指针和指向非成员的函数或静态成员函数的指针。自然地,struct和内建类型(如int、char等)的变量也可自由通过。
如果你在C++版本中增加了非虚函数,其内存结构没有改变,所以,只有非虚函数的结构(或类)的对象兼容于它们在C中的孪生版本(其定义只是去掉了这些成员函数的申明)。增加虚函数将打破这种兼容性,因为其对象将会使用一个不同的内存结构(参见Item M24)。从其它struct(或class)继承而来的struct,通常也改变其内存结构,所以有基类的struct也不能与C函数交互。
结论是:如果某个structure的定义既可以在C++中编译,又可以在C 中进行编译,在C++和C之间这样相互传递数据结构是安全的。在C++版本中增加非虚成员函数基本不影响兼容性,但几乎其它的改变都将影响兼容性。
TOP总结
如果想在同一程序下混合C++与C编程,记住下面的指导原则:
- 确保C++和C编译器产生兼容的obj文件。
- 将在两种语言下都使用的函数申明为extern "C"。
- 只要可能,用C++写main()。
- 总用delete释放new分配的内存;总用free释放malloc分配的内存。
- 将在两种语言间传递的东西限制在用C编译的数据结构的范围内;C++版本的struct可以包含非虚成员函数。
TOP条款35:让自己熟悉C++语言标准
C++发生的主要变化:
- 增加了新的特性:RTTI、命名空间、bool,关键字mutable和explicit,对枚举的重载操作,已及在类的定义中初始化const static成员变量。
- 模板被扩展了:现在允许了成员模板,增加了强迫模板实例化的语法,模板函数允许无类型参数,模板类可以将它们自己作为模板参数。
- 异常处理被细化了:异常规格申明在编译期被进行更严格的检查,unexpected()函数现在可以抛一个bad_exception对象了。
- 内存分配函数被改良了:增加了operator new[]和operator delete[]函数,operator new/new[]在内存分配失败时将抛出一个异常,并有一个返回为0(不抛异常)的版本供选择。(见Effective C++ Item 7)
- 增加了新的类型转换形式:static_cast、dynamic_cast、const_cast,和reinterpret_cast。
- 语言规则进行了重定义:重定义一个虚函数时,其返回值不需要完全的匹配了
C++的这些变化在标准运行库的变化面前将黯然失色。
- 支持标准C运行库。
- 支持string类型。
- 支持本地化。
- 支持I/O操作。
- 支持数学运算。
- 支持通用容器和运算。
在介绍STL前,必须先知道标准C++运行库的两个特性。
- 在运行库中的几乎任何东西都是模板。
- 标准运行库将几乎所有内容都包含在命名空间std中。
STL基于三个基本概念:
- 容器(container)
- 迭代器(iterator)
- 算法(algorithms)。
另外一点:STL是可扩充的。
1. 参考Symbian OS代码
