Effective C++读书笔记

狂风中的少年 提交于 2020-11-13 11:44:27

让自己习惯C++

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

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

(宁可以编译器替换预处理器)

#define在预处理中处理,宏不会被记号表(symbol table)记录

关于const

  • 常量指针的定义需要将指针声明为const,因为通常常量指针在头文件中可以被其他源文件使用,const两次

const char* const authorname = “Scott Meyers”

  • class专属常量,#define并不重视作用域,也不具有封装性

为了将常量作用域(scope)限制于class内,必须让他称为类的成员,且为了让他只有一份,必须让他成为static成员:

class CostEstimate{

  private:

         static const double FudgeFactor; //类内声明

}

const double CostEstimate::FudgeFactor == 1.35; //类外实现

关于enum hack:

class GamePlayer{

private:

       static const int NumTurn = 5;

       int scores[NumTurm];

}

当编译器不支持static const int NumTurm = 5;时,使用enum { NumTurm = 5};替换

  • enum和#define编译器不会分配内存,而const有些编译器会,避免使用指针取常量值

关于inline

       define函数容易造成误用,例如

#define MAX(a, b) a>b?a:b

int a = 5, b = 0;

MAX(++a, b) // ++a调用两次

MAX(++a, b+10) // ++a调用一次

Note:

       对于单纯常量,最好以const对象或enums 替换#define

       对于形似函数的宏,最好使用inline函数替换#define

条款03:尽可能使用const

  • const声明迭代器相当于声明指针本身不可变,声明迭代器所指东西不可变用const_iterator
  • 令函数返回一个常量值,可以降低因使用错误而造成的意外

如自定义operate*

class Rational {…};

const Rational operate*(const Rational& lhs, const Rational& rhs);

If (a*b = c) …

将==误写成=造成的错误

  • const成员函数  (待解决

const 成员函数可以使用类中的所有成员变量,但是不能修改它们的值,这种措施主要还是为了保护数据而设置的。

    1. 使class接口容易理解,哪些可以改动对象内容,哪些不行
    2. 使操作const对象成为可能
      1. bitwise constness:成员函数不可以更改对象内任何non-static成员变量
      2. logical constness:添加mutable释放掉non-static成员变量的bitwise-constness约束
  • 在const和non-const成员函数中避免重复

当const和non-const成员函数有着实质等价实现时,令non-const版本调用const版本可避免代码重复。

Note:

       将某些东西声明为const可帮助编译器侦测出错误用法,const可被施加于任何作用域内的对象、函数参数、函数返回类型、成员函数本体。

       编译器强制实施bitwise constness,但你编写程序时应该使用“概念上的常量性”

       当const和non-const成员函数有着实质等价实现时,令non-const版本调用const版本可避免代码重复。

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

class PhoneNumber{…};

class ABEntry{

       public:

              ABEntry(const std::string& name, const std::string& address, const std::list<PhoneNumber>& phone);

private:

       std::string theName;

       std::string theAddress;

       std::list<PhoneNumber> thePhone;

       int numTimesConsulted;

};

 

ABEntry::ABEntry(const std::string& name, const std::string&address, const std::list<PhoneNumber>& phone)

{

       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) {}  //初始化

无参构造也可以使用成员初值列进行初始化

 

对于non-local static对象,不能确定其初始化顺序,如以下两个类的调用

class FileSystem {…};

class Directory {…};

使用

FileSystem tfs()

{

       Static FileSytem fs;

       Return fs;

}

Directory::Directory (params)

{

       Std::size_t disks = tfs().numDisks();

}

而不是

Extern FileSystem tfs;

Directory::Directory (params)

{

       Std::size_t disks = tfs.numDisks();

}

Note:

为内置型对象进行手工初始化,因为C++不保证初始化他们

构造函数最好使用成员初始列,而不要在构造函数本体内使用赋值操作,初值列列出的成员变量,其排列次序应该和他们在class中的声明次序相同

为避免跨编译单元初始化次序问题,以local static对象替换non-local static对象。

构造/析构/赋值运算

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

编译器会自动声明一个默认构造函数,一个拷贝构造函数,一个拷贝赋值操作符,一个析构函数(non virtual)

当这些函数被调用时,他们才会被编译器创建出来

class Empty{};

 

Empty el;       //默认构造函数

                     //析构函数

Empty e2(e1);//拷贝构造函数

e2 = e1; //拷贝赋值操作符

如果类自己构造了带参数的构造函数,编译器不会产生默认构造函数

当含有引用成员变量或者const成员变量时,编译器不会产生赋值操作符

当基类将拷贝赋值操作符声明为private,编译器将拒绝为派生类生成一个拷贝赋值操作符。

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

将拷贝构造函数和拷贝赋值操作符声明为private且不去定义实现他,将不会被调用,但是可以被类的成员函数和友元函数访问。

class HomeForSale {

public:

private:

       HomeForSale(const HomeForSale&);  //只有声明

       HomeForSale& operator=(const HomeForSale&);

};

HomeForSale h1;

HomeForSale h2;

HomeForSale h3(h1); //编译不通过

h1 = h2;    //编译不通过

为求HomeForSale对象被拷贝,可以继承Uncopyable

class Uncopyable{

       protected:

       Uncopyable(){};

       ~Uncopyable(){};

private:

       Uncopyable(const Uncopyable&);

       Uncopyable& operator = (const Uncopyable&);

}

 

class HomeForSale:private Uncopyable{…};  //此时member函数和friend函数都会被编译器拒绝

note:

为驳回编译器自动提供的机能,可将相应的成员函数声明为private并且不予实现。使用像Uncopyable这样的base class也是一种做法。

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

#include <iostream>

using namespace std;

class CShape  //基类

{

public:

    ~CShape() { cout << "CShape::destrutor" << endl; }

};

class CRectangle : public CShape  //派生类

{

public:

    int w, h;  //宽度和高度

    ~CRectangle() { cout << "CRectangle::destrutor" << endl; }

};

int main()

{

    CShape* p = new CRectangle;

    delete p;

    return 0;

}

delete p;只引发了 CShape 类的析构函数被调用,没有引发 CRectangle 类的析构函数被调用。这是因为该语句是静态联编的,编译器编译到此时,不可能知道此时 p 到底指向哪个类型的对象,它只根据 p 的类型是 CShape * 来决定应该调用 CShape 类的析构函数。

虚析构函数的原理分析

由于基类的析构函数为虚函数,所以派生类会在所有属性的前面形成虚表,而虚表内部存储的就是基类的虚函数。

当delete基类的指针时,由于派生类的析构函数与基类的析构函数构成多态,所以得先调动派生类的析构函数;之所以再调动基类的析构函数,是因为delete的机制所引起的,delete 基类指针所指的空间,要调用基类的析构函数。

Note:

如果class带有任何virtual函数,就应该拥有一个virtual析构函数。

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

Note:

析构函数绝对不要吐出异常。如果一个被析构函数调用的函数可能抛出异常,析构函数应该捕捉任何异常,然后吞下他们(不传播)或结束程序。

如果客户需要对某个操作函数运行期间抛出的异常做出反应,那么class应该提供一个普通函数(而不是在析构函数中)执行该操作。

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

Class Transaction{

public:

       Transaction();

       virtual void logTransaction() const = 0;

}

Transaction::Transaction()

{

       logTransaction();

}

Class BuyTransaction:public Transaction{

public:

       virtual void logTransaction() const;

};

BuyTransaction b; //此时base class构造函数会首先调用虚函数,引发与意图不同的矛盾

Note:

在构造和析构期间不要调用virtual函数,因为这类调用从不下降至derived class(比起当前执行构造函数和析构函数的那层)

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

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;

}

};

