《总之,好记性不如烂笔头!把你遗忘的都记下来吧!》
Effective C++ 改善程序和设计的55个具体做法
01:视C++为一个语言联邦
- C。C++以C为基础;
- Object-Oriented C++。包括类、封装、继承、多态、动态绑定等;
- Template C++。 C++泛型编程的基础;
- STL库。包括容器、算法、迭代器、配置器、仿函数;
02:尽量以const,enum,inline替代#define
1、使用const/enum替换#define
1
| #define ASPECT_RATIO 1.653
|
ASPECT_RATIO不能被编译器所看到,导致错误信息可能只是数字。
使用常量来替换宏:
1
| const double ASPECTRATIO = 1.653
|
使用常量来替换宏的两种特殊情况:
第一:定义常量指针
由于常量定义式常放在头文件内,有必要将指针声明为const。
1
| const char* const authorName = "Scott Meyers";
|
第二:类的静态常量
为了将常量的作用域限制在类中,必须声明为类的成员
1 2 3 4 5 6
| class GamePlayer{ private: static const int NumTurns = 5; int scores[NumTurns]; };
|
只要不取地址就可以声明并使用它们而无须提供定义,但是要取类静态常量的地址,就必须提供定义式!
1
| const int GamePlayer::NumTurns;
|
顺便的#define在类内不提供任何封装性。
如果不支持静态常量类内初始化,将初值放在定义式
1 2 3 4 5
| class CostEstimate{ private: static const double FudgeFactor; }; const double CostEstimate::FudgeFactor = 1.35;
|
但是这样做不能在类内使用这个常量,可以使用“ the enum hack”的补偿做法。
1 2 3 4 5
| class GamePlayer{ private: enum { NumTurns = 5 }; int scores[NumTurns]; };
|
2、改写#define形似函数的宏
对于像是这样的形似函数的宏:
1
| #define CALL_WITH_MAX(a, b) f((a) > (b) ? (a) : (b))
|
使用这样的宏,必须为宏上的所有实参加上括号。
应该使用template inline来替代这样的宏,它具有和宏一般的效率,且更加安全
1 2 3 4
| template<typename T> inline void callWithMax(const T& a, const T&b){ f(a > b ? a : b); }
|
03:尽可能的使用const
const可以和函数返回值、参数、函数自身产生关联。
const作用于函数返回值:
令函数返回一个常量值,可以降低因错误而产生的意外。
1 2 3 4 5 6 7
| class Rational{ ... }; const Rational operator* (const Rational& lhs, const Rational& rhs);
Rational a, b, c; (a * b) = c;
if( a * b = c)
|
const参数:
对于在函数中不改变的参数,应当将它们声明为const的。
const成员函数:
将const作于成员函数是为了const对象能够使用这个成员函数,存在两方面:1、使得接口容易被理解,因为可以很容易看出这个接口不改变对象内容;2、能够被const对象使用
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23
| #include<iostream> class TextBlock { public: TextBlock(std::string txt):text(txt){}; const char& operator[](std::size_t position) const{ std::cout << "this is use the const" << std::endl; return text[position]; } char &operator[](std::size_t position){ std::cout << "this is use the non-const" << std::endl; return text[position]; } private: std::string text; }; int main(int argc, char const *argv[]) { TextBlock tb("hello"); std::cout << tb[0]<<std::endl; TextBlock ctb("haha"); std::cout << ctb[1]<<std::endl; return 0; }
|
const成员函数的bitwise constness 和 logical constness
bitwise const:成员函数只有在不改变对象内的任何成员变量时才可以说是const的。一个更改了”指针所指向之物“的成员函数仍算数bitwise const,不会引发编译异常。
1 2 3 4 5 6 7 8 9 10 11 12 13
| class CTextBlock { public: char& operator[](std::size_t position)const{ return pText[position]; } private: char *pText; };
const CTextBlock cctb("Hello"); char *pc = &cctb[0]; *pc = 'J';
|
logical const: 一个const成员函数可以修改它所处理的的对象的某些bits,但是只有在客户端侦测不出的情况下。
为了确保能够在const成员函数中修改成员变量,需要将需要修改的成员变量声明为mutable的。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
| class CTextBlock { public: std::size_t length() const{ if(!lengthIsValid){ textLength = std::strlen(pText); lengthIsValid = true; } return textLength; } private: char *pText; mutable std::size_t textLength; mutable bool lengthIsValid; };
|
避免const和non-const成员函数的重复:
运用const成员函数来实现出其non-const成员函数的方法。因为在non-const中对象可能被改动,因此使用const成员函数调用non-const成员函数是一种错误的行为。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
| class CTextBlock2 { public: const char& operator[](std::size_t position)const{ return pText[position]; } char& operator[](std::size_t position) { return const_cast<char &>( static_cast<const TextBlock &>(*this)[position]); } private: char *pText; };
|
04:确定对象使用前已先被初始化
使用内置型对象进行手工初始化
因为C++不保证初始化它们,读取未初始化的值会导致不明确的行为。
使用初始化成员列表
不要混淆赋值和初始化
1 2 3 4
| ABEntry::ABEntry(const std;:string &name){ theName = name; }
|
对象的初始化在进入构造函数本体之前,在构造函数内不是初始化而是赋值。
改用初始化能够减少一次赋值动作
1 2
| ABEntry::ABEntry(const std;:string &name):theName(name) {}
|
规定总是在初值列中列出所有成员变量,C++有十分固定的成员初始化次序,基类早于继承类初始化,成员变量总是按照生命次序被初始化。在初始化成员列表中,最好以声明次序列出各个成员。
不同编译单元(不同文件)的non-local static对象的初始化次序
函数内的static对象成为local static对象,其它static对象称为non-local static对象
由于C++对于不同编译单元内的non-local static对象的初始化次序没有明确定义,导致一个编译单元的non-local static对象使用另一个编译单元的non-local static对象可能未初始化。
解决方法:
将non-local static对象搬到自己的专属函数中使之变为local static,在函数内部被声明为static,函数返回一个reference指向它所含的对象。
1 2 3 4 5
| class FileSystem { ... }; FileSystem& tfs(){ static FileSystem fs; return fs; }
|
05:了解C++默默编写并调用哪些函数
对于一个类如果没有声明,编译器就会自动生成一个copy构造函数,一个copy赋值运算符和一个析构函数,如果没有声明默认构造函数,同时也会生成一个默认构造函数。所有这些函数都是public且inline的。
**对于内含引用、const类型的成员变量,编译器会拒绝为class生成operator=**。
如果一个基类将拷贝赋值运算符声明为private,那么编译器会拒绝为派生类生成拷贝赋值运算符。
06:若不想使用编译器自动生成的函数,就应该明确拒绝
1、声明一个具有private类型的拷贝赋值运算符作为基类,并继承;
2、使用=delete直接删除;
07:为多态基类声明virtual析构函数
使用factory工厂方法,返回一个基类指针,指向新生成的继承类对象
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26
| class Product{ public: ~Product(){} }; class ConcreteProduct1 : public Product{}; class ConcreteProduct2 : public Product{}; class Creator{ public: ~Creator(){} virtual Product *FactoryMethod() const = 0; void SomeOperation() const{ Product *product = this->FactoryMethod(); delete product; } }; class ConcreteCreator1 : public Creator{ public: Product *FactoryMethod() const override { return new ConcreteProduct1(); } }; class ConcreteCreator2 : public Creator{ public: Product *FactoryMethod() const override { return new ConcreteProduct2(); } };
|
C++指出,当继承对象经由一个基类指针被删除,而基类指针带有一个non-virtual析构函数其结果是未定义的。产生的结果是基类部分被析构,而派生类的部分没有被析构,导致资源泄露,破坏数据结构。
任何类中只要带有virtual函数几乎确定也应该有一个virtual析构函数。反之亦然。
声明一个带纯虚析构函数的抽象类:
1 2 3 4 5 6 7 8
| class AMOV{ public: virtual ~AWOV() = 0; };
AMOV::~AMOV(){ }
|
析构函数从最深层的派生类开始调用其析构函数,然后每一个基类的析构函数被调用。编译器会在AMOV的继承类的析构函数中创建一个对~AMOV的调用动作,因此必须为这个函数提供一份定义。
08:别让异常逃离析构函数
在析构函数中必须执行一个动作,但该动作会在失败时抛出异常。
1 2 3 4 5 6 7 8 9 10
| class DBConn{ public: ... ~DBConn(){ db.close(); } private: DBConnection db; };
|
如果调用导致异常,析构函数将传播异常,允许其离开这个析构函数。
解决方法:
1、通过调用abort终止
1 2 3 4 5 6
| DBConn::~DBConn(){ try { db.close();} catch(...){ std::abort(); } }
|
这个方法将导致程序被强迫结束。
2、记录close的失败调用
1 2 3 4 5 6
| DBConn::~DBConn(){ try { db.close();} catch(...){ 记录失败调用。 } }
|
3、 在新接口中操作,将责任从析构函数中转移
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
| class DBConn{ public: void close(){ db.close(); closed = true; } ~DBConn(){ if(!closed()){ try{ db.close(); }catch(...){ 记录close失败调用。 } } } };
|
将close责任从析构函数中转移到DBConn的用户手中。
如果某个操作可能在失败时抛出异常,而又必须处理该异常,这个异常必须来自析构函数之外,析构函数抛出异常会带来”过早结束程序“和”不明确行为“的风险。
09:绝不在析构和构造函数过程中调用virtual函数
10:令operator=返回一个reference to *this
为类实现赋值操作符时应该遵守的协议:赋值操作符必须返回一个reference指向操作符的左侧实参(左值)
1 2 3 4 5 6 7 8 9 10 11 12 13 14
| class Widget{ public: ... Widget& operator=(const Widget& rhs){ ... return *this; } Widget& operator+=(const Widget& rhs){ ... return *this; } };
|
11:在operator=中处理“自我赋值”
自我赋值发生在对象赋值给自己,这种情况可能会隐式发生。
为了防止自我赋值时销毁自身
1、自我检验
1 2 3 4 5 6
| Widget& Widget::operator=(const Widget& rhs){ if(this == &rhs) return *this; delete pb; pb = new Bitmap(*rhs.pb); return *this; }
|
这样做存在潜在的隐患:如果new失败会导致pb指向一片被删除的Bitmap
2、使用拷贝实现自我赋值
1 2 3 4 5 6
| Widget& Widget::operator=(const Widget& rhs){ Bitmap* pOrig = pb; pb = new Bitmap(*rhs.bp); delete pOrig; return *this; }
|
这样即使new失败也能保证pb的指向是安全的。但是这样会导致多一次拷贝构造,影响效率。
3、使用copy and swap技术
1 2 3 4 5 6 7 8 9 10
| class Widget{ ... void swap(Widget &rhs); ... }; Widget& Widget::operator=(const Widget &rhs){ Widget temp(rhs); swap(temp); return *this; }
|
12:复制对象时勿忘其每一个成分
为一个类增加一个成员变量,必须同时修改拷贝函数。
在继承类的拷贝函数中,必须小心的复制其基类的部分,通常它们是private的,需要通过派生类调用基类的拷贝构造函数/拷贝赋值运算符。
1 2 3 4 5 6 7 8 9 10 11
| PriorityCustomer::PriorityCustomer(const PriorityCustomer &rhs) :Customer(rhs), priority(rhs.priority) { logCall("PriorityCustomer copy constructor"); } PriorityCustomer& PriorityCustomer::operator=(const PriorityCustomer &rhs){ logCall("PriorityCustomer copy assignment operator"); Customer::operator=(rhs); priority = rhs.priority; return *this; }
|
13:以对象管理资源
1 2 3 4 5 6 7 8
| Investment* createInvestment();
void f(){ Invesetment *pInv = creatInvestment(); ... delete pInv; }
|
由于在…代码块中过早的退出,或者触发异常以及人为的遗忘delete到会导致内存泄漏。
为了确保createInvestment()返回的资源总是被释放,我们需要将资源放入对象内,依靠析构函数自动调用确保资源正确被释放(即为RAII对象)。
1 2 3 4 5
| void f(){ std::auto_ptr<Investment> pInv(createInvestment()); ... }
|
以对象管理资源的两个关键想法:
- 获得资源后立即放入管理对象内。“以对象管理资源”的观念常被称为 RAII(Resource Acquisition Is Initialization);
- 管理对象运用析构函数确保资源被释放。
由于std::auto_ptr不能同时指向同一个对象。如果通过copy构造函数或者copy assignmen操作符复制它们,它们将变成null
使用std::shared_ptr就不会有这个问题,因此建议使用std::shared_ptr
14:在资源管理类中小心copy行为
当需要建立自己的RAII对象时,需要明确对象被复制后发生的行为,多数情况下有以下两种选择:
- 禁止复制。许多时候允许RAII对象被复制并不合理。如果复制动作对RAII对象不合理,应该明确禁止;
- 对底层资源使用引用计数法。当希望保有资源直到最后一个使用对象被销毁,这时候复制RAII对象应该将资源的引用计数递增;
通常只需要内含一个std::shared_ptr成员变量,RAII类就可以实现引用计数复制行为。通过自定义std::shared_ptr 的删除器,就可以实现指定的删除。
1 2 3 4 5 6 7 8 9 10 11
| void lock(Mutex *pm); void unlock(Mutex *pm); class Lock{ public: explicit Lock(Mutex* pm):mutexPtr(pm, unlock){ lock(mutexPtr.get()); } private: std::shared_ptr<Mutex> mutexPtr; };
|
其它的选择:
- 复制底部资源。在此情况下复制资源管理对象,应该同时复制其包含的资源。
- 转移底部资源的拥有权。在某些罕见场合下需要确保永远只有一个RAII对象指向一个未加工资源,即使发生了复制。如
std::auto_ptr
15:在资源管理类中提供对原始资源的访问
有些时候需要直接访问原始资源,需要有一个函数将RAII对象转化为原始资源,通过以下两个方法:
1、显示转化:
如std::shared_ptr提供了get()成员函数,用来执行显示转化。
2、隐式转化:
std::shared_ptr同时重载了操作符operator->和operator*,它们允许隐式转化至底部原始指针。
有时还是必须取得RAII对象内的原始资源,提供隐式转换函数
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
| FontHandle getFont(); void releaseFont(FontHandle fh); class Font{ public: explicit Font(FontHandle fh):f(fh){} ~Font(){ releaseFont(f); } FontHandle get() const { return f; } operator FontHandle() const { return f; } private: FontHandle f; };
|
但是隐式转化函数会增加错误发生的机率。
1 2 3
| Font f1(getFont()); ... FontHandle f2 = f1;
|
当f1被销毁,字体释放,f2就会成为悬垂的。
16:成对使用new和delete时要采取相同形式
使用new,有两件事发生。第一,内存被名为operator new的函数分配出来;第二,针对内存会有构造函数被调用。对于delete,先调用析构函数,然后使用operator delete释放内存。
1 2 3 4 5 6 7
| std::string *stringPtr1 = new std::string; std::string *stringPtr2 = new std::string[100]; ... delete stringPtr1;
delete [] stringPtr2;
|
如果调用new时使用[],必须在delete时也使用[],反之亦然。
17:以独立语句将newed对象置入智能指针
1 2 3 4 5 6 7 8
|
int priority(); void processWidget(std::shared_ptr<Widget> pw, int priority);
processWidget(std::shared_ptr<Widget>(new Widet), priority());
|
执行上面的函数有三件事:1、调用new Widget 2、调用std::shared_ptr构造函数 3、 调用priority()
但是它们的顺序确是不确定的,存在new Widget调用后,再调用priority()导致异常,从而智能指针接受不到的情况。
解决的方法:
1 2 3
| std::shared_ptr<Widget> pw(new Widget); processWidget(pw, priority());
|
18:让接口容易被正确使用,不易被误用
19:设计class犹如设计type
C++中,当你定义了一个新class,也就定义了一个新type。如何设计高效的类,必须了解下面的问题:
- 新type的对象应该如何被创建和销毁?
- 对象的初始化和对象的赋值该有什么样的差别?
- 新type的对象如果被passed by value,意味着什么?
- 什么是新type的“合法值”
- 你的新type需要配合某个继承系吗?
- 你的新type需要什么样的转化?
- 什么样的操作符和函数对此新type而言是合理的?
- 什么样的标准函数应该驳回?
- 谁该取用新的type成员?
- 什么是新type的“未声明接口”?
- 新type有多么一般化?
- 你真需要一个新type吗?
20:宁以pass-by-reference-to-const替换pass-by-value
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20
| class Person{ public: Person(); virtual ~Person(); private: std::string name; std::string address; }; class Student: public Person{ public: Student(); ~Student(); private: std::string schoolName; std::string schoolAddress; };
bool validateStudent(Student s); Student plato; bool platoIsOk = validateStudent(plato);
|
使用by value方式会导致一次Student拷贝构造,一次Person拷贝构造,四次string拷贝构造,六次析构函数!
而使用传递const引用参数能够避免这些构造和析构动作:
1
| bool validateStudent(const Student& s);
|
使用引用作为参数同时可以避免对象切割的问题
对象切割:当继承类对象以值传递方式被视为基类对象,基类构造函数将被调用,造成继承类的特化性质被切割,仅仅留下一个基类对象。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
| class Window{ public: std::string name() const; virtual void display() const; }; class WindowWithScrollBars: public Window{ public: virtual void display() const; };
void printNameAndDisplay(Window w){ std::cout<<w.name(); w.display(); }
WindowWithScrollBars wwsb; printNameAndDisplay(wwsb);
|
一般而言,可以合理假设”pass by value”并不昂贵的对象为内置类型,STL迭代器和函数对象,其它都需要传递常量引用。
21:必须返回对象时,别妄想返回其reference
并不是所有情况传递引用都是合理的。任何情况下看到一个reference声明式,都应该询问它的另一个名字是什么?
函数中创建新对象的途径有两种:在stack空间或者在heap空间创建。
1 2 3 4 5
| const Rational& operator* (const Rational &lhs, const Rational &rhs){ Rational result(lhs.n * rhs.n, lhs.d * rhs.d); return result; }
|
local对象将在函数返回前销毁,其引用将无定义。
1 2 3 4 5
| const Rational& operator* (const Rational &lhs, const Rational &rhs){ Rational *result = new Rational(lhs.n * rhs.n, lhs.d * rhs.d); return result; }
|
没有办法取得reference背后的隐藏指针,导致无法释放new开辟的内存空间。
一个必须返回新对象的函数就应该让函数返回一个新对象:
1 2 3 4
| inline const Rational operator* (const Rational &lhs, const Rational &rhs){ return Rational(lhs.n * rhs.n, lhs.d * rhs.d); }
|
在现在的编译器中:编译器会进行RVO/NRVO优化,直接构造出返回的对象,避免了拷贝构造。
22:将成员变量声明为private
语法一致性:客户唯一访问对象的方法就是通过成员函数。
实现访问控制:如果将成员变量声明为public,每个人都能读写;声明为private变量,通过成员函数来取得设定其值,可以实现“出准访问”,“只读访问”,“读写访问”。
封装:隐藏成员变量,只有成员函数可以影响它们,保留了日后变更实现的权利。如果不隐藏它们,当改变public类型变量将破坏大多数客户代码。从封装的角度观察:只有两种访问权限:private(提供封装)和其他(不提供封装)。
23:宁以non-member、non-friend替换member函数
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
| class WebBrowser{ public: void clearCache(); void clearHistory(); void removeCookies(); void clearEverything(){ clearCache(); clearHistory(); removeCookies(); } };
void clearBrowser(WebBrowser &wb){ wb.clearCache(); wb.clearHistory(); wb.removeCookies(); }
|
采用哪一个版本的函数更好?
从封装的角度考虑:如果某些东西额被封装,它就不在可见。愈多的东西被封装,愈少的人能够看见。我们就能有愈大的弹性来改变它。我们计算能够访问数据的函数的数量,作为粗糙的度量。愈多的函数可访问它,数据的封装性愈低。
因此:由于能够访问private成员变量的函数只有类的成员函数和友元函数,提供较大封装性的是non-member non-friend函数。
值得注意的是,只在意封装性让函数成为class的non-member并不意味着它不可以是另一个class的member
1 2 3 4 5 6 7 8 9 10 11 12 13
| namespace WebBrowserStuff{ class WebBrowser{ ... }; void clearBrowser(WebBrowser &wb); }
namespace WebBrowserStuff{ ... }
namespace WebBrowserStuff{ ... }
|
将所有便利函数放在多个头文件内但属于同一个命名空间,意味着客户可以轻松拓展便利函数。通过添加更多的non-member non-friend函数到此命名空间。
24:若所有参数皆需类型转换,请为此采用non-member函数
1 2 3 4 5
| class Rational{ public: const Rational operator* (const Rational &rhs) const; };
|
当需要混合运算就会出现错误
1 2 3
| Ratinoal oneHalf(1, 2); Rational result = oneHalf * 2; Rational result = 2 * oneHalf;
|
只有当参数被列于参数列内,这个参数才是隐式类型转化的合格参与者。
将operator*成为一个non-member函数,让编译器在每一个实参上进行隐式类型转化:
1 2 3
| const Rational operator*(const Rational& lhs, const Rational &rhs){ return Rational(lhs.numerator() * rhs.numerator(), lhs.denominator() * rhs.denominator()); }
|
1
| Rational result = 2 * oneHalf
|
本例中operator操作都能够通过共有接口来实现,因此就不需要将这个函数声明为friend函数了。
25:考虑写出一个不抛出异常的swap函数
当
26:尽可能延后变量定义式出现的时间
不应该只延后变量的定义,尝试延后定义直到能够赋予它初值为止。
1 2 3 4 5 6 7 8 9 10
|
Widget w; for(int i = 0; i < n; ++i){ w = 取决于i的值; }
for(int i = 0; i < n; ++i){ Widget w(取决于i的值); }
|
除非效率优先否则应该尽量使用方法B。
27:尽量少做转型动作
28:避免返回handles指向对象内部部分
29:为”异常安全“而努力是值得的
30:透彻了解inlining的里里外外
在做出任何有关inline的决定之前,都应该注意这个条款。
31:将文件间的编译依存关系降至最低
读书笔记_Effective_C++_条款三十一:将文件间的编译依存关系降至最低(第一部分) - Jerry19880126 - 博客园 (cnblogs.com)
32:确定你的public继承塑模出is-a关系
33:避免遮掩继承而带来的名称
34:区分接口继承和接口实现
35:考虑virtual函数以外的其它选择
36:绝不重新定义继承而来的non-virtual函数
37:绝不重新定义继承而来的缺省参数值
38:通过复合塑模出has-a或”根据某物实现出“
复合是类型之间的一种关系,当某种类型的对象内含它种类型对象就是复合。
复合同义词:分层、内含、聚合、内嵌。
复合有两个意义:意味着has-a或者根据某物实现出
区分is-a和根据某物实现出
1 2 3 4 5 6 7 8 9 10
| template<class T> class Set{ public: bool member(const T &item) const; ... private: std::list<T> rep; };
|
39:明智而审慎地使用private继承
40:明智而审慎地使用多重继承