effecttive c++ 笔记

1. 让自己习惯C++

条款01:视C++为一个语言联邦

为了更好的理解C++,我们将C++分解为四个主要次语言:

  • C。说到底C++仍是以C为基础。区块,语句,预处理器,内置数据类型,数组,指针统统来自C。
  • Object-Oreinted C++。这一部分是面向对象设计之古典守则在C++上的最直接实施。类,封装,继承,多态,virtual函数等等...
  • Template C++。这是C++泛型编程部分。
  • STL。STL是个template程序库。容器(containers),迭代器(iterators),算法(algorithms)以及函数对象(function objects)...

请记住:这四个次语言,当你从某个次语言切换到另一个,导致高效编程守则要求你改变策略。C++高效编程守则视状况而变化,取决于你使用C++的哪一部分。

条款02:尽量以const,enum,inline替换#define

这条可理解为**让编译器代替预处理器做更多的事情。

预处理过程扫描源代码,对其进行初步的转换,产生新的源代码提供给编译器。检查包含预处理指令的语句和宏定义,并对源代码进行相应的转换。预处理过程还会删除程序中的注释和多余的空白字符。可见预处理过程先于编译器对源代码进行处理。预处理指令是以#号开头的代码行。

例:#define ASPECT_RATIO 1.653
记号名称ASPECT_RATIO也许从未被编译器看见,也许在编译器开始处理源代码之前它就被预处理器移走了。即编译源代码时ASPECT_RATIO已被1.653取代。ASPECT_RATIO可能并未进入记号表(symbol table)。
替换:const double AspectRatio = 1.653;
好处应该有:多了类型检查,因为#define 只是单纯的替换,而这种替换在目标码中可能出现多份1.653;改用常量绝不会出现相同情况。

请记住:对于单纯常量,最好以const对象或enums替换#defines;对于形似函数的宏,最好改用inline函数替换#defines。

条款03:尽可能使用const

const的多才多艺:

  • 在classes外部修饰global或namespace作用域中的常量,或修饰文件、函数、或被声明为static的对象。
  • 修饰classes内部的static和non-static成员变量。
  • 可以指出指针自身所指事物或两者都(或都不)是const。
  • 在一个函数声明式内,const可以修饰函数返回值、各参数、函数自身(如果是成员函数)
char greeting[] = "Hello";        
char *p = greeting;    //指针p及所指的字符串都可改变;        
const char *p = greeting;    //指针p本身可以改变,如p = &Anyother;p所指的字符串不可改变;        
char * cosnt p = greeting;    //指针p不可改变,所指对象可改变;
const char * const p = greeting;    //指针p及所致对象都不可改变;

请记住:如果关键字const出现在星号左边,表示被指物事常量。const char *p和char const *p两种写法意义一样,都说明所致对象为常量;如果关键字const出现在星号右边,表示指针自身是常量。

将const实施于成员函数,是为了1)判断哪个函数可以改动对象内容而哪个函数不行,2)使操作const对象称为可能。

常量对象不能调用非常量成员函数,只能调用常量成员函数;常量成员函数不能改变对象的成员变量,除非该变量声明为mutable。

请记住:

  • 将某些东西声明为const可帮助编译器侦测出错误用法。const可被施加于任何作用域内的对象、函数参数、函数返回类型、成员函数本体;
  • 编译器强制实施bitwise constness,但你编写程序时应该使用“概念上的车辆”(conceptual constness);
  • 当cosnt和non-const成员函数有着实质等价的实现时,令non-const版本调用const版本可避免代码重复。

条款04:确定对象被使用前已先被初始化

永远在使用对象之前先将它初始化。对于无任何成员的内置类型,你必须手工完成此事。至于内置类型以外的任何其它东西,初始化责任落在构造函数身上,确保每一个构造函数都将对象的每一个成员初始化。
赋值和初始化:
C++规定,对象的成员变量的初始化动作发生在进入构造函数本体之前。所以应将成员变量的初始化置于构造函数的初始化列表中。

ABEntry::ABEntry(const std::string& name, const std::string& address,
                                     const std::list<PhoneNumber>& phones)     
{             theName = name;                //这些都是赋值,而非初始化
              theAddress = address;          //这些成员变量在进入函数体之前已调用默认构造函数,接着又调用赋值函数,
              thePhones = phones;            //即要经过两次的函数调用。
              numTimesConsulted = 0;    
}     
ABEntry::ABEntry(const std::string& name, const std::string& address,
                                    const std::list<PhoneNumber>& phones) :
        theName(name),                    //这些才是初始化
        theAddress(address),                //这些成员变量只用相应的值进行拷贝构造函数,所以通常效率更高。
        thePhones(phones), 
        numTimesConsulted(0)
 {    }     