Note:

令赋值(assignment)操作符返回一个reference to *this, 这只是一个协议,并无强制性。如果不遵循它,代码一样可通过编译。

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

Class Bitmap{…};

Class Widget{

Private:

       Bitmap* pd;

};

如上假设建立一个class用来保存一个指针指向一块动态分配的位图(bitmap)

Widget& Widget::operator=(const Widget& rhs)//一份不安全的operator=实现版本

{

       Delete pb;

       pb = new Bitmap(*rhs.pb);

       return *this;

}

这里的问题是,operator=函数内的*this和rhs有可能是同一个对象,此时delete就不只是销毁当前对象的bitmap,也销毁rhs的bitmap。函数发生了自己持有一个指针指向一个已被删除的对象。

欲阻止错误,可通过认同测试达到自我赋值检验的目的:

Widget& Widget::operator=(const Widget& rhs)

{

       If (this = &rhs) return *this;

       delete pb;

       pb = new Bitmap(*rhs.pb);

       return *this;

}

但是仍然存在异常方面的麻烦,如果new bitmap导致异常,widget最终会持有一个指针指向一块被删除的bitmap。解决这种只需要在复制pb所指东西之前别删除pb;

Widget& Widget::operator=(const Widget& rhs)

{

       Bitmap* pOrig = pb;

       pb = new Bitmap(*rhs.pb);

       delete pOrig;

       return *this;

}

如果new bitmap抛出异常,pb保持原状,因为我们对原bitmap做了一份复件,删除原bitmap、然后指向新制造的那个复件。

另一个替代方案 copy and swap

class Widget{

void swap(Widget& rhs); //交换*this和rhs数据

}

Widget& Widget::operator=(const Widget& rhs)

{

       Widget temp(rhs);

       Swap(temp);

       Return *this;

}

Note:

确保对象自我赋值时operator=有良好的行为。其中可使用的方法包括比较“来源对象”和“目标对象”的地址、精心周到的语句顺序、以及copy and swap

确定任何函数如果擦欧一个以上的对象,而其中多个对象是同一个对象时,其行为仍然正确。

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

当编写一个copying函数,确保(1)复制所有的local成员变量,(2)调用所有的base classes内的适当的copying函数。

如果发现copy构造函数和copy assignment操作符有相近的代码,消除重复代码的做法是建立一个新的成员函数给两者调用,而不要相互调用。

Note:

拷贝构造函数应该确保复制对象内的所有成员变量以及所有基类成分

不要尝试以某个拷贝构造函数实现另一个拷贝构造函数,应该将共同机能放进第三个函数中,并由两个拷贝构造函数共同调用。

资源管理

条款13:以对象管理资源

class Investment{…}; // root class

Investment* createInvestment(); //通过一个工厂函数返回特定的Ivestment对象

void f()

{

       Investment* pInv = createInvestment();//调用factory函数

       …

       delete pInv; //释放pInv所指对象

}

当在…区域内过早return或者抛出异常,或delete在循环内,过早continue,都会不被释放。

使用RAII管理对象,避免资源泄露的可能性。

Void f()

{

       Std::auto_ptr(Investment) pInv(createInvestment());

       …   // 调用factory函数,一如既往的使用pInv,经由auto_ptr的析构函数自动删除pInv

}

注意: 不要让多个auto_ptr同时指向同一对象,如果这样对象可能被删除一次以上或出现未定义。为了预防这个问题,通过auto_ptr的copy构造函数或copy assignment操作符复制他们会变成null,而复制所得的指针将取得资源的唯一拥有权。

Std:auto_ptr<Investment> pInv1(creatInvestment());

Std:auto_ptr<Investment> pInv2(pInv1); // pInv2指向对象, pInv1被设置为NULL

pInv1 = pInv2;  // pInv1指向对象,pInv2被设置为null

shared_ptr是引用计数型智慧指针(reference-counting smart pointer,RCSP),持续追踪共有多少对象指向某笔资源,并在无人指向它时自动删除该资源。、

void f()

