Effective C++

《总之,好记性不如烂笔头!把你遗忘的都记下来吧!》

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; // 常量的声明式并初始化(C++11)
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);
// 返回const对象可以防止下面的错误
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:
// bitwise const声明,在函数内并不修改成员变量
char& operator[](std::size_t position)const{
return pText[position];
}
private:
char *pText; // 文本长度,可能是随时变化的, 如果变换文本记录长度失效
};
// 但是利用返回的引用,仍可能修改成员变量
const CTextBlock cctb("Hello");
char *pc = &cctb[0];
*pc = 'J'; // 将'H'修改为了'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){
// 即使是在const成员函数中也能修改,因为其被定义为mutable的了
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) {
// 转调用const版本的operator[]
return const_cast<char &>( // 利用const_cast<>()去除返回值的const性质
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赋值运算符和一个析构函数,如果没有声明默认构造函数,同时也会生成一个默认构造函数。所有这些函数都是publicinline的。

**对于内含引用、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(){} // 正确的定义:virtual ~Product(){}
};
class ConcreteProduct1 : public Product{};
class ConcreteProduct2 : public Product{};
class Creator{
public:
~Creator(){} // 正确的定义:virtual ~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
2
// px和py指向同一个对象
*px = *py;

为了防止自我赋值时销毁自身

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
// 使用auto_ptr管理
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);// 锁住pm
void unlock(Mutex *pm); // 解锁pm
class Lock{
public:
// unlock是指定的删除器,mutexPtr析构函数将在引用计数为0时调用unlock
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隐式转化为FontHandle后复制它

当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 [] stringptr1 错误,未定义的行为
delete [] stringPtr2;
//delete stringPtr2; 错误,未定义的行为

如果调用new时使用[],必须在delete时也使用[],反之亦然。

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

1
2
3
4
5
6
7
8
// 说明C++参数的构建顺序是不确定的
// 函数定义
int priority();
void processWidget(std::shared_ptr<Widget> pw, int priority);

// 调用
//processWidget(new Widget, priority());// 调用将失败,由于不存在Widget*到std::shared_ptr<Widget>
processWidget(std::shared_ptr<Widget>(new Widet), priority());// 成功的调用,但是可能导致资源泄露

执行上面的函数有三件事:1、调用new Widget 2、调用std::shared_ptr构造函数 3、 调用priority()

但是它们的顺序确是不确定的,存在new Widget调用后,再调用priority()导致异常,从而智能指针接受不到的情况。

解决的方法:

1
2
3
// 先创建Widget放入智能指针,再作为参数
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); // 以pass by value方式调用函数

使用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;
};
// 打印函数 使用const Window& w参数就不会被切割
void printNameAndDisplay(Window w){
std::cout<<w.name();
w.display();
}
// 传递WindowWithScrollBars
WindowWithScrollBars wwsb;
printNameAndDisplay(wwsb);// 总是调用Window::display

一般而言,可以合理假设”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
// 让non-member函数位于class的同一个命名空间
namespace WebBrowserStuff{
class WebBrowser{ ... };
void clearBrowser(WebBrowser &wb);
}
// 头文件"webbrowserbookmarks.h"
namespace WebBrowserStuff{
...// 与书签相关的便利函数
}
// 头文件“webbrowsercookies.h"
namespace WebBrowserStuff{
...// 与cookie相关的便利函数
}

将所有便利函数放在多个头文件内但属于同一个命名空间,意味着客户可以轻松拓展便利函数。通过添加更多的non-member non-friend函数到此命名空间。

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

1
2
3
4
5
class Rational{
public:
// 将operator*写成成员函数的写法
const Rational operator* (const Rational &rhs) const;
};

当需要混合运算就会出现错误

1
2
3
Ratinoal oneHalf(1, 2);
Rational result = oneHalf * 2;// 正确 2被隐式类型转化了
Rational result = 2 * oneHalf;// 错误 2不在*参数内,不能隐式转化

只有当参数被列于参数列内,这个参数才是隐式类型转化的合格参与者。

将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
// 对于下面两个方法
// 方法A:定义在循环外
Widget w;
for(int i = 0; i < n; ++i){
w = 取决于i的值;
}
// 方法B:定义于循环内
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
// 例如set可以根据一个list实现
template<class T>
class Set{
public:
bool member(const T &item) const;
...
private:
// 内含list,根据list实现
std::list<T> rep;
};

39:明智而审慎地使用private继承

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


Effective C++
http://example.com/2024/07/31/Effective/
作者
John Doe
发布于
2024年7月31日
许可协议