所以,对于非内置类型变量的初始化应在初始化列表中完成,以提高效率。而对于内置类型对象,其初始化和赋值的成本相同,但为了一致性最好也通过成员初始化表来初始化。如果成员变量时const或reference,它们就一定需要初值,不能被赋值。
C++有着十分固定的“成员初始化次序”。基类总是在派生类之前被初始化,而类的成员变量总是以其说明次序被初始化。所以:当在成员初始化列表中列各成员时,最好总是以其声明次序为次序。
请记住:

  • 为内置对象进行手工初始化,因为C++不保证初始化它们;
  • 构造函数最好使用成员初始化列表,而不要在构造函数本体内使用赋值操作。初始化列表列出的成员变量,其排列次序应该和它们在类中的声明次序相同;
  • 为免除“跨编译单元之初始化次序”问题,请以local static对象替换non-local static对象。

2. 构造、析构、赋值运算

条款05:了解C++默默编写并调用哪些函数

如果你在类声明时没有声明拷贝构造函数、拷贝赋值操作符、析构函数、构造函数,编译器会自动为你提供一份声明。惟有当这些函数被需要(被调用),它们才会被编译器创建出来。

默认构造函数和析构函数主要是给编译器一个地方用来放置“藏身幕后”的代码,像是调用基类和非静态成员变量的构造函数和析构函数;至于拷贝构造函数和拷贝赋值操作符,编译器创建的版本只是单纯地将来源对象的每一个非静态成员变量拷贝到目标对象。

请记住:

  • 编译器可以暗自为类创建默认构造函数、拷贝构造函数、拷贝赋值操作符,以及析构函数。
  • 如一个类声明了一个构造函数(无论有没参数),编译器就不再为它创建默认构造函数。
  • 编译器生成的拷贝赋值操作符:对于成员变量中有指针,引用,常量类型,我们都应考虑建立自己“合适”的拷贝赋值操作符。因为指向同块内存的指针是个潜在危险,引用不可改变,常量不可改变。

条款06:若不想使用编译器自动生成的函数,就该明确拒绝

通常如果你不希望类支持某一特定技能,只要不说明对应函数就是了。但这个策略对拷贝构造函数和拷贝赋值操作符却不起作用。因为编译器会“自作多情”的声明它们,并在需要的时候调用它们。

由于编译器产生的函数都是public类型,因此可以将拷贝构造函数或拷贝赋值操作符声明为private,来阻止人们在外部调用它;但是类中的成员函数和友元函数还是可以调用private函数,解决方法可能是在一个专门为了阻止拷贝动作而设计的基类。(Boost提供的那个类名为noncopyable)。

条款07:为多态基类声明virtual析构函数

当基类的指针指向派生类的对象的时候,当我们使用完,对其调用delete的时候,其结果将是未有定义——基类成分通常会被销毁,而派生类的充分可能还留在堆里。这可是形成资源泄漏、败坏之数据结构、在调试器上消费许多时间。

消除以上问题的做法很简单:给基类一个virtual析构函数。此后删除派生类对象就会如你想要的那般。

任何类只要带有virtual函数都几乎确定应该也有一个virtual析构函数。如果一个类不含virtual函数,通常表示它并不意图被用做一个基类,当类不企图被当做基类的时候,令其析构函数为virtual往往是个馊主意。因为实现virtual函数,需要额外的开销(指向虚函数表的指针vptr)。

条款08:别让异常逃离析构函数

C++并不禁止析构函数吐出异常,但它不鼓励你这样做。C++不喜欢析构函数吐出异常。

如果可能导致异常:
如果抛出异常,就结束程序。(强迫结束程序是个合理选项,毕竟它可以阻止异常从析构函数传播出去。)
捕获异常,但什么也不做。

如果某个操作可能在失败时抛出异常,而又存在某种需要必须处理该异常,那么这个异常必须来自析构函数以外的某个函数。
请记住:

  • 析构函数绝对不要吐出异常。如果一个被析构函数调用的函数可能抛出异常,析构函数应该捕捉任何异常,然后吞下它们(不传播)或结束程序。
  • 如果客户需要对某个操作函数运行期间抛出的异常做出反应,那么类应该提供一个普通函数(而非在析构函数中)执行该操作。

条款09:决不在构造和析构过程中调用virtual函数

你不该在构造函数和析构函数中调用virtual函数,因为这样的调用不会带来你预想的结果。