{

       …

       Std::tr1::shared_ptr<Investment> pInv1(createInvestment()); 

       Std::tr1::shared_ptr<Investment> pInv2(pInv1); //pInv1 和 pInv2指向同一对象

       pInv1 = pInv2; //  同上,无任何改变

       …    //pInv1和pInv2被销毁,他们所指的对象也就被自动销毁

}

Note:

为防止资源泄露,请使用RAII对象,他们在构造函数中获得资源并在析构函数中释放资源

两个常被使用的RAII class分别是tr1::shared_ptr和auto_ptr。前者通常是叫价选择,因为其copy行为比较直观。若选择auto_ptr,复制动作会使它(被复制物)指向null。

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

规则1. 禁止复制 依据条款06,使用uncopyable{};类

假设有两个函数实现上锁解锁

void lock(Mutex* pm);

void unlock(Mutex* pm);

建立一个class用来管理锁,在构造期间获得,在析构期间释放

Class Lock{

Public:

       Explicit Lock(Mutex* pm):mutexPtr(pm) {lock(mutexPtr);}

~Lock(){unlock(mutexPtr};)

Private:

       Mutex* mutexPtr;

}

当使用Lock时符合RAII:

Mutex m; //定义你需要的互斥锁

{Lock m1(&m);

… //在区块末尾,自动解除锁

}

但是当Lock对象被复制时

Lock ml1(&m); // 锁定m

Lock ml2(ml1); // 将ml1复制到ml2上,发生什么事?(复制出的锁不被释放?)

规则2:对底层资源使用引用计数法(????????)

我们希望当计数器为0时,而不删除对象

借助shared_ptr的计数器及删除器

Class Lock

{

       Public:

              explicit Lock(Mutex* pm) :mutexPtr(pm, unlock) //以某个Mutex初始化shared_ptr,并以unlock函数为删除器

       {

              Lock(mutexPtr.get());

}

       Private:

       Std::tr1::shared_ptr<Mutex> mutexPtr; //使用shared_ptr替换raw pointer

}

本例的Lock class不在声明析构函数,因为没有必要。条款5说过,class析构函数(无论是编译器生成的,或用户自定的)会自动调用其non-static成员变量(本例为mutexPtr)的析构函数。而mutexPtr的析构函数会在互斥器的引用次数为0时自动调用tr1::shared_ptr的删除器(本例为unlock)

规则3:复制底部资源

资源管理类中使用深度拷贝

规则4:转移底部资源的拥有权

Copying函数有可能被编译器自动创建出来,因此除非编译器所生版本做你了想要的,否则你得自己编写他们。

Note:

复制RAII对象必须一并复制它所管理的资源,所以资源的copying行为决定RAII对象的copying行为。

普遍而常见的RAII class copying行为是:抑制copying、施行引用计数法。不过其他行为也都可能被实现。

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

Note:

APIs往往要求访问原始资源(raw resource),所以每一个RAII class应该提供一个“取得其所管理的资源”的方法

对原始资源的访问可能经由显示转换或隐式转换。一般而言显示转换比较安全,但隐式转换对客户比较方便。

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

Note:

如果你在new表达式中使用[],必须在响应的delete表达式中也使用[]。如果你在new表达式中不使用[],一定不要在相应的delete表达式中使用[].

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

假设执行processWidget(std::tr1::shared_ptr<Widget>(new Widget), priority());

编译器会不按照预想顺序,假设

  1. 执行“new Widget”
  2. 调用priority
  3. 调用tr1::shared_ptr构造函数

当priority调用异常时,new Widget出来的指针会遗失,导致资源泄露。

避免这类问题的办法,分步操作

Std::tr1::shared_ptr<Widget>pw (new Widget); //在单独语句内以智能指针存储newed所得对象

processWidget(pw, priority()); // 这个调用动作绝不至于造成泄露

Note:

以独立语句将newed对象存储于智能指针内,如果不这样做,一旦异常被抛出,有可能导致难以察觉的资源泄露。

设计与声明

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

Note:

好的接口很容易被正确使用,不容易被误用。你应该在你的所有接口中努力达成这些性质。

“促进正确使用”的办法包括接口的一致性(stl容器的接口),以及与内置类型的行为兼容。

“阻止误用”的办法包括建立新类型、限制类型上的操作(const、non-local static初始化顺序问题,条款04),束缚对象值,以及消除客户的资源管理责任(智能指针)

Tr1:shared_ptr支持定制型删除器(custom delete).这可防范DLL问题,可被用来自动解除互斥锁(mutexes)等等(???)。

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

  1. 新type对象创建销毁问题,考虑构造析构及内存分配与释放函数
  2. 对象的初始化和赋值问题 条款04
  3. 新type的对象以值传递,意味着什么?(????) tips, copy构造函数用来定义一个type的以值传递如何实现?
  4. 新type类型的合法值。  注意有效成员变量的维护与抛出异常
  5. 考虑继承图系:继承自某些已有的类,就要受到基类的约束,如virtual或non-virtual影响(条款34和36),同时若允许其他类继承自己的类,要做好声明,如析构函数为virtual(条款7)
  6. 新type的类型转换,做好函数实现,考虑隐式转换和显示转换(例条款15)
  7. 合理的成员函数和操作符(条款23,24,46)
  8. 将哪些默认函数声明为private  (条款6)
  9. 谁该取用新type的成员(public、protect、private权限声明)
  10. 新type的“未声明接口”,他对效率、异常安全性(条款29)以及资源运用(例如多任务锁定和动态内存)提供何种保证?为你的类提供保证并加上相应的约束条件
  11. 考虑一般化,定义class template
  12. 考虑是不是必须创建一个类

Note:

Class的设计就是type的设计,在定义一个新type之前,请确定你已经考虑过本条款覆盖的所有讨论主题

条款20:宁以pass-by-reference-to-const(常量引用)替换pass-by-value

  1. 值传递会产生副本,可能多次调用构造函数等操作
  2. 以常量引用替换值传递注意添加const,这样调用者不用担心函数内部会改变其传入值
  3. 传引用可以避免对象切割的问题(slicing)

