TR1 和 Boost 条款 54 Tr1 “Technical Report 1”, 是一份规范,描述加入C++标准程序库的诸多新机能.条款 55 Boost Boost 是个组织/网站 , 提供开源的C++程序库条款 01 : 1.C 2.Object-Oriented C++ (oop) 3.Template C++ 4.STL 条款 02 : 使用const,enum,inline替换#define 1.const 2.enum 类内使用相当于常量 3.inline 替换 #define 定义的函数 条款 03 : 尽可能使用const 1.对于不做更改打算的,尽量使用const,防止出现 a*b == c 写成赋值的错误 2.mutabel 释放掉const 的约束 3.const 与 non-const 一个op[] 的例子,说明可以籍由non-const 调用const ,但绝对不能反向操作,籍由const 调用non-const , 注意事项: 1.声明为const有助于编译器侦察错误用法 2.编译器强制实施bitwise constness (const一切都不能更改),用户应该使用”概念上的常量性”,用mutable实现 3.用const 来避免代码复用,通过non-const 调用 const 条款 04 : 确定对象被使用前已初始化 1.对于C part of C++ 和 non-C parts of C++ 初始化规则有点不同,最好的办法是对于所有的对象都初始化 2.对于构造函数可以使用初始化列表技术,对于部分构造可以选择性的在构造函数体中使用赋值来替换初始化 3.“成员初始化次序” ,base classes 更早于其他derived classes ,而class的成员变量总以其声明次序被初始化 4.“不同编译单元内定义non-local static对象” 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 #1. h class FileSystem { public : ... std::size_t numDisks () const ; ... }; extern FileSystem tfs; #2. cpp class Directory { public : Directory ( params ); ... }; Directory::Directory ( params ) { ... std::size_t disks = tfs.numDisks (); ... }
如果想调用Directory的构造 则 tfs 的初始必须在之前,但C++对“定义于不同的编译单元内的non-local static对象” 的初始化相对次序无明确定义. 一个小小的设计:将每个non-local static对象搬到自己的专属函数内,这些函数返回一个reference指向它所含的对象,用户调用这些函数,而不直接涉猎这些对象 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 #1. h class FileSystem { ... };FileSystem & tfs () { static FileSystem fs; return fs; } #2. cpp class Directory { ... };{ ... std::size_t disks = tfs ().numDisks (); ... } Directory & tempDir () { static Directory td; return td; }
注意事项: 1.为内置对象手动初始化,C++不保证初始化他们 2.对构造函数使用初始化列表技术 3.对于跨编译单元的non-local static 对象的处理 条款 05 : 了解C++默认编写并调用哪些函数 1.default构造,copy构造,copy assignment 赋值,析构 2.编译器提供的copy为浅拷贝 3.编译器产出的析构为non-vitrual 析构 4.如果已声明构造,编译器不会再提供default构造 5.对于内含reference 成员的类,应提供自定义copy assignment 6.如果将base class 的 copy assignment 声明为 private ,则 derived class 编译器不会提供copy assignment 条款 06 : 若不想使用编译器自动生成的函数,应该明确拒绝 1.如何拒绝使用编译器提供的copy 构造和copy assignment 2.将其声明为private (但不定义),对其进行copy 行为时会给出编译错误或连接性错误,编译错误源自private 类外不能访问,链接性错误源自member 或 friend 函数链接不到其定义 注意事项: 1.为驳回编译器自动提供的机能,可将其对应的成员函数声明为private 并且不予定义,或使用像 Uncopyable 这样的 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 24 25 #第一种方式 class HomeForSale { public : ... private : ... HomeForSale (const HomeForSale&); HomeforSale & operator =(const HomeForSale&); }; #第二种方式 class Uncopyable { protected : Uncopyable (){} ~Uncopyable (){} private : Uncopyable (const Uncopyable&); Uncopyable & operator =(const Uncopyable&); }; class HomeForSale :private Uncopyable{ ... };
条款 07 : 为多态基类声明virtual析构函数 1.如果base class 析构不是vitrual ,当derived class 对象经由一个base class 指针删除,通常发生derived 成分没销毁,导致”局部销毁”,内存泄露,应给base class 一个virtual析构函数,这样就能正常销毁整个对象,包括derived class 成分 1 2 3 4 5 6 7 8 9 10 #例子 class TimeKeeper { public : virtual ~TimeKeeper (); ...; }; TimeKeeper * ptk = getTimeKeeper (); ... delete ptk;
2.如果没有意图用于base class 则不应该令其析构为virtual,virtual 声明会导致类创建vptr(虚表指针),会导致不必要的内存开支,只有当class 内至少含有一个virtual函数,才为它声明virtual 析构函数 1 2 3 4 5 6 7 8 9 10 #例子 class Point { public : Point (int xCoord, int yCoord); ~Point (); private : int x,y; };
3.pure virtual (纯虚)->abstract (抽象) classes — 不能被实例化的class 1 2 3 4 5 6 class AMOV { public : virtual ~AMOV ()=0 ; }; AMOV::~AMOV (){}
只要class含有一个pure virtual 函数,则这个类为抽象类,注意事项:你必须为这个pure virtual 析构函数提供一份定义 4.析构函数的运作规则,由最外层的 derived class 开始析构,然后是其每一个 base class 的析构函数被调用.编译器会在AMOV的 derived classes 的析构函数中创建一个对~AMOV的调用动作,所以你必须为这个函数提供一份定义(第3点),否则连接器会报错 注意事项: 1.对于具有多态性质的base classes 应该声明一个virtual 析构函数,或者class带有任何virtua l函数,它也应该拥有virtual 析构 2.如果class的目的不是作为base class 则不应该声明virtual 析构函数 条款 08 : 别让异常逃离析构函数 1.如果析构吐出异常,则会导致程序提前结束或出现不明确的行为 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 #例子:class 负责数据库连接 class DBConnection { public : ... static DBConnection creat () ; void close () ; }; #为防止客户忘记调用close (),一个合理的想法是创建一个class 用来管理 class DBConn { public : ... ~DBConn () { db.close (); } private : DBConnection db; }; { DBConn dbc (DBConnection::creat()) ; }
2.如果调用失败则会抛出异常,导致问题,两种解决方法 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 #1. 如果close抛出异常就结束程序,通过调用abort完成 DBConn::~DBConn () { try { db.close (); } catch (...){ 制作运转记录,记下对close的调用失败; std::abort (); } } #2. 吞下close导致的异常 DBConn::~DBConn () { try { db.close (); } catch (...){ 制作转运记录,记下对close的调用失败; } }
3.上述方法用处不大,一个更好的解决方法为为客户提供一个处理发生异常的机会 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 class DBConn { public : ... void close () { db.close (); closed = true ; } ~DBConn () { if (!closed) { try { db.close (); } catch (...){ 制作运转记录,记下对close的调用失败; ... } } } private : DBConnection db; bool closed; };
如果某个操作可能在失败时抛出异常,而又存在某种需要必须处理该异常,那这个异常必须来自析构函数以外的某个函数,因为析构函数抛出异常很危险,会导致提前结束程序的风险,或发生不明确行为 注意事项: 1.析构函数不应抛出异常,如果一个被析构函数调用的函数可能抛出异常,析构函数应该捕捉任何异常,吞下它或提前结束程序 2.可以给客户提供一个处理程序中异常的接口,一个普通函数 条款 09 : 绝不在构造和析构过程中调用virtual函数 1.在base class 构造期间,virtual 函数不是 virtual 函数 2.在derived class 对象的base class 构造期间,对象类型是base class,不是derived class 3.确定你的构造函数都没有(在对象被创建和被销毁期间)调用virtual函数,而它们调用的函数也都服从同一约束, 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 #错误示例1 class Transaction { public : Transaction () { init (); } virtual void logTransaction () const = 0 ; ... private : void init () { ... logTransaction (); } }; #正确示例2 class Transaction { public : explicit Transaction (const std::string & logInfo) ; void logTransaction (const std::string & logInfo) const ; ... }; Transaction::Transaction (const std::string & logInfo) { ... logTransaction (logInfo); } class BuyTransaction :public Transaction{ public : BuyTransaction ( parameters ) :Transaction (createLogString ( parameters )) { ... } ... private : static std::string creatLogString ( parameters ) ; };
注意本例中比起 初始化列表内直接给予数据 利用辅助函数创建一个值传给构造函数更方便(更可读),声明为static 防止出现 “那些成员变量处于为定义状态” 注意事项: 1.在构造和析构期间不要调用virtual函数,因为这类调用不会下降至derived class,换句话说在base class 构造期间,virtua函数调用属于base的那个 条款 10 : 令operator=返回一个reference to *this 1.不仅仅适用于=,也适用于+=,-=,*=等等 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 #示例1 class Widget { public : ... Widget& operator =(const Widget& rhs) { ... return * this ; } ... }; #适用于其他赋值相关运算 class Widget { public : ... Widget& operator +=(const Widget& rhs) { ... return * this ; } Widget& operator =(int rhs) { ... return * this ; } ... };
2.这仅仅是个协议,无强制性,如果你有自己的实现需求,可以不遵守它 注意事项: 1.令assignment(赋值)操作符返回一个 reference to * this 条款 11 : 在operator=中处理”自我赋值” 1.不处理自我赋值的后果,在“停止使用资源前意外释放了它” 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 #示例 class Bitmap { ... };class Widget { ... private : Bitmap * pb; }; Widget& Widget::operator =(const Widget & rhs) { delete pb; pb = new Bitmap (*rhs.pb); return * this ; }
2.解决方式 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 Widget& Widget::operator =(const Widget & rhs) { if (this == &rhs) return * this ; delete pb; pb = new Bitmap (*rhs.pb); return * this ; } Widget& Widget::operator =(const Widget & rhs) { Bitmap * pOrig = pb; pb = new Bitmap (*rhs.pb); delete pOrig; return * this ; } class Widget { ... void swap (Widget & rhs) ; ... }; Widget& Widget::operator =(const Widget & rhs) { Widget temp (rhs); swap (temp); return * this ; } Widget& Widget::operator =(Widget rhs) { swap (rhs); return * this ; }
注意事项: 1.确保处理”自我赋值” ,包括上述三种方式的技术可以处理 2.确定任何函数如果操作一个以上的对象,而其中多个对象是同一个对象时,其行为任然正确,就像第一个示例中 赋值 和 被赋值 的对象是同一个 条款 12 : 复制对象时勿忘其每一个成分 1.往类中新添成员 ,同时也需要修改copying函数,编译器不会提醒 ,导致可能局部拷贝 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 void logCall (const std::string & funcName) ; class Customer { public : ... Customer (const Customer & rhs); Customer& operator =(const Customer & rhs); ... private : std::string name; }; Customer::Customer (const Customer & rhs):name (rhs.name) { logCall ("Customer copy constructor" ); } Customer& Customer::operator =(const Customer & rhs) { logCall ("Customer copy assignment operator" ); name = rhs.name; return * this ; } class Date { ... }; class Customer { public : ... private : std::string name; Date lastTransaction; } class PriorityCustomer :public Customer { public : ... PriorityCustomer (const PriorityCustomer & rhs); PriorityCustomer& operator =(const PriorityCustomer & rhs); ... private : int priority; }; PriorityCustomer::PriorityCustomer (const PriorityCustomer & rhs) :Priority (rhs.Priority) { logCall ("PriorityCustomer copy constructor" ); } PriorityCustomer& PriorityCustomer::operator =(const PriorityCustomer & rhs) { logCall ("PriorityCustomer copy assignment operator" ); Priority = rhs.Priority; return * this ; } PriorityCustomer::PriorityCustomer (const PriorityCustomer & rhs) :Customer (rhs),Priority (rhs.Priority) { logCall ("PriorityCustomer copy constructor" ); } PriorityCustomer& PriorityCustomer::operator =(const PriorityCustomer & rhs) { logCall ("PriorityCustomer copy assignment operator" ); Customer::operator =(rhs); Priority = rhs.Priority; return * this ; }
2.当你编写一个copying函数,确保复制所有local成员变量,调用所有base class内适当的copying函数 3.你不该令copy assignment操作符 调用 copy构造函数,反过来也不能令copy构造函数调用copy assignment操作符 注意事项: 1.copying函数应确保复制”对象内的所有成员变量”及”所有base class 成分” 2.不要尝试以某个copying函数实现另一个copying函数,应将共同机能放进第三个函数,并由两个copying函数共同调用 条款 13 : 以对象管理资源 1.为何需要以对象管理资源 1 2 3 4 5 6 7 8 9 10 class Investment { ... }; Investment * createInvestment () ; void f () { Investment * pInv = createInvestment (); ... delete pInv; }
2.利用auto_ptr(pointer-like) ,也就是智能指针,其析构函数会自动调用delete 1 2 3 4 5 6 void f () { std::auto_ptr<Investment> pInv (createInvestment()) ; ... }
“以对象管理资源”的两个关键想法 a.获得资源后立刻放进管理对象,该观念被称为“资源取得时机便是初始化时机”(RAII) b.管理对象运用析构函数确保资源被释放 3.由于其自动销毁,注意别让多个auto_ptr指向同一对象,否则会导致”未定义行为”,对于这个问题,auto_ptr有一个不寻常的性质: 若通过copy构造或copy assignment 复制它们,它们会变成null,复制所得指针获取资源的唯一拥有权 1 2 3 4 std::auto_ptr<Investment> pInv1 (createInvestment()) ; std::auto_ptr<Investment> pInv2 (pInv1) ; pInv1 = pInv2;
“受auto_ptr管理的资源必须绝对没有一个以上 的auto_ptr同时指向它” 4.另一替代方案“引用计数型智能指针”(RCSP) .RCSP持续追踪共有多少对象指向某资源,并在无人指向它时自动删除该资源 Tr1的tr1::shared_ptr 就是个RCSP 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 void f () { ... std::tr1::shared_ptr<Investment> pInv (createInvestment()) ; ... } void f () { ... std::tr1::shared_ptr<Investment> pInv1 (createInvestment()) ; std::tr1::shared_ptr<Investment> pInv2 (pInv1) ; pInv1 = pInv2; ... }
5.auto_ptr和shared_ptr都在析构函数内做delete而不是delete[],这意味着动态分配的array不能在auto_ptr或shared_ptr上使用 (如有需求见条款55) 注意事项: 1.为防止资源泄漏,应使用RAII对象,它们在构造时获取资源,析构时释放资源 2.两个常用的RAII对象,tr1::shared_ptr 和 auto_ptr,两只区别在于复制行为的不同 条款 14 : 在资源管理类中小心copying行为 1.对于并非heap_based的资源 ,auto_pt 或 tr1::shared_ptr 往往不适合,因此你需要建立自己的资源管理类 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 void lock (Mutex * pm) ; void unlock (Mutex * pm) ; class Lock { public : explicit Lock (Mutex * pm) :mutexPtr(pm) { lock (mutePtr); } ~Lock (){ unlock (mutexPtr); } private : Mutex * mutexPtr; }; Mutex m; ... { Lock m1 (&m) ; ... }
2.考虑如果Lock对象被复制,会发生什么 1 2 Lock m11 (&m) ; Lock m12 (m11) ;
a.禁止复制,许多时候允许RAII对象被复制并不合理.如果复制动作对RAII class 并不合理, 应该将其禁止,通过将copying操作声明为private(条款6) b.对底层资源祭出“引用计数法” ,如 tr1::shared_ptr. 通常内含一个tr1::shared_ptr 成员变量,RAII class 便可实现 reference-counting copying 行为,如 若前述的Lock打算使用reference couting ,它可以改变mutexPtr的类型,改为 tr1::shared_ptr<Mutex.> ,tr1::shared_ptr 的默认行为为“当引用次数为0时删除其所指物” ,这不是Lock想要的行为,但tr1::shared_ptr允许指定所谓的”删除器” ,那是一个函数或函数对象,当引用次数为0的时候调用 1 2 3 4 5 6 7 8 9 10 11 12 13 class Lock { public : explicit Lock (Mutex * pm) :mutexPtr(pm,unlock) //将unlock 函数 作为删除器 { lock (mutexPtr.get ()); } private : std::tr1::shared_ptr<Mutex> mutexPtr; }
c.复制底层资源 ,复制资源管理类对象时,进行的应是 “深度拷贝” d.转移底部资源的拥有权,如条款13 ,这是auto_ptr的复制意义 注意事项: 1.复制RAII对象必须一并复制它所管理的资源,所以资源的copying行为决定RAII对象的copying行为 2.常见的的RAII class copying 行为为:抑制copying(禁止复制) , reference counting (引用计数法), 其他行为也可能被实现如 c,d. 条款 15 : 在资源管理类中提供对原始资源的访问 1.为什么引入对原始资源的直接访问 : 下述例子想返回一个int型的天数 却通不过编译,因为daysHeld需要的是 Investment*指针 但传给它的却是个类型为tr1::shared_ptr<.Investment>的对象 1 2 3 4 tr1::shared_ptr<Investment> pInv (createInvestment()) ; int dayHeld (const Investment * pi) ; int days = dayHeld (pInv);
2.两种做法可以达成目的,显示转换和隐式转换. tr1::shared_ptr和auto_ptr都提供一个get函数,用来执行显示转换,也就是它会返回智能指针内部的原始指针(的复件). 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 int days = dayHeld (pInv.get ()); class Investment { public : bool isTaxFree () const ; ... }; Investment* createInvestment () ; std::tr1::shared_ptr<Investment> pil (createInvestment()) ; bool taxable1 = !(pi1->isTaxFree ()); ... std::auto_ptr<Investment> pi2 (createInvestment()) ; bool taxable2 = !((*pi2).isTaxFree ()); ...
3.显示转换和隐式转换 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 FontHandle getFont () ;void releaseFont (FontHandle fh) ;class Font { public : explicit Font (FontHandle fh :f(fh) { } ~Font(){ releaseFont(f); } private : FontHandle f; }; class Font{ ... FontHandle get()cosnt{ return f; } ... } void changeFontSize(FontHandle f,int newSize);Font f(getFont()); int newFontSize;... changeFontSize(f.get(), newFontSize) class Font{ ... operator FontHandle()const ; { return f; } ... } Font f(getFont()); int newFontSize;... changeFontSize(f,newFontSize) Font f1(getFont()); ... FontHandle f2 = f1;
4.通常显示转换用的更多,隐式转换会增加错误的发生.RAII class 并不是为了封装而存在: 其目的为”确保一个特殊行为–资源释放–会发生” ,所以RAII class中返回原始资源的函数并非设计灾难注意事项: 1.APIs往往要求访问原始资源,所以每一个RAII class 应该提供一个”获取原始资源”的方法 2.通过显示转换或隐式转换 都可以访问,但显示更安全,隐式增加了错误的发生但更方便 条款 16 : 成对使用new和delete时采用相同形式 1.new 与 delete 应该成对配套出现 , new 与 delete , new [] 与 delete[] 2.原因,数组所有内存通常还包括”数组大小”的记录,以便delete知道调用多少次析构,单一内存则没有这笔记录
单一对象
Object
对象数组
n
Object
Object
Object
上述大概描述了编译器中的实现,但并非所有编译都是如此,但足够解释为什么配套使用 3.对typedef也是如此,考虑下述例子 1 2 3 4 5 6 typedef std::string AddressLines[4 ];std::string* pal = new AddressLines; delete pal; delete [] pal;
注意事项: 1.new 和 delete ,new [] 和 delete [] 需成对配套出现使用 条款 17 : 以独立语句将newd对象置入智能指针 1.对于下述例子 1 2 3 4 5 6 7 8 9 int priority () ;void processWidget (std::tr1::shared_ptr<Widget> pw, int priority) ;processWidget (new Widget, priority ());processWidget (std::tr1::shared_ptr <Widget>(new Widget),priority ());
a.调用priority c.调用tr1::shared_ptr 构造函数 编译完成这些事情的次序具有弹性 ,如果最终获得这样的操作序列 b.调用priority c.调用tr1::shared_ptr 构造函数 3.如何处理: 1 2 std::tr1::shared_ptr<Widget> pw (new Widget) processWidget (pw,priority()) ;
注意事项: 1.以独立语句将newd对象存储于智能指针 ,如果不这么做,一旦抛出异常,则可能导致难以察觉的资源泄漏 条款 18 : 让接口容易被正确使用,不易被误用 1.导入简单的外覆类型 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 class Data { public : Date (int month,int day,int year); ... }; Date d (30 ,3 ,1995 ) struct Day { explicit Day (int d) :val(d){ } int val; }; struct Month { explicit Month (int d) :val(d){ } int val; }; struct Year { explicit Year (int d) :val(d){ } int val; }; class Date { public : Date (const Month& m, const Day& d, const Year& y); ... }; Date d (30 ,3 ,1995 ) ; Date d (Day(30 ),Month(3 ),Year(1995 )) ; Date d (Month(3 ),Day(30 ),Year(1995 )) ;
2.提供行为一致的接口 ,如果STL容器每个都有一个size成员函数,告诉调用者容器内有多少个对象 3.对于下述例子 1 2 3 4 Investment* createInvestment () ;std::tr1::shared_ptr<Investment> createInvestment () ;
4.假设调用者期许将指针传给一个特定的函数来执行特定的销毁而不是delete 可以尝试将其绑定一个删除器 1 2 3 4 5 6 7 8 9 10 11 12 13 14 std::tr1::shared_ptr<Investment> pInv (0 ,getRidOfInvestment) ; std::tr1::shared_ptr<Investment> pInv (static_cast <Investment*>(0 ),getOfInvestment) ; std::tr1::shared_ptr<Investment> createInvesment () { std::tr1::shared_ptr<Investment> retVal (static_cast <Investment*>(0 ), getRidOfInvestment) ; reVal = ...; return reVal; }
5.防范“cross-DLL-problem” (不清楚DLL是什么,似乎与多线程有关) 注意事项: 1.应尽量在接口中实现,不容易误用的性质 2.实现接口的一致性,以及与内置类型的行为兼容 3.”阻止误用” a.建立新类型 b.限制类型上的操作 c.束缚对象值 d.消除客户的资源管理自认 4.tr1::shared_ptr 可以自定义删除器 , 这可防范DLL 问题 条款 19 : 设计class犹如type 1.新type的对象该如何创建和销毁? 考虑构造和析构,以及new 和 delete 重载的设计 2.初始化和对象的赋值该有什么差别? 不要混淆对象的构造函数和operator= 3.新type对象如果 pass-by-value 意味着什么? 通常用copy构造函数来实现pass by value 4.新type的合法值? 对class范围的约束,对成员函数需要进行的错误检测工作 5.你的新type需要配合某个继承图系吗? 对于继承中non-virtual 和 virtual 声明的考虑,尤其是析构函数是否为virtual (条款 34,36) 6.你的新type需要声明样的转换? types之间的转换考虑, 隐式转换 或 i按时转换函数的设计 7.什么样的操作符和函数对此新type是合理的? 考虑该type需要什么函数和操作符,如list需要insert (条款 23,24,46) 8.什么样的标准函数该驳回? 如你不需要编译器默认提供的copying 函数,即应该将其声明为private (条款 6) 9.谁该取用type的成员? 考虑成员的访问权,是声明为private , public, 还剩 protected 10.什么是新type的”未声明接口”? 不理解(见条款29) 11.你的新type有多么一般化? 考虑是否需要使用 template class 12.你真的需要一个新的type吗? 考虑继承 自 base class 即在 derived class 上添加新机能 注意事项: 1.再设计class 之前确定你已经考虑过本条款所有讨论主题 条款 20 : 以pass-by-reference-to-const替换pass-by-value 1.考虑下述继承体系 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 class Person { public : Person (); virtual ~Person (); ... private : std::string name; std::string address; }; class Student :public person{ public : Student (); ~Student (); ... private : std::string schoolName; std::string schoolAddress; }; bool validateStudent (Student s) ; Student plato; bool platoIsOK = validateStudent (plato);
上述函数实现上没有问题 ,但 pass-by-value 时构造一个临时的Student,Student内又包含两个string,同时继承自Perosn Base-Class 又要构建Person ,Person又包含两个string,总体上进行了六次构造和六次析构,效率极低 2.对此的解决方式即为pass-by-reference 1 bool validateStudent (const Student& s) ;
3.by-reference可以避免slicing(对象切割) ,此问题产生于将derived class 以 by-value的方式传递给形参为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 24 25 26 27 28 class Window { public : ... std::string name () const ; virtual void display () const ; }; class WindowWithScrollBars :public Window{ public : ... virtual void display () const ; }; void printNameAndDisplay (Window w) { std::cout << w.name (); w.display (); } WindowWithScrollBars wwsb; printNameAndDisplay (wwsb);void printNameAndDisplay (const Window& w) { std::cout << w.name (); w.display (); }
4.pass-by-refernce 往往比 pass-by-value 更高效 5.对于大部分类型应该选用by-reference 但对于内置类型,及STL迭代器和函数对象选择pass-by-value并非没有道理 注意事项: 1.尽量以pass-by-reference 替换 pass-by-value ,高效且避免切割问题 2.以上规则不适用于内置类型 ,以及STL迭代器和函数对象 条款 21 : 必须返回对象时,别妄想返回reference 1.返回 reference 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 class Rational { public : Rational (int numerator = 0 , int denominator = 0 ); ... private : int n,d; friend Rational operator * (const Rational& lhs, const Rational& rhs); }; Rational a (1 ,2 ) ; Rational b (3 ,5 ) ; Rational c = a * b;
2.在stack 或 heap 空间创建并返回 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 const Rational& operator *(const Rational& lhs, const Rational& rhs){ Rational result (lhs.n * rhs.n , lhs.d * rhs.d) ; return result; } const Rational& operator *(const Rational& lhs, const rational& rhs){ Rational* result = new Rational (lhs.n * rhs.n , lhs.d * rhs.d); return * result; } Rational w,x,y,z; w = x * y * z;
3.如果使用static 呢 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 const Rational& operator *(const Rational& lhs, const Rational& rhs){ static Rational result; result = ...; retur retsult; } bool operator ==(const Rational& lhs, const Rational& rhs);Rational a,b,c,d; if ((a*b)==(c*d)){ ...; } else { ...; }
4.综上,一个”必须返回新对象”的函数正确写法是:return 一个新对象 而不是refernce 1 2 3 4 inline const Rational operator *(const Rational& lhs, const Rational& rhs){ return Ratioanl (lhs.n*rhs.n,lhs.d*rhs.d); }
注意事项: 1.绝不要返回pointer或reference指向local stack 对象 ,或返回reference 指向 heap-allocated 对象, 或返回 reference 指向local-static 对象.(条款4)为”在单线程环境中合理返回reference指向一个local statci对象”提供了一份设计实例 条款 22 : 将成员变量声明为private 1.保证语法一致性 2.对成员变量进行更精确的控制 3.保证封装性,从封装的角度出发,其实只有两种访问权限:private 和 其他 4.条款23:某些东西的封装性于”当其内容改变时造成的代码破坏量”成反比 注意事项: 1.切记将成员变量声明为private,赋予客户访问数据的一致性(即通过Public方法来访问),可细微划分访问控制,允诺约束条件获得保证,提供class 作者充分的实现弹性 2.proteced 并不比 public 更具封装性 条款 23 : 宁以non-member-non-friend替换member函数 1.对于下述例子 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 class WebBrowser { ... void clearCache () ; void clearHistory () ; void removeCookies () ; ... }; class WebBrowser { ... void clearEverything () ; ... }; void clearBrowser (WebBrowser& wb) { wb.clearCache (); wb.clearHistory (); wb.removeCookies (); }
clearBrowser具有更大的封装性 2.C++中更自然的做法是让clearBrowser 作为一个non-member 且与其在同一namespace 1 2 3 4 5 6 namespace WebBrowserStuff{ class WebBrowser { ... }; void clearBrowser (WebBrowser& wb) ; ... }
3.namespcae 的好处: a.可跨越多个源码文件,而后者不能 b.将所有便利函数放在多个头文件但隶属于同一个namespace 意味着可以更轻松的扩展 ,class 对于客户是不能扩展的 注意事项: 1.以non-member-non-friend 替换 member 函数,依次增加封装性 ,包裹弹性? 和机能扩充性 条款 24 : 若所有参数皆需类型转换,为此采用non-member 函数 1.对于下述例子 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 Rational { public : Rational (int numerator=0 , int denominator=1 ); int numerator () const ; int denominator () const ; private : ... }; class Rational { public : ... const Rational operator *(const Rational& rhs)const ; }; Rational oneEighth (1 ,8 ) ;Rational oneHalf (1 ,2 ) ;Rational result = oneHalf * oneEighth; result = result * oneEighth; 1. result = oneHalf.operator *(2 ); 2. result = 2. operator *(oneHalf); 3. result = operator *(2 ,oneHalf); const Rational temp (2 ) ;result = oneHalf * temp; result = oneHalf * 2 ; result = 2 * oneHalf;
2.对此可以以non-member函数实现 1 2 3 4 5 6 7 8 9 10 class Raional { ... };const Rational operator *(const Rational& lhs, const Rational& rhs){ return Rational (lhs.numerator () * rhs.numerator (), lhs.denominator () * rhs.denominator ()); } Rational oneFourth (1 ,4 ) ;Rational result; result = oneFourth * 2 ; result = 2 * oneFourth;
3.member函数的反面是non-member函数,不是friend函数 注意事项: 1.如果你需要为某个函数的所有参数 进行类型转换,那么这个函数必须是个non-member 条款 25 : 考虑写出一个不抛出异常的swap函数 1.考虑下述例子 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 namespace std{ template <typename T> void swap (T& a, T& b) { T temp (a) ; a = b; b = temp; } } class WidgetImpl { public : ... private : int a,b,c; std::vector<double > v; ... }; class Widget { public : Widget (const Wdiget& rhs); Widget& operator =(const Widget& rhs) { ... *pImpl = *(rhs.pImpl); ... } ... private : WidgetImpl * pImpl; }
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 namespace std{ template <> void swap <Widget>(Widget& a, Widget& b) { swap (a.pImpl, b.pImpl); } } class Widget { public : ... void swap (Widget& other) { using std::swap; swap (pImpl, other.pImpl); } ... }; namespace std{ template <> void swap <Widget>(Widget& a, Widget& b) { a.swap (b); } }
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 template <typename T>class WidgetImpl { ... };template <typename T>class Widget { ... };namespace std{ template <typename T> void swap< Widget<T> >(Widget<T>& a, Widget<T>& b) { a.swap (b); } } namespace std{ template <typename T> void swap (Widget<T>& a, Widget<T>& b) { a.swap (b); } }
4.一般而言重载function template 没有问题,但std是个特殊的namespace ,其管理规则比较特殊, 客户可以全特化 其中的templates 但不可以添加新的templates,即为此处为什么重载std::swap不合法的原因,所以请不要添加新的东西到std里头 5.所以对 3 中的解决办法为 声明一个non-member-swap,让他调用member-swap,但不再将这个swap声明为std::swap的特化或重载版本 1 2 3 4 5 6 7 8 9 10 11 12 13 namespace WidgetStuff{ ... template <typename T> class Widget { ... }; ... template <typename T> void swap (Widget<T>& a, Widget<T>& b) { a.swap (b); } }
6.应该调用哪个swap? std::swap的一般化,还是可能存在的特化版本,栖身于某个命名空间的T专属版本? 1 2 3 4 5 6 7 8 9 template <typename T>void doSomething (T& obj1, T& obj2) { using std::swap; ... swap (obj1,obj2); ... }
7.此处已经讨论过 default swap ,member swap ,non-member swap ,std::swap 特化版本,以及对swap的调用,如果swap默认版本效率不够,尝试做以下事 a.提供一个public swap 成员函数,此函数绝不该抛出异常 b.在你的class 或 template 所在的命名空间内提供一个non-member swap c.如果你在编写一个class ,为你的class 特化std::swap d.调用时确定包含一个using 声明式,使std::swap在函数内曝光 注意事项: 1.当std::swap 效率不高时,提供一个绝不抛出异常的swap成员函数 2.如果你提供一个member swap,也该提供一个non-member swap来调用函数,队与classes(而非templates) 请特化std::swap 3.调用swap时应该针对std::swap使用using 声明 4.不要在std namespcae 中添加新的东西,但可以提供template的特化 条款 26 : 尽可能延后变量定义式的出现时间 1.对于下述例子 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 std::string encryptPassword (const std::string& password) { using namespace std; string encrypted; if (password.length () < MinmumPasswordLength){ throw logic_error ("Password is too short" ); } ... return encrypted; } std::string encryptPassword (const std::string& password) { using namespace std; if (password.length () < MinmumPasswordLength){ throw logic_error ("Password is too short" ); } string encrypted; ... return encrypted; } void encrypt (std::string& s) ;std::string encryptPassword (const std::string& password) { ... std::string encrypted; encrypted = password; encrypt (encrypted); return encrypted; } std::string encryptPassword (const std::string& password) { ... std::string encrypted (password) ; encrypt (encrypted); return encrypted; }
2.对于循环来说呢? 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 Widget w; for (int i = 0 ; i < n; ++i){ w = i; ... } for (int i = 0 ; i < n; ++i){ Widget w (i) ; ... }
大体上A更高效,尤其当n很大的时候,否则做法B或许比较好.A造成名称w的作用域比B大,可能对程序的可理解性和易维护性造成冲突,因此除非你知道: a.赋值成本比”析构+构造“低 b.你正在处理代码中效率高度敏感的部分 注意事项: 1.尽可能延后变量定义式的出现,以增加程序清晰度,并改善效率 条款 27 : 尽量少做转型动作 1.”旧式转换”: (T)expression 和 T(expression) , C++提供四种新式类型转换 1 2 3 4 5 const_cast <T>(expression)dynamic_cast <T>(expression)reinterpret_cast <T>(expression)static_cast <T>(expression)
a. const_cast 通常用来将对象的常量性转除 (cast away the constness),也是唯一有次能力的C++-style操作符 b. dynamic_cast 用来执行”安全向下转型” (safe downcasting) ,也就是来决定某对象是否归属继承体系中的某个类型,唯一无法由旧语法执行的动作,而且开销巨大 c. reinterpret_cast 意图执行低级转型 d. static_cast 用来强迫隐式转换,例如将non-const 转换为 const对象(条款3),或像int 转为 double,但他无法做到const转为non-const 2.C++新式转换很受欢迎,但旧式转换仍然有更适合的使用时机 ,参考下述例子 1 2 3 4 5 6 7 8 9 class Widget { public : explicit Widget (int size) ; ... }; void doSomeWork (const Widget& w) ;doSomeWork (Widget (15 )); doSomeWork (static_cast <Widget>(15 ));
对此蓄意的”对象生成”,动作上不像”类型转换”,所以使用旧式转换可能更恰当 ,但始终使用新式转换也是好的习惯 3.对于类型转换,编译器什么都没做吗?参考下述例子 1 2 3 4 5 6 class Base { ... };class Derived :public Base { ... };Derived d; Base * pb = &d;
4.参考下述例子,一个base class 和 derived class ,两者都定义了virtual 函数,要求先在derived class 的virtual 中 调用 base class 的virtua 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 class Window { public : virtual void onResize () { ... } ... }; class SpecialWindow :public Window{ public : virtual void onResize () { static_cast <Window>(*this ).onResize (); ... } ... };
5.解决的办法应该是拿掉转型动作,采用下述例子的方法 1 2 3 4 5 6 7 8 9 10 class SpecialWindow :public Window{ public : virtual void onResize () { Window::onResize (); ... } ... };
6.对于dynamic_cast 应该注重他对效率的影响,之所以需要使用:dynamic_cast ,通常是因为你想要在一个你认定为derived class 对象身上执行derived class操作,但你的手上只有一个”指向base”的pointer或reference ,一般两种做法可以解决: 第一,使用容器并在其中存储指向 derived class 对象的指针(通常是智能指针,条款13) 假设先前的Window / SpecialWindow 继承中,只有SpecialWindow 支持闪烁 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 class Window { ... };class SpecialWindow :public Window{ public : void blink () ; ... }; typedef std::vector<std::tr1::shared_ptr<Window> > VPM;VPM winPtrs; ... for (VPM::iterator iter = winPtrs.begin ();iter != winPtrs.end (); ++iter){ if (SpecialWindow *psw = dynamic_cast <SpecialWindow*>( iter->get () ) ) psw->blink (); } typedef std::vector<std::tr1::shared_ptr<SpecialWindow> > VPSW;VPSW winPtrs; ... for (VPSW::iterator iter = winPtrs.begin ();iter != winPtrs.end (); ++iter) (*iter)->blink (); class Window { public : virtual void blink () {} ... }; class SpecialWindow :public Window{ public : virtual void blink () { ... }; ... }; typedef std::vector<std::tr1::shared_ptr<Window> > VPW;VPW winPtrs; ... for (VPM::iterator iter = winPtrs.begin (); iter != winPtrs.end (); ++iter) (*iter)->blink ();
无论哪种写法 1.使用类型安全容器 2.将virtual函数往继承体系上方移动 —-都并非完美的方案,但在许多情况下提供了一个可行的dynamic_cast替代方案 7.绝对需要避免的是所谓的 “连串 dynamic-cast” 1 2 3 4 5 6 7 8 9 10 11 12 class Window { ... };... typedef std::vector<std::tr1::shared_ptr<Window> > VPM;VPM winPtrs; ... for (VPM::iterator iter = winPtrs.begin ();iter != winPtrs.end (); ++iter){ if (SpecialWindow1 * psw1 = dynamic_cast <SpecialWindow1>(iter->get ())) { ... } else if (SpecialWindow2 * psw2 = dynamic_cast <SpecialWindow2>(iter->get ())) { ... } else if (SpecialWindow3 * psw3 = dynamic_cast <SpecialWindow3>(iter->get ())) { ... } ... }
这样的代码又大又慢,且一旦base class 发生改变,需要花费大量的时间检查是否需要修改,应当采用上述替代方案,将其替换 注意事项: 1.尽量避免转型动作,尤其是在效率敏感 的代码中执行 dynamic_cast 2.如果转型是必要的,试着将它隐藏于某个函数背后,客户通过调用该函数达到目的,而不是直接将他放入代码 3.使用新式而不用旧式,新式方便辨认目的 条款28 : 避免返回handles指向对象内部成分 1.成员变量的封装性最多只等于”返回其reference”的函数的访问级别,如返回一个pirvate变量的引用,将导致该变量的访问权限降到public 2.如果const成员函数传出一个reference ,后者所指数据与对象自身有关联,而它又被存储于对象之外,那么这个函数的调用者可以修改那笔数据 , 这正式bitwise constness 的附带结果(见条款3) 3.上述事情都是发生于”函数返回reference”.如果它们返回的式指针或迭代器,同样如此. reference,指针,迭代器 都是所谓的 handles ,返回它们将导致”降低对象封装性 4.对于成员内部,变量或函数,绝不应该令public 中的成员函数返回一个指针指向它们,这样会导致后者的访问级别提高 5.返回handles 易导致 指向一个不存在的对象,即该指针变为 空悬,虚掉的 6.这并不意味着绝对不可以让函数返回handle, operator[] 就是一个允许返回引用的例子,但这是例外,并不是常态 注意事项: 1.避免返回handles(reference,pointer,iterator )指向内部,将发生指针悬吊的可能性降到最低 条款 29 : 为”异常安全”而努力是值得的 1.基本承诺 :如果抛出异常,程序内的任何事物仍然保存在有效状态,没有任何对象或数据结构会因此损坏,例如 对于changeBackground 一旦对象被抛出, 对象可能继续拥有原背景图像,也可能拥有一个缺省背景图像,但客户无法预期哪一种情况,如果想知道,他们恐怕必须调用某个成员函数以得知当时的背景图像是什么 2.强烈保证 :如果异常被抛出,程序状态不改变,即如果成功,就是完全成功,如果失败,则会回复到”调用函数前的状态” 3.不抛掷保证 ,承诺绝不抛出异常,总是能完成它们承诺的功能 4.对于强烈保证的一个一般化设计策略:copy and swap 实现上通常是将所有”隶属于对象的数据”从原对象放进另一个对象内,然后赋予原对象一个指针,指向那个所谓的实现对象,这种手法通常被称为 pimpl idiom 条款31 详细描述了它 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 struct PMImpl { std::tr1::shared_ptr<Image> bgImage; int imageChanges; }; class PrettyMenu { ... private : Mutex mutex; std::tr1::shared_ptr<PMImpl> pImpl; }; void PrettyMenu::changeBackground (std::istream& imgSrc) { using std::swap; Lock m1 (&mutex) ; std::tr1::shared_ptr<PMImpl> pNew (new PMImpl(*pImpl)) ; pNew->bgImage.reset (new Image (imgSrc)); pNew->imageChanges++; swap (pImpl,pNew); }
5.但一般而言它并不保证整个函数有强烈的异常安全性 1 2 3 4 5 6 7 8 9 void someFunc () { ... f1 (); f2 (); ... }
6.问题出在“连带影响” ,只操作局部性状态很容易提供强烈保证,但是当函数对非局部性数据有连带影响时,提供强烈保证就困难的多 7.上述议题会组织你为函数提供强烈保证,即使你想这么做.另一个原因是效率,copy-and-swap的关键在于”修改对象数据的副本”,然后再不抛出异常的前提下置换,因此必须为每一个将被改动的对象做一个副本 8.当”强烈保证”不切实际时,应当提供”基本保证 注意事项: 1.异常安全函数(即使发生异常也不会泄漏资源或允许任何数据结构败坏):这样的函数分为三种级别:基本型,强烈型,不抛异常型 2.”强烈保证”通常的实现策略为copy-and-swap ,但并非所有函数都可以实现或具备实现意义 3.函数提供的”异常安全保证”,通常只等于其调用各个函数的”异常安全保证”中的最弱者,短板效应? 条款 30 : 透彻了解inlining的里里外外 1.inline 虽然“免除函数调用成本” ,但导致程序体积增大,适用于本体很小的函数 2.inline 是对编译器提出的一个申请,而不是强制命令 .可以隐喻提出,也可以明确提出 1 2 3 4 5 6 7 8 9 10 class Person { public : ... int age () const { return theAge; } ... private : int theAge; };
3.inlining在大多数C++程序中是编译期行为 4.有时候即使inline但还是会生成函数本体(是否意味着inline代表着没有函数本体?),如下述例子 1 2 3 4 5 6 inline void f () {...} void (*pf) () = f; ... f (); pf ();
5.构造和析构函数不适用于inline 6.inline 不适用于 virtual ,virtual 运行时确定, inline编译时确定 ,互相冲突 7.inline 函数难以调试(不知道现在的编译器解决这个问题没) 注意事项: 1.将inline 限制在小型,调用频繁的函数身上 2.不要只因为function template 出现在头文件,就将它们声明为 inline 3.重点 inline 是个申请,最终是否为inlined函数取决于编译器(自己的理解) 条款 31 : 将文件间的编译依存关系降至最低 1.编译时定义,往往会包含其他文件中的定义式,如果其他文件中的定义被更改,那么则该文件需要重新编译,这既是 “编译依存关系” , 不过通常包含标准头文件不会造成问题 2.采用pimpl idiom 设计 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 #include <string> #include <memory> class PersonImpl ; class Date ; class Address ;class Person { public : Person (const std::string& name, const Date& birthday, const Address& addr); std::string name () const ; std::string birthDate () const ; std::string address () const ; ... private : std::tr1::shared_ptr<PersonImpl> pImpl; };
这样的设计下,Person的客户完全与Dates,Addresses以及Person的实现细节分离,”接口与实现分离” 这个分离关键在于,”声明的依存性” 替换 “定义的依存性”, 那正是编译依存最小化的本质,这个简单的设计策略: a.如果使用 object references 或 object pointers 可以完成任务,则不用使用objects b.如果能够,尽量以class 声明式替换class 定义式 c.为声明式和定义式提供不同的头文件 (类似于我自己写的?将声明与定义分文件编写,声明包含定义头文件) 3.这样的pimpl设计,使classes 往往被称为Handle classes ,这种做法不会改变它要做的事情,只会改变它做事的方式.另一种制作Handle class 的方法是,令Person class 成为abstract class ,称为interface class 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 class Person { public : virtual ~Person (); virtual std::string name () const = 0 ; virtual std::string birthDate () const = 0 ; virtual std::string address () const = 0 ; ... }; class Person { public : ... static std::tr1::shared_ptr<Person>create (const std::string& name, const Date& birthday, const Address& addr); ... }; std::string name; Date dateOfBirth; Address address; ... std::tr1::shared_ptr<Person> pp (Person::create(name,dateOfBith,address)) ; ... std::cout << pp->name () << " was born on " << pp->birthDate () <<" and now lives at" << pp->address (); ...
当然支持interface class 接口的具象类必须被定义出来(p147) 4.Handle classes 和 Interface classes 解除了接口和实现之间的耦合关系,从而降低文件间的编译依存性 注意事项: 1.支持”编译依存性最小化”的一般构想是:相依于声明式,不要相依于定义式 2.程序库头文件,应该以”完全且仅有声明式”,(类似于main->声明.h->实现.cpp ?) 条款 32 : 确定你的public 继承塑模出 is-a 关系 注意事项: 1.”public”继承,意味着 is-a 关系,适用于base class 身上的每一件事情也一定适用于derived class 身上 条款 33 : 避免遮掩继承而来的名称 1.同名的函数会导致隐藏 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 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 () ; ... }; Derived d; int x;... d.mf1 (); d.mf1 (x); d.mf2 (); d.mf3 (); d.mf3 (x);
2.覆盖 / 隐藏 3.通过using 声明取消隐藏 4.转交函数 forwarding funciton 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 class Base { public : virtual void mf1 () =0 ; virtual void mf1 (int ) ; ... }; class Derived :private Base{ public : virtual void mf1 () { Base::mf1 (); } ... }; ... Derived d; int x; d.mf1 (); d.mf1 (x);
注意事项: 1.deirved class 内的名称会隐藏base class 内的名称 2.可用using 声明或转交函数来让被隐藏的名称再见天日 条款 34 : 区分接口继承和实现继承 1.继承时可能出现三种我们希望的情况,”只继承接口”,”同时继承接口和实现,但希望能override”,”继承接口和实现,但不允许override”. 2.成员函数的接口总会被继承 3.pure virtual函数的目的是为了第一种情况,只继承函数接口(Interface class 类似条款31?) 4.impure virtua函数目的是为了第二种情况,继承函数的接口和默认实现 1 2 3 4 5 6 7 class Shape { public : virtual void error (const std::string& msg) ; ... };
但这种情况可能出现危险 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 class Airport { ... };class Airplane { public : virtual void fly (const Airport& destination) ; ... }; void Airplane::fly (const Airport& destination) { default 实现 } class ModelA ::public Airplane { ... };class ModelB ::public Airplane { ... };class ModelC ::public Airplane{ ... }; Airport PDX (...) ; Airplane* pa = new ModelC; ... pa->fly (PDX)
如果忘记override,则采用默认实现,可能导致错误,问题不在于采用了默认实现,关键在于ModelC在没有override的时候,继承了fly,获得了一个它没有的行为,切断virtual接口 和其默认实现之间 的联系,可以解决这种问题 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 class Airplane { public : virtual void fly (const Airport& dstination) =0 ; ... protected : void defalutFly (const Airport& destination) ; }; void Airplane::defaultFly (const Airplane& destination) { default 实现 } class ModelA ::public Airplane{ public : virtual void fly (const Airplane& destination) { defaultFly (destination); } ... }; class ModelB ::public Airplane{ public : virtual void fly (const Airplane& destination) { defaultFly (destination); } ... };
由于pure-virtual函数一定要被override,则可以采用另一种方式 (如果不覆盖,则派生类也会由于继承了纯虚函数变为抽象类–个人理解) 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 class Airplane { public : virtual void fly (const Airport& destination) =0 ; ... }; void Airplane::fly (const Airport& destination) { default 实现 } class ModelA ::public Airplane{ public : virtual void fly (const Airport& destination) { Airplane::fly (destination); } ... }; class ModelC :public Airplane{ public : virtual void fly (const Airport& destination) ; ... }; void ModelC::fly (const Airport& destination) { C的实现 }
5.non-virtual函数用于令derived classes 继承函数的接口以及一份强制性实现 1 2 3 4 5 6 7 class shape { public : int objectID () const ; ... };
6.pure-virtual , simple(impure)virtual , non-virtual :”只继承接口”, “继承接口和一份默认实现” ,”继承接口和一份强制实现” 注意事项: 1.接口继承和实现继承的区别,在public继承下,derived class 总是继承base class的接口 2.pure-virtual 只继承接口 3.impure-virutal 继承接口和默认实现 4.non-virtual 继承接口和强制实现 条款 35 : 考虑virtual函数以外的选择 对于下述例子 , 通常会用virtual 来overred它,不妨考虑其他设计 1 2 3 4 5 6 class GameCharacter { public : virtual int healthValue () const ; ... };
1.籍由non-virtual-Interface 实现 Template Method 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 class GameCharacter { public : int healthValue () const { ... int reVal = doHealthValue (); ... return reVal; } ... private : virtual int doHealthValue () const { ... } };
这一设计”令客户通过public non-virtual成员函数间接调用 private virtual 函数”,称为non-virtual interface (NVI)手法. 它是所谓Template Method 设计模式(与c++templa es并无关联),的一个独特表现形式,这个non-virtual 被称为 virtual 的外覆器 此手法优点: “做一些事前工作”和”做一些事后工作”, -> 确保得以在一个virtual函数被调用前设定好适当场景,并在调用结束之后清理场景 2.籍由Function Pointer 实现 Strategy模式 例如我们可能会要求每个人物的构造函数接受一个指针,指向一个健康计算函数 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 class GameCharacter ; int defaultHealthCalc (const GameCharacter& gc) ;class GameCharacter { public : typedef int (*HealthCalcFunc) (const GameCharacter&) ; explicit GameCharacter (HealthCalcFunc hcf = defaultHealthCalc) :healthFunc(hcf) { } int healthValue () const { return healthFunc (*this ); } ... private : HealthCalcFunc healthFunc; };
与virtual 做法相比,它提供了弹性 a.同一类型不同实体,可以有不同的计算函数 b.已知类型的计算函数可以在运行期变更,例如提供一个成员函数setHealthCalculator,替换当前计算函数 3.籍由tr1::function 完成Strategy模式 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 class GameCharacter ;int defaultHealthCalc (const GameCharacter& gc) ;class GameCharacter { public : typedef std::tr1::function<int (const GameCharacter&)> HealthCalcFunc; explicit GameCharacter (HealthClacFunc hcf = defaultHealthCalc) :healthFunc(hcf) { } int healthValue () const { return healthFunc (*this );} ... private : HealthCalcFunc healthFunc; };
和前一个设计相比,几乎相同.但如果需要更惊人的弹性 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 short calcHealth (const GameCharacter&) ; struct HealthCalculator { int operator () (const GameCharacter*) const {...}}; class GameLevel { public : float health (const GameCharacter&) const ; ... }; class EvilBadGuy :public GameCharacter{ ... }; class EyeCandyCharacter :public GameCharacter{ ... }; EvilBadGuye ebg1 (calcHealth) ; EyeCandyCharacter ecc1 (HealthCalculator()) ; GameLevel currentLevel; ... EvilBadGuy ebg2 ( std::tr1::band(&Game::health, currentLevel, _1) ) ;
前两个都很好理解,对于第三个的理解因为此处的构造只接受一个参数,但此处有两个参数,所以用tr1::bind 将currentLevel绑定为GameLevel对象,让它在”每次GameLevel被调用计算ebg2的健康”时被使用,那正是tr1::bind的作为:它指出ebg2的健康计算函数应该总是以currentLevel作为GameLevel对象 4.古典的Strategy 将健康计算函数做成一个分离的继承体系中的virtual函数. 对于上述例子,GameCharacter 是某个继承体系的根类,体系中的EvilBadGuy 和 EyeCandyCharacter 都是derived class;HealthCalcFunc是另一个继承体系的根类,体系中的SlowHealthLoser 和 FastHealthLoser 都是derived class, 每一个GameCharacter对象都内含一个指针,指向一个来自HealthCalcFunc继承体系的对象(参考p176图) 5.摘要 a.使用non-virtual interface (NVI)手法,这是 Template Method 设计模式的一种特殊形式. b.将virtual函数替换为”函数指针成员变量”,这是Strategy设计模式的一种分解表现形式 c.以tr1::function成员变量替换virtual函数,这也是Strategy设计模式的某种形式 d.将继承体系中的virtual函数替换为另一个继承体系内的virtual函数,这是Strategy设计模式的传统实现手法 注意事项: 1.virtual函数的替代方案包括NVI手法及Strategy设计模式的多种形式 2.将机能从成员函数移到class外部函数 3.tr1::function对象的行为就像一般函数指针 条款 36 : 绝不重新定义继承而来的non-virtual函数 1.non-virtual函数的性质为”不变性凌驾于特异性”,如果打算重定义,则应当使用virtual函数,当使用non-virtual函数并进行重定义,会导致指针/引用 指向一个实际的类时,所指的函数不是有实际的类决定,而是由指针/引用的类型决定 注意事项: 1.绝对不要重新定义继承而来的non-virtual函数 条款 37 : 绝不重新定义继承而来的缺省参数值 1.virtual函数是动态绑定(dynamically bound),而缺省参数值却是静态绑定(statically) 2.考虑以下继承体系 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 Shape { public : enum ShapeColor { RED, GREEN, BLUE}; virtual void draw (ShapeColor color = RED) const =0 ; ... }; class Rectangle :public Shape{ virtual void draw (ShapeColor color = GREEN) const ; ... }; class Circle :public Shape{ public : virtual void draw (ShapeColor color) const ; } Shape* ps; Shape* pc = new Circle; Shape* pr = new Rectangle;
virtual函数系动态绑定而来,调用哪一份实现代码,取决于动态类型 1 2 pc->draw (Shape::RED); pr->draw (Shape::RED);
但对于缺省参数值,由于是静态绑定,所有derived类的缺省参数值全部取决于base中的 3.对于这些问题,当你想令virtual函数表现出你想要的行为但却遭遇麻烦,可以考虑virtual函数的替代设计(条款35) 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 class Shape { public : enum ShapeColor { Red, Green, Blue}; void draw (ShapeColor color = Red) const { do Draw (color) ; } ... private : virtual void doDraw (ShapeColor color) const =0 ; }; class Rectangle :public Shape{ public : ... private : virtual void doDraw (ShapeColor color) const ; };
注意事项: 1.绝不要重定义一个继承而来的缺省参数值,因为缺省参数值是静态绑定,而virtual函数却是动态绑定 条款 38 : 通过复合塑模出has-a或”根据某物实现出” 1.复合是类型之间的一种关系,当某种类型的对象含有它种类型的对象,便是这种关系 1 2 3 4 5 6 7 8 9 10 11 12 class Address { ... };class PhoneNumber { ... };class Person { public : ... private : std::string name; Address address; PhoneNumber voiceNumber; PhoneNumber faxNumber; };
2.条款32曾说,”public继承”带有 is-a的意义. 复合也有它的意义即: a.has-a b.根据某物实现出(“is-implemented-in-terms-of”) 当复合发生于应用域(人,汽车,一张张视频画面等)内的对象之间,表现出has-a关系;当它发生于实现域(缓冲区,互斥器,查找树等)则是表现出 “is implemented-in-terms-of”关系 3.has-a 和 is-a 的区别很好区分,关键在于is-a 和 is-implemented-in-terms-of(根据某物实现出),书中给出一个由list 实现 set 的例子,由于set并不是list,只是借由list的机能复用代码实现set,所以不符合is-a关系,采用 is-implemented-in-terms-of关系 注意事项: 1.复合(composition)的意义和public继承完全不同 2.在应用域,复合意味着 has-a,在实现域,复合意味着 is-implemented-in-terms-of 条款 39 : 明智还审慎地使用private继承 1.private继承下,编译器不会自动将一个derived class 对象转换成一个 base class 对象 2.private继承意味这implemented-in-terms-o(根据某物实现出),如果D以private形式继承B,意思是D对象根据B对象实现而得,再没有其他意涵了 private在设计层面上没有意义,其意义只存在软件实现层面 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 class Timer { public : explicit Timer (int tickFrequency) ; virtual void onTick () const ; ... }; class Widget ::private Timer{ private : virtual void onTick () const ; ... };
这是个好设计,但private继承绝非必要,另一种做法是以复合取而代之 1 2 3 4 5 6 7 8 9 10 11 12 class Widget { private : class WidgetTimer :public Timer { public : virtual void onTick () const ; ... }; WidgetTimer time; ... };
5.何时优先选择”private继承”而不是”继承加复合”?,这一种激进情况涉及空间最优化 只适用于你所处理的class不带任何数据是.这样的class没有non-static变量,没有virtual函数(会带来vptr,条款7),也没有virtual base classes(也会导致体积上的额外开销,条款40), 这种所谓的 Empty Class 不使用任何空间,但C++官方会安插一个char到空对象中,裁定凡是独立(非附属)对象都必须有非零大小 1 2 3 4 5 6 7 8 class Empty { };class HoldAnInt { private : int x; Empty e; };
此时采用复合的话,HoldAnInt的对象不仅仅变大,还可能不止获得一个char的大小,由于齐位需求(条款50),如在clion下,HoldAnInt的大小变为 8 1 2 3 4 5 6 7 class HoldAnInt :private Empty{ private : int x; };
这种情况较少,所以通常采用复合 注意事项: 1.private 意味着 根据某物实现出.它通常比复合的级别低. 但是当derived class 需要访问 protected base class 的成员,或需重新定义继承而来的virtual 函数时,可以这样 2.private继承可以实现,EBO(空白基类最优化),这对于程序库开发者可能很重要 条款 40 : 明智而审慎地使用多重继承 1.不同base class 中含有同名函数会导致调用发生歧义 p192 2.菱形继承时,应采用virtual 继承 防止含有多份成员变量 3.virtual 继承会导致额外的开销代价 4.p195给出一个良好的多重继承例子 5.多重继承只是一个工具,是否采用取决于你的设计方案,采用多重还是单一哪种效果更好 注意事项: 1.多重继承比单一继承复杂,他可能导致新的歧义性,以及对virtual函数的需要 2.virtual继承会增加大小,速度,初始化复杂度等成本. 3.多重继承的确有正当用途. 其中一个情节涉及”public继承某个Interface class” 和 “private 继承某个协助实现的class”的两者组合,参考 4 中的例子 条款 41 : 了解隐式接口和编译期多态 1.对于oop 一般以显示接口和运行期多态解决问题,如下述例子 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 class Widget { public : Widget (); virtual ~Wdiget (); virtual std::size_t size () const ; virtual void normalize () ; void swap (Widget& other) ; ... }; void doProcessing (Widget& w) { if (w.size () > 10 && w != someNastyWidget) { Widget temp (w) ; temp.normalize (); temp.swap (w); } }
2.对于Template及泛型编程,显式接口和运行期多态仍存在,但重要性变低,隐式接口和编译期多态移到前头 1 2 3 4 5 6 7 8 9 10 11 12 13 14 template <typename T>void doProcessing (T& w) { if (w.size () > 10 && w != someNastyWidget) { Widget temp (w) ; temp.normalize (); temp.swap (w); } }
注意事项: 1.classes 和 templates 都支持接口(interfaces) 和 多态(ploymorphism) 2.对class,接口是显式的,多态则是通过virtual函数发生于运行期 3.对template,接口是隐式的,奠基于表达式.多态则是通过template具现化和函数重载解析发生于编译期. 条款 42 : 了解typename的双重意义 1.参考下述例子 1 2 3 4 5 6 7 8 9 10 11 template <typename C>void print2nd (const C& container) { if (container.size () >= 2 ) { C::const_iterator iter (container.begin()) ; ++iter; int value = *iter; std::cout << value; } }
上述例子中出现的两个变量, iter 和 value. iter的类型是C::const_iterator , 实际是什么取决于template参数C. template内出现的名称如果相依于某个template参数,称之为从属名称 .如果从属名称在class内呈嵌套状,称它为嵌套从属名称 ,此处的C::const_iterator就是这样的名称,实际上它还是个嵌套从属类型名称 2.嵌套从属名称可能导致解析困难 1 2 3 4 5 6 7 8 template <typename C>void print2nd (const C& container) { C::const_iterator* x; ... }
3.解析器在template中遭遇一个嵌套从属名称,它便假设这个名称不是类型,除非你告诉它是.所以缺省情况下,嵌套从属名称不是类型,但有例外 4.想要告诉解析器是,需要在前面加上关键字 1 2 3 4 5 6 7 8 9 10 11 12 template <typename C>void print2nd (const C& container) { if (container.size () >= 2 ) { typename C::const_iterator iter (container.begin()) ; } } template <typename T> void f (const C& container, typename C::iterator iter) ;
5.例外是,typename不可以出现在base classes list内的嵌套从属类型名称之前,也不可在member initialization list 中作为base class 修饰符 1 2 3 4 5 6 7 8 9 10 11 12 template <typename T>class Derived : public Base<T>::Nested { public : explicit Derived (int x) :Base<T>::Nested(x) //不允许 { typename Base<T>::Nested (temp); ... } ... };
注意事项: 1.声明template 参数时,前缀关键字class 和 typename 可以呼唤 2.请使用关键字typename标识嵌套从属类型名称,但不得在base class lists(基类列) 或 member initialization list(成员初值列)内以它作为base class 修饰符 条款 43 : 学习处理模板化基类的名称 1.对于下述例子 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 template <typename Company>class LoggingMsgSender :public MsgSender<Company>{ public : ... void sendClearMsg (const MsgInfo& info) { sendClear (info); } ... };
2.对于下述例子 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 template <>class MsgSender <CompanyZ>{ public : ... void sendSecret (const MsgInfo& info) {...} }; template <typename Company>class LoggingMsgSender :public MsgSender<Company>{ public : ... void sendClearMsg (const MsgInfo& info) { ... sendClear (info); ... } ... };
如注释所言,base class 被指定为MsgSender<.CompanyZ>不合理.以为那个特化中没有提供sendClear函数,这就是为什么拒绝调用. 个人理解:也是为什么会出现遭遇class template时不清楚它继承了什么,因为它知道base class templates可能被特化,而那个特化版本不提供和一般性template相同的接口. 因此它往往拒绝在 templatized base classes(模板化基类)内寻找继承而来的名称 3.对此有三种办法使得C++”不进入 templatized base classes观察”的行为失效 a.使用this-> 1 2 3 4 5 6 7 8 9 10 11 12 13 template <typename Company>class LoggingMsgSender :public MsgSender<Company>{ public : ... void sendClearMsg (const MsgInfo& info) { ... this ->sendClear (info); ... } ... };
b.使用using 声明式(条款33) 虽然使用using也可以在这里有效运作,但两处解决的问题不相同,这里的情况并不是base class名称被遮掩,而是编译器不进入base class作用域查找,于是我们通过using告诉它,让它这么做 1 2 3 4 5 6 7 8 9 10 11 12 13 14 template <typename Company>class LoggingMsgSender :public MsgSender<Company>{ public : using MsgSender<Company>::sendClear; ... void sendClearMsg (const MsgInfo& info) { ... endClear (info); ... } ... };
c.明白指出被调用的函数位于base class 内 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 template <typename Company>class LoggingMsgSender :public MsgSender<Company>{ public : ... void sendClearMsg (const MsgInfo& info) { ... MsgSender<Company>::sendClear (info); ... } ... };
4.上述的每个解法做的事情都相同,对编译器承诺: “base class templates的任何特化版本都将支持其一般(泛化)版本所提供的接口” 1 2 3 4 5 LoggingMsgSender<CompanyZ> zMsgSender; MsgInfo msgData; ... zMsgSender.sendClearMsg (msgDate);
注意事项: 1.可在derived class templates 内通过 “this->”指涉base class templates 内的成员名称,或籍由一个明白写出的”base class 资格修饰符”完成 条款 44 : 将于参数无关的代码抽离templates 1.对于non-template代码中,重复十分明确:你可以”看”到两个函数或两个classes之间有重复,但在template代码中,重复很隐晦 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 template <typename T,std::size_t n>class SqueareMatrix { public : ... void invert () ; }; SquareMatrix<double ,5 > sm1; ... sm1.invert (); SquearMatrix<double ,10 > sm2; ... sm2.invert ();
这导致具现化了两次invert函数 2.对此可以将重复的代码抽离出,共享一份 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 template <typename T>class SquareMatrixBase { protected : ... void invert (std::size_t matrixSize) ; ... }; template <typename T,std::size_t n>class SquareMatrix :private SquareMatrixBase<T>{ private : using SquareMatrixBase<T>::invert; public : ... void invert () { this ->invert (n); } };
此处的几个细节 a.private继承,因为这里的base class 只是用来帮助实现derived class ,不是is-a关系 另外几点的理解我的和书上的有出入 书上:此处的using声明用来避免隐藏, 对于this->若不用,模板化基类的函数名会被derived classes遮盖 我的:此处的using声明避免隐藏的同时,让不进入模板化基类的行为失效,this->在此处有点多余了,且无法做到排除derived的遮盖,此处的例子刚好参数不同导致了重载,若变为无参,调用的还是derived 中的函数 3.SquareMatrixBase::invert如何知道该操作什么数据,完整的实现如下 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 template <typename T>class SquareMatrixBase { protected : SquareMatrixBase (std::size_t n,T* pMem) :size (n),pData (pMem){} void setDataPtr (T* ptr) { pData = ptr; } ... private : std::size_t size; T* pData; } template <typename T,std::size_t n>class SquareMatrix :private SquareMatrixBase<T>{ public : SquareMatrix () :SquareMatrixBase <T>(n,data){} ... private : T data[n*n]; } template <typename T,std::szie_t n>class SquareMatrix :private SquareMatrixBase<T>{ public : SquareMatrix () :SquareMatrixBase <T>(n,0 ) ,pData (new T[n*n]) { this ->setDataPtr (pData.get ());} ... private : boost::scoped_array<T> pData; };
4.p216面下半部分未理解 大概意思是1.抽取公共代码到新的类导致对象大小的增加,此处就是多了一个data指针2.此处的n在模板参数中的话可能会有更多的优化,但作为函数参数的话就失去了这些优化 是否应该抽取公共代码,需要根据实际做出选择 5.目前这个条款只讨论了non-type template parameters (非类型模板参数)带来的膨胀,也就是代码的膨胀是由于相同算法多次具现化导致的,在本例就是矩阵的逆运算算法带来的.类型参数(type parameters)也会导致膨胀,例如许多平台上 int 和 long 有完全相同的二进制表述, 所以像vector<int.>和vector<long.>的成员函数有可能完全相同,这就导致了膨胀 注意事项: 1.Templates生成多个classes和多个函数,所以任何template代码都不该于某个造成膨胀的template参数产生相依关系(理解:使用模板时应避免产生代码膨胀) 2.因非类型模板参数而造成的代码膨胀,往往可以消除,做法是以函数参数或class成员变量替换template参数 3.因类型参数而造成的代码膨胀,往往可以降低,做法是带有完全相同的二进制表述的具现类型共享实现码 条款 45 : 运用成员函数模板接受所有兼容类型 1.本条款给出下述例子,定义智能指针,指向某继承体系 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 class Top { ... };class Middle :public Top { ... };class Bottom :public Middle { ... };Top* pt1 = new Middle; Top* pt2 = new Bottom; const Top* pct2 = pt1; template <typename T>class SmartPtr { public : explicit SmartPtr (T* realPtr) ; ... }; SmartPtr<Top> pt1 = SmartPtr <Middle>(new Middle); SmartPtr<Top> pt2 = SmartPtr <Bottom>(new Bottom); SmartPtr<Top> pct2 = pt1;
对于上述例子,同一个template的不同具现体之间不存在什么关系 这里意指如果以带有base-derived 关系的B,D两类型分别具现化某个template,产生的两个具现体,并不带有base derived关系,所以完全为不同的两个classes 2.对此需要的应该是member function template ,需要一个构造模板 1 2 3 4 5 6 7 8 template <typename T>class SmartPtr { public : template <typename U> SmartPtr (const SmartPtr<U>& other) ; ... };
上述例子的意思,对任何类型T和任何类型U,这里可以根据SmartPtr<.U>生成一个Smart<T.> 3.由此可以实现我们想要的 1 2 3 4 5 6 7 8 9 10 11 12 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; }
以初值列标来初始化,这个行为仅当U指针转为一个T指针时才能通过编译 4.泛化copyi构造并不阻止编译器生成自己的copy构造,你必须同时声明泛化版本和正常版本 1 2 3 4 5 6 7 8 9 10 11 12 13 14 template <class T >class shared_ptr { public : shared_ptr (shared_ptr const & r); template <class Y> shared_ptr (shared_ptr<Y> const & r) ; shared_ptr& operator =(shared_ptr const & r); template <class Y > shared_ptr& operator =(shared_ptr<T> const & r); ... };
注意事项: 1.请使用member function templates 生成 “可接受所有兼容类型”的函数 2.如果你声明的member template 用于”泛化copy构造”或”泛化assignment”操作,你还是需要声明正常的copy构造和copyassignment操作符 条款 46 : 需要类型转换时请为模板定义非成员函数 1.对于下述例子 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 template <typename T>class Rational { public : Rational (const T& numerator=0 ,const T& denominator=1 ); const T numerator () const ; const T denominator () 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 ;
因为在template实参推到过程中从不将隐式类型转换函数纳入考虑 2. e.g 1 2 3 4 5 6 7 8 9 10 11 12 13 template <typename T>class Rational { public : ... friend const Rational operator *(const Rational& lhs,const Rational& rhs); ... }; template <typename T>const Rational<T> operator *(const Rational<T>& lhs,const Rational<T>& rhs){ ... }
此处编译可以通过,frined 函数在class被声明的时候,随着class同时被具现出来,后者由此作为一个函数而非模板函数,因此编译器可以调用它时用隐式转换函数
但此处,无法链接,因为具现化的时候class内只是声明了,但没有定义,类外那个template和它关系不大,所以连接器链接不上 3.对此最简单的解决方法 1 2 3 4 5 6 7 8 9 10 11 template <typename T>class Rational { public : ... friend const Rational operator *(const Rational& lhs,const Rational& rhs) { return Rational (lhs.numerator () * rhs.numerator (), lhs.denominator () * rbs.denominator ()); } };
这里的friend与传统的目的不同,传统的friend是用来让non-member函数访问non-public成员 ,但此处是为了实现类型转换 ,而为了实现类型转应采用non-member 函数,而为了在class内声明non-member函数,我们需要 friend
4.另一种做法是为了减小inline的影响,采用辅助函数的方法 1 2 3 4 5 6 7 8 9 10 11 12 13 template <typename T> class Rational ;template <typename T>const Rational<T> doMultiply (const Rational<T>& lhs,const Rational<T>& rhs) ;template <typename T>class Rational { public : ... friend const Rational<T> operator *(const Rational<T>& lhs,const Rational<T>& rhs) { return doMultiply (lhs,rhs); } ... };
这里是让operator*只是用来进行转换 ,用来支持混合式乘法,实现交给doMultiply ,有种各司其职 的感觉,因为doMultiply因为是template class 不支持混合式乘法,但operator提供了类型转换
5.还有一种做法,教官提出来的,但我的编译器上实现有问题 注意事项: 1.当编写的class template , 而它提供的template相关的函数支持”所有参数的隐式转换”时,请将那些函数定义为class template 内的 friend函数 条款 47 : 请试用traits classes表现类型信息 首先为更好的理解下述例子,给出不同迭代器的定义:
Input迭代器:只能向前移动,一次一步,只读
Output迭代器:只能向前移动,一次一步,只写
forward迭代器:做前两种迭代器能做的事情,可读可写
Bidirectional迭代器:比前一个能做的事更多,既可以向前还可以向后
random access迭代器:可以移动任意距离
它们之间具有一系列的继承关系: 1 2 3 4 5 struct input_iterator_tag {};struct output_iterator_tag {};struct forward_iterator_tag :public input_iterator_tag{};sturct bidirectional_iterator_tag:public forward_iterator_tag{}; struct random_access_iterator_tag :public bidirectional_iterator_tag{};
1.文中给出一个advance函数,用来对某个迭代器移动某个给定距离,代码如下 1 2 3 4 5 6 7 8 9 10 11 12 13 template <typename IterT,typename DistT>void advance (IterT& iter,DistT d) { if (iter is a random access iterator) { iter += d; } else { if ( d>=0 ) { while (d--) ++iter; } else { while (d++) --iter; } } }
“traits必须能够施行于内置类型”意味着”类型内的嵌套信息”这种东西出局了,因为我们无法将信息嵌套于原始指针内,因此类型的traits信息必须位于类型自身之外.标准技术是把它放进一个template及其一个或多个特化版本中,这样的templates在标准程序库中有若干个,其中针对迭代器的被命名为iterator_traits
对于这段话的理解,一般实现traits技术时,是在每个类型中,此例子中就是每个迭代器类型中,声明一个typedef这也就是”类型内的嵌套信息”,而指针也相当于迭代器,但普通指针不是类,也就不存在”类型内的嵌套信息”,对于这种类型,tratis就需要提供特化版本 2.iterator_traits的运作方式,针对每一个类型IterT,在struct iterator_traits<IterT.>内一定声明某个typedef名为iterator_category,这个typedef用来确认IterT的迭代器分类 同时Iterator_traits以两个部分来实现,首先它要求自定义的迭代器类型必须嵌套一个typedef ,名为iterator_ category 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 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; ... }; ... };
而对于iterator_traits的另一部分实现如下 1 2 3 4 5 6 7 8 9 10 11 12 13 template <typename IterT>struct iterator_traits { typedef typename IterT::iterator_category iterator_category; ... }; template <typename IterT>struct iterator_traits { typedef random_access_iterator_tag iterator_category; }
3.这就是如何实现traits class
确认若干你希望将来可取得的类型相关信息.例如对于迭代器,我们希望将来可取得其分类
为该信息选择一个名称(如iterator_category)
提供一个template 和 一些特化版本
4.接下来就是如何使用tratis class了,对于先前的代码可以如下改写 1 2 3 4 5 6 7 8 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) )) ... }
5.还有一个问题是,IterT类型可以在编译期确定,但if要在运行期才被核定,为解决这个问题,引出来另一做法,通过重载来决定,因为重载会优先选择最匹配的重载件 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 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::bidirectionla_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 ()}; }
上述即是怎么使用traits class
建立一组重载函数,根据不同的traits信息,给出不同实现
建立一个控制函数,调用上述重载函数,并传递tratis class 所提供的信息
注意事项: 1.Tratis classes使得”类型相关信息”在编译期可用.它们以templates和 “templates 特化” 完成实现 2.整合重载技术后,tratis classes可能在编译期对类型执行if…else测试,如同书中上述例子 条款 48 : 认识template元编程 1.以C++写成,执行与C++编译期内的程序.一旦TMP程序结束执行,其输出,也就是从template具现出来的若干C++源码,便会一如既往地被编译 2.TMP的两个优势 a.让某些事情更容易,如果不用TMP甚至不可能 b.TMP执行于编译期,编译期执行带来的好处,较小的文件,较短的运行期,较少的内存需求,但是编译时间变长. 3.条款47曾说过下属例子,会编译错误 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 <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)) { iter += d; } else { if ( d>=0 ) {while (d--) ++iter; } else {while (d++) --iter; } } } std::list<int >::iterator iter; ... advance (iter,10 ); void advance (std::iter<int >::iterator& iter,int d) { if (typeid (std::iterator_traits<std::list<int >::iterator>::iterator_category) == typeid (std::random_access_iterator_tag)) { iter += d; } else { ... } }
TMP是一个**”函数式语言”**
4.一个TMP程序的例子(魔法) 1 2 3 4 5 6 7 8 9 10 template <unsigned n>struct Factorial { enum {value = n*Factorial<n-1 >::value}; }; template <>struct Factorial <0 >{ enum {value = 1 }; };
为何使用TMP的三个原因(p237)
确保量度单位正确
优化矩阵运算
可用生成客户定制的设计模式
注意事项: 1.TMP (模板元编程)可讲工作由运行期移至编译期,由此可用实现早期错误侦测,以及更高的效率 2.TMP可被用来生成”基于政策选择组合”的客户定制代码,也可以避免生成对某些特殊类型并不适合的代码 条款 49 : 了解new-handler的行为 new-handler相当于一个处理错误的函数 ,当new遇到分配内存失败的情况时就会调用这个函数 ,new_handler也能通过set_new_handler来设置我们自己的
1.这是一个声明于<new.>的标准程序库函数 1 2 3 4 5 6 7 namespace std{ typedef void (*new_handler) () ; new_handler set_new_handler (new_handler p) throw () ; }
2.下面是一些简单的使用例子 1 2 3 4 5 6 7 8 9 10 11 12 void outOfMem () { std::cerr << "Unable to satisfy request for memory\n" ; std::abort (); } int main () { std::set_new_handler (outOfMem); int * pBigDataArray = new int [100000000L ]; ... }
对此一个设计优秀的new_handler函数,必须做以下事情
让更多内存可被使用 ,我的理解就是清理内存,或者提前准备一部分内存以防发生分配不足的情况
安装另一个new_handler ,当前new_handler不能处理,那么交给另一个能处理的new_handler
卸除new_handler ,将null指针传给set_newhandler,一旦没有安装任何new_handler,在不足时会抛出异常
抛出bad_alloc异常 ,这样的异常不会被operator new 捕捉,因此会传播到内存索求处
不返回,通常调用abort或eixt
3.C++并不支持class专属的new_handler,但你可以自己实现,只需要自己提供set_new_handler和operator new即可 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 class Widget { public : static std::new_handler set_new_handler (std::new_handler p) throw () ; static void * operator new (std::size_t size) throw (std::bad_alloc) ; private : static std::new_handler currentHandler; } std::new_handler Widget::set_new_handler (std::new_handler p) throw () { std::new_handler oldHandler = currentHandler; currentHandler = p; return olHandler; } void * Widget::operator new (std::size_t size) throw (std::bad_alloc) { NewHandlerHolder h (std::set_new_handler(currentHandler)) ; return ::operator new (size); } class NewHandlerHolder { public : explicit NewHandLerHolder (std::new_handler nh) :handler(nh){ } ~NewHandlerHolder () { std::set_new_handler (handler);} private : std::new_handler handler; NewHandlerHolder (const NewHandlerHolder&); NewHandlerHolder& operator =(const NewHandlerHolder&) } void outOfMem ();int main () { Widget::set_new_handler (outOfMem); Widget* pw1 = new Widget; std::String* ps = new std::string; Widget::set_new_handler (0 ); Widget* pw2 = new Widget; }
4.另一种做法是采用继承和模板 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 template <typename T> class NewHandlerSupport { public : static std::new_handler set_new_handler (std::new_handler p) throw () ; static void * operator new (std::size_t size) throw (std::bad_alloc) ; ... private : static std::new_handler currentHandler; }; ... class Widget :public NewHandlerSupport<Widget>{ ... };
该技术被称为 “怪异的循环模板模式” CRTP 使用”mixin”风格的继承肯定导致多重继承的争议,见条款40 5.nothrow new 和 普通的new , nothrow-new在分配失败时返回null nothrow 的一些理解见p247最上面一段 注意事项: 1.set_new_handler允许客户指定一个函数,在内存分配失败时被调用 2.Nothrow new 是一个颇为局限的工具. 条款 50 : 了解 new 和 delete的合理替换时机 就是为啥不用编译期提供的而要自己定义
用来检测运用上的错误 ,能够实现将使用出错时记录下来
为了强化效能 ,编译期提供的主要用于一般目的,而在例如(网页服务器,web servers)则可能需要进行替换
为了收集使用上的统计数据 ,收集分配删除时的一些信息
e.g 是一个例子,促进并协助检测”overruns”(写入点在分配区块尾端之后)或”underruns”(写入点在分配区块起点之前)的定制 operator new 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 static const int signature = 0XDEADBEEF ;typedef unsigned char Byte;void * operator new (std::size_t size) throw (std::bad_alloc) { using namespace std; size_t realSize = size + 2 *sizeof (int ) void * pMem = malloc (realSize); if (!pMem) throw bad_alloc (); *(static_cast <int *>(pMem)) = signature; *(reinterpret_cast <int *>(static_cast <Byte*>(pMem)+realSize-sizeof (int ))) = signature; return static_cast <Byte*>(pMem)+sizeof (int ); }
此处的一些问题
“没有坚持c++的规矩”, 条款51说所有operator new 都应该内含一个循环,反复调用某个new_handing函数,z这里却没有,另一个问题 就是 齐位
“齐位”,C++要求所有的operator new 返回的指针都有适当的齐位,否则不安全,具体的详细介绍见p249
摘要:
为了检测运用错误(如前所述)
为了收集动态分配内存的使用统计信息(如前所述)
为了增加分配和归还的速度
为了降低默认版本带来的空间额外开销
为了弥补默认分配器中的非最佳齐位
为了将相关对象成簇集中
为了获得非传统行为
注意事项: 1.你有许多理由需要写个自定的new和delte,本条款就是介绍了一些使用的理由 条款 51 : 编写new和delete时需固守常规 条款50说过了你何时需要写一个自己的new和delete,本条款主要用来说明写自己的new和delete时需要遵守的一些规范
一个循环,不断尝试分配内存并在失败时调用new_handler
有能力处理0bytes申请
class的专属版本还应处理 继承时 “比正确大小更大的错误申请”
delete在收到null时应该不做任何事
class的版本还应处理”比正确大小更大的错误申请”
1.下面是一些伪码 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 void * operator new (std::size_t size) throw (std::bad_alloc) { using namespace std; if (size == 0 ){ size = 1 ; } while (true ) { 尝试分配; if (分配成功) return (指针,指向分配的内存) new_handler globalHandler = set_new_hander (0 ); set_new_handler (globalHandler); if (globalHandler) (*globalHandler)(); else throw std::bad_alloc (); } }
用set_new_handler找出new_handler指针,因为没有别的好方法,这样虽然拙劣,但有效 2.在出现继承时的情况 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 class Base { public : static void * operator new (std::size_t size) throw (std::bad_alloc) ; ... }; class Derived :public Base { ... }; Derived* p = new Derived; void * operator new (std::size_t size) throw (std::bad_alloc) { if (size != sizeof (Base)) return ::operator new (size); ... }
对于 new [] 详细见 p254下面段落 3.对于delete情况 1 2 3 4 5 void operator delete (void * rawMemory) throw () { if (rawMemory == 0 )return ; 归还内存; }
4.对于class情况 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 class Base { public : ... static void operator delete (void * rawMemory,std::size_t size) throw () ; ... }; void Base::operator delete (void * rawMemory,std::size_t size) throw () { if (rawMemory == 0 ) return ; if (size != sizeof (Base)){ ::operator delete (rawMrmory) ; return ; } 归还内存; return ; }
注意事项: 1.operator new 应该内含一个无穷循环,不断尝试分配内存,若失败则调用new_handler,有能力处理0 byte申请,class版本还应该处理”并正确大小更大的申请” 3.operator delete 应该在收到null时不做任何事情,class版本还应该处理”并正确大小更大的申请” 条款 52 : 写了placement new也要写placement delete 1.条款首先引入一条例子,告诉我们为什么要如此做 Widget* pw = new Widget;
此处公有两个函数被调用,一是用以分配内存的operator new , 另一个是 Widget的构造函数
问题就出在此,假设第一个函数调用成功 ,而构造的时候抛出异常 ,这个时候就需要取消分配的内存 ,否则会造成内存泄漏,但Widget抛出异常导致**,pw并未被赋值**,也就是客户没有能力归还内存,所以取消内存的责任落在C++运行期系统 身上,运行期系统会调用与operator new 相应的 operator delete版本 ,如果是正常形式的new 和 delete的话,运行期系统毫无问题可以知道该用那个delete
2.首先简单介绍一下术语placement new placement new 实际上不只是 一个特殊的new,之前我以为只是一个能在指定区域分配的内存的new ,实际上placement new 是一系列特殊new的统称,意味着带任意额外参数的new ,而那个能在指定区域分配内存的new,是最早的placement new 版本,这也是这个函数命名的根据,由此placement new 有多重定义,大多数时候是这个**”接受一个指针指向对象被构造之处”**,也就是下面这个C++标准库中的placement new
1 void * operator new (std::size_t ,void * pMemory) throw () ;
3.一个非正常 operator new 的例子 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 class Widget { public : ... static void * operator new (std::size_t size,std::ostream& logStream) throw (std::bad_alloc) ; static void operator delete (void * pMemory,std::size_t size) throw () ; ... }; Widget* pw = new (std::cerr)Widget; void operator delete (void *,std::ostream&) throw () ;
4.对此应该提供对应的operator delete 1 2 3 4 5 6 7 8 9 10 11 12 13 class Widget { public : ... static void * operator new (std::size_t size,std::ostream& logStream) throw (std::bad_alloc) ; static void operator delete (void * pMemory) throw () ; static void operator delete (void * pMemory,std::ostream& logStream) throw () ; ... }; Widget* pw = new (std::cerr)Widget; delete pw;
在与placement new 相关内存泄漏宣战时,必须同时提供一个正常的operator new(用于构造无任何异常被抛出)和一个placement 版本(用于构造期间有异常抛出)
5.注意名称遮盖问题,比较好理解就是如果class只声明了一种则会遮掩其他的,解决方法就是声明一个base class 储存正常的,让derivde继承,并using 使其在derived中可见 注意事项: 1.当你写一个placement new 请确定写出placement operator delete,否则回导致隐微的内存泄漏 2.当你声明placement new 和 placement delete时,注意不要遮掩它们的正常版本 条款 53 : 不要轻忽编译器的警告 编译期提示的警告信息,可能与你想的含义不一样,不能忽略它们,要了解编译期真正想告诉你的是什么,并从中吸取经验,尽量做到no-warming,不同的编译器警告可能不同,在这个编译器无警告的代码,在另一可能会有警告
1 2 3 4 5 6 7 8 9 10 11 class B { public : virtual void f () const ; }; class D :public B{ public : virtual void f () ; };
上述代码你可能觉得想得和你一样,D::f 遮掩了 B::f ,实际上编译器是想告诉你,B中的f没有被override,而是被遮掩了,因为B中的签名式const 而D中没有
注意事项: 1.严肃对待编译器发出的警告,尽量在编译期的最高警告级别下争取”no-warming” 2.不要过度依赖编译器的警告,因为不同的编译器对待事情的态度并不相同 条款 54 : 让自己熟悉包括TR1在内的标准程序库 各种标准程序库
注意事项: 1.C++标准程序库主要由STL,iostreams,locales组成.并包括c99标准程序库 2.TR1添加了智能指针,一般化函数指针(tr1::function 包装器),hash-based容器,正则表达式(regular expression)以及另外10个组件 3.TR1自身只是一份规范.为获得TR1提供的好处,你需要一份实物,来自Boost 条款 55 : 让自己熟悉Boost Boost一个社区,由于和C++委员会重合很大,所以与其他的C++社区相比比较有分量
注意事项: 1.Boost是一个社群,也是一个网站,提供免费,开源的C++程序库 2.Boost提供许多TR1组件实现品