基类的构造函数的执行要早于派生类的构造函数,当基类的构造函数执行时,派生类的成员变量尚未初始化。派生类的成员变量没初始化,即为指向虚函数表的指针vptr没被初始化又怎么去调用派生类的virtual函数呢?析构函数也相同,派生类先于基类被析构,又如何去找派生类相应的虚函数?

唯一好的做法是:确定你的构造函数和析构函数都没有调用虚函数,而它们调用的所有函数也不应该调用虚函数。

解决的方法可能是:既然你无法使用虚函数从基类向下调用,那么我们可以使派生类将必要的构造信息向上传递至基类构造函数。即在派生类的构造函数的成员初始化列表中显示调用相应基类构造函数,并传入所需传递信息。

条款10:令operator= 返回一个reference to *this

对于赋值操作符,我们常常要达到这种类似效果,即连续赋值:

int x, y, z;
x = y = z = 15;

为了实现“连锁赋值”,赋值操作符必须返回一个“引用”指向操作符的左侧实参。

Widget & operator = (const Widget &rhs)
{
    ...
    return *this;
}

所有内置类型和标准程序库提供的类型如string,vector,complex或即将提供的类型共同遵守。

条款11:在operator =中处理“自我赋值”

看下面的例子:

 Widget& Widget::operator=(const Widget& rhs) 
 {     
     delete pb;         //这里对pb指向内存对象进行delete,试想 *this == rhs?情况会如何
     pb = new Bitmap(*rhs.pb);     //如果*this == rhs,那么这里还能new吗?“大事不妙”。
     return *this; 
  }

也许以下代码能解决以上问题:

 Widget& Widget::operator=(const Widget& rhs)
 { 
     if (this == &rhs) 
     return *this;            //解决了自我赋值的问题。

     delete pb; 
     pb = new Bitmap(*rhs.pb); 
     return *this; 
 }

“许多时候一群精心安排的语句就可以导出异常安全(以及自我赋值安全)的代码”,以上代码同样存在异常安全问题。如果new Bitmap导致异常,Widget最终会指向一块被删除的Bitmap。

Widget& Widget::operator=(const Widget& rhs)
 { 
     Bitmap *pOrig = pb;                //记住原先的pb
     pb = new Bitmap(*rhs.pb);      //令pb指向*pb的一个复本
     delete pOrig;                           //删除原先的pb
     return *this;  //这样既解决了自我赋值,又解决了异常安全问题。自我赋值,将pb所指对象换了个存储地址。
 } 

条款12:复制对象时勿忘其每一个成员

如果你在类中添加一个成员变量,你必须同时修改相应的copying函数(所有的构造函数,拷贝构造函数以及拷贝赋值操作符)。在派生类的构造函数,拷贝构造函数和拷贝赋值操作符中应当显示调用基类相对应的函数,否则编译器可能又“自作聪明了”。

当你编写一个copying函数,请确保:

  • 复制所有local成员变量;
  • 调用所有基类内的适当copying函数。

但是,我们不该令拷贝赋值操作符调用拷贝构造函数,也不该令拷贝构造函数调用拷贝赋值操作符。想想,一个是拷贝(建立对象),一个是赋值(对象已经存在)


3. 资源管理

所谓资源就是,一旦用了它,将来必须还给系统。C++程序中最常使用的资源就好似动态分配内存(如果你new了,却忘了delete,会导致内存泄露),但内存只是你必须管理的众多资源之一。其它常见的有文件描述符(file descriptors)、互斥器(mutex)、图形界面中的字形和画刷。数据库连接以及网络sockets。当你不使用它们时,记得还给系统。

条款13:以对象管理资源

把资源放进对象内,我们便可依赖C++的“析构函数自动调用机制”确保资源被释放。

许多资源被动态分配于堆内而后被用于单一区块或函数内。它们应该在控制流离开那个区块或函数时被释放。标准程序库提供的auto_ptr正是针对这种形势而设计的特制产品。auto_ptr是个“类指针对象”,也就是所谓的“智能指针”,其析构函数自动对其所指对象调用delete。

void f()
 { 
     std::auto_ptr<Investment> pInv(createInvestment()); 
     ... 
 }          //函数退出,auto_ptr调用析构函数自动调用delete,删除pInv;无需显示调用delete。

“以对象管理资源”的两个关键想法:

  • 获得资源后立刻放进管理对象内(如auto_ptr)。每一笔资源都在获得的同时立刻被放进管理对象中。“资源取得时机便是初始化时机”(Resource Acquisition Is Initialization;RAII)。
  • 管理对象运用析构函数确保资源被释放。即一旦对象被销毁,其析构函数被自动调用来释放资源。