假设有个图像窗口

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时,

Windowwithscrollbars wwsb;

printNameAndDisplay(wwsb);

此时wwsb的所有特化信息将会被切除,只会调用window::display而不会调用windowwithscrollbars::display

当以引用传递时不会发生这种情况。

Void printNameAndDisplay(const Window& w)

{

       Std::cout << w.name();

       w.display();

}

Note:

尽量以pass-by-reference-const替换pass-by-value。前者通常比较高效,并且可以避免切割问题

以上规则并不适用于内置类型,以及STL的迭代器和函数对象。对他们而言,pass-by-value往往比较适当。

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

Note:

绝不要返回pointer或reference指向一个local stack对象(函数结束时被删除),或返回reference指向一个heap-allocated对象(难以处理分配内存删除的问题),或返回pointer或reference指向一个local static对象(多线程安全)而有可能同时需要多个这样的对象。条款4已经为“在单线程环境中合理返回reference指向一个local static对象”提供了一份设计实例。

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

Note:

切记将成员变量声明为private。这可赋予客户访问数据的一致性、可细微划分访问控制、允诺约束条件获得保证,并提供class作者以充分的实现弹性。

Protected并不比public更具封装性。(假设一个public成员变量,若消灭了它,所有使用它的客户代码都会被破坏,假设一个protect变量,所有使用他的派生类都会被破坏,因此protected成员变量和public一样缺乏封装性)

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

Note:

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

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

Class Rational{

Public:

       Rational(int numerator=0, int denomninator=1);  //构造函数刻意不为explicit,允许int-to-rational隐式转换 

       int numerator() const;

       int denomninator() const;

};

 

//重载乘法运算符

Class Rational{

Public:

       …

       Const Rational operator*(const Rational& rhs) const;

};

//这可以实现如下

Rational oneEighth(1, 8);

Rational oneHalf(1, 2);

Rational result = oneHalf * oneEight;

Result = result*oneEight;  //都允许

 

但进行混合运算,只有一半行的通

 

Result = onehalf*2; //允许  相当于Result = onehalf.operator*2;   //发生隐式转换   

Result = 2*onehalf; /不允许    相当于 Result = 2.operator*nehalf;    // 2中没有.operator只能在non-member中寻找对应函数

 

Result = operator*(2, onehalf); //

 

让operator*成为一个non-member函数,允许编译器在每个实参上执行隐式转换

 

Class Rational{

};  // 不包括 operator*

Const Rational operator* (const Rational& lhs, const Rational& rhs) //现在成为了一个non-member函数

{

       Return Rational(lhs.numerator()*rhs.numerator(), lhs.denominator()*rhs.denominator());

}

 

Rational oneFourth(1, 4);

Rational result;

Result = oneFourth*2;

Result = 2*onefourth; // 都可以编译

 

Note:如果你需要为某个函数的所有参数(包括被this指针所指的那个隐喻参数)进行类型转换,那么这个函数必须是个non-member。

实现

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

Class Widget{

Public:

       Void swap(Widget& other)   //

{     

       Using std::swap;  //若不声明则Swap(pIml, other.pImpl)内部调用该类的swap,变成了递归

       Swap(pIml, other.pImpl);

}

Private:

       WidgetImpl* pImpl;

};

Template<typename T>

Void swap(Widget<T>&a, Widget<T>&b)  // non-member swap函数

{

       a.swap(b);

}

在类中提供一个public的swap(T& b)函数(T为一个类),将每个成员进行交换(如果成员中包含其他非内置对象,调用这个对象的swap函数即可)。然后提供一个non-member的swap(T& a, T& b)重载函数,在函数内部调用类中的a.swap(b).

Note:

当std::swap对你的类型效率不高时(高内存的数据类型),提供一个swap成员函数,并确定这个函数不抛出异常。(拷贝函数和赋值拷贝是深拷贝)

在类内的swap交换内置类型时必须使用using std::swap,否则为递归

作者说std命名空间不要加入新的东西,比如重载swap,以上类和函数应定义在另外的命名空间。

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

  1. 尽量在变量使用时才开始定义,若变量定义与使用之间定义了异常函数,那么变量白白多了一次构造和析构过程
  2. 定义结构体时最好使用拷贝构造一次性初始化,少一次默认构造。

Std::string encrypted;    // 先调用默认构造

Encrypted = password;   / 赋值

 

Std::string encrypted(password);  // 一次拷贝构造

3) 循环情况

A的代价: 1次构造+1次析构+n次赋值

B的代价:n次构造+n次析构

如果n较大,那么应该选择A,如果n较小,可以选择B

Note:

       尽可能延后变量定义式的出现。这样做可增加程序的清晰度并改善程序效率

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

最好使用C++的新式类型转换函数,因为这很容易辨识,代码可读性提高

尽量避免使用dynamic_cast,因为这种转换效率低,可以考虑使用虚函数避免转型

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

Class Rectangle{

Public:

Point& uperleft() const {return pData->ulhc;}

};

Uperleft被声明为const不希望改变内部private数据,而指针引用可以改变,降低封装性,矛盾

另外,如果获得类内一个成员的引用或指针,但在使用之前,对象被释放掉,那么这个引用或指针变成了野指针。

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

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

假设一个class用来表现夹带背景图案的GUI菜单

Class prettyMenu{

Public:

Void changeBackground(std::istream& imgSrc);

Private:

       Mutex mutex;

       Image* bgImage;

       Int imageChange;

};

changeBackground的一个可能实现:

void PrettyMenu::changeBackground(std::istream& imgSrc)

{

       Lock(&mutex);

       Delete bgImage;

       ++imageChange;

       bgImage = new Image(imgSrc);

       unlock(&mutex);

}

 

这个函数没有满足“异常安全”的任何一个条件:

当异常被抛出时,带有异常安全的函数会:

不泄露任何资源。 上述代码若newImage(imgSrc)出现异常,unlock就不会执行,于是互斥器就永远锁住了

