来源: <http://www.cnblogs.com/fanzhidongyzby/archive/2012/11/18/2775603.html>
最近刚读完侯捷的《Effective C++》,相对来说,这本书的内容比较贴近基础,对于刚掌握C++基础的人会有不少的提高。不过书中还是涉及了不少C++的高级特性,阅读起来需要查阅相关的资料。书中给出了大量的示例和代码来说明具体规则的原理,我按照书中给出的标题将每个条目的关键内容整理如下。一方面是保留一份读书笔记,另一方面也是为了方便日后查阅方便。当然,如果不能从简单摘要的内容回忆起具体信息,到时再查书也不迟。同时也期望大家能从中找到自己没有注意的知识点,有所提高,大牛勿喷~ ☺。
条款01:视C++为一个语言联邦
(View C++ as a federation of languages)
多重范型编程语言:过程式(procedural)、面向对象式(object-oriented)、函数式编程(functional)、泛型编程(generic)、模板元编程(metaprogramming)。
为了理解C++,你必须认识其主要的次语言,总共只有四个:
来源: <http://blog.csdn.net/scofieldzhu/article/details/4235941>
比如你写下如下常量定义:#define MAX_BUFFER_LEN (50),假如编译代码的时候由于MAX_BUFFER_LEN导致的编译错误时,编译的信息里面也许会提到50这个数字而不是MAX_BUFFER_LEN,因为MAX_BUFFER_LEN有可能就没有被写入符号表(symbol table),这就给你带来了困惑了,如果你的项目比较大,源码文件较多,你就会对50这个数字毫无概念,不知道它来自哪里,那是你唯一的方法就是花时间去定位代码,而如果你写下:
const int MaxBufferLen = 50;
这样的语句时,如果编译出错的话,MaxBufferLen字串就会出现在你的编译信息中,因为MaxBufferLen是被写入了符号表中的,这样就为你节省了不少时间.
这里书上提到了一个注意点就是关于class的专属常量问题,你可以这样定义一个类的常量来保证一个类有且仅有一个此常量实体:
class GamePlayer{ private: static const int NumTurns = 5; //常量声明式 int scores[NumTurns]; //使用该常量 ... };
class GamePlayer{ private: enum { NumTurns =5}; int socores[NumTurns]; ... };
<span style="font-size:14px;">const char* p = "adc"; //non-const pointer,const data char* const p = "abc"; //const pointer,non-const data const char* const p = "abc";//const pointer,const data const std::vector::iterator it = vec.begin();//(it的类型为 T* const)const pointer std::vector::const_iterator it = vec.begin();//const data</span>
void a(const T b);//保护形参不被修改 void a(const T* b);//const pointer void a(const T& b);//const reference</span>
const T fun1();//这个其实无意义,因为参数返回本身就是赋值。 const T* fun2();//调用时 const T *pValue = fun2(); //我们可以把fun2()看作成一个变量,即指针内容不可变。 T* const fun3(); //调用时 T* const pValue = fun3(); //我们可以把fun2()看作成一个变量,即指针本身不可变。 //引用类似
class A { … const int nValue; //成员常量不能被修改 … A(int x): nValue(x) { } ; //只能在初始化列表中赋值 }
class A { … void function()const; //常成员函数, 它不改变对象的成员变量.也不能调用类中任何非const成员函数。 }
class AAA { void func1(); void func2() const; } const AAA aObj; aObj.func1(); //× aObj.func2(); //正确 const AAA* aObj = new AAA(); aObj-> func1(); //× aObj-> func2(); //正确
class Point{ int x,y; }
int x=0; const char* text="A C-style string"; double d; std::cin>>d;//以读取input stream方式完成初始化
class FileSystem{ public: …… std::size_t numDisks()const; …… }; extern FileSystem tfs;//定义在global作用域
class Directory{ public: Directory( params ) { …… std::size_t disks=tfs.numDisks(); …… } }; Directory tempDir( params);
class FileSystem{ public: …… std::size_t numDisks()const; …… }; FileSystem& tfs() { static FileSystem fs; return fs; } class Directory{ public: Directory( params ) { …… std::size_t disks=tfs.numDisks(); …… } }; Directory tempDir() { static Directory td; return td; }
有人提出了一个问题:如果我写了一个空类,里面什么都没有,什么功能都没有实现,这样的类合法么?我能用它么?比如:
class Null{};
也许你也有相同的问题,而我的答案是----->当然合法,你完全可以像一般类一样用它.提出这个问题的人把我们今天讨论的话题给引导出来了:编译器到底默默地对我们自己写的类做了些什么,进行了哪些"暗箱操作"?
当你写了个新类时,你有可能忘记写它的构造函数、析构函数、拷贝构造、赋值函数,会不会编译不过?Don't worry! 编译器会为你提供这些函数的默认实现的.比如上面的Null类,编译器会自动将它转换为如下等价代码:
class Null{ public: Null(){...} ~Null(){...} Null(const Null& newNull){...} Null& operator = (const Null& newNull){...} };对于编译器默认构造函数,很多情况下它不会自动为你初始化内置型变量的值,这点很可怕,常常会导致一些莫名其妙的情况甚至是系统无迹象地崩溃,最好的做法还是老老实实的显式提供构造函数为上上之策,呵呵,对于一些非内置型变量你就必须提供它的默认构造函数,否则编译不过.
class BookEntry{ public: BookEntry(string& title,double price,string& author) :title_(title),price_(price),author_(author){ } ... private: string title_,author_; double price_; }; //test.cpp BookEntry firstBook("Effective C++",65.0,"Scott Meyers"); BookEntry secondBook(firstBook); //copy constructor BookEntry thirdBook=secondBook; //notice: here call copy constructor too.到这里生成的三个对象保持了相同的数据拷贝,这里我们注意一下最后一条语句调用的是copy constructor还是对它进行assign操作符运算,这里不要认为是调用了assign操作符,因为编译器优化的结果,编译器不需要先初始化以后再调用assign来完成一个新对象的构造过程,它可以直接调用copy构造函数直接完成,如果连这点都不能做到的话,那你这款这编译器也够糟糕的了!呵呵.
class BookEntry{ public: BookEntry(string& title,double price,...) :title_(title),price_(price),...{ } ... private: string& title_; double price_; }; BookEntry oldBook("prison break",25.5,...); BookEntry newBook("Unix network programming",43.0); newBook = oldBook; // compiler error: 'operator =' function is unavailable in 'BookEntry'
你必须自己定义copy assign操作的实现!
到这里我要对copy assign操作中的特殊的情况进行总结一下,一是对于"含reference"的类,如果你想用'='操作符的话,不好意思,你得自定义它的实现体;二是对于"含const对象"的类,编译器的反应当然是一样的,修改const对象本来就是非法的;还有一种情况是如果基类base class中
operator=被声明为private那么编译器就拒绝为其子类derived class提供默认的copy assign操作实现体.这三条大家要注意一下.
请记住(最后一条是我自己加的,呵呵):
■ 编译器可以暗自为class创建default构造函数,copy构造函数,copy assignment操作符以及析构函数.
■ 请一定要清楚编译器为我们做了什么?在一些情况下,不要去麻烦编译器为我们自动生成的东西,因为这些东西不一定就是我们想要的,compiler is not god!,我们最好自己去实现它们,believe yourself!
在条款05中,我们弄清楚了编译器为我们自动生成的函数,而在现实中,我们有时并不需要编译器为我们提供某些函数的实现,因为它们的出现会导致我们现实中的某些最基本的常识变的不合理,违背事物发展的基本规律和准则,显然这些都不是我们想要的.那怎么办?最好的办法就是让编译器去拒绝这种特定的机能,具体如何做呢?
比如举个例子,某一公司里面的员工类Employee,你立刻会写下如下类似实现:
class Employee{ public: Employee(){} Employee(int id,const string& name,...) :id_(id),name_(name),...{ } ... private: int id_; string name_; ... }; //test.cpp Employee employee1(60015207,"scofieldzhu",...); Employee employee2(60012345,"xiaohewang",...); Employee employee3(employee1); //legal? Line1: Employee employee4; employee4 = employee2; //legal? Line2:
class Employee{ public: Employee(){} Employee(int id,const string& name,...) :id_(id),name_(name),...{ } ... private: Employee(const Employee&); Employee& operator = (const Employee&); int id_; string name_; ... };
class Uncopyable{ protected: Uncopyable(){} //only derived can generator object ~Uncopyable(){} private: Uncopyable(const Uncopyable&); //prevent copy Uncopyable& operator = (const Uncopyable&); //prevent '=' };
class Empolyee:private Uncopyable{ public: ... };
请记住:
■ 为驳回编译器自动(暗自)提供的机能,可将相应的成员函数声明为private并且不予实现.使用像Uncopyable这样的base class也是一种做法.
条款07:为多态基类声明virtual析构函数
(Declare destructors virtual in polymorphic base classes.)
内容:
一看到这个条款,你可能立刻就会提出你的疑问来推翻这个结论:如果不为多态基类声明virtual析构函数,会发生什么灾难?好,其实答案是:当delete掉一个指向子类对象的基类指针时,它的行为是不可定义的----------指向derived部分没有被销毁,析构函数也没有被调用而基类部分通常会被销毁,这样就造成了一个可怕的"局部销毁"对象,造成了资源泄露、数据结构被损坏、浪费调试时间的惨状!这肯定不是你想要的结局.当然解决的办法就是为base class析构函数加上virtual.
如果class当中没有virtual函数,通常表示它并不意图被当做base class,而你为它强行加上virt-ual析构函数的话,可能会造成不好的后果.书上的一个例子是:
class SpecialString:public std::string{ //std::string有个non-virtual析构函数 ... }; //test.cpp SpecialString* pss=new SpecialString("Impending Room"); std::string* ps; ... ps = pss; ... delete ps; //这里出现未定义错误!!!现实中*ps的SpecialString资源会泄露.有时我们为基类提供一个pure virtual析构函数并且提供它的一份定义,这样我们会方便很多,因为析构函数的运作发式是,derived类析构先调用然后自动调用base部分析构函数.例如有个类Shape想被作为基类来使用,那么可以这样定义:
class Shape{ public: virtual ~Shape() = 0; }; Shape::~Shape(){}//implement好了,今天就到这里.
条款08:别让异常逃离析构函数
(Prevent exception from leaving destructors).
内容:
一般而言,只要析构函数抛出异常,程序就会可能过早结束或出现不确定行为,C++不喜欢析构函数吐出异常!!那么我们如何避免呢?书上举的一个例子是:假设你使用一个class负责数据库连接:
class DBConnection{ public: ... static DBConnection create(); //get a DBConnection omit parameter for simplicity void close(); //close DBConnection };为了防止客户使用完DBConnection以后忘记调用close(),我们可以创建一个新类DBConn来管理这个对象,我们可以在这个新类的析构函数中调用该DBConnection对象的close();
class DBConn{ public: DBConn(DBConnection& db):db_(db){} ~DBConn(){ db_.close(); } ... private: DBConnection& db_; };
{ DBConn connection(*(DBConnection.create())); ... //自动析构时候调用close方法 }
DBConn::~DBConn(){ try{ db_.close(); }catch(...){ .... std::abort();//结束程序,可以强制"不明确行为"死掉 } }二是吞掉异常:
DBConn::~DBConn(){ try{ db_.close(); }catch(...){ ... } }一般而言吞掉异常不是很好的处理发式,但总比为"粗暴的结束程序"或"出现不明确行为"担负代价和风险要好,这样即使程序遭遇了一个错误或者异常情况下都可以继续运行,在另一方面提高了软件的健状性.
class DBConn{ public: DBConn(DBConnection& db):db_(db){} void close(){ db_.close(); isClosed_ = true; } ~DBConn(){ if(!isClosed_){ //使用以上两种解决方案之一来进行解决. } } ... private: DBConnection& db_; bool isClosed_; };这样我们来重新审理这段代码,一个新的接口close(),提供给客户调用,如果出现异常客户可以第一时间来进行处理,如果可以确定这里不会出现异常的话,也可以不处理,客户还可以选择不调用close(),放弃对可能出现的异常处理,选择让析构函数自动调用.这样的设计你不觉得更加的合理吗?呵呵
请记住:
▲ 析构函数绝对不要吐出异常.如果一个被析构函数调用的函数可能抛出异常,析构函数应该捕捉异常,然后吞下它们(不传播)或结束程序.
▲ 如果,客户需要对某个操作函数运行期间的异常做出反应,那么class应该提供一个普通函数(而非在析构函数中)执行该操作.
条款09:绝不在构造和析构过程中调用virtual函数
(Never call virtual functions during construction or destruction.)
内容:
我以一个书上的例子开始:一个正规一点的大型超市一般都有一个管理进货、卖出、订货等交易动作的管理系统,该系统必须维护一个交易操作记录,方便查询,故每进行一笔交易时,需要把此项交易信息log入数据管理系统进行存储,这里我们有了一个交易类Transaction,但交易有很多种类型所以我们log它们的时候需要virtual函数来实现,故你很容易写出如下代码:
class Transaction{ public: Transaction(){ ... logTransaction(); //记录此笔交易 } virtual void logTransaction()const=0; ... }; class BuyTransaction:public Transaction{ public: virtual void logTransaction()const; ... }; class SellTransaction:public Transaction{ public: virtual void logTransaction()const; ... };
BuyTransaction buy; //error LNK2019: unresolved external symbol "public: virtual void __thiscall //Transaction::logTransaction(void)const " (?logTransaction@Transaction@ //@UBEXXZ) referenced in function "public: __thiscall Transaction:: //Transaction(void)" (??0Transaction@@QAE@XZ)
晕,这啥玩意?是不是编译器坏了?呵呵,不要急.我们来看看是你的问题还是编译器的问题,首先我们来看看buy这个对象的构造过程,注意构造顺序是:先构造基类,然后才是子类,当执行基类的构造函数中的logTransaction()方法时,该调用哪个版本的logTransaction呢?编译器这个时候调用的是基类版本Transaction::logTransaction,因为这个时候子类对象压根还没有被构造出来,这个时候编译器认为当前的对象类型是base class而不是derived class,不仅仅virtual函数被解析至base class,就算使用运行期类型信息(runtime type information)比如:dynamic_cast和typeid,也会把它当做base class类型的.
这样的解释能看懂了么?如果还是看不懂,那么我们来用反证法论证它最终调用的就是base的版本.假设基类构造函数调用子类版本logTransaction(此例子中为:BuyTransaction::logTransaction),那么此时的子类对象的成员变量还没有被初始化,而子类版本的logTransaction内部则就使用了未被初始化的变量,C++编译器不会容许你这么做的,原因很简单因为使用未初始化变量会承担"出现不明确定义"的巨大风险(条款:04).
同样的道理,析构函数也不能用virtual函数,对象析构函数的顺序与构造的顺序相反,当调用析构基类的析构函数时,子类已经销毁,那么它就没有就会调用子类版本的virtual函数.
现在我们已经知道不能在构造析构函数中调用virtual函数,那么如何避免这种现象的发生,这可不是一件好差事,例如你可以在你的构造函数中用一个中间函数,而此中间函数实现中却调用了virtual函数,这样的调发式编译器是检测不出来的.而如果这个virtual函数是pure virtual且没提供默认实现代码时,当你屁颠屁颠的运行这个程序,当这个函数被调用时,大多数执行系统会终止程序,弄的你丈二和尚摸不着头脑;而如果你这个virtual函数已经有了一份实现代码,那么该版本就会被调用,而你的程序也顺利地大步向前走,正当你准备高兴之余你却突然发现它调用的不是子类的版本virtual函数,而留下你一人在作痛苦状思考:为什么它会调用错误的virtual版本呢?要是你连"编译器调用了错误的函数版本"都没检测出来的话,那你就等着你自己的心理防线彻底崩溃吧?
那我该这么办啊?我偏要达到我的目的,让基类执行子类的实现代码?这个问题有解决方案么?好,我们再来看一下这个问题,既然我们不能让基类调用子类virtual函数版本,那么我们可以把子类的virtual函数所要实现的功能转交给基类来完成不就可以了嘛,想法很好!具体做法是:将基类的virtual函数变为non-virtual函数,然后在子类的构造函数当中传递特定信息给基类,让基类来完成子类的功能.
实现代码可以这样修改:
class Transaction{ public: explicit Transaction(const std::string& logInfo){ ... logTransaction(logInfo); } void logTransaction(const std::string& logInfo)const{ } }; class BuyTransaction:public Transaction{ public: BuyTransaction(Parameters):Transaction(createLogString(Parameters)){ } private: static std::string createLogString(Parameters)const; };
条款10:令operator= 返回一个reference to *this
(Have assignment operators return a reference to *this)
内容:
令赋值操作符返回一个自身对象的引用,究其根本原因就是为了实现"连锁赋值",比如,你可以这样写:
int x,y,z;
x=y=z=15;
类似的假如你要使你写的类能实现"连锁赋值",那么你就你的赋值操作符必须返回一个指向操作符的左侧实参,这个是你应该遵循的协议:
class Interger{ public: ... Interger& operator=(const Interger& rhs){ ... return *this; } };
请记住:
◆ 令赋值(assignment)操作符返回一个reference to *this.
条款11: 在operator= 中处理"自我赋值"
(Handle assignment to self operator=.)
内容:
我们在写operator= 函数实现时,要注意一个问题:要考虑对象自我赋值的情况,因为客户完全可以写下如下代码:
Widget w; ... w=w;
class Bitmap{}; class Widget{ public: ... Widget& operator=(const Widget& rhs){ delete hBitmap_;//hBitmap_可能为NULL,注意:delete 删除NULL指针是可以的,它会什么都不做. hBitmap_ = new Bitmap(*rhs.hBitmap_); return *this; } private: Bitmap* hBitmap_; };
Widget& Widget::operator=(const Widget& rhs){ if(this == &rhs){ //增加了判断条件 return *this; } delete hBitmap_; hBitmap_ = new Bitmap(*rhs.hBitmap_); // throw possibel exception? return *this; }
Widget& Widget::operator=(const Widget& rhs){ Bitmap* holdBitmap_ = hBitmap_; hBitmap_ = new Bitmap(*rhs.hBitmap_); delete holdBitmap_; return *this; }
class Widget{ public: Widget& operator=(const Widget& rhs){ Widget tempt(rhs); //make a source copy swap(rhs); return *this; } ... private: void swap(const Widget& rhs); //交换两个Widget对象数据 ... };由于改实现要将产生一个参数的副本所以说我们可以通过"pass by value"来传递参数,我们再次改写代码:
class Widget{ public: Widget& operator=(Wiget rhs){ swap(rhs); return *this; } .... private: void swap(const Widget& rhs); //交换两个Widget对象数据 ... };
条款12:复制对象时勿忘其每一个成分
(Copy all parts of an object)
内容:
从条款05中我们知道编译器为你提供了一些默认copying函数,而在某些情况下,我们不想要编译器提供的版本,我们自定义copying构造函数和copying assign操作函数,这个时候你无形之中惹恼了我们的编译器,你不用它的东西,它很"生气",于是它就开始复仇:你的实现代码几乎出错时,它就是不告诉你.
这里考虑一个类用来表现一个应用用程序事件类型Event:
class Event{ public: Event(int id):eventID_(id){ } Event(const Event& rhs):eventID(rhs.eventID_){ } Event& operator=(const Event& rhs){ eventID_ = rhs.eventID_; return *this; } ... private: int eventID_; };
class DateTime{...}; class Event{ .... //代码与上面相同 private: int eventID_; DateTime dateTime_; };
class SystemEvent:public Event{ public: SystemEvent(int id,int priority):Event(id),priority_(priority){} SystemEvent(const SystemEvent& rhs):Event(rhs),priority_(rhs.priority_){ //这里调用了base class的copying 构造函数 } SystemEvent& operator=(const SystemEvent& rhs){ Event::operator=(rhs); //调用base class的copying assign 操作函数 priority_ = rhs.priority_; return *this; } private: int priority_; };
条款13:以对象管理资源
(use objects to manage resources.)
内容:
我们还是以例子来说明问题,假设我们使用一个图片资源的类Image:
class Image{...};
接下来我们需要一个工厂函数供应我们特定的Image对象:
Image* createImage();//返回创建的Image对象的地址。
这里我们有一个隐性的协议:如果客户使用该工厂函数来获得Image对象,当你使用完了后,你有责任去删除它。而如果违反了这个协议,那么显然造成了"资源浪费"等一些列问题,客户一般这样用这个对象:
{ Image* pcurImage=createImage(); ... delete pcurImage; }
标准库中auto_ptr就是对这种情形设置的特制产品,也就是"智能指针",下面我们来示范如何使用它:
{ std::auto_ptr<Image> aptrImage(createImage()); ... //退出作用域的时候自动调用析构函数释放占用的资源 }
{ using namespace std; auto_ptr<Image > aptrImage1(createImage()); //aptrImage1指向createImage()返回的对象 auto_ptr<Image > aptrImage2(aptrImage1); //aptrImage2指向对象,aptrImage1指向null aptrImage1 = aptrImage2; //aptrImage1指向对象,aptrImage2指向null ... }这里的经过copying构造和copying assign操作函数后,原自动对象管理器就失去了指向原先拥有对象的机会,而将此机会交给了目标自动管理对象,自己指向null对象。也就是说"auto_ptr管理的资源必须绝对没有一个以上的auto_ptr同时指向它",从另外一方面说auto_ptr也非管理动态分配资源的最佳工具。于是auto_ptr的替代方案出来了,它就是"引用计数型智慧指针"(reference-couting smart pointer;RCSP).其实就是在目标对象上引用计数机制,当引用计数为0时,说明没有其它对象在引用它,它就对目标对象进行资源回收,伪代码形如下:
void xxx::release(){ if(--refCount == 0){ delete this; } } TR1的tr1::shared_ptr就是个RCSP,你可以这么用它: { ... using std::tr1; shared_ptr<Image > sptrImage1(createImage()); //sptrImage1指向createImage()的返回值对象 shared_ptr<Image > sptrImage2(sptrImage1); //sptrImage2与sptrImage1指向同一个对象 sptrImage1 = sptrImage2; //同上 ... //sptrImage1与sptrImage2与他们所指向对象被自动销毁。 }
条款14: 在资源管理类中小心copying行为
(Think carefully about copying behavior in resource-managing classes.)
内容:
在上一款中我们谈到,用资源管理类来管理动态分配的资源及如何运用auto_ptr和tr1::share_ptr管理heap-based资源来展示这个观点的,而在实际编写代码中,我们要用的资源不一定是来自heap,那么这样的auto_ptr和tr1::share_ptr就不适合作为资源管理类,这个时候我们需要建立自己的资源管理类。
书上举了一个例子是互斥器对象mutex,那么mutex资源管理类就可以这么写:
class Mutex{...}; //资源mutex类
class Lock{ public: explicit Lock(Mutex* pMutex):pcurMutex_(pMutex){ lock(pcurMutex_); } ~Lock(){ unLock(pcurMutex_); } private: Mutex* pcurMutex_; };接下来这么用:
Mutex m; ... { ... Lock autoLock(&m);//自动锁定mutex ... //区块结束时自动释放mutex }
Lock m1(&m); Lock m2(m1);
class Lock{ public: explicit Lock(Mutex* pm):sptrMutex_(pm,unLock){ lock(sptrMutex_.get()); //锁住它的目标资源 } private: std::tr1::shared_ptr<Mutex > sptrMutex_; };对于管理资源的"复制行为",我们也可能采取下面两种策略实现(即在copy构造函数和赋值运算函数实现体内所采取的策略):
条款15: 在资源管理类中提供对原始资源的访问
(Provide access to raw resources in resource-managing classes.)
内容:
使用资源管理类使我们在一定程度上能够摆脱对原始资源的访问而造成的诸多风险,但在有有些情况下,当我们需要直接对资源进行访问操作时候,管理类这样的封装无疑给我们开发带来了不便,我们来举个例子来说明这种情况.
现在我们有个图片资源工厂方法createImageResource();现在我们用auto_ptr对其进行管理:
auto_ptr<Image> sptrImageRes(createImageResource);
而现在我们要对这张图片进行去色处理,处理函数为int decolourize(Image* psourceImage)我们这样调用它:
decolourize(sptrImageRes);//当然不对啦,类型不匹配嘛
这里我们就需要这个图片管理类来提供一个接口,此接口能够提供实现我们直接访问资源功能.幸好auto_ptr提供了get函数来执行显式转换,即返回管理对象的内部原始的资源指针.
decolourize(sptrImageRes.get());//get方法就是其提供的接口函数
一般来说,管理资源类都提供用户对原始资源的访问接口,通常通过显式转换或者隐式转换达到目的.上面举的例子是通过函数接口执行显式转换来获得原始的资源指针,我们也可以通过重载操作符->或者*来执行隐式转换获得原始资源指针,例如tr1::shared_ptr与auto_ptr都重载了operator->与operator*函数实现,我们就可以通过它们直接访问image提供的接口操作:
class Image{ public: bool loadDataFromFile(const string& filename); void blacklize(); void whiteBalance(); ... }; //test.cpp sptrImageRes->loadDataFromFile("shania twain.jpg"); sptrImageRes->blacklisze(); (*sptrImage).whiteBalance();
class Image{ public: operator HANDLE()const{ return imageHandle_; } ... private: HANDLE imageHandle_; };
Image theImage; HANDLE handle = theImage; //OK! perfect! hehe书上有这么一段话:是否该提供一个显式转换函数(例如get成员函数)将RAII class转换为其底部资源,或是应该提供隐式转换,答案主要取决于RAII class被执行的特定工作以及它被使用情况.最佳设计是坚持忠告:"让接口容易被正确使用,不易被误用".通常显式转换函数如get是比较受欢迎的路子,因为它将"非故意之类型转换"的可能性最小化了.然而有时候,隐式类型转换所带来的"自然用法"也会引发天平倾斜.
请记住:
■ APIs往往要求访问原始资源(raw resources),所以每一个RAII class应该提供一个"取得其所管理之资源"的办法.
■ 对原始资源的访问可能经由显式转换或隐式转换.一般而言显式转换比较安全,但隐式转换对客户比较方便.
条款16:成对使用new和delete时要采取相同形式
(Use the same form in corresponding uses of new and delete.)
内容:
我们来看下面这段简单的代码:
class Phone{...}; //test.cpp Phone* pPhoneArray=new Phone[4]; assert(pPhoneArray != 0); ... delete pPhoneArray;
从上面我们可以看出来,数组对象内存中包含了一个指示该数组的大小的变量size,有了这个指示我们就能确定总共要释放的内存大小,那我们如何标识出来让编译器知道倪,我们这里采用给delete带个小标识符'[]',这样编译器就知道了它要释放的指针原来是一个对象数组,问题解决!呵呵.
我们来举个例子:
{ ... using std::string; string pstringMultiObject = new string[4]; ... delete[] pstringObject; string pstringSingleObject =new string; ... delete pstringSingleObject; ... }
typedef std::string addressList[4]; std::string* paddressList=new addressList; assert(paddressList != 0); delete[] paddressList; //注意这里不是delete paddressList;这里的addressList是一个含4个元素的数组类型变量,故不能使用delete paddressList;为了避免诸类错误的发生,我们一般不使用typedef定义数组类型,代替它的是标准程序库的vector<string>类型.
来源: <http://blog.csdn.net/scofieldzhu/article/details/4264214>
条款17:以独立语句将newed对象置入智能指针
(Store newed objects in smart pointers in standalone statements.)
内容:
假设现在我们需要一个函数去处理一个消息processMessage,它需要一个消息对象和一个消息处理函数,由此我们遵循"对象管理资源"的原则,写出如下代码:
class WindowsMesssage{...}; int messageProcedure(); void processMessage(std::tr1::shared_ptr<WindowsMesssage> message,messageProcedure);
class PaintMessage:public WindowsMesssage{ ... }; //test.cpp processMessage(new PaintMessage,messageProcedure);
std::tr1::shared_ptr<WindowsMesssage> sptrPaintMessage(new PaintMessage); processMessage(sptrPaintMessage,messageProcedure);
条款18: 让接口容易被正确使用,不易被误用
(Make interfaces easy to use correctly and hard to use incorrectly.)
内容:
假设现在的你需要提供一些接口给你的客户去使用,而现在的你没有任何这个方面的经验,那么你就要考虑下面这些情况的发生:
class Date{ public: Date(int month,int day,int year); ... }; //test.cpp Date oneDay(29,3,1995); //晕,月日颠倒了.应该是Date oneDay(3,29,1995);
struct Day{ explicit Day(int dayValue):value_(dayValue){} int value_; }; struct Month{ explicit Month(int monthValue):value_(monthValue){} int value_; }; struct Year{ explicit Year(int yearValue):value_(yearValue){} int value_; }; class Date{ public: Date(const Month& m,const Day& d,const Year& y); ... }; Date d(29,3,1995); //error,wrong type Date d(Day(29),Month(3),Year(1995)); //error,wrong type Date d(Month(3),Day(29),Year(1995)); //OK Date d(Month(15),Day(29),Year(1995)); //wow,can't detected this
class Month{ public: static Month Jan(){return Month(1);} static Month Feb(){return Month(2);} ... static Month Dec(){return Month(12);} ... private: explicit Month(int m); //prvent create object from outward ... }; //test.cpp Date d(Month::Mar(),Day(30),Year(1995));
Image* pImage = createImage(); ... //delete pImage;//这里忘记调用delete pImage ...这里你很不服气,他(客户)忘记调用是他的责任,怎么能够怪我的接口不好呢?他没按照我的步骤来导致得不到最终的效果,这能怪我嘛?可气!呵呵,你说的貌似有道理,那么我们这里说明一点:导致用户用错或者遗漏你强调的必须步骤,可能是你的接口让客户用起来复杂,不方便导致的.这可就是你的问题了,你要让你的接口更加简单,更加适合客户调用,这样就会在一定程度上减少了客户由于误用该接口而失败的概率.
std::tr1::shared_ptr<Image> createImage(){ std::tr1::shared_ptr<Image> retVal(static_cast<Image*>(0),getRidOfImage); ... retVal=...;//让retval指向正确的对象 return retVal; }
条款19:设计class犹如设计type
(Treat class design as type design.)
内容:
今天我们讨论的这款对于现在的我来说深有体会,随着做的项目越来越多,我越来越感觉到一个设计非常优秀的class对于开发者来说是多么的重要,我一直都在思考和探寻着如何设计优秀的classes,写一个class出来我相信大家都能够写的出来,而设计出优秀的classes则是一项艰巨的工作,本条款中提到设计class就像设计types一样,为什么这么说呢?因为设计好的types是一项艰巨的工作.好的types有自然的语法,直观的语
义,以及一或多个高效实现品.在C++中,一个不良的规划下的class定义恐怕无法达到上述任何一个目标,书中提到了如果你想设计高效的classes你至少要考虑下面的几个问题,而这几个问题的答案导致了你的设计规范:
★ 新type的对象应该如何被创建和销毁?
这个问题主要影响到你的构造函数、析构函数以及内存分配和释放函数设计.
★ 对象的初始化和对象的复制该有什么样的区别?
这个是很多人容易忽略的问题,而规范的做法下你需要考虑这点,它决定了你的构造函数,拷贝构造,复制操作符的行为以及它们之间的差别,别混淆了"初始化"和"赋值",如果忘记了赶紧回过头看一下条款4.
★ 新type的对象如果被pass by value(以值传递),意味着什么?
注意pass by value与pass by reference传递参数时候的区别,而在处理赋值操作符时注意到"自我赋值"的情况.
★ 什么是新type的"合法值"?
考虑你的class必须维护的约束条件,你的成员函数的错误检查工作.它也影响函数抛出的异常.
★ 你的class需要配合某个继承体系么?
考虑一下你的class需要被其它class继承,那会影响你的声明函数,尤其是析构函数是否为virtual(条款7).
★ 你的新type需要什么样的转换?
考虑一下你的新type是否需要提供隐式或者显示转换函数(条款15).
★ 什么样的操作符和函数对此新type而言是合理的?
★ 什么样的标准函数应该被驳回?
★ 谁该取用新type的成员?
这将决定哪个成员为public,哪个为protected,哪个为private.它也帮助你决定哪一个class和/或functions应该是friends,以及将它们嵌套与另一个之内是否合理.
★ 什么是新type的"未声明接口"(undeclared interface)?
它对效率,异常安全性(条款29我们将讨论这个问题)以及资源运用(例如多任务锁定和动态内存)提供何种保证?你在这些方面提供的保证将为你的class实现代码加上相应的约束条件.
★ 你的新type有多么一般化?
或许你其实并非定义一个新type,而是定义一整个types家族.果真如此你就不该定义一个新class,而是应该定义一个新的class template.
★ 你真的需要一个新type吗?
如果只是定义新的derived class以便为既有的class添加机能,那么说不定单纯定义一或多个non-member函数或templates,能够达到目标.
这些问题不容易回答,所以定义出高效的classes是一种挑战.然而如果能够设计出至少像C++内置类型一样好的用户自定义(user-defined) classes,一切汗水便都值得.
请记住:
■ class的设计就是type的设计.在定义一个新type之前,请确定你眼睛考虑过本条款覆盖的所有讨论主题.
条款20:宁以pass-by-reference-to-const替换pass-by-value
(Prefer pass-by-reference-to-const to pass-by-value.)
内容:
先看一个例子,假设我们有一个Person类:
class Person{ public: Person(const string& name,const string& address) :name_(name),address_(address){ }; virtual ~Person(){} ... private: ... string name_; string address_; };
class Student:public Person{ public: Student(...){...}; //omit parameters for simplicity ~Student(){} ... private: ... string schoolName_; string schoolAddress_; };现在这里有一个函数函数要处理Person对象,其原型签名是这样:
... Person firstPerson("michael scofield","New York"); queryPersonInfo(firstPerson); // Label1 Student firstStudent(...); queryPersonInfo(firstStudent); //Label2 ...
class Window{ public: ... std::string name()const; //返回窗口名称 virtual void display()const; //显示窗口和它的内容 };
class WindowWithScrollBars:public Window{ public: ... virtual void display()const; };
void printNameAndDisplay(Window win){ //critical warning: object-slicing std::cout<<win.name(); w.display(); }
WindowWithScrollBars windowbar; printNameAndDisplay(windowbar);
void printNameAndDisplay(const Window& win){ ...//同上 }
条款21:必须返回对象时,别妄想返回其reference
(Don't try to return a reference when you must return an object.)
内容:
前面一款我们提到了pass-by-value方式参数传递的效率带来的一系列可能引发的问题,你可能对pass-by-reference-const的参数传递方式特别的'青睐',而现在我们需要在在一次函数调用中要返回一个对象,问题是我们将以何种方式返回这个对象,你这个时候迫不及待的告诉我你的方案:直接返回该对象的reference.嗯,我们先用你的方案去解决下面这个问题,假设现在我们需要处理有理数Rational类:
class Rational{ public: Rational(int numerator=0,int denominator=1); ... private: int numerator_,denominator_; friend Rational& operator* (const Rational& lhs,const Rational& rhs); }; ... Rational& operator*(const Rational& lhs,const Rational& rhs){ Rational result( lhs.numerator_ * rhs.numerator_, lhs.denominator_ * rhs.denominator_ ); return result; }哦吼,问题来了,你返回的对象result是一个local对象,此对象在函数退出时候自动调用析构函数来销毁自己,不存在了,你现在却在返回一个不存在对象的引用,你就会陷入"未定义"的危地.你这时立刻就想到了一个解决方案:在heap上产生该对象不就问题解决了嘛,因为堆上的对象不会自动销毁自己(除非你手工调用delete操作).ok,那么我们再来改一下代码看这种方案是否可行:
Rational& operator*(const Rational& lhs,const Rational& rhs){ Rational* result=new Rational(lhs.numerator_ * rhs.numerator_, rhs.denominator_ * rhs.denominator_); return *result; }这样实现貌似很合理,其实这里还是存在一个问题:你new出来的对象何时被delete掉,谁负责执行delete操作.如果客户在使用这个类的时候写出如下代码:
Rational x,y,z,w; w=x*y*z; //operator*(x,operator*(y,z));这个时候operator*(y,z)对象的reference(其实就是指针)就会丢失,你就无法执行delete操作,那么这里明显就存在了内存泄露的风险.晕,这样也不行,这时聪明的你灵感又来了:用static修饰local对象.你这样想的理由可能是下面两点:(1)static对象常驻内存,一旦对象被创建,你就不能随意去销毁它,除非程序结束;(2)每次调用该函数只是修改该对象的值,该对象始终只会存在一个.听起来想法很不错,我们依然还是先修改代码,看这方案是否可行?
Rational& operator*(const Rational& lhs,const Rational& rhs){ static Rational result; ... result=...; return result; }而现在我们的客户却一不小心写下了这样的代码:
Rational a(2,3); Rational b(4,5); Rational c(4,7); Rational d(7,8); if( (a*b) == (c*d)){ ... }
const Rational operator*(const Rational& lhs,const Rational& rhs){ return result(lhs.numerator_ * rhs.numerator_,lhs.denominator_ * rhs.denominator_); }
条款22:将成员变量声明为private
(Declare data members private.)
内容:
首先我们讨论一下成员变量声明为public的情况,我们先从语法一致性角度来分析,如果成员变量不是public,那么客户访问这些变量唯一的方法只能是通过访问成员函数,由于这个时候public接口内只有函数,我们的客户就不需要在打算访问class成员时迷惑地试着记住是否该使用小括号(每个方法调用都会有括号),因为客户调用的只能是函数,每样别的形式,在客户使用你的class,这样就节省客户使用你的class时间(他/她省去了这些思考的时间).
呵呵,上述的一致性的理由可能不会另你信服,那我们从另一个角度来描述这样做的理由:将成员变量说明为private可以控制对成员变量的访问控制属性(read-only,write-only,read-write-only).我们来举个例子来说明比较形象一点,这里我们有个Person类:
class Person{ public: Person(const string& name,unsigned char age,const string& address) :name_(name),age_(age),address_(address){} string getName()const{ return name_; } string setAddress(string address){ address_ = address; } void setAge(unsigned char age){ age_ = age; } unsigned char getAge()const{ return age_; } private: string name_; //read-only unsigned char age_; //write-read-only string address_; //write-only };
看了上面这个例子,是不是觉得这样做是有道理的,有些认可我的观点了?为了让对这个观点确信不疑,我不得不拿出我的'绝招'了:封装性.假如你声明成员变量为public,那么任何对象都可以访问它们了,这样毫无封装性可言,给代码的维护带来了很大的困难(成员变量值的改变可能是任何拥有可以随意修改它们值权限的地方).让客户通过成员函数访问(而不是通过直接访问变量),这样另一方面也隐藏了实现,变量成员的任何变动都不会影响接口(对外暴露的成员函数),客户不需要关心接口内部对成员变量到底做了些什么.
不能将成员变量声明在protected内的论点也十分相似."语法一致性"和"细微划分之访问控制"等理由也适合于protected数据,但是对于封装性呢?protected成员变量封装性是不是高过public成员变量?这里的答案却不是肯定的.
封装性到底跟什么有直接或者间接的关系呢?我们将在条款23中谈到,成员变量的封装性与"当其内容改变时可能造成的代码破坏量"成反比,这里的成员变量改变也学就是从class中移除它.
现在假设我们已经有了一个public成员变量,现在我们取消它,我们的代码将有多少被破坏呢?所有使用它的代码都会被破坏,那是一个不可知的大量的修改工作;假设我们有一个protected成员变量,现在移除它,那么所有它的derived classes都会被破坏,同样也是一个不可知的大量的修改过程,由此这里我们得出一个结论:protectecd成员变量就像public成员变量一样缺乏封装性.因为在这两种情况下,一旦成员变量被修改,都会有不可预知的大量代码将会被破坏.
好了,this topic is over!
请记住:
■ 切记将成员变量声明为private.这可赋予客户访问数据的一致性,可细微划分访问控制,允诺约束条件获得保证,并提供class作者以充分的实现弹性.
■ protected并不比public更具封装性.
1、non-member方法与member方法没有本质区别,对于编译器来说,都是non-member方法,因为member方法绑定的对象,会被编译器转化为non-member方法的第一个形参。non-member方法与member方法唯一的区别是:member方法封装性更差,因为它可以访问private成员。
2、根据面向对象的要求,数据与方法应该和对象捆绑在一起,这意味着应该使用member方法。其实,这个建议是错误的。为什么?
3、首先,non-member、non-friend方法提供更大的封装性。
4、其次,考虑下面的需求,我只需要类中的一个方法。如果是member方法,必须把整个class定义包含进来,即使其他的接口我不使用。如果使用non-member方法,我只需要包含需要的方法声明就好了。因此,non-member降低编译的依赖关系。举例来说,member方法,需要一点东西也要把整个class包含进来,而整个class中又关联其他东西,导致当前需要的东西与其他东西的依赖。如果是non-member方法,相当于把整个class分成一个一个小块,需要那个小块,就包含哪个小块。这是因为class 的定义不能跨越多个源文件,而namespace可以跨越多个源文件。
条款24:若所有参数皆需类型转换,请为此采用non-member函数
(Declare non-member functions when type conversions should apply to all parameters.)
内容:
平时在我们的coding过程当中,我们有时候会碰到这样一些情况:我传进去的实参与声明函数的形参并不是同一类型的,函数调用竟然能够成功!这个听起来很不可思议是不是,那么我这里给你说一个编译器的"秘密",也许你在听完以后,你或许有可能对你说的那些"不可思议的事情"就不以为然了.
我在这里要说的"秘密"就是:当函数的实参类型与形参的出现不一致时候,编译器会尽它最大的能力去寻找合理类型转换方案进行隐式转换,使它们之间的类型达到一致,从而完成函数的调用过程,当然如果最后还是没有找到合适的类型转换策略,编译器就会给出"类型不匹配"的编译错误.不怎么理解?好,下面我来举个例子来说明这个问题.
假设现在有一个内置类型int的代理类IntDelegate,当然它就有很多特性,而这里我们要说的是它的乘法操作,你自然就写出了如下的代码:
class IntDelegate{ public: IntDelegate(int value = 0):intValue_(value){} IntDelegate(const IntDelegate& rhs){ intValue_ = rhs.intValue_; } const IntDelegate operator*(const IntDelegate& rhs){ IntDelegate result; result.intValue_ = intValue_ * rhs.intValue_; return result; } int getValue()const{ return intValue_; } ... private: int intValue_; ... };
//test.cpp ... IntDelegate a(1),b(2); IntDelegate c = a*b; //ok. c = a * 2; //Line1: ok. c = 2 * a; //Line2: error,喔欧,乘法交换律失败罗!shit!
IntDelegate tempt(2); c = a * tempt;
const IntDelegate operator*(const IntDelegate& lhs,const IntDelegate& rhs){ return IntDelegate(lhs.getValue()*rhs.getValue()); }
请记住:
■ 如果你需要为某个函数的所有参数(包括被this指针所指的那个隐喻参数)进行类型转换,那么这个函数必须是个non-member.
条款25:考虑写出一个不抛异常的swap函数
(Consider support for a non-throwing swap.)
内容:
swap函数就是相互交换两个对象的内容,标准程序库中已经提供了该函数的实现版本,其实现过程也是那么的如你预期所愿:
namespace std{ template <typename T> void swap(T& a,T& b) { T tempt(a); a = b; b = tempt; } }
class Window{ //这个class使用了pimpl手法 public: Window(const Window& rhs); Window& operator=(const Window& rhs){ ... *pImpl_ = *(rhs.pImpl_); ... } ... private: class WindowImpl{ public: ... private: string title_; std::vector<Window* > childList_; }; WindowImpl* pImpl_; };
namespace std{ template<> void swap<Window>(Window& a,Window& b){ swap(a.pImpl_,b.pImpl_); } }
class Window{ public: ... void swap(Window& other){ using std::swap; swap(pImpl_,other.pImpl_); } ... }; namespace std{ template<> void swap<Window>(Window& a,Window& b){ a.swap(b); } }
template<typename T> class WindowImpl{...}; template<typename T> class Window{...};
namespace std{ template<typename T> void swap<Window<T>>(Window<T>& lhs,Window<T>& rhs){ a.swap(b); } }
namespace std{ template<typename T> void swap(Window<T>& lhs,Window<T>& rhs){ //这里是一个重载版本(没有<...>) lhs.swap(rhs); } }
namespace WindowStuff{ ... template<typename T> class Window{...}; ... template<typename T> void swap(Window<T>& lhs,Window<T>& rhs){ a.swap(b); } }
template<typename T> void doSomething(T& obj1,T& ojb2){ using std::swap; ... swap(obj1,obj2); ... }
条款26:尽可能延后变量定义式的出现时间
(Postpone variable definitions as long as possible.)
内容:
由于定义一个类变量时,你就必须承担起构造和析构的负担.所以我们要尽量减少定义一些我们不用的对象,估计你现在对这条约束很不在意,我不用它为啥要定义它,我定义对象变量后肯定我会用它的嘛,呵呵,话不要说的太早有些时候,你的"不留心"会"出卖"你做出的这个承诺.
比如下面这个例子,你需要加密密码,如果密码太短,抛出异常,否则返回加密后的版本:
std::string encryptPassword(const std::string& password){ using namespace std; string encrypted; if(password.length() < MinimumPasswordLength){ throw logic_error("Password is too short"); } ... //开始加密 return encrypted; }
std::string encryptPassword(const std::string& password){ using namespace std; if(password.length() < MinimumPasswordLength){ throw logic_error("Password is too short"); } string encrypted; ... //开始加密 return encrypted; }
Widget w; for(int i = 0; i < n; i++){ w = ..; ... }
//B:变量定义于循环内 for(int i = 0; i < n; i++){ Widget w(xxx); ... }
条款27:尽量少做转型动作
(Minimize casting.)
内容:
我们先来看一下转型语法,C风格的转型语法大概就是下面这种形式:
(T)expression //将expression转换为T类型</span>
T(expression) ////将expression转换为T类型</span>
class Widget{ public: explicit Widget(int size); ... }; void doSomething(Widget& w); doSomething(Widget(15)); //"旧式转型"中的函数转型 doSomething(static_cast<Widget>(15));//"新式转型"
class Window{ public: virtual void onResize(){...} ... }; class SpecialWindow:public Window{ public: virtual void onResize(){ static_cast<Window>(*this).onResize();//调用基类的实现代码 ... //这里进行SpecialWindow的专属行为. } ... };
void SpecialWindow::onResize(){ Window::onResize(); //此时才是真正的调用基类部分的onResize实现. ... //同上 }
条款28:避免返回handles指向对象内部成分
(Avoid returning "handles" to object internals.)
内容:
这里所谓的handles一般包括内部对象的references、pointer、iterator,这条款是如何产生的呢?理由是什么呢?我先不必急着回答你这些问题,我们先来看一个例子,假设我们的程序中要用到矩形,于是我们写了Rectangle类去描述它,该类的内部对象为矩形左上角(upper left-hand corner)、右下角(Lower right-hand corner)两个Point对象,为了得到这两个内部对象数据我们提供了upperLeft与lowerRight函数接口.由于这里Point类是自定义类型,故我们先开始定义该类:
class Point{ public: Point(int x,int y); ... void setX(int value); void setY(int value); ... };
struct RectData{ Point upperLeftPoint,lowerRightPoint; }; class Rectangle{ public: ... Point& upperLeft()const{ //返回内部对象数据handle return pData->upperLeftPoint; } Point& lowerRight()const{ //返回内部对象数据handle return pData->lowerRightPoint; } private: std::tr1::shared_ptr<RectData> pData; };
Point coord1(0,0); Point coord2(100,100); const Rectangle rec(coord1,coord2); rec.upperLeft().setX(50);//wow, it changes the internal data,that's amazing!
这显然不是我们想要的结果.怎么解决?其中的一个解决方案就是在返回类型上加上const即可:
class Rectangle{ public: ... const Point& upperLeft()const{return pData->upperLeftPoint;} const Point& lowerRight()const{return pData->lowerRightPoint;} ... };
class GUIObject{...};//GUI base class const Rectangle boundingBox(const GUIObject& obj);//该函数返回GUI对象的外框.
GUIObject* pObject = NULL; ... //create GUIObject object const Point* pUpperLeft = &( boudingBox(*pObject).upperLeft());
条款29:为"异常安全"而努力是值得的
(Strive for exception-safe code.)
内容:
看了这款的内容,我对C++的难以控制的"脾气"又有了进一步的了解,C++的安全性问题一直是广大非C++程序员所抨击C++语言"恶行"的主要方面,我们今天讨论的其"异常安全性"也是其复杂之处之一,看完这一款之后,你也许会发觉你以前写的代码可能会给最终产品带来多大的"风险隐患",废话我就不多说了,开始进入今天的话题.
按照老规矩来,先看一个例子:假设有个class用来表现夹带背景图案的GUI菜单,这个class也要用于多线程环境当中,所以我们考虑用了一个互斥器(mutex)作为并发控制(concurrency control)之用:
class PrettyMenu{ public: ... void changeBackground(std::istream& imgSrc); //改变图片背景 ... private: Mutex mutex_; //互斥器 Image* bgImage_; //目前的背景图像 int imageChangesCounts_; //背景图像被改变的次数 };
void PrettyMenu::changeBackground(std::istream& imgSrc){ lock(&mutex_); //取得互斥器 delete bgImage_; //摆脱旧的背景图像 ++imageChangesCounts_; //修改变更次数 bgImage_ = new Image(imgSrc); //安装新的背景图像 unlock(&mutex_); //释放互斥器 }
void PrettyMenu::changeBackground(std::istream& imgSrc){ Lock(&mutex_); //获得互斥器并确保它稍后被释放 delete bgImage_; bgImage_ = new Image(imgSrc); ++imageChangesCounts_; }
class PrettyMenu{ ... std::tr1::shared_ptr<Image> bgImage_; }; void PrettyMenu::changeBackground(std::istream& imgSrc){ Lock(&mutex_); //获得互斥器并确保它稍后被释放 bgImage_.reset(new Image(imgSrc)); ++imageChangesCounts_; }
struct PMImpl{ std::tr1::shared_ptr<Image> bgImage_; int imageChangesCounts_; }; class PrettyMenu{ ... void changeBackground(std::istream& imgSrc){ using std::swap; //哒哒哒哒,条款25我们谈到这个swap的实现方法的喔! Lock m1(&mutex_); std::tr1::shared_ptr<PMImpl> pNewImpl(new PMImpl(*pImpl_)); //make copy pNewImpl->bgImage_.reset(new Image(imgSrc));//handle copy ++pNew->imageChangesCounts_; swap(pImpl_,pNewImpl); //swap } private: Mutex mutex_; std::tr1::shared_ptr<PMImpl> pImpl_; };
条款30:透彻了解inlining的里里外外
(Understand the ins and outs of inlining.)
内容:
对于该款的描述,原文中用了6页的篇幅进行阐述,这里我就将重要的知识点罗列出来,方便大家更好的理解这一条款.
(1)inline函数的优缺点.
优点:对于一个inline函数,你可以调用它们又不蒙受函数调用招致的额外开销,编译器就会对函数本体执行语境相关最优化,而大部分编译器不会对着一个非inline函数调用动作执行如此之最优化.
缺点:由于对inline函数的每一个调用都以函数本体替换之,所以说这样就可能增加你的目标代码,在一台有限的机器上,过度热衷inlining会造成程序体积太大,即使拥有虚拟内存,inline造成的代码膨胀亦会导致额外的换页行为,降低指令高速缓存装置的击中率,以及伴随而来的效率损失.
(2)inline只是对编译器的一个申请不是强制命令,该申请可以隐喻提出也可明确提出,隐喻提出就是将函数定义于class定义式内,这样的函数通常是成员函数或者是friend函数.明确声明就是在函数之前加关键字"inline".
(3)inline函数通常一定要被置于头文件内,其在大多数C++程序中是编译器行为.
(4)大部分编译器拒绝将太过复杂的函数inlining,而所有的virtual函数都不能inlining,因为virtual意味着"等待,知道运行期才确定调用哪个函数",而inline意味"执行前先将动作替换为被调用函数的本体".如果编译器不知道该调用哪个函数,你就很难责备它们拒绝将函数本体inlining.
(5)有时候编译器inline某个函数的同时,还可能为其生成一个函数本体(比如程序要取某个line函数地址),值得一提的是,编译器通常不对"通过函数指针而进行的调用"实施inling,这就是说line函数的调用可能是被inlined,也可能不被inlined,取决于调用的实施方式.
(6)"将构造函数和析构函数进行inling"是一个很糟糕的想法.看下面这段代码:
class Base{ public: ... private: std::string bm1,bm2; }; class Derived:public Base{ public: Derived(){} //空函数耶,够简单了吧?我想让它inlining,可以么? ... private: std::string dm1,dm2,dm3; };
Derived::Derived(){ //"空白Derived构造函数"的观念性实现 Base::Base();//初始化"Base成分" try{ dm1.std::string::string(); }catch(...){ Base::~Base(); throw; } try{ dm2.std::string::string(); }catch(...){ dm1.std::string::~string(); Base::~Base(); throw; } try{ dm3.std::string::string(); }catch(...){ dm2.std::string::~string(); dm1.std::string::~string(); Base::~Base(); throw; } }
条款31:将文件间的编译依存关系降至最低
(Minimize compilation dependencies between files.)
内容:
在你们的开发团队中,一些有经验的工程师时不时地会教导新手一些基本的编程原则,其中"将接口从实现中分离"可能是他(她)要你必须牢记原则,因为C++并没有把它做的很好,这只能靠我们在平时的编写代码中注意这一点了,如果你不小心违背了这一原则,可能招致的后果就是:当你轻微的修改了某个类的实现,注意不是接口的时候,再次重新BUILD一次你的工程,Oh,My God!很多文件被重新编译和链接了,Build的时间大大超出你的预期,而这种事情的发生,估计你当时就会只恨编译器的BUILD的速度太慢,效率太低.呵呵.避免陷入这种窘境的一种有效的方法就是本条款要提出的内容:将文件间的编译依存关系降至最低.
现在假设你写了一个Person类,一般你会这么构思你的代码:
#include <string> #include "date.h" #include "address.h" 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::string theName_; Date theBirthDate_; Address theAddress_; };
//person.h #include <string> #include <memory> using std::string; class Date; class Address; class Person{ public: Person(const string& name,const Date& birthday,const Address& addr); string name()const; string birthDate()const; string address()const; ... private: struct Impl; std::tr1::shared_ptr<Impl> pImpl_; }; //person.cpp #include "person.h" struct Person:Impl{ Impl(const string& name,const Date& birthday,const Address& addr) :theName_(name),theBirthDate_(birthday),theAddress_(addr){} string name()const{ return theName_; } ... string theName_; Date theBirthDate_; Address theAddress_; }; Person::Person(const string& name,const Date& birthday,const Address& addr) :pImpl_(new Impl(name,birthday,addr)){ } string Person::name()const{ return pImpl_->name(); } ...
//Person.h ... using std::string; class Date; class Address; class Person{ public: virtual ~Person(); virtual string name()const = 0; virtual string birthDate()const = 0; virtual string address()const = 0; ... static std::tr1::shared_ptr<Person> create(const string& name,const Date& birthday,const Address& addr); }; ... //person.cpp ... class RealPerson:public Person{ public: RealPerson(const string& name,const Date& birthday,const Address& addr); virtual ~RealPerson(){} string name()const; ... private: string name_; Date theBirthDate_; Address theAddress_; }; std::tr1::shared_ptr<Person> Person::create(const string& name, const Date& birthday, const Address& addr){ return std::tr1::shared_ptr<Person>(new RealPerson(name,birthday,addr)); }
条款32:确定你的public继承塑模出is-a关系
(Make sure public inheritance model is "is-a".)
内容:
我想对于类之间的public继承关系,每一本C++语言教程都会花费不少的篇幅去阐述,而本款所说的内容也许你早已是耳熟能详了,但在实际编码涉及中你是否从本条款出发去认真地考虑过你的设计的合理性呢?我觉得大多数的人没有真正做到这一点的.什么教"is-a"关系?如果你另class D以public形式继承class B,你便告诉编译器(以及你的代码阅读者)说,你D的每一个对象同时也是类型为B的一个对象.这听起来好像挺简单,但有的时候你必须注意你的直觉可能误导你.比如说,南极的企鹅其实是一种鸟类(这时你要是瞪大了疑惑眼睛就说明你生物没学好,呵呵),鸟是可以飞的,这是事实,要让你用C++描述这层关系的话,你毫不犹豫地写了出来,结果如下:
class Bird{ public: virtual void fly(); //bird can fly ... }; class Penguin:public Bird{ //Penguin is a kind of bird ... };
class Bird{ ...//没有声明fly函数 }; class FlyingBird:public Bird{ public: virtual void fly(); ... }; class Penguin:public Bird{ ... //没有声明fly函数 };
void error(const std::string& msg); //定义于另外某处 class Penguin:public Bird{ public: virtual void fly(){error("Attempt to make a penguin fly!");} };
class Bird{ ... //没有声明fly函数 }; class Penguin:public Bird{ ... //没有声明fly函数 };
Penguin p; p.fly();//喔欧,压根就没有fly方法,被编译器骂了吧!
class Rectangle{ public: virtual void setHeight(int newHeight); virtual void setWidth(int newWidth); virtual int height()const; virtual int width()const; ... }; class Square:public Rectangle{ ... };
void makeBigger(Rectangle& r){ //这个函数用以增加r的面积 int oldHeight = r.height; r.setWidth(r.width() + 10); //宽度加10 assert(r.height() == oldHeight); //r的高度是否改变 } //test.cpp Square s; ... assert(s.width() == s.height()); makeBigger(s); assert(s.width() == s.height()); //对所有正方形应该为真,但事实却不是
请记住:
★ "public继承"因为is-a.适用于base class身上的每一件事情一定也适用与derived class身上,因为每一个derived class对象也都是一个base class对象.
条款33:避免遮掩继承而来的名称
(Avoid hiding inherited names.)
内容:
相信大家对变量作用域的"名称遮掩"现象已经很熟悉了,比如下面这段代码:
int x; //global variable void someFunc() { double x; //local variable std::cin>>x; //read a new value to local x }
这时当编译器处于someFunc的作用域内并遭遇到名称x时,它先在本作用域中查找具有x名称的变量,如果找不到再到其他作用域找.但这里的两个作用域中的具有相同名称x的变量具有不同的数据类型(someFun中的为double而global中的为int),这不是需要在意的问题.C++的名称遮掩规则只关注名称的遮掩,而名称是否具有相同的类型并不重要.下面我们来重点看一下类中的名称遮掩问题.
就像独立的普通函数一样,类中的函数也具有作用域的问题,先看下面这段代码:
class Base { public: virtual void mf1() = 0; virtual void mf2(); void mf3(); ... private: int x_; }; class Derived:public Base { public: virtual void mf1(); void mf4() { //一个可能的实现 ... mf2(); ... } ... };
这段代码所构造的类作用域大体上可以用下面图示表示:
当编译器执行在mf4中遇到mf2名称时候,开始在本作用域内(Derived作用域)寻找名称匹配,未果,开始在上一层作用域中(Base作用域中)查找,哦也,找到了,调用这个版本的mf2(Base::mf2).如果它没有找到,则它开始在可能的namespace中查找,还是没找到的话就到global区域中查找.
现在我们在类中添加一下重载函数,看"名称遮掩"会怎么发生,新代码如下:
class Base { public: virtual void mf1() = 0; virtual void mf1(int); virtual void mf2(); void mf3(); void mf3(double); ... private: int x_; }; class Derived:public Base { public: virtual void mf1(); void mf3(); void mf4(); ... };
这时候作用域的大概图示可以描述如下:
我们现在写一段测试代码去test!
Derived d; int x; ... d.mf1(); //no problem. call Derived::mf1 d.mf1(x); //error. Derived::mf1 hiden Base::mf1 d.mf2(); //no problem. call Base::mf2 d.mf3(); //no problem. call Derived::mf3 d.mf3(x); //error. Derived::mf3 hiden Base::mf3
class Base{...}; //同上 class Derived:public Base{ public: //让Base class内名为mf1与mf3的所有东西在 //Derived作用域内都可见(并且都是public) using Base::mf1; using Base::mf3; ....//同上 };
class Base { public: virtual void mf1() = 0; virtual void mf1(int); ... //same as above }; class Derived:private Base { public: virtual void mf1()//转交函数(forwarding function) { Base::mf1(); } ... }; ... Derived d; int x; d.mf1(); //good! call Derived::mf1 d.mf1(x); //Error as suppose,Base::mf1() was hidden.
条款34:区分接口继承和实现继承
Differentiate between inheritance of interface and inheritance of implementation
内容:
当你开始设计你的class时候,有时候你希望它的Derived class只继承它的接口(声明);有时候你希望Derived class同时继承它的接口和实现,而且能够重新改写其继承的实现;有时候你又希望继承接口和实现的同时,但不能重新改写其继承的实现.以上这些情况当然在我们实际的开发中是很常见的,对于上述的选择我们该选择怎么样的class设计呢?
我们先看pure virtual 函数draw,这类函数的显著的特性是:它们必须被任何继承它们的子类重新声明和定义,它本身通常没有定义实现体,也就是说它存在的意义就是为了让derived class继承它的接口,注意我这里说的是通常它没有实现体,而你也可以为它提供实现体,C++对此也不会发脾气的,一般这种做法的用途很有限,主要是用在为子类提供一份默认的行为实现版本,稍后还要提到.为不同的函数分别提供接口可缺省实现可解决由于过度雷同的函数名称而引起class命名空间污染问题,其实我们可以用之前提到的"pure virtual 函数必须在derived classes中重新声明,但也可以拥有自己的实现"这一事实来解决掉这个缺点:
non-virtual函数,声明它就是为了给子类继承接口及其的一份强制实现,由于non-virtual函数代表意义不变性凌驾于特异性,所以它绝对不该在derived class中被重新定义(我们将在条款36中重点讨论这类函数).
pure virtual函数、simple virtual函数、non-virtual函数之间的差异,使得你能以精确指定你要的子类继承的东西:只继承接口,或是继承接口和一份缺省实现,或是继承接口和一份强制实现.由于这些不同类型的声明意味着根本意义并不相同的事情,当你声明你的成员函数时,必须谨慎选择.
请记住:
■ 接口继承和实现继承不同.在public继承下,derived classes总是继承base class的接口.
■ pure virtual函数只具体指定接口继承.
■ impure virtual函数具体指定接口继承及其缺省实现继承.
■ non-virtual函数具体指定接口继承以及强制性实现继承.
条款35: 考虑virtual函数以外的选择
内容:
假如现在你正在写一个游戏软件,游戏里面有各种游戏任务角色,人一多了嘛,就容易出现各个方面的利益冲突,而游戏设计者让他们解决冲突的直接方法就是--战斗,于是游戏中各种人物相互之间砍杀的画面就经常出现,这样就出现了由于受伤或者其它因素导致了健康系数的下降,这个时候作为游戏设计者的你,显然在这里要提供一个函数来计算各种人物当前的健康系数。这个难不倒你,由于各中人物的角色不同,身体素质不同等决定了它们的健康系数也不同,用virtual函数来计算看来是很合理的想法。
class GameCharacter{ public: virtual int healthValue()const; // 返回人物的健康指数并提供默认的实现体, 子类可以改写 ... };
class GameCharacter{ public: int healthValue()const{ ... //调用之前准备工作 int value = calcHealthValue(); ...//调用之后的一些清理工作等 } ... private: virtual int calcHealthValue()const{ ... } ... };
class GameCharacter; int defaultHealthCalc(const GameCharacter& gc); class GameCharacter{ public: typedef int (*HealthCalcFun)(const GameCharacter&); explicit GameCharater(HealthCalcFun hcf = defaultHealthCalc) :health_calculate_function_(hcf){ } int healthValue()const{ return health_calculate_function_(*this); } ... private: HealthCalcFunc health_calculate_function_; };
... class GameCharacter{ public: typedef std::tr1::function<int (const GameCharacter& )> HealthCalcFunc; .... //同上 };
std::tr1::function<int (const GameCharacter& )></span>
short calcHealth(const GameCharaceter&); //健康指数计算函数,注意它的返回类型为non-int struct HealthCalculator{ //为计算健康而设计的函数对象 int operator(const GameCharaceter&)const{...} }; class GameLevel{ public: float health(const GameCharacter&)const; //为计算健康指数设计的成员函数 ... } //定义两种游戏角色 class EvilBadBuy:public GameCharaceter{ ... //同前面 }; class EyeCandyCharacter:public GameCharacter{ ... //同前面 }; EvilBadGuy ebg1(calcHealth); //人物1 使用函数计算健康指数 EyeCandyCharacter ecc1(HealthCalculator());//人物2:使用函数对象计算健康指数 GameLevel curLevel; ... //人物3:使用某个成员函数计算健康指数 EvilBadGuy ebg2( std::tr1::bind( &GameLevel::health, curLevel, _1 ) );
class GameCharacter; class HealthCalcFunc{ public: ... virtual int calc(const GameCharacter& gc)const{ ... } ... }; HealthCalcFunc default_health_calculator; class GameCharacter{ public: explicit GameCharacter(HealthCalcFunc* function = &default_health_calculator ) :health_calculator_function(function){} int healthValue()const{ return health_calculator_function_->calc(*this); } ... private: HealthCalcFunc* health_calculator_function_; };
请记住:
■ virtual函数的替代方案包括NVI手法及Strategy设计模式的多种形式。NVI手法自身是一个特殊形式的template Method 设计模式。
■ 将机能从成员函数移到class外部函数,带来的一个缺点是,非成员函数无法访问class的 non-public成员。
■ tr1::function对象的行为就像一般函数指针。这样的对象可接纳"与给定之目标签名式兼容"的所以可调 用物。
条款36:绝对不要重新定义继承而来的non-virtual函数
内容:
我们来看个例子,class B为基类,class D为public继承自B的子类:
class B{ public: void func(){...} }; class D:public B{...}
D dObject; B* basePtr = &dObject; D* dOjbectPtr = &dObject;
basePtr->func(); dOjbectPtr->func();
条款37:绝不重新定义继承而来的缺省参数值
内容:
审视了一下条款以后,我们可以换一种说法:绝不重新定义继承而来的virtual函数或non-virtual函数的缺省参数值.而在上一款中我们提到,绝对不要试图去重新定义继承而来的non-virtual函数,将这句话与本条款合并的话,我们就可以将本条款的内容简化为:绝不重新定义继承而来的virtual函数的缺省参数值.为什么继承而来的virtual函数的缺省参数值不能被重新定义呢?其实原因也挺简单:缺省参数是静态绑定,而virtu
al函数是动态绑定. 至于什么是动态绑定与静态绑定,我想大家都应该清楚,不过在这里我还是要简单的唠叨一下,加强一下理解也不是件坏事,呵呵!所谓对象的静态绑定也叫前期绑定,它是说该对象类型和行为在程序编译期间就可以确定,例如:
class Shape{ public: enum Color{RED,GREEN,BLUE}; virtual void draw(Color color = RED)const = 0; ... }; class Circle:public Shape{ public: //哦欧! 竟然改变缺省参数值 virtual void draw(Color color = GREEN)const{ ... } }; class Rectangle:public Shape{ public: //没用指定参数类型,需要用户去明确的指定其值 //静态绑定下不继承基类的缺省值,若以指针或引用调用则不需要指定缺省值,因为动态绑定 //继承基类的参数缺省值 virtual void draw(Color color)const{ ... } };
Shape* ps; Shape* pc = new Circle; Shape* pr = new Rectangle;
class Shape{ public: enum Color{RED,GREEN,BLUE}; virtual void draw(Color color = RED)const = 0; ... }; class Circle:public Shape{ public: virtual void draw(Color color = RED)const {...} };
class Shape{ public: enum Color{RED,GREEN,BLUE}; void draw(Color color = RED) const{ ... doDraw(color); ... } ... private: virtual void doDraw(Color color) const = 0; }; class Circle:public Shape{ ... private: virtual void doDraw(Color color){ ... } };
条款38:通过复合塑膜出has-a或"根据某物实现"
内容:
所谓的复合就是类型之间的一种关系,它的基本的表现形式在我们平时的编写代码过程当中常常出现,比如你准备设计一个类的时候,你写出来的类的对象不是包含了其它的对象?呵呵,对了,这就叫复合.举个例子:
class Address{...}; class PhoneNumber{...}; class Person{ ... private: std::string name_; Address address_; PhoneNumber voiceNumber_; PhoneNumber faxNumber_; };
template<typename T> class Set:public std::list<T>{...}
template <class T> class Set{ public: bool member(const T& item)const{ return std::find( list_.begin(), list_.end(), item ) != list_.end() ); } void insert(const T& item){ if( !member(item) ) list_.push_back(item); } void remove(const T& item){ typename std::list<T>::iterator iter = std::find( list_.begin(), list_.end(), item ); if( iter != list_.end() ) list_.erase(iter); } size_t size()const{ return list_.size(); } private: std::list<T> list_; };
条款39:明智而审慎地使用private继承
Use private inheritance judiciously.
内容:
前面的条款中我们谈到将public继承视为一种is-a关系,在这里你不免有产生一个疑问:那对于private继承,我们应该视为什么关系呢?我们来看下面这个测试:
class Person{...}; class Student:private Person{...}; void eat(const Person& p); void study(const Student& s); Person p; //p is a person Student s; // s is a student. eat(p); //ok,p is a person,can eat something eat(s); //damned! it's surprise ! isn't student person?????
class Timer{ public: explicit Timer(int tickFrequency); virtual void onTick()const; //定时器每滴答一次,此函数就被自动调用一次. ... };
class Widget:private Timer{ private: virtual void onTick()const; //查看Widget状态... ... };
class Widget{ private: class WidgetTimer:public Timer{ public: virtual void onTick()const; ... }; WidgetTimer timer_; ... };
class Empty{};//啥都没用 class HoldsAnInt{ private: int x_; Empty empty_; };
class HoldsAnInt:private Empty{ private: int x_; };
条款40:明智而谨慎地使用多重继承
Use multiple inheritance judiciously.
内容:
当我们提到多重继承(multiple inheritance:MI)时,就不得不面对当前C++的两个基本阵营:一个认为单一继承SI(single inheritance) 是好的,MI一定更好!令一个阵营认为,SI是好的,但MI不值得我们去频繁使用.本款我们就带领大家来了解一下关于MI的两个基本观点.
先看下面这个例子:
class BorrowableItem{ //图书馆允许你借出的东西 public: void checkOut(); //离开时进行检查 ... }; class ElectronicGadget{ //电子机电 private: bool checkOut()const;//执行监测,返回监测结果 ... }; //MP3Player同时继承BorrowableItem与ElectronicGadget,(图书馆可以借出MP3 player) class MP3Player:public BorrowableItem,public ElectronicGadget{ ...}; //下面我们来测试上面类的功能,看是否完整 MP3Player player; player.checkOut(); //哒哒哒哒,问题来了:你说哪个checkOut将被调用?</span>
class IPerson{ public: virtual ~IPerson(); virtual std::string name() const = 0; virtual std::string birthDate() const = 0; };
//根据数据库id创建一个Person对象 std::tr1::shared_ptr<IPerson> makePerson( DatabaseID personIdentifier ); //取得一个数据库id DatabaseID askUserForDatabaseID(); //执行代码 DatabaseID id( askUserForDatabaseID() ); std::tr1::shared_ptr<IPerson> person( makePerson(id) ); ...
class PersonInfo{ public: using std::string; explicit PersonInfo(DatabaseID personID); virtual ~PersonInfo(); virtual const string theName()const; virtual const string theBirthDate()const; ... private: //返回左界限符 virtual const string valueDelimOpen()const; //返回右界限符 virtual const string valueDelimClose()const; };
const string PersonInfo::valueDelimOpen()const{ return "["; //'['并不是每个人都喜欢的,子类可以重新改写实现 } const string PersonInfo::valueDelimClose()const{ return "]";//']'并不是每个人都喜欢的,子类可以重新改写实现 } const string PersonInfo::theName()const{ string value = valueDelimOpen(); //先加入左界限符 .....//现在将对象的name名字添加到value后面 value += valueDelimClose(); //加入右界限符 return value; }
class CPerson:public IPerson,private PersonInfo{ public: explicit CPerson(DatabaseID personID):PersonInfo(personID){} virtual std::string name()const{ return PersonInfo::theName(); } virtual std::string birthDate()const{ return PersonInfo::theBirthDate(); } private: const std::string valueDelimOpen()const{return "";} const std::string valueDelimClose()const{return "";} };
条款41:了解隐式接口和编译期多态
(understand implicit interfaces and compile-time polymorphism.)
内容:
今天遇到这样一个处理Widget的函数doProcessing,其实现如下:
void doProcessing(Widget& w) { if( w.size() > 10 && w != someNastyWidget ){ Widget temp( w ); temp.normalize(); temp.swap( w ); } }
class Widget{ public: Widget(); virtual ~Widget(); virtual std::size_t size()const; virtual void normalize(); void swap(Widget& other); ... };
template<typename T> void doProcessing(T& w) { if( w.size() > 10 && w != someNastyWidget ){ T temp( w ); temp.normalize(); temp.swap( w ); } }
条款42:了解typename的双重意义
Understand the two meanings of typename.
内容:
看下面template声明式中,class与typename有什么不同?
template<class T> class Widget;//use "class " template<typename T> class Widget;//use "typename"某些程序员喜欢用class,因为可以少打几个字母.还有一部分开发人员喜欢在接受任何类型时使用typename,而在接受用户自定义类型时保留旧式的class.然而实际上,从C++角度来看,声明template参数时,不论使用关键字class或typename意义完全相同.但这并不等于说C++总是 把class和typename视为等价.有时候你一定得使用typename.
tempalte <typename C> void print2nd(const C& container)//打印容器内的第二元素 { if( containter.size() >= 2 ){ C::const_iterator iter( containter.begin() ); ++iter; int value = *iter; std::cout << value; } }
template <typename C> void print2nd(const C& containter) { if( container.size() >= 2 ){ typename C::const_iterator iter( container.begin() ); ... } }
template <typename T> class Derived:public Base<T>::Nested{ //base class list中不允许出现"typename" public: explicit Dervied(int x) : Base<T>::Nested(x){ //成员初始化列表中不允许"typename" typename Base<T>::Nested temp; //既不在base class list也不在初始化列表中,作为一个base class修饰符需加上typename. ... } ... };在本款快结束的时候,我们要注意这样的一个问题:typename相关规则在不同的编译器上有不同的实践.某些编译器接受的代码原本该有typename却遗漏了;原本不该有typename却出现了;还有少数编译器(通常是较旧版本)根本就拒绝typename.这意味typename和"嵌套从属类型名称"之间的互动,也许会在移植性方面带给你某种温和的头疼(==!作者你在搞什么鬼).
条款43:学习处理模板化基类内的名称
(Know how to access names in templatized base classes.)
内容:
现在我们接到一个编码任务,任务要求我们的目标程序能够传送信息到不同的公司去.这里的信息可以分为:被译成密码的信息和未经加工信息明文信息.我们分析了任务以后认为,在目标程序的编译期间我们就可以决定哪一个信息传送至哪一家公司.所以我们采用了template来实现,大概的伪代码实现如下:
class CompanyA{ public: ... void sendClearText(const std::string& msg); void sendEncrypted(const std::string& msg); ... }; class CompanyB{ public: ... void sendClearText(const std::string& msg); void sendEncrypted(const std::string& msg); ... }; ... //其它公司的classes. class MsgInfo{...};//这个class为将来生产保存信息 template<typename Company> class MsgSender{ public: ... void sendClear(const MsgInfo& info){ std::string msg; ...//根据info产生信息 Company c; c.sendClearText(msg); } void sendSecret(const MsgInfo& info){...} //这里调用的是c.sendEncrypted. };
template <typename Company> class LoggingMsgSender:public MsgSender<Comany>{ public: ... void sendClearMsg(const MsgInfo& info){ //为避免"名称遮掩"现象的发生,采用了一个不同的名称 ...// record status information before sending message sendClear(info); ...//record status information after sending message. } ... };
class CompanyZ{ //这个class 不提供sendClearText函数 public: ... void sendEncrypted(const std::string& msg); ... };
template<> class MsgSender<CompanyZ>{ public: ... void sendSecret(const MsgInfo& info){...} //只提供sendSecret方法 ... };
tempalte <typename Company> void LoggingMsgSend<Company>::(const MsgInfo& info){ ...// sendClear(info); //如果这里Company == CompanyZ,这个函数就不存在 ...// }
template <typename Company> void LoggingMsgSender<Company>::sendClearMsg(const MsgInfo& info){ ... this->sendClear(info); //ok ... }
template <typename Company> class LoggingMsgSender:public MsgSender<Company>{ public: //这里的情况不是base class名称被derived class名称遮掩,而是编译器不进入base base //作用域查找,于是我们通过using声明式告诉它,请它这么做 using MsgSender<Company>::sendClear;//告诉编译器,请它假设sendClear位于base class内 ... void sendClearMsg(const MsgInfo& info){ ... sendClear(info);//ok ... } };
template <typename Company> class LoggingMsgSender:public MsgSender<Company>{ public: ... void sendClearMsg(const MsgInfo& info){ ... MsgSender<Company>::sendClear(info); //ok ... } ... };
条款44:将与参数无关的代码抽离templates
(Factor parameter-independent code out of templates.)
内容:
自从我们进入模板编程的第一天,我们就意识到template不仅大大地减少了代码的执行时间,而且能够避免代码潜在的重复.我们不在需要为20个相似的classes而每一个都带有15个成员函数编写独立的一份代码去分别实现,我们只需要键入一个class template,留给编译器去具现化那20个你需要的相关classes和300个函数.如此神奇的技术是不是令你感到很兴奋.是的,这个对每一个软件开发人员来说无疑是一个重大的编码优化手段,然而有时候,如果你不小心,使用templates可能会导致代码膨胀:其二进制码带着重复(或几乎重复)的代码、数据,或两者.
在你编码的时候,通常你有一个强大的工具.那就是:共性与变性分析.其概念从字面上我们就可以了解,即使你从未写过一个template,你始终在进行着这样的分析.当你在写两个函数的时候,你发现它们之间有着相同的部分,此时的你惯性般地将他们之间"公共部分"提出来,作为第三个函数,然后让前两个函数都调用这个第三个函数;同样的道理,当你编写某个class时候,你发现其某些部分与另外一个class的某些部分相同,你也不会去重复该相同部分,而是把相同部分搬移到新的class去,然后在使用复合或继承,令原classes取用这些共同特性.而原classes的特异部分则保持不动. 好,我们将这种分析方法推而广之,它对template同样适用.但在template代码中,重复是隐晦的:毕竟只存在一份template源码,所以你编码时必须万分小心去防止template具现化多次时可能造成的重复.
举个例子,假设现在你要为固定尺寸的矩阵编写一个template类,该类声明要支持矩阵的逆运算.你开始了你的代码:
template <typename T, std::size_t n> //矩阵元素类型T,尺寸大小为n class SquareMatrix{ public: ... void invert(); //逆运算 };
SquareMatrix<double,5> square1; ... square1.invert(); //调用 SquareMatrix<double,5>::invert SquareMatrix<double,10> square2; ... square2.invert(); //调用 SquareMatrix<double,10>::invert</span>
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; //避免遮掩base版本的invert public: ... void invert(){ this->invert(n); } };
template <typename T> class SquareMatrixBase{ protected: SquareMatrixBase(std::size_t n, T* memory) :size_(n),data_(memory){} void setData(T* data){ data_ = data; } ... private: std::size_t size_; //矩阵大小 T* data_; //矩阵数据 }; template <typename T, std::size_t n> class SquareMatrix:private SquareMatrixBase<T>{ public: SquareMatrix():SquareMatrixBase<T>(n,data){} //将矩阵大小和数据指针给base class. ... private: T data_[n*n]; };
template <typename T,std::size_t n> class SquareMatrix:private SquareMatrixBase<T>{ public: SquareMatrix():SquareMatrixBase<T>(n,0),data_(new T[n*n]){ this->setData(data_.get()); //使得base class获得矩阵数据 } ... private: boost::scoped_array[T] data_; };
其实type parameter(类型参数)也会导致膨胀。例如许多平台上int和long有相同二进制表述,所以像vector<int>和vector<long>的成员函数有可能完全相同——这正是膨胀的最佳意义。某些链接器会合并完全相同的函数实现码,但有些不会,后者意味着某些templates被具现化为int和long两个版本,并因此造成代码膨胀。类似情况,大多数平台下,所有指针类型都有相同的二进制表述,因此凡是templates持有指针者(例如list<int*>,list<const int*>,list<SquareMatrix<long,3>*>等等)往往应该对每一个成员函数使用唯一一份底层实现。这很具代表性的意味,如果你实现某些成员函数而他们操作强型指针(即 T*),你应该令他们调用另一个操作无类型指针(即 void*)的函数,而后者完成实际工作。某些c++标准程序库实现版本的确为vector,deque,list等templates做了这件事。如果你关心你的templates可能出现代码膨胀,也许你会想让你的templates也做相同的事情。
来源: <http://www.cnblogs.com/lidan/archive/2012/02/15/2353160.html>
请记住:
■ Templates生成多个classes和多个函数,所以任何template代码都不该与某个造成膨胀的template参数产生相依关系.条款45:运用成员函数模板接受所有兼容类型
(Use member function templates to accept "all compatible types.")
内容:
不知道大家注意到没有,在类的继承体系中,对象指针一直在为我们做一件很好的事情:支持隐式转换(implicit conversions).Derived class指针可以隐式转换为base class指针,"指向non-const对象"的指针可以转换为"指向const对象"......等等.下面例子展示了继承体系之间的可能进行的转换操作:
class Top{ ... }; class Middle:public Top{...}; class Bottom:public Middle{...}; Top* top1 = new Middle; //将Middle*转换为Top* Top* top2 = new Bottom; //将Bottom*转换为Top* const Top* const_top2 = top1; //将Top*转换为const Top*
template <typename T> class SmartPtr{ public: explicit SmartPtr(T* realPtr);//智能指针通常以原始指针完成初始化 ... }; SmartPtr<Top> top1_smart_ptr = SmartPtr<Middle>(new Middle); SmartPtr<Top> top2_smart_ptr = SmartPtr<Bottom>(new Bottom); SmartPtr<const Top> const_top2_ptr = top1_smart_ptr;
template <typename T> class SmartPtr{ public: template <typename U> SmartPtr(const SmartPtr<U>& other); //copy constructor ... };
template <typename T> class SmartPtr{ public: template <typename U> SmartPtr(const SmartPtr<U>& other):held_ptr_( other.get() ){...} //用other.held_ptr_初始化this->held_ptr_,这里的other的原始对象如果是 //this原始对象的子类的话,这里就完成子类向父类的隐式转换过程. T* get()const{ return held_ptr_;} ... private: T* held_ptr_; //这是SmartPtr持有的内置指针. };
呵呵,不错吧!其实member function template(成员函数模板的效用不限于构造函数),它们常常扮演的另一个角色就是支持赋值操作.这点在TR1的shared_ptr中获得了绝好的发挥.下面是TR1规范中关于tr1::shared_ptr的一份摘录.
template<class T> class shared_ptr{ public: template<class Y> explicit shared_ptr(Y* p); template<class Y> shared_ptr(shared_ptr<Y> const& r); template<class Y> explicit shared_ptr(weak_ptr<Y> const& r); template<class Y> explicit shared_ptr(auto_ptr<Y>& r); //为什么这里不要const? 因为当你复制一个auto_ptr,它们其实被改动了. template<class Y> shared_ptr& operator=(shared_ptr<Y> const& r); template<class Y> shared_ptr& operator=(auto_ptr<Y>& r);//为什么这里不要const? 原因同上 ... };
template<class T> class shared_ptr{ public: shared_ptr(shared_ptr const& r); //copy 构造函数 template<class Y> shared_ptr(shared_ptr<Y> const& r); //泛化的copy构造 shared_ptr& operator=(shared_ptr const& r); //copy assignment template<class Y> shared_ptr& operator=(shared_ptr<Y> const& r); //泛化copy assignment ... };请记住:
条款46:需要类型转换时请为模板定义非成员函数
Define non-member functions inside templates when type conversion are desired.
还记得在条款24中,我们提到只有non-member函数才有能力'在所有实参身上实施隐式类型转换'.今天我们讨论的东西与该条款相关,如果现在的你恰好忘记了前面这条款的内容,那么我建议你还是先把那条款在消化一下,再开始这一款吧,呵呵,'磨刀不误砍柴工'嘛!废话我就不多说了,我们进入正题.
'只有non-member函数才有能力在所有实参身上实施隐式类型转换',在泛型编程的世界里,这是否也成立呢?下面我们来稍微修改一下前面这款例子中的代码,然后验证一下是这句话是否依然成立.
template<typename T> class Rational{ public: Rational(const T& numberator = 0, const T& denominator = 1); const T numerator()const; const T denominator()const; ... };
template<typename T> const Rational<T> operator*(const Rational<T>& left_handle_side, const Rational<T>& right_handle_side );
Rational<int> one_half( 1, 2 ); Rational<int> result = one_half * 2; //compile error.
但事实却是很惨酷的:它们没有那样做.因为在template实参推导过程中从不将隐式类型转换函数纳入考虑.这下麻烦了,那有什么办法能够解决这一问题的呢?别急,其实我们只需要利用一个事实就可以缓和编译器在template实参推导方面受到的挑战,这个事实就是:template class内的friend声明式可以指涉某个特定的non-member函数.class templates并不依赖template实参推导(后者只施行于function templates身上),所以编译器总是能够在class Rational<T>具现化时得知T(机智的小伙伴).因此,我们的问题的解决方案就出来了:令Rational<T> class声明适当的operator*为其friend函数.
template<typename T> class Rational{ public: ... //同上 friend const Rational operator* ( const Rational& left_handle_side, const Rational& right_handle_side ); }; template <typename T> const Rational<T> operator*(const Rational<T>& left_handle_side, const Rational<T>& right_handle_side ) {...}
template<typename T> class Rational{ public: ... //同上 friend const Rational operator*(const Rational& left_handle_side, const Rational& right_handle_side ){ return Rational( left_handle_side.numerator() * right_handle_side.numerator(), left_handle_side.denominator() * right_handle_side.denominator() ); } };
template<typename T> class Rational; template<typename T> const Rational<T> doMultiply(const Rational<T>& left_handle_side, const Rational<T>& right_handle_side); template<typename T> class Rational{ public: ... friend const Rational<T> operator*(const Rational<T>& left_handle_side, const Rational<T>& right_handle_side){ return doMultiply( left_handle_side, right_handle_side ); } ... };
条款47:请使用traits classes表现类型信息
Use traits classes for information about types.
今天我们讨论的这款内容涉及到一个STL实现上的一个关键技术traits技术,简单的说就是类的型别判定技术.
我们知道STL迭代器可分为五类,我再来简单的唠叨一下:input迭代器只能向前移动,一次一步,客户只可读取(不能修改)它们所指的内容,而且只能读取一次;output迭代器情况类似,只是为了输出;以上这两种分别模仿文件的读写指针,分类的代表为istream_iterators和ostream_iterators.它们两个属于能力最小的迭代器分类,只适合一次性操作算法.第三类迭代器为forward迭代器,该种迭代器能够做上述两种类所能做的每一件事情,而且可以读写所指物一次以上.第四类迭代器为bidirectional迭代器,比前一种威力更大,除了可以向前移动还可以向后移动.STL的list,set,multiset,map,multimap的迭代器都是属于这一分类.最后一种分类也是威力最强的迭代器当属random access迭代器,它可以在常量时间内向前或向后迢遥任意距离.vector,deque和string提供的迭代器就属于这一分类.
对于这五种分类,C++标准库提供专门的类型标记结构对它们进行区分:
template <typename IterT, typename DistT> void advance(IterT& iter, DistT d);
template <typename IterT, typename DistT> void advance(IterT& iter, DistT d) { if( iter is a random access iterator ){ iter += d; //针对random access迭代器使用迭代器算术运算 } else { //针对其它迭代器分类反复调用++或者-- if( d >= 0 ){ while( d-- ) ++iter; } else { while( d++ ) --iter; } } }
template<typename IterT> struct iterator_traits;
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; ... }; ... };
template <typename IterT> struct iterator_traits{ typedef typename IterT::iterator_category iterator_category; ... };
template<typename IterT> struct iterator_traits<IterT*>{ typedef random_access_iterator_tag iterator_category; ... };
继续前面讨论来讲述如何使用traits classes来表现类型信息.好,我们现在有了iterator_traits,我们可以可以这样来实现advance函数:
template<typename IterT, typename DistT> void advance(IterT& iter, DistT d) { if( typeid( typename std::iterator_traits<IterT>::iterator_category ) == typeid ( std::random_access_iterator_tag ) ) ... }
//这份实现用于random access迭代器 template<typename IterT,typename DistT> void doAdvance( IterT& iter, DistT d, std::random_access_iterator_tag) { iter += d; } //这份实现用于bidirectional迭代器 template<typename IterT,typename DistT> void doAdvance( IterT& iter, DistT d, std::bidirectional_iterator_tag) { if( d >= 0 ){ while( d-- ){ ++iter; } } else { while( d++ ){ --iter; } } } //这份实现用于input迭代器 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() ); }
条款48:认识template元编程
Be aware of template metaprogramming.
Template metaprogramming(TMPS,模板元编程)是编写template-based C++程序并执行编译期的过程.它本质上是以C++写成、执行于C++编译器内的程序.一旦TMP程序结束执行,其输出,也就是从templates具现出来的若干C++源码,便会一如往常地被编译.TMP有两个伟大的效力.第一,它让某些事情更容易.如果没有它,那些事情将是困难,甚至不可能.第二,欲与TMP执行于C++编译期,因此可将工作从运行期转移到编译期.这导致一个结果是,某些错误原本通常在运行期才能检测到,现在可在编译期找出来.这样带来的结果是很美好的.因为使用TMP的C++程序可能在每一方面都更高效:较小的可执行文件、较短的运行期、较少的内存要求.然而它也会带来另一个令人感到不愉快的结果是,编译时间变长了.前面一款我们提到了使用typeid的advance的伪代码实现版本:
template <typename IterT, typename DistT> void advance( IterT& iter, DistT d ) { if( typeid( typename std::iterator_traits<IterT>::iterator_category ) == typeid( std::random_access_iterator_tag ) ){ iter += d; } else { if( d >= 0 ){ while( d-- ){ ++iter; } } else { while( d++ ){ --iter; } } } }
std::list<int>::iterator iter; ... advance( iter, 10 ); //compile error:error C2676: binary '+=' : 'std::list<_Ty>:: //_Iterator<_Secure_validation>' does not define this operator or a conversion to //a type acceptable to the predefined operator.
条款49:了解new-handler的行为
Understand the behavior of the new-handler.
在我们平时的编写代码过程中,总是避免不了去new出一些对象出来.我们知道new操作符私底下通过调用operator new来实现内存分配的.当operator new抛出异常以反映一个未获满足的内存需求之前,它会调用一个客户指定的错误处理函数,一个所谓的new-handler.而客户是通过set_new_handler将自己的new-handler传递给它的,其声明在<new>标准库函数中:
namespace std{ typedef void (*new_handler)(); new_handler set_new_handler( new_handler p ) throw(); }当operator new无法满足内存申请时,它会不断调用new-handler函数,直到找到足够内存.关于反复调用代码我们在条款51中我们会讨论.一个设计良好的new-handler函数必须做以下事情:
struct TestA{ static void outOfMemory(); ... }; struct TestB{ static void outOfMemory(); ... }; TestA* a = new TestA;//if memory runs out,call TestA::outOfMemory TestB* b = new TestB;//if memory runs out,call TestB::outOfMemory但是C++不支持class专属之new-handlers,但你可以自己实现这种行为.好,我们来试试!假设我们现在打算处理Widget class的内存分配失败情况,我们需要声明一个类型为new_handler的static成员,用以指向class Widget的new-handler,看起来应该像这样:
struct Widget{ 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 st_current_handler_; }; Widget内的set_new_handler函数会将它获得的指针存储起来,然后返回调用之前的指针,这跟标准版set_new_hander的行为是一样的: std::new_handler Widget::set_new_handler( std::new_handler p ) throw (){ std::new_handler old_handler = st_current_handler_; st_current_handler_ = p; return old_handler; }
struct NewHandlerHolder{ explicit NewHandlerHolder(std::new_handler handler) :handler_( handler ){} ~NewHandlerHolder(){ std::set_new_handler( handler_ ); } private: NewHandlerHolder(const NewHandlerHolder&); NewHandlerHolder& operator=(const NewHandlerHolder&); std::new_handler handler_; };这样Widget内的operator new的实现就变的容易了:
void* Widget::operator new(std::size_t size) throw(std::bad_alloc) { NewHandlerHolder alloc_handler( std::set_new_handler( st_current_handler_ ) ); return ::operator new( size ); }客户现在可以这样用:
void outOfMemory(); Widget::set_new_handler( outOfMemory ); Widget* new_widget_obj = new Widget; //if memory allocation failed, //call outOfMemory std::string* new_string = new std::string; //if memory allocation failed, //call global new-handling function //if exists. Widget::set_new_handler( 0 ); Widget* another_widget = new Widget; // if memory allocation failed,throw //exception at once.
template<typename T> struct NewHandlerSupport{ static std::new_handler set_new_handler( std::new_handler p ) throw (){ std::new_handler old_handler = st_current_handler_; st_current_handler_ = p; return old_handler; } static void* operator new( std::size_t size ) throw ( std::bad_alloc ){ NewHandlerHolder h( std::set_new_handler( st_current_handler_ ) ); return ::operator new( size ); } ... private: static std::new_handler st_current_handler_; }; template<typename T> std::new_handler NewHandlerSupport<T>::st_current_handler_ = 0;
struct Widget:public NewHandlerSupport<Widget>{ ... };
typedef const int signature = 0xDEADBEEF; typedef unsigned char Byte; void* operator new( std::size_t size)throw(std::bad_alloc) { using namespace std; size_t real_size = size + 2 * sizeof(int); void* applied_memory = malloc( real_size ); if( applied_memory == 0 ){ throw bad_alloc(); } //将signature写入内存的最前段落和最后段落. *(static_cast<int*>( applied_memory ) = signature; *(reinterpret_cast<int*>(static_cast<Byte*>( applied_memory ) + real_size - sizeof(int) ) ) = signature; //返回指针,指向恰位于第一个signature之后的内存位置. return static_cast<Byte*>( applied_memory ) + sizeof(int); }
条款51:编写new和delete时需固守常规
Adhere to convention when writing new and delete.
大家好,前一条款我们已经讨论了你在什么时候想要写个自定义的operator new和operator delete,但并没有解释当你这么做时必须遵守什么规则.我先来总体说一下,然后再分析这些规则.
要实现一致性operator new必得返回正确的值,内存不足时必得调用new-handling函数(见条款49),必须有对付零内存需求的准备,还需避免不慎掩盖正常形式的new(虽然这比较偏近class的接口要求而非实现要求).
先来说说关于其返回值,如果它有能力供应客户申请的内存,就返回一个指针指向那块内存.如果没有那个能力,就遵循49描述的原则,并抛出一个bad_alloc异常.
而实际上operator new不止一次尝试进行内存分配,并在每次失败后调用new-handing函数.这里假设new-handling函数也许能够做某些动作释放某些内存.只有当当前的new-handling函数指针为null时,operator new才会抛出异常.
C++规定,即使客户要求0 bytes,operator new也得返回一个合法指针.这种诡异的行为其实是为了简化语言的其它部分.下面是个non-member operator new伪码:
void* operator new(std::size_t size) throw(std::bad_alloc) { using namespace std; if( size == 0 ){ size = 1; } while( true ){ ...//try to allocate size bytes memory. if( allocate_succeed ){ return (point_to_allocted_memory); } //allocate failed:find current new-handling function(as following) new_handler global_handler = set_new_handler( 0 ); set_new_handler( global_handler ); if( global_handler ){ ( *global_handler )(); } else { throw std::bad_alloc(); } } }
struct Base{ static void* operator new(std::size_t size) throw( std::bad_alloc ); ... }; struct Derived:public Base{...}; Derived* p = new Derived;//call Base::operator new.
void* Base::operator new(std::size_t size) throw(std::bad_alloc) { if( size != sizeof(Base) ){ return ::operator new( size ); //call standard operator new version. } ... }如果你打算控制class专属之'arrays内存分配行为',那么你需要实现operator new的array兄弟版:operator new[].这个通常被称为"array new".如果你要写这个operator new[],记住,唯一需要做的事情就是分配一块未加工内存.因为你无法对array之内迄今为止尚未存在的元素对象做任何事情.实际上你甚至无法计算这个array将含有多少个元素.首先你不知道每个对象多大,因此你不能在Base::operator new[]内假设每个元素对象大小是sizeof(Base),此外传递给它的参数size的值有可能比'将被填以对象'的内存数量更多.
void operator delete( void* raw_memory ) thrwo() { if( raw_memory == 0 ){ return; } ...//now,free raw memory block. }</span>而对于member版本的也很简单.
<span style="font-size:14px;"> void Base::operator delete( void* raw_memory,std::size_t size ) throw() { if( raw_memory == 0 ){ return; } if( size != sizeof(Base) ){ //if size error, call standard operator delete ::operator delete( raw_memory ); return; } ...//now,free your raw memory block. return; }如果即将删除的对象派生自某个base class而后者欠缺virtaul析构函数,那么C++传给operator delete的size_t数值可能不正确.这是'让你的base class拥有virtual析构函数'的一个够好的理由.
条款52:写了placement new也要写placement delete
Write placement delete if you write placement new.
我们都知道当你在写一个new表达式像这样:
Widget* new_widget = new Widget;
共有两个函数被调用:一个是用以分配内存的operator new,一个是Widget的default构造函数.
那么假设我们现在遇到的情况是:第一个函数调用成功,第二个函数却抛出异常.按照常理,在步骤一中所分配的内存必须取消,否则就会造成内存泄露.而在这个时候客户已经没有能力归还内存了,因为手上没有指向这块内存的指针,故此任务就落到了C++运行期系统身上.
为了完成任务,运行期系统当然就会调用步骤一所用的operator new的相应operator delete版本.如果当前要处理的是拥有正常签名的new和delete版本,这好办!因为正常的operator new签名式:
void* operator new(std::size_t) throw (std::bad_alloc);</span>
void operator delete(void* raw_memory)throw();//global 作用域中的正常签名式 void operator delete(void* raw_memory,std::size_t size)throw();//class作用域中典型的签名式
struct Widget{ //非正常形式的new static void* operator new(std::size_t size,std::ostream& log_stream)throw(std::bad_alloc); //正常class专属delete. static void operator delete(void* memory, std::size_t size)throw(); ... };
void* operator new( std::size_t, void* memory ) throw();//placement new</span>该版本的new已经被纳入C++标准程序库,你只要#include <new>就可以取用它,它的用处就是负责在vector的未使用空间上创建对象.
Widget* new_widget = new( std::cerr ) Widget;</span>在说一下我们先前提到的问题,如果内存分配成功,而构造抛出异常,运行期就有责任取消operator new的分配并恢复旧观.然而运行期系统无法知道真正被调用的那个operator new如何运作,因此它无法取消分配并恢复旧观,所以上述做法行不通.取而代之的是,运行期系统寻找'参数个数和类型都与operator new相同'的某个operator delete.如果找打,那就是它该调用的对象.既然这里的operator new接受的类型为ostream&的额外实参,所以对应的operator delete就应该是:
void operator delete(void*,std::ostream&)throw();</span>类似于new的placement版本,operator delete如果接受额外参数,便称为placement deletes.现在,既然Widget没有声明placement版本的operator delete,所以运行期系统不知道如何取消并恢复原先对placement new的调用.于是什么也不做.本例之中如果构造抛出异常,不会有任何operator delete被调用.
struct Widget{ static void* operator new(std::size_t size, std::ostream& log_stream)throw(std::bad_alloc); static void operator delete(void* memory) throw(); static void operator delete(void* memory,std::ostream& log_stream)throw(); ... };这样改变之后,如果以下语句引发Widget构造函数抛出异常:
Widget* new_widget = new (std::cerr) Widget; //一如既往,但这次就不在发生泄漏.</span>然而如果没有抛出异常(大部分是这样的),客户代码中有个对应的delete,会发生什么事情:
delete pw; //call normal operator delete</span>
void* operator new(std::size_t)throw(std::bad_alloc);//normal new. void* operator new(std::size_t,void*)throw();//placement new void* operator new(std::size_t, const std::nothrow_t&)throw();//nothrow new.see Item 49.如果你在class内声明任何operator news,它会遮掩上述这些标准形式.除非你的意思就是要阻止class的客户使用这些形式,否则请确保它们在你所生成的任何定制型operator new之外还可用.对于每一个可用的operator new也请确定提供对应的operator delete.如果希望这些函数都有着平常的行为,只要令你的class专属版本调用global版本即可.
struct StandardNewDeleteForms{ //normal new/delete static void* operator new(std::size_t size)throw(std::bad_alloc) { return ::operator new( size ); } static void operator delete(void* memory) throw() { ::operator delete( memory ); } //placement new/delete static void* operator new(std::size_t size,void* pointer)throw() { return ::operator new( size, pointer );} static void operator delete(void* memory,void* pointer)throw() { return ::operator delete( memory, pointer ); } //nothrow new/delete static void* operator new(std::size_t size,const std::nothrow_t& no_throw)throw() { return ::operator new( size, no_throw ); } static void operator delete(void* memory,const std::nothrow_t&)throw() { ::operator delete( memory ); } };凡是想自定形式扩充标准形式的客户,可利用继承机制及using声明式(Item 33)取得标准形式:
struct Widget:public StandardNewDeleteForms{ using StandardNewDeleteForms::operator new; using StandardNewDeleteForms::operator delete; static void* operator new(std::size_t size, std::ostream& log_stream) throw(std::bad_alloc); static void operator delete(void* memory,std::ostream& log_stream) throw(); ... };好了,本款讨论结束.
条款53:不要轻忽编译器的警告
Pay attention to compiler warnings.
许多程序员习惯性地忽略编译器警告.他们任务如果是问题真的很严重的话,编译器就应该给一个错误提示信息而不是警告信息.这种想法看起来似乎很合理,但是在C++语言上,我觉得编译器作者对代码即将会爆发的事情应该比你有更加深入的理解,你说呢?下面我举的这个例子是多多少少在大部人身上都发生过一个错误:
struct B{ virtual void f()const; }; struct D:public B{ virtual void f(); };我现在的编译器给出了这样的警告信息:warning:D::f()hides virtual B::f()
条款54:让自己熟悉包括TR1在内的标准程序库
Familiarize yourself with the standard library,including TR1.
本款原书主要介绍了标准程序库和TR1程序库.书里面介绍的很详细,我在这里就不想再次累赘介绍了,有兴趣的可以阅读原书的内容.我只把原书中关于本款要记住的要点帖出来:
请记住:
■ C++标准程序库的主要机能是由STL,iostreams,locales组成.并包含C99标准程序库.
■ TR1添加了智能指针(例如tr1::shared_ptr)、一般化函数指针(tr1::function)、hash-based容器、正则表达式(regular expressions)以及另外10个组件的支持.
■ TR1自身只是一份规范.为获得TR1提供的好处,你需要一份实物.一个好的实物来源是Boost.
条款55(最一款):让自己熟悉Boost
Familiarize yourself with Boost.
本款是本书的最后一款,主要介绍了Boost库.书里面介绍的很详细,我在这里就不想再次累赘介绍了,有兴趣的可以阅读原书的内容.我只把原书中关于本款要记住的要点帖出来.
请记住:
■ Boost是一个社群,也是一个网站.致力于免费、源码开发、同僚复审的C++程序库开发.Boost在C++标准化过程过程中扮演深具影响力的角色.
■ Boost提供许多TR1组件实现品,以及其他许多程序库.