由于auto_ptr被销毁时会自动删除它所指之物,所以不能让多个auto_ptr同时指向同一对象。所以auto_ptr若通过copying函数复制它们,它们会变成NULL,而复制所得的指针将取得资源的唯一拥有权!

std::auto_ptr<Investment> pInv1(createInvestment()); //pInv1指向createInvestment()返回物;
std::auto_ptr<Investment> pInv2(pInv1);                      //现在pInv2指向对象,而pInv1被设为NULL;
pInv1 = pInv2;                         //现在pInv1指向对象,而pIn2被设为NULL;

受auto_ptr管理的资源必须绝对没有一个以上的auto_ptr同时指向它。即“有你没我,有我没你”。auto_ptr的替代方案是“引用计数型智能指针”(reference-counting smart pointer;SCSP)、它可以持续跟踪共有多少对象指向某笔资源,并在无人指向它时自动删除该资源。

TR1的tr1::shared_ptr就是一个"引用计数型智能指针"。

 void f()
 { 
     ... 
     std::tr1::shared_ptr<Investment>  pInv1(createInvestment()); //pInv1指向createInvestment()返回物;
     std::tr1::shared_ptr<Investment>  pInv2(pInv1);                     //pInv1,pInv2指向同一个对象;
     pInv1 = pInv2;                                      //同上,无变化
     ... 
 }         //函数退出,pInv1,pInv2被销毁,它们所指的对象也竟被自动释放。

auto_ptr和tr1::shared_ptr都在其析构函数内做delete而不是delete[],也就意味着在动态分配而得的数组身上使用auto_ptr或tr1::shared_ptr是个潜在危险,资源得不到释放。也许boost::scoped_array和boost::shared_array能提供帮助。还有,vector和string几乎总是可以取代动态分配而得的数组。

请记住:

  • 为防止资源泄漏,请使用RAII对象,它们在构造函数中获得资源并在析构函数中释放资源。
  • 两个常被使用的RAII类分别是auto_ptr和tr1::shared_ptr。后者通常是较佳选择,因为其拷贝行为比较直观。若选择auto_ptr,复制动作会使他(被复制物)指向NULL。

条款14:在资源管理类中小心拷贝行为

我们在条款13中讨论的资源表现在堆上申请的资源,而有些资源并不适合被auto_ptr和tr1::shared_ptr所管理。可能我们需要建立自己的资源管理类。

例:假设我们使用Mutex类型的互斥器对象,共有lock和unlock两函数可用

void lock(Mutex *pm);     //锁定pm所指的互斥量     
unlock(Mutex *pm);        //将pm解除锁定

我们建立的资源管理类可能会是这样:

 class Lock 
{
    public: 
    explicit Lock(Mutex *pm)
    : mutexPtr(pm) 
    {
         lock(mutexPtr); 
    } 
    ~Lock() 
    { 
         unlock(mutexPtr); 
    } 
    private: 
    Mutex *mutexPtr; 
}; 

“当一个RAII对象被复制,会发生什么事?”大多数时候你会选择一下两种可能:

  • 禁止复制。如果复制动作对RAII类并不合理,你便应该禁止之。禁止复制类的copying函数参见条款6。
  • 对底层资源使用”引用计数法“。有时候我们又希望保有资源,直到它的最后一个使用者被销毁。这种情况下复制RAII对象时,应该将资源的”被引用计数“递增。tr1::shared_ptr便是如此。通常只要内含一个tr1::shared_ptr成员变量,RAII类便可实现”引用计数“行为。

条款15:在资源管理类中提供对原始资源的访问

前几个条款提到的资源管理类很棒。它们是你对抗资源泄漏的堡垒。但这个世界并不完美,许多APIs直接指涉资源,这时候我们需要直接访问原始资源。这时候需要一个函数可将RAII对象(如tr1::shared_ptr)转换为其所内含之原始资源。有两种做法可以达成目标:显示转换和隐式转换。

tr1::shared_ptr和auto_ptr都提供一个get成员函数,用来执行显示转换,也就是返回智能指针内部的原始指针(的复件)。就像所有智能指针一样, tr1::shared_ptr和auto_ptr也重载了指针取值操作符(operator->和operator),它们允许隐式转换至底部原始指针。(即在对智能指针对象实施->和操作时,实际被转换为被封装的资源的指针。)

class Font 
{
    public: 
    ... 
    FontHandle get() const         //FontHandle 是资源;    显示转换函数
    { 
        return f; 
    }
    operator FontHandle() const         //隐式转换    这个值得注意,可能引起“非故意之类型转换”
    { 
        return f; 
    } 
    ... 
}; 