不允许数据破坏。如果newImage(imgSrc)异常,bgimage指向一个被删除的对象,imageChange已累加,其实并没有新的图像成功建立。

 

解决方法:使用资源管理类(如智能指针)确保互斥器被及时释放

void PrettyMenu::changeBackground(std::istream& imgSrc)

{

       Lock  ml(&mutex);

       Delete bgImage;

       ++imageChange;

       bgImage = new Image(imgSrc);

}

异常安全函数提供以下三个保证之一:

基本承诺:如果异常被抛出,程序内的任何事物仍然保持在有效的状态下。没有任何对象或数据结构会因此而败坏。

强烈保证:如果异常被抛出,程序状态不改变。调用这样的函数有这样的认知:如果函数成功,就是完全成功,如果函数失败,程序会回复到“调用函数之前”的状态。

不抛掷保证,承诺绝不抛出异常。

Tips:1.将bgImage变成智能指针,2.重新排列changeBackground的语句次序,策略:不要为了表示某件事情发生而改变对象状态,除非那件事情真的发生了。

Class PrettyMenu{

       …

       Std::tr1::shared_ptr<Image> bgImage;

       …

};

Void PrettyMenu::changeBackground(std::istream& imgSrc)

{

       Lock ml(&mutex);

       bgImage.reset(new Image(imgSrc));

       ++imageChange;

}

Note:

异常安全函数即使发生异常也不会泄露资源或允许任何数据结构败坏。这样的函数区分为三种可能的保证:基本型、强烈型、不抛异常型。

强烈保证往往能够以copy-and-swap实现出来,但强烈保证并非对所有函数都可实现或具备现实意义。

函数提供的异常安全保证通常最高只等于其所调用之各个函数的异常安全保证中的最弱者。

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

Note:

将大多数inlining限制在小型、被频繁调用的函数身上。这可使日后的调试过程和二进制升级更容易,也可使潜在的代码膨胀问题最小化,使程序的速度提升机会最大化。

不要只因为function templates出现在头文件,就将他们声明为inline。

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

1)指针实现

 

// ComplexClass.h

#include “SimpleClass1.h”

#include “SimpleClass2.h”

 

class ComplexClass:

{

    SimpleClass1 xx;

    SimpleClass2 xxx;

};

当SimpleClass1.h中的接口声明改变,ComplexClass需要重编,而且所有使用ComplexClass类对象的文件,都需要重新编译。

 

// ComplexClass.h

#include “SimpleClass2.h”

 

class SimpleClass1;

 

class ComplexClass:

{

    SimpleClass1* xx;  //若不使用指针,编译器不知道类型内存分配多少,编译不通过

    SimpleClass2 xxx;

};

此时编译器视所有指针为一个字长,因此class SimpleClass1的声明够用了,然后在ComplexClass.cpp中包含SimpleClass1.h,达到降低依赖的目的。

 

总结一下,对于C++类而言,如果它的头文件变了,那么所有这个类的对象所在的文件都要重编,但如果它的实现文件(cpp文件)变了,而头文件没有变(对外的接口不变),那么所有这个类的对象所在的文件都不会因之而重编。

 

因此,避免大量依赖性编译的解决方案就是:在头文件中用class声明外来类,用指针或引用代替变量的声明;在cpp文件中包含外来类的头文件

 

2)interface class,即类中全部都是pure virtual函数,这样的类在使用的时候只能是以指针的形式出现,这样同样达到了减少编译依赖的效果。

3)两种方式都存在一定的代价:指针方式的实现要多分配指针大小的内存,每次访问都是间接访问。接口形式的实现方式要承担虚函数表的代价以及运行时的查找表的代价。但是一般这两种实现对资源和效率的影响通常不是关键的。因此可以放心使用。

继承与面向对象设计

条款32:确定你的public继承塑模出is-a(是一种的)关系

例:人是一般化的,学生是特殊化的,人能做的学生能做,学生能做的人不能做。

Void eat(const Person& p); //任何人都会吃

Void study(const Student& s);  //只有学生才到校学习

 

Person p;

Student s;

Eat(p);   //p是人

Est(s);   // s是学生,而学生也是(is-a)人

Study(s); //s是学生

Study(P);  // p不是学生 ,错误

 

另一个非直觉例子:

Class Bird{

Public:

       Virtual void fly();  //鸟可以飞

       …

};

Class Penguin:public Bird{  //企鹅是一种鸟

};   //这个例子中企鹅也会飞,反直觉

 

 

Class Bird{

…    //没有声明fly函数

};

Class FlyingBird:public Bird{

Public:

       Virtual void fly();

       …

};

Class Penguin:public Bird{…};  //没有声明fly函数

或者也可以继承有fly定义的bird,让其产生一个运行错误。

Class Penguin:public Bird{

Public:

       Virtual void fly() {error(“attempt to make a penguin fly!”)};  //不推荐

};

 

Note:

Public继承意味着is-a。适用于base classes身上的每一件事情一定也适用于derived class身上,因为每一个derived class对象也都是一个base class对象。

没有一个正确的is-a标准,只有最适合你的场景的代码。

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

子类会遮掩父类同名的函数,可以使用类名作用域决定调用父类还是子类的函数。

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

Class shape{

Public:

       Virtual void dram() const = 0;

       Virtual void error(const stdLLstring& msg);

       Int objectID() const;

       …

};

Class Rectangle:public Shape{…};

Class Ellipse:public Shape{…};

  1. 成员函数的接口总是会被继承的。
  2. 声明一个pure virtual函数的目的是为了让derived classes只继承函数接口。派生类可以提供一个draw函数,可自由实现它。
  3. 声明非纯impure virtual函数的目的是让derived classes继承该函数的接口和缺省实现。可以通过覆写(override)实现另外功能。
  4. 声明non-virtual函数的目的是为了令derived classes继承函数的接口及一份强制性实现。

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

  1. 使用non-virtual interface(NVI)手法,那是Template Method设计模式的一种特殊形式。它以public non-virtual成员函数包裹较低访问性(private或protected)的virtual函数。

class Object{

public:

    void Interface(){

        ···    //做一些事前工作

        doInterface();

        ···     //做一些事后工作

    }

private/protected:

    virtual doInterface(){}

}

事前工作可以保证virtual函数别调用之前设定好适当场景(加锁、验证约束条件等),事后工作可以清理场景。

  1. 将virtual函数替换为“函数指针成员变量”,这是Strategy设计模式的一种分解表现形式。(????)
  2. 以tr1::function成员变量替换virtual函数,因而允许使用任何可调用物搭配一个兼容与需求的签名式。这也是Strategy设计模式的某种形式。(???)
  3. 将继承体系内的virtual函数替换为另一个继承体系内的virtual函数。这是Strategy设计模式的传统实现手法。(????)

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

考虑:

Class B{

Public:

       Void mf();

       …

};

Class D:public B{…};

虽然我们对B、D和mf一无所知,但面对一个类型为D的对象x:

D x; // x是一个类型为D的对象

如果一下行为:

B* pB = &x;

pB->mf();

异与以下行为:

D* pD = &x;

pD->mf();

这是因为,如果mf是个non-virtual函数而D定义有自己的mf版本,那就执行结果就不同。

Class D:public B{

Public:

       Void mf();

       …

};

pB->mf();  //调用B::mf

pD->mf(); //调用D::mf

造成这一行为的原因是non-virtual函数是静态绑定的,而virtual函数是动态绑定的所以不存在这个问题。

Note:

绝对不要重新定义继承而来的non-virtual函数。

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

Virtual函数系动态绑定(dynamically)的,而缺省参数值却是静态绑定的(statically bound)。

Class shape{

Public:

       Enum shapecolor{red, green, blue};

       Virtual void draw(shapecolor color=red) const = 0;

       …

};

Class Rectangle:public shape{

Public:

       Virtual void draw(shapecolor color=green) const; //注意,赋予不同的缺省参数值,这很糟糕。

};

Shape* pr = new Rectangle; //静态类型为shape*

 

pr->draw(); //调用rectangle::draw(shape::red)!!!

 

Pr的动态类型是rectangle*,所以调用的是rectangle的virtual函数,一如你所预期(???),rectangle::draw函数的缺省参数值应该是green,但由于pr的静态类型是shape*,所以此一调用的缺省参数值来自shape class而非rectangle class;

Note:

绝对不要重新定义一个继承而来的缺省参数值,因为缺省参数值都是静态绑定,而virtual函数,你唯一应该重写的东西,却是动态绑定。

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

Class Address{…};

Class PhoneNumber{…};

Class Person{

Public:

       …

Private:

       Std::string name;

       Address address;

       PhoneNumber voiceNumber;

       PhoneNumber faxNumber; 

};   //has-a  有一种

 

Set对象可根据一个list对象实现出来

 

Template<class T>

Class set{

Public:

       Bool member(const T& item) const;

       Void insert(const T& item);

       Void remove(const T& item);

       Std::size_t size() const;

Private:

       Std::list<T> rep;

};

Template<typename T>

Bool set<T>::member(const T& item) const

{

       Return std::find(rep.begin(), rep.end(), item) != rep.end();

}

Template<typename T>

Void set<T>::insert(const T& item)

{

       If (!member(item)) rep.push_back(item);

}
template<typename T>

Void set<T>::remove(const T& item)

{

       Typename std::list<T>::iterator it = std::find(rep.begin(), rep.end(), item);

       If (it != rep.end()) rep.erase(it);

}

Template<typename T>

Std::size_t set<T>::size() const

{

       Return rep.size(); 

}  // is implemented in term of(根据某物实现出)

Note:

复合(composition)的意义和public继承完全不同

在应用域(application domain),复合意味has-a(有一个)。在实现域(implementation domain),复合意味is implemented in term of(根据某物实现出)

 

条款39:明智而审慎的使用private继承

子类继承父类的方式决定了在子类中父类函数的属性,一般规则就是所有属性都按照继承方式对其。比如采用protected继承方式,那么父类中的public成员在子类都升级为protected,其他保持不变。如果采用private继承方式,父类中的所有成员全部变为private,特殊之处之一是父类中原本就是private的成员不可继承,即在子类中也无法使用父类的private成员

 

Class empty{};  //没有non-static成员变量,没有virtual函数,也没有virtual base classes。

 

Class HoldsAnInt{

Private:

       Int x;

       Empty e;

};

你会发现sizeof(HoldsAnInt) > sizeof(int);  //空类会留一个char或int大小空间

而class HoldsAnInt:private Empty{

Private:

       Int x;

}; //几乎可以确定sizeof(HoldsAnInt)==sizeof(int), 这就是所谓的EBO(empty base optimization, 空白基类最优化)

Note:

Private继承意味is implement in terms of(根据某物实现出)。它通常比复合(composition)的级别低,推荐复合

和复合不同,private继承可以造成empty base最优化。

条款40:明智而审慎地使用多重继承

Class A{…}; //包含A对象

Class B:public A{…}; //包含A,B对象

Class C:public A{…}; //包含A,C对象

Class D:pubic B, public C{…}; //包含A,A,B,C,D对象

由于菱形继承,基类被构造了两次。

Class A{…}; //包含A对象

Class B:virtual public A{…}; //包含A,B对象

Class C:virtual public A{…}; //包含A,C对象

Class D:pubic B, public C{…}; //包含A,B,C,D对象

使用虚继承,B,C对象里面会产生一个指针指向唯一一份A对象。这样付出的代价是必须再运行期根据这个指针的偏移量寻找A对象。

Note:

多重继承比单一继承复杂,它可能导致新的歧义性,以及对virtual继承的需要。