是否该提供一个显示转换函数(例如get成员函数)将RAII类转换为其底部资源,或是应该提供隐式转换,答案主要取决于RAII类被设计执行的特定工作,以及它被使用的情况。显示转换可能是比较受欢迎的路子,但是需要不停的get,get;而隐式转换又可能引起“非故意之类型转换”。

条款16:成对使用new和delete时要采取相同形式

当我们使用new,有两件事情发生:第一,内存被分配出来;第二,针对此内存会有一个(或更多)构造函数被调用。当你使用delete,也有两件事发生:针对此内存会有一个(或多个)析构函数被调用,然后内存才被释放。delete的最大问题在于:即将被删除的内存之内究竟有多少对象?这个问题的答案决定了有多少个析构函数必须被调用起来。

解决以上问题事实上很简单:如果你调用new时使用[],你必须在对应调用delete时也使用[]。如果你调用new时没有使用[],那么也不该在对应调用delete时使用[]。

条款17:以独立语句将newed对象置入智能指针

为了避免资源泄漏的危险,最好在单独语句内以智能指针存储newed所得对象。

int priority();
void processWidget(std::tr1::shared_ptr<Widget> pw, int priority);

std::tr1::shared_ptr<Widget> pw(new Widget);    //即在传入函数之前对智能指针初始化,而不是在传入参数中
 //对其初始化,因为那样可能引起操作序列的问题。
processWidget(pw, priority()); 

请记住:以独立语句将newed对象存储于(置入)智能指针内。如果不这样做,一旦异常抛出,有可能导致难以察觉的资源泄漏。


4. 设计与声明

所谓软件设计,是“令软件做出你希望它做的事情”的步骤和做法,通常以颇为一般性的构想开始,最终变成十足的细节,以允许特殊接口的开发。

条款18:让接口容易被正确使用,不易被误用

  • 好的接口很容易被正确使用,不容易被误用。你应该在你的所有接口中努力达成这些性质。
  • “促进正确使用”的办法包括接口的一致性,以及与内置类型的行为兼容。
  • “阻止误用”的办法包括建立新类型、限制类型上的操作,束缚对象值,以及消除客户的资源管理责任。
  • tr1::shared_ptr支持定制删除器。这可防范DLL问题,可被用来自动解除互斥量等等。

条款19:设计class犹如设计type

设计一个良好的类,或者称作类型,考虑一下设计规范:

  • 新类型的对象应该如何被创建和销毁?
  • 对象的初始化和对象的赋值该有什么样的差别?
  • 新类型的对象如果被passed by value(值传递),意味着什么?
  • 什么是新类型的“合法值”?
  • 你的新类型需要配合某个继承图系吗?
  • 你的新类型需要什么样的转换?
  • 什么样的操作符和函数对此新类型而言是合理的?
  • 什么样的标准函数应该驳回?
  • 谁该取用新类型的成员?
  • 什么是新类型的“未声明接口”?
  • 你的新类型有多么一般化?
  • 你真的需要一个新类型吗?

条款20:宁以pass-by-reference-to-const替代psss-by-value

缺省情况下C++以by value方式传递对象至函数。除非你另外指定,否则函数参数都是以实际实参的副本为初值,而调用端所获得的亦是返回值的一个副本。这些副本由对象的拷贝构造函数产生。所以在以对象为by value时,可能会调用相应的构造函数(成员对象的构造、基类对象的构造),然后调用对应的析构函数。所以以by value的形式开销还是比较大的。

如果我们用pass-by-reference-to-const,例如:bool validateStudent(const Student& s);,这种传递方式效率高得多:没有任何构造函数或析构函数被调用,因为没有任何新对象被创建。以传引用方式传递参数也可以避免对象切割问题:即当一个派生类对象以传值的方式传递并被视为一个基类对象,基类对象的拷贝构造函数会被调用,而“造成此对象的行为像个派生类对象”的那些特化性质全被切割掉了,仅仅留下了基类对象。这一般不是你想要的。

所以我们一般的做法应该是这样:内置对象和STL的迭代器和函数对象,我们一般以传值的方式传递,而其它的任何东西都以传引用的方式传递。

条款21:必须返回对象时,别妄想返回其reference

  • 不要返回一个指向局部变量的指针或引用,因为局部变量在函数返回后就没有存在的意义。
  • 不要返回一个在堆上对象的引用,这会埋下了资源泄漏的危险。谁该对这对象实施delete呢?别把这种对资源的管理寄托完全寄托于用户。
  • 不要返回一个被定义于函数内部的静态对象的引用。
  • “必须返回新对象”的函数的正确写法是:就让那个函数返回一个新对象。

条款22:将成员变量声明为private

将成员变量隐藏在函数接口的背后,可以为“所有可能的实现”提供弹性。例如,这可使得成员变量被读或写时轻松通知其它对象、可以验证calss的约束条件以及函数的前提和事后状态、可以在多线程环境中执行同步控制......

不封装意味不可改变!成员变量的封装性与“成员变量的内容改变时所坏量的代码数量”成反比。

条款23:宁以non-member、non-friend替换member函数

宁可拿non-member non-friend函数替代member函数。这样做可以增加封装性、包裹弹性和机能扩充性。

一般我们相当然以为类中的成员函数更具封装性,而实际上并不是那么一回事,因为成员函数不仅可以访问private成员变量,也可以取用private函数、enums、typedefs等等。而非成员非友元函数能实现更大的封装性,因为它只能访问public函数。

将所有便利函数放在多个头文件内但隶属同一个命名空间,意味客户可以轻松扩展这一组便利函数。需要做的就是添加更多non-member non-friend函数到此命名空间内。

条款24:若所有参数皆需类型转换,请为此采用non-member函数

通常,令类支持隐式类型转换通常是个糟糕的主意。当然这条规则有其例外,最常见的例外是在建立数值类型时。

const Rational operator*(const Rational& rhs) const; 
//如果定义一个有理数类,并实现*操作符为成员函数,如上所示;那么考虑一下调用:
Rational oneHalf(1, 2); 
result = oneHalf * 2; // 正确,2被隐式转换为Rational(2,1)
                             //编译器眼中应该是这样:const Rational temp(2); result = oneHalf * temp; 
result = 2 * oneHalf; // 错误,2,可不被认为是Rational对象;因此无法调用operator*

可见,这样并不准确,因为乘法应该满足交换律,不是吗?所以,支持混合式算术运算的可行之道应该是:让operator*成为一个non-member函数,允许编译器在每一个实参上执行隐式类型转换:

class Rational 
{
... // contains no operator* 
}; 
const Rational operator*(const Rational& lhs,  Rational& rhs)
{ 
return Rational(lhs.numerator() * rhs.numerator(), 
                        lhs.denominator() * rhs.denominator()); 
} 
Rational oneFourth(1, 4); 
Rational result; 
result = oneFourth * 2; 
result = 2 * oneFourth;  //这下两个都工作的很好,通过隐式转换实现

成员函数的方面是非成员函数,而不是友元函数。可以用类中的public接口实现的函数,最好就是非成员函数,而不是采用友元函数。
请记住:如果你需要为某个函数的所有参数(包括被this指针所指的那个隐喻参数)进行类型转换,那么这个函数必须是个non-member。

条款25:考虑写出一个不抛异常的swap函数

  • 当std::swap对你的类型效率不高时,提供一个swap成员函数,并确定这个函数不抛出异常。
  • 如果你提供一个member swap,也该提供一个non-member swap用来调用前者。对于class(而非templates),也请特化std::swap。
  • 调用swap时应针对std::swap使用using声明式,然后调用swap并且不带任何“命名空间资格修饰”。
  • 为“用户定义类型”进行std templates全特化是好的,但千万不要尝试在std内加入某些对std而言全新的东西。

5. 实现

大多数情况下,适当提出你的类定义及函数声明,是花费最多心力的两件事。尽管如此,还是有很多东西需要小心:太快定义变量可能造成效率上的拖延;过度使用转型(casts)可能导致代码变慢又难维护,又招来微妙难解的错误;返回对象“内部数据之号码牌(handls)”可能会破坏封装并留给客户虚吊号码牌;为考虑异常带来的冲击则可能导致资源泄漏和数据败坏;过度使用inline function可能引起代码膨胀;过度耦合则可能导致让人不满意的冗长build times。

条款26:尽可能延后变量定义式的出现时间

只要你定义了一个变量而其类型带有一个构造函数或析构函数,那么当程序的控制流到达这个变量定义式时,你便得承受构造成本;当这个变量离开其作用域时,你便得承受析构成本。即使这个变量最终并未被使用,仍需耗费这些成本,所以应该尽量避免这种情形。因此,最好延后该变量的定义式,直到非得使用该变量的前一刻为止,甚至应该尝试延后这份定义直到能够给它初值实参为止。

std::string encryptPassword(const std::string& password)
{ 
    using namespace std; 
    string encrypted;  //1
    if (password.length() < MinimumPasswordLength) 
    { 
        throw logic_error("Password is too short");     //注意:可能抛出异常
    }
    string encrypted; //2
     ... 
    return encrypted; 
 } 

如上代码,encrypted在2处定义是个不错的选择,因为如果抛出异常,那么encrypted的构造和析构可是做了无用功啊!

条款27:尽量少做转型动作

C++规则的设计目标之一是,保证“类型错误”绝不可能发生。不幸的是,转型(casts)破坏了类型系统。那可能导致任何种类的麻烦,有些容易辨识,有些非常隐晦。
两种旧式转型:

 (T)expression    //C风格的转型动作
 T(expression)    //函数风格的转型动作

C++还提供四种新式转型:

  • const_cast:通常被用来将对象的常量性转除;即去掉const。
  • dynamic_cast:主要用来执行“安全向下转型”,也就是用来决定某对象是否归属继承体系中的某个类型。
  • reinterpret_cast:意图执行低级转型,实际动作可能取决于编译器,这也就表示它不可移植。
  • static_cast:用来强迫隐式转换,例如将non-const转型为const,int转型为double等等。

尽量使用新式转型:它们很容易在代码中被辨识出来,因而得以简化“找出类型系统在哪个地点被破坏”的过程;各转型动作的目标愈窄化,编译器愈可能诊断出错误的运用。

请记住:

  • 如果可以,尽量避免转型,特别是在注重效率的代码中避免dynamic_casts。如果有个设计需要转型动作,试着发展无需转型的替代设计。
  • 如果转型是必要的,试着将它隐藏于某个函数背后。客户随后可以调用该函数,而不需将转型放进他们自己的代码内。
  • 宁可使用C++-style(新式)转型,不要使用旧式转型。前者很容易辨识出来,而且也比较有着分门别类的执掌。

条款28:避免返回handls指向对象内部成分

struct RectData 
 { 
    Point ulhc; 
    Point lrhc; 
 }; 
class Rectangle 
{
    public: 
        ... 
        Point& upperLeft() const { return pData->ulhc; }//1  const只对函数内进行保护,函数返回后呢??
        Point& lowerRight() const { return pData->lrhc; } //2  const只对函数内进行保护,函数返回后呢??
    private: 
        std::tr1::shared_ptr<RectData> pData; 
        ... 
}; 

1,2两函数都返回引用,指向private内部数据,调用者于是可通过这些引用更改内部数据!这严重破坏了数据的封装性,对私有成员进行直接操作?太不可思议了!

const Point& upperLeft() const { return pData->ulhc; } //3
const Point& lowerRight() const { return pData->lrhc; } //4     

或者将1,2改为3,4,这就限制了客户的“涂改权”,只有“读取权”。但终究“返回一个handle代表对象内部成分”总是危险的。特别是将返回的指针或引用赋值给其它指针或引用,那么久造成了“悬空”。

请记住:避免返回handles(包括reference、指针、迭代器)指向对象内部。遵守这个条款可增加封装性,帮助const成员函数的行为像个const,并将发生“虚吊号码牌”(dangling handles)的可能性降至最低。

条款29:为“异常安全”而努力是值得的

  • 异常安全函数(Exception-safe functions)即使发生异常也不会泄漏资源或允许任何数据结构败坏。这样的函数区分为三种可能的保证:基本型、强烈型、不抛异常型。
  • “强烈保证”往往能够以copy-and-swap实现出来,但“强烈保证”并非对所有函数都可实现或具备现实意义。
  • 函数提供的“异常安全保证”通常最高只等于其所调用之各个函数的“异常安全保证”中的最弱者。

条款30:透彻了解inlining的里里外外

Inline函数看起来像函数,动作像函数,比宏好得多,可以调用它们又不需蒙受函数调用所招致的额外开销。你实际获得的比想象的还多,编译器有能力对执行语境相关最优化。然而编写程序就像现实生活一样,没有白吃的午餐。inline函数也不例外,这样做可能增加你的目标码。

如果inline函数的本体很小,编译器针对“函数本体”所产生的码可能比针对“函数调用”所产出的码更小。果真如此,将函数inlining确实可能导致较小的目标码和较高的指令高速缓存装置击中率。

Inline函数通常一定被置于头文件内,因为大多数建置环境在编译过程中进行inlining,而为了将一个“函数调用”替换为“被调用函数的本体”,编译器必须知道那个函数长什么样子。Template通常也被置于头文件内,因为它一旦被使用,编译器为了将它具现化,需要知道哦啊它长什么样子。Template的具现化与inlining无关。如果你正在写一个template而你认为所有根据此template具现出来的函数都应该inlined,请将此template声明为inline;但如果你写的template煤油理由要求它所具现的每一个函数都是inlined,就应该避免将这个template声明为inline。

一个表面上看似inline的函数是否真实inline,主要取决于编译器。有时候虽然编译器有意愿inlining某个函数,还是可能为该函数生成一个函数本体(函数指针,构造函数,析构函数)。对程序开发而言,将上述所有考虑牢记在新很是重要,但若从纯粹实用观点出发,有一个事实比其它因素更重要:大部分调试器面对inline函数都束手无策。

这使我们在决定哪些函数该被声明为inline而哪些函数不该时,掌握一个合乎逻辑的策略。一开始先不要将任何函数声明为inline,或至少将inlining施行范围局限在那些“一定成为inline”或“十分平淡无奇”的函数身上。

条款31:将文件间的编译依存关系降至最低


6 继承与面对对象设计

条款32:确定你的public继承塑模出is-a关系

以C++进行面向对象编程,最重要的一个规则是:public inheritance(公有继承)意味is-a(是一种)的关系。如果你令class D以public形式继承class B,你便是告诉C++编译器(以及你的代码读者)说,每一个类型为D的对象同时也是一个类型为B的对象,反之不成立。B对象可派上用场的任何地方,D对象一样可以派上用场。

在C++领域中,任何函数如果期望获得一个类型为基类的实参(而不管是传指针或是引用),都也愿意接受一个派生类对象(而不管是传指针或是引用)。(只对public继承才成立。)

条款33:避免遮掩继承而来的名称

C++的名称遮掩规则所做的唯一事情就是:遮掩名称。至于名称的类型是否相同并不重要。即只要名称相同就覆盖基类相应的成员,类型和参数个数都无关紧要。

派生类的作用域嵌套在基类的作用域内。C++的继承关系的遮掩名称也并不管成员函数是纯虚函数,非纯虚函数或非虚函数等。只和名称有关。 如果你真的需要用到基类的被名称遮掩的函数,可以使用using声明式或转交函数(forwarding function),引入基类的成员函数。若是基类中存在函数重载的情况,在派生类中应注意同名函数覆盖的问题!

条款34:区分接口继承和实现继承

表面上直截了当的public继承概念,经过更严密的检查之后,发现它由两部分组成:函数接口继承和函数实现继承。

  • 成员函数的接口总是会被继承。
  • 声明一个纯虚函数的目的是为了让派生类只继承函数接口。
  • 声明一个虚函数的目的是让派生类继承该函数的接口和缺省实现。
  • 声明一个非虚函数的目的是为了令派生类继承函数的接口及一份强制性实现。

由于不同类型的声明意味着本质并不相同的事情,当你声明你的函数成员时,必须谨慎选择。如果将所以函数都声明为非虚函数,这将使得派生类没有余裕空间进行特化工作,而基类的非虚析构函数尤其会带来问题,实际上任何被当作基类来使用的类都会拥有若干虚函数。将所有成员函数声明为虚函数也是一种常见的错误,当然有时也是正确的,如果你的不变性凌驾于特异性,别害怕说出来。

条款35:考虑virtual函数以外的其它选择

【C++】考虑virtual函数以外的其他选择

条款36:绝不重新定义继承而来的non-virtual函数

假设基类B中有一个非虚函数mf,派生类D在继承实现时定义了自己的mf版本,将会导致基类的mf被隐藏。如果定义一个基类指针指向一个派生类对象并调用mf函数时,其结果将会调用基类版本的mf函数,而不是派生类版本的mf函数!这与虚函数的动态绑定并不一样,非虚函数是静态绑定的。

条款37:绝不重新定义继承而来的缺省参数值

对于non-virtual函数,上一条款说到,“绝不重新定义继承而来的non-virtual函数”,而对于继承一个带有缺省参数值的virtual函数,也是如此。即绝不重新定义继承而来的缺省参数值。因为:virtual函数是动态绑定,而缺省参数值却是静态绑定。

一个指向派生类的基类指针,如果两者的一个虚函数指定了不同的缺省参数值 ,那么,利用基类指针调用派生类虚函数值时,其缺省参数值将是基类指定的!这是因为缺省参数值却是静态绑定,而该指针是一个基类对象。

条款38:通过复合塑模出has-a或“根据某物实现出”

复合(composition)的意义和public继承(is-a)完全不同。当你的对象是你所塑造的世界中的某些事物,复合意味着has-a的关系,如人拥有名字、地址、电话等对象;当你的对象纯粹是实现细节上的人工制品(如链表、集合、互斥器等),复合则是表现的“由某某实现出”的关系,比如由链表实现出集合。

条款39、40:明智而审慎地使用私有继承和多重继承

私有继承意味着“由某物实现出”的关系,但是实现中,尽可能使用复合而不是私有继承来实现这一关系。

推荐阅读更多精彩内容