Virtual继承会增加大小、速度、初始化(及赋值)复杂度等成本。如果virtual base classes不带任何数据,将是最具实用价值的情况

 

模板与泛型编程

条款41:了解隐式接口和编译期多态

Note:

Classes和templates都支持接口(interfaces)和多态

对classes而言接口是显示的,以函数签名(函数名称、参数类型、返回类型)为中心。多态则是通过virtual函数发生与运行期。

对template参数而言,接口是隐式的,基于有效表达式,多态则是通过template具现化和函数重载解析(function overloading resolution)发生与编译期。

条款42:了解typename的双重意义

template<typename T>

class Derived:public Base<T>::Nested{  //base class list中

public:                                                     //不允许“typename”

       explicit Derived(int x)

:Base<T>::Nested(x)            //mem.init.list中

{                                                       //不允许“typename”

       Typename Base<T>::Nested temp;   //嵌套从属类型名称

       …                    //即不在base class list中也不在mem.init.list中

}                                         //作为一个base class修饰符需加上typename;

};

 

 ①在声明template参数时,class和typename可互换。

 

  ②typename的第二个用处是告诉编译期某一个嵌套从属类型是类型,最典型的就是STL中容器的迭代器类型,例如:T::iterator(T是个容器的类型,例如:vector<int>),这个时候就要在T::iterator前面加一个typename,告诉编译器这是一个类型,否则编译器不能确定这是什么,因为有可能iterator是个静态变量或者某一namespace下的变量。

 

③类的继承列表和初始化列表中的类型不需要typename指定类型,因为继承的一定是个类,而初始化列表一定是调用父类的构造或者初始化某个成员。(?????)

条款43:学习处理模板化基类内的名称

当一个类的基类包含模板参数时,需要通过this->的方式调用基类内的函数,例如 class F: public S<C>,在F中的成员函数中调用S中的成员函数this->test(),而直接写test()无法通过编译,原因是因为C是个模板没有办法确定类S的具体长相,或者说无法确定S中一定有test函数,即使你写的所有C都包含test函数,但是在编译器看来它是不确定这个问题的,因此无法通过编译。

 

解决办法是:①使用this->test,这样做告诉编译器假设这个test已经被继承了。②使用using声明式:using S<C>::test告诉编译期这个test位于S内。相当于必须手动通知编译器这个函数是存在的。

条款44:将与参数无关的代码抽离templates

非类型模板参数造成的代码膨胀,以函数参数或者class成员变量替换template参数

类型模板参数造成的代码膨胀,可以让具有完全相同二进制表述的具现类型共享实现码

条款45:运用成员函数模板接收所有兼容类型

template<typename T>

class shared_ptr{

public:

    //拷贝构造函数,接受所有能够从U*隐式转换到T*的参数

    template<typename U>

    shared_ptr(shared_ptr<U> const &rh):p(rh.get()){

        ...

    }

    //赋值操作符,接受所有能够从U*隐式转换到T*的参数

    template<typename U>

    shared_ptr& operator= (shared_ptr<U> const &rh):p(rh.get()){

        ...

    }

   

    //声明正常的拷贝构造函数

    shared_ptr(shared_ptr const &rh);

    shared_ptr& operator= (shared_ptr const &rh);

private:

    T *p;

}

Note:

使用成员函数模板生成“可接受所有兼容类型”的函数

即使有了泛化拷贝构造函数和泛化的赋值操作符,仍然需要声明正常的拷贝构造函数和赋值操作符

在一个类模板内,template名称可被用来作为template和其参数的简略表达式。

条款46:需要类型转换时请为模板定义非成员函数

template <class T>

class Rational

{

    …

    friend Rational operator* (const Rational& a, const Rational& b)

    {

        return Rational (a.GetNumerator() * b.GetNumerator(),

            a.GetDenominator() * b.GetDenominator());

    }

    …

}

相比于条款24,换成模板之后为什么就无法通过编译了呢?原因在于模板的运行需要进行模板推算,即operator*函数的两个参数类型的T要根据传入的参数类型进行确认,第一个参数因为是oneHalf,其本身就是Rational<int>类型,因此第一个参数的类型中的T很容易进行推理,但是第二个传入的参数是int,如何根据这个int参数推导出第二个参数的类型T呢?显然编译器无法进行推理,条款24能推理的原因是进行了隐式类型转换,或者说Rational的构造函数中有一个以int为参数的构造函数,但是template在进行参数推到的过程中从不将隐士类型转换函数考虑在内,这也是合理的因为你没法根据参数类型推导出模板参数,这个Ratinal的例子貌似看起来可以,因为构造函数的参数类型是const T& 但是假如其构造参数类型是个固定类型,比如说float,那么难道模板参数能永远是float么。因此编译器不考虑隐士类型转换也是有道理的。

  那么这个问题怎么解决呢,该如何让这个模板函数的参数能进行隐式类型转换,答案就是:先具象化这个函数,相当于先确定T,然后就可以进行隐士类型转换了,做法是在类中声明一个非成员函数,这该如何做到呢,答案就是友元函数,在类中定义的友元函数都被视为非成员函数。

Note:

当我们编写一个模版类,某个相关函数都需要类型转换,需要把这个函数定义为非成员函数,但是模版的类型推到遇见了问题,需要把这个函数声明为友元函数帮助推导,模版函数只有声明编译器不会帮忙具现化,所以我们需要实现的是友元模版函数

条款47:请使用traits classes表现类型信息

先看这样一个例子。如果有一个模板类Test:

template <typename T>

class Test {

     ......

};

假设有这样的需求,类Test中的某部分处理会随着类型T的不同而会有所不同,比如希望判断T是否为指针类型,当T为指针类型时的处理有别于非指针类型,怎么做?模板里再加个参数,如下?

template <typename T, bool isPointer>

class Test {

     ......// can use isPointer to judge whether T is a pointer

};

然后用户通过多传一个模板类型来告诉Test类当前T是否为指针。(Test<int*, true>)

很抱歉,所有的正常点的用户都会抱怨这样的封装,因为用户不理解为什么要让他们去关心自己的模板类型是否为指针,既然是Test类本身的逻辑,为什么麻烦用户呢?

由于我们很难去限制用户在使用模板类时是使用指针还是基本数据类型还是自定义类型,而用常规方法也没有很好的方法去判断当前的T的类型。traits怎么做呢?

定义traits结构:

template <typename T>

struct TraitsHelper {

     static const bool isPointer = false;

};

template <typename T>

struct TraitsHelper<T*> {

     static const bool isPointer = true;

};

也许你会很困惑,结构体里就一个静态常量,没有任何方法和成员变量,有什么用呢?解释一下,第一个结构体的功能是定义所有TraitsHelper中isPointer的默认值都是false,而第二个结构体的功能是当模板类型T为指针时,isPointer的值为true。也就是说我们可以如下来判断当前类型:

TraitsHelper<int>::isPointer值为false, 可以得出当前类型int非指针类型

TraitsHelper<int*>::isPointer值为true, 可以得出当前类型int*为指针类型

也许看到这里部分人会认为我简直是在说废话,请再自己品味下,这样是否就可以在上面Test类的定义中直接使用TraitsHelper<T>::isPointer来判断当前T的类型了。

if (TraitsHelper<T>::isPointer)

     ......

else

     ......

再看第二个例子:

还是一个模板类Test:

template <typename T>

class Test {

public:

     int Compute(int d);

private:

     T mData;

};

它有一个Compute方法来做一些计算,具有int型的参数并返回int型的值。

现在需求变了,需要在T为int类型时,Compute方法的参数为int,返回类型也为int,当T为float时,Compute方法的参数为float,返回类型为int,而当T为其他类型,Compute方法的参数为T,返回类型也为T,怎么做呢?还是用traits的方式思考下。

template <typename T>

struct TraitsHelper {

     typedef T ret_type;

     typedef T par_type;

};

template <>

struct TraitsHelper<int> {

     typedef int ret_type;

     typedef int par_type;

};

template <>

struct TraitsHelper<float> {

     typedef float ret_type;

     typedef int par_type;

};

然后我们再把Test类也更新下:

template <typename T>

class Test {

public:

     TraitsHelper<T>::ret_type Compute(TraitsHelper<T>::par_type d);

private:

     T mData;

};

可见,我们把因类型不同而引起的变化隔离在了Test类以外,对用户而言完全不需要去关心这些逻辑,他们甚至不需要知道我们是否使用了traits来解决了这个问题。

当函数,类或者一些封装的通用算法中的某些部分会因为数据类型不同而导致处理或逻辑不同时,traits会是一种很好的解决方案。

条款48:模板元编程

//上楼梯,每次上一步或者两步,有多少种

int climb(int n){

    if(n == 1)

        return 1;

    if(n == 2)

        return 2;

    return climb(n - 1) + climb(n - 2);

}

 

//元编程,采用类模版

template<int N>

class Climb{

public:

  const static int n = Climb<N-1>::n + Climb<N-2>::n;

};

 

template<>

class Climb<2>{

public:

  const static int n = 2;

};

 

template<>

class Climb<1>{

public:

  const static int n = 1;

};

采用模板编程的好处是:①可将工作由运行期移动到编译器完成,造成更高的执行效率(占用内存小,运行速度快)和更早的侦测错误②编码更加简洁;坏处:①编译时间长②代码不易理解

定制new和delete

条款49:了解new-handler的行为

 

//以下是当operator new无法分配足够内存时,该被调用的函数

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];

       …

}

 

类实现????

如果operator new无法为100000000个整数分配足够的空间,outofMem会被调用。

当operator new无法满足某一内存分配需求时,它会抛出异常;抛出异常之前,也可以先调用一个客户指定的错误处理函数(new-handler),调用set_new_handler可以指定该函数

Nothrow(在无法分配足够内存时返回NULL)是一个颇为局限的工具,它只适用于内存分配,后继的构造函数调用还是可能抛出异常

条款50:了解new和delete的合理替换时机

Note:有许多理由需要写个自定的new和delete,包括改善效能、对heap运用错误进行调试、收集heap使用信息。

条款51:编写new和delete时需固守常规

Note:

Operator new应该内含一个无穷循环,并在其中尝试分配内存,如果它无法满足内存需求,就该调用new-handler。它也应该有能力处理0bytes申请。Class专属版本则还应该处理“比正确大小更大的(错误)申请”.

条款52:写了placement new也要写placement delete

Note:

当你写一个placement operator new,请确定也写出对应的placement operator delete。如果没有这样做,你的程序可能会发生隐微而时断时续的内存泄漏

当你声明placement new和placement delete,请确定不要无意识(非故意)的遮掩他们的正常版本。

杂项讨论

条款53:不要轻忽编译期的警告

Note:

严肃对待编译器发出的警告信息,努力在你的编译器的最高警告级别下争取“无任何警告”的荣誉。

不要过度依赖编译器的报警能力,因为不同的编译器对待事情的态度并不相同,一旦移植到另一个编译器上,原本依赖的警告信息有可能消失。

条款54:让自己熟悉包括TR1在内的标准程序库TR1(Technical Report 1)

Note:

C++标准程序库的主要机能由STL、iostreams、locales组成。并包含C99标准程序库。

TR1添加了智能指针(例如tr1::shared_ptr)、一般化函数指针(tr1::function)、hash-based容器、正则表达式(regular expressions)以及另外10个组件的支持。

TR1自身是一份规范。为获得TR1提供的好处,你需要一份实物。一个好的实物来源是Boost。

条款55:让自己熟悉Boost

Note:

Boost是一个社群,也是一个网站。致力于免费、源码开放、同僚复审的C++程序库开发。Boost在C++标准化过程中扮演深具影响力的角色

Boost提供许多TR1组件实现品,以及其他许多程序库。

http://boost.org

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!