《Effective C++》读书笔记(第一部分)

有人说C++程序员可以分为两类,读过Effective C++的和没读过的。世界顶级C++大师Scott Meyers 成名之作的第三版的确当得起这样的评价。

本书并没有你告诉什么是C++语言,怎样使用C++语言,而是从一个经验丰富的C++大师的角度告诉程序员:怎么样快速编写健壮的,高效的,稳定的,易于移植和易于重用的C++程序。

本书共分为9节55个条款,从多个角度介绍了C++的使用经验和应遵循的编程原则。

本系列文章分两部分概括介绍了《Effective C++》每个条款的核心内容,本文是第一部分,第二部分为:《Effective C++》读书笔记(第二部分)

1. 让自己习惯C++(Accustoming your self to C++)

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

本条款提示读者,C++已经不是一门很单一的语言,而应该将之视为一个由相关语言组成的联邦。从语言形式上看,它是一个多重范型编程语言(multiparadigm programminglanguage) ,一个同时支持过程形式(procedural)、面向对象形式(object-oriented)、函数形式(functional) 、泛型形式(generic) 、元编程形式(metaprogramming )的语言,从语言种类上看,它由若干次语言组成,分别为:

(1) C。说到底C++ 仍是以C 为基础。区块(blocks) 、语句( statements) 、预处理器( preprocessor) 、内置数据类型(built-in data types) 、数组(arrays) 、指针(pointers) 等统统来自C。

(2) Object-Oriented C++。这部分也就是C with Classes 的: classes (包括构造函数和析构函数) ,封装( encapsulation) 、继承( inheritance) 、多态(polymorphism) 、virtual 函数(动态绑定) ……

(3) Template C++。这是C++ 的泛型编程(generic programming) 部分,也是大多数程序员经验最少的部分。Template 相关考虑与设计己经弥漫整个C++,实际上由于templates 威力强大,它们带来崭新的编程范型(programming paradigm) ,也就是所谓的templatemetaprogramming (TMP,模板元编程)

(4) STL。 STL 是个template 程序库,它是非常特殊的一个。它对容器(containers) 、迭代器(iterators) 、算法(algorithms) 以及函数对象(function objects) 的规约有极佳的紧密配合与协调。

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

本条款讨论了C语言中的#define在C++程序设计中的带来的问题并给出了替代方案。

C语言中的宏定义#define只是进行简单的替换,对于程序调试,效率来说,会带来麻烦,在C++中,提倡使用const,enum和inline代替#define;然而,有了consts 、enums 和inlines,我们对预处理器(特别是#define) 的需求降低了,但并非完全消除。#include 仍然是必需品,而#ifdef/#ifndef 也继续扮演控制编译的重要角色。目前还不到预处理器全面引迫的时候。

条款03: 尽可能使用const

本条款总结了Const的使用场景和使用它带来的好处。

关键字canst 多才多艺。你可以用它在classes 外部修饰global 或namespace作用域中的常量,或修饰文件、函数、或区块作用域(block scope) 中被声明为static 的对象。你也可以用它修饰classes 内部的static 和non-static 成员变量。面对指针,你也可以指出指针自身、指针所指物,或两者都(或都不〉是const。你应该尽可能地使用const,这样降低程序错误,使程序易于理解。

此外,一个编程技巧是:当const 和non-const 成员函数有着实质等价的实现时,令non-const 版本调用const 版本可避免代码重复:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
class TextBlock {
 
  public :
 
   const char & operator[] (std:: size_t position) const {
 
   ……
 
   return text[position];
 
  }
 
  char & operator[] (std::size t position)  {
 
   return const_cast < char &>( static_cast < const TextBlock&>(* this ) [position]);
 
   //将op[]返回值的const 转除为*this 加上cons, 调用const op[]
 
}

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

本条款告诫程序员,在C++程序设计中,应该对所有对象初始化,以避免不必要的错误,同时,给出了高效初始化对象的方法和正确初始化对象的方法。

(1)初始化构造函数最好使用成员初值列(member initialization list) ,而不要在构造函数本体内使用赋值操作(assignment) 。初值列出的成员变量,其排列次序应该和它们在class 中的声明次序相同。

考虑一个用来表现通讯簿的class ,其构造函数如下:

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
27
28
29
30
31
32
33
34
35
class PhoneNumber { ... };
 
class ABEntry { //ABEntry =“Address Book Entry"
 
  public :
 
   ABEntry( const std::string& name, const std::string& address , const std::list& phones);
 
  private :
 
   std::string theName;
 
   std::string theAddress;
 
   std::list thePhones;
 
   int numTimesConsulted;
 
};
 
ABEntry: :ABEntry( const std: :string& nane , const std: : string& address,
 
const std::list& phones)
 
   theName = narne; //这些都是赋值(assignments) ,
 
   theAddress = address; //不是始化(initializations)。
 
   thePhones = phones;
 
   numTimesConsulted = 0;
 
   int num TimesConsulted;
 
}

正确而又高效的初始化对象的方法是:

1
2
3
4
5
6
7
8
9
10
11
12
13
ABEntry: :ABEntry( const std: :string& nane , const std: : string& address,
 
const std::list& phones)
 
: theName(name),
 
theAddress(address), //这些都是初始化
 
thePhones(phones),
 
numTimesConsulted(0)
 
{} // 构造函数体是空的

C++ 有着十分固定的”成员初始化次序”。次序总是相同: base class早于其derived classes 被初始化,而class 的成员变量总是以其声明次序被初始化。回头看看ABEntry. 其theName 成员永远最先被初始化,然后是theAddress,再来是thePhones,最后是numTimesConsulted。即使它们在成员初值列中以不同的次序出现(很不幸那是合法的),也不会有任何影响。

(2)C++ 对”定义于不同编译单元内的non-local static 对象”的初始化次序并无明确定义。为免除”跨编译单元之初始化次序”问题,请以local static 对象替换non-local static 对象。

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
27
28
29
30
31
32
33
34
35
class FileSystem { ... };
 
FileSystem& tfs() //代替tfs对象
 
{
 
   static FileSystem fs; // 以local static的方式定义和初始化object
 
   return fs; // 返回一个引用
 
}
 
class Directory { ... };
 
Directory::Directory( params )
 
{
 
   ...
 
   std:: size_t disks = tfs().numDisks();
 
   ...
 
}
 
Directory& tempDir() // 代替tempDir对象,
 
{
 
   static Directory td;
 
   return td;
 
}

2. 构造/析构/赋值运算(Constructors,Destructors,and Assignment Operators)

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

本条款告诉程序员,编译器自动为你做了哪些事情。

用户定义一个empty class (空类),当C++ 处理过它之后,如果你自己没声明,编译器就会为它声明(编译器版本的)一个copy 构造函数、一个copy assignment操作符和一个析构函数。此外如果你没有声明任何构造函数,编译器也会为你声明一个default 构造函数。所有这些函数都是public 且inline 。举例,如果你写下:

1
class Empty { };

这就好像你写下这样的代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
class Empty {
 
  public :
 
   Empty() { ... }
 
   Empty( const Empty& rhs) { ... )
 
   -Empty( ) { ... }
 
   Empty& operator=( const Empty& rhs) { ... }
 
};

需要注意的是,只要你显式地定义了一个构造函数(不管是有参构造函数还是无参构造函数),编译器将不再为你创建default构造函数。

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

本条款告诉程序员,如果某些对象是独一无二的(比如房子),你应该禁用copy 构造函数或copy assignment 操作符,可选的方案有两种:

(1) 定义一个公共基类,让所有独一无二的对象继承它,具体如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
class Uncopyable {
 
  protected : //允许derived对象构造和析构
 
   Uncopyable () {}
 
   -Uncopyable(} { }
 
  private :
 
   Uncopyable( const Uncopyable&}; //但阻止copying
 
   Uncopyable& operator=( const Uncopyable&);
 
};

为阻止HomeForSale对象被拷贝,唯一需要做的就是继承Uncopyable:

1
2
3
4
5
class HomeForSale: private Uncopyable {
 
  
 
};

这种方法带来的问题是,可能造成多重继承,这回导致很多麻烦。

(2) 创建一个宏,并将之放到每一个独一无二对象的private中,该宏为:

1
2
3
4
5
6
7
8
9
// 禁止使用拷贝构造函数和 operator= 赋值操作的宏
 
// 应该类的 private: 中使用
 
#define DISALLOW_COPY_AND_ASSIGN(TypeName) \
 
TypeName( const TypeName&); \
 
void operator=( const TypeName&)

这种方法比第一种方法好,google C++编程规范中提倡使用该方法。

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

本条款阐述了一个程序员易犯的可能导致内存泄漏的错误,总结了两个程序员应遵守的百编程原则:

(1)polymorphic (带多态性质的) base classes 应该声明一个virtual 析构函数。如果

class 带有任何virtual 函数,它就应该拥有一个virtual 析构函数。这样,但用户delete基类指针时,会自动调用派生类的析构函数(而不是只调用基类的析构函数)。

(2)Classes 的设计目的如果不是作为base classes 使用,或不是为了具备多态性(polymorphically) ,就不该声明virtual 析构函数。这是因为,当用户将一个函数声明为virtual时,C++编译器会创建虚函数表以完成动态绑定功能,这将带来时间和空间上的花销。

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

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

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

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

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

本条款告诉程序员一个默认的法则:为了实现“连锁赋值“,应令operator= 返回一个reference to *this。

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

本条款讨论了几种编写复制构造函数的正确方法。给出的结论是:确保当对象自我赋值时operator= 有良好行为。其中技术包括比较”来源对象”和”目标对象”的地址、精心周到的语句顺序、以及 copy-and-swap。

(1) 复制构造函数的一种编写方式如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
Widget& Widget::operator=( const Widget& rhs)
 
{
 
   if ( this == &rhs) return * this ; //判断是否为同一个对象,如果是自我复制,直接返回
 
   delete pb;
 
   pb = new Bitmap(*rhs.pb);
 
   return * this ;
 
}

这个版本存在异常方面的麻烦,即,如果”new Bitmap” 导致异常(不论是因为分配时内存不足或因为Bitmap 的copy构造函数抛出异常) , Widget 最终会持有一个指针指向被删除的Bitmap 。

(2) 让operator= 具备”异常安全性”往往自动获得”自我赋值安全”的回报。因此愈来愈多人对”自我赋值”的处理态度是倾向不去管它,把焦点放在实现”异常安全性” (exception safety) 上,即:

1
2
3
4
5
6
7
8
9
10
11
12
13
widget& Widget::operator=( const Widget& rhs)
 
{
 
   Bitmap* pOrig = pb;
 
   pb = new Bitmap(*rhs.pb);
 
   delete pOrig;
 
   return * this ;
 
}

如果”newBitmap” 抛出异常, pb (及其栖身的那个Widget) 保持原状。即使没有证同测试(identity test) ,这段代码还是能够处理自我赋值,但这种方法效率比较低。

(3) 另外一种比较高效的方法是:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
class Widget {
 
   ……
 
   void swap(Widget& rhs); //交换*this 和rhs 的数据:详见条款29
 
   ……
 
};
 
Widget& Widget::operator=(Widget rhs) //rhs是被传对象的一份复件(副本),注意这里是pass by value.
 
{
 
   swap(rhs); //将*this 的数据和复件/副本的数据互换
 
   return * this ;
 
}

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

本条款阐释了复制对象时容易犯的一些错误,给出的教训是:

(1) Copying 函数应该确保复制”对象内的所有成员变量”及”所有base class 成分”。

(2) 不要尝试以某个copying 函数实现另一个copying 函数。应该将共同机能放进第三

个函数中,并由两个coping 函数共同调。换句话说,如果你发现你的copy 构造函数和copy assignment 操作符有相近的代码,消除重复代码的做法是,建立一个新的成员函数给两者调用。这样的函数往往是private 而且常被命名为init。

3. 资源管理(Resource Management)

条款13: 以对象管理资源

本条款建议程序员使用对象管理资源(如申请的内存),给出的经验是:

(1) 为防止资源泄漏,请使用RAII(“资源取得时机便是初始化时机” (Resource Acquisition Is Initialization; RAII))对象,它们在构造函数中获得资源并在析构函数中释放资源。

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

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

本条款提醒程序员,使用资源管理类时需根据实际需要管理copying行为,常见的有:抑制copying、施行引用计数法。

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

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

(2) 对原始资源的访问可能经由显式转换或隐式转换。一般而言显式转换(如调用get()函数)比较安全,但隐式转换对客户比较方便。

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

本条款给出了程序员在申请和释放资源时常犯的错误,给出的经验是:

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

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

本条款指出了一个使用智能指针时常犯的错误,避免该错误可以这样做:

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

processWidget(std::trl::shared ptr (new Widget) , priority());

在调用processWidget之前,编译器必须创建代码,做以下三件事:

(1) 调用priority

(2) 执行”new Widget”

(3) 调用trl: : shared_ptr 构造函数

不同的C++ 编译器执行这三条语句的顺序不一样,但对priority的调用可以排在第一或第二或第三执行。如果编译器选择以第二顺位执行且priority函数抛出了异常,则新创建的对象Widget将导致内存泄漏,解决方法如下:

std::trl::shared_ptr pw(new Widget); //在独立语句内以智能指针存储Widget对象

processWidget(pw, priority()); //这个调用肯定不存在内存泄漏

4. 设计与声明(Designs and Declarations)

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

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

条款20: 提倡以pass-by -reference-to-const 替换pass-by-value

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

切割问题(slicing problem)(所谓切割问题,是指派生类的对象传给基类类型的参数时,派生对象中的一些属性会被截断),需要注意的是,该规则并不适用于内置类型,以及STL 的迭代器和函数对象。对它们而言,pass-by-value 往往比较适当(实际上,STL中的迭代器和函数对象只支持值传递)。

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

本条款告诫程序员:绝不要返回pointer 或reference指向一个local stack 对象,或返回reference 指向一个heap-allocated对象,或返回pointer 或reference指向一个local static 对象而有可能同时需要多个这样的对象。

下面一一举例说明。

(1) 如果返回pointer 或reference指向一个local stack 对象:

1
2
3
4
5
6
7
const Rational& operator* ( const Rational& lhs, const Rational& rhs) {
 
   Rational result(lhs.n * rhs.n, lhs.d * rhs.d); //警告!糟糕的代码!
 
   return result;
 
}

解释:result是local对象,而local 对象在函数退出前被销毁,这导致返回值坠入”无定义行为”。

(2) 返回reference 指向一个heap-allocated对象

1
2
3
4
5
6
7
const Rational& operator* ( const Rational& lhs, const Rational& rhs) {
 
   Rational* result = new Rational(lhs.n * rhs.n, lhs.d * rhs.d);
 
   return *result;
 
}

这种方式很容易造成内存泄露,如:

1
2
3
Rational w, x, y , z;
 
w = x * y * z; //与operator*(operator*(x, y) , z) 相同,内存泄露

(3) 返回pointer 或reference指向一个local static

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
const Rational& operator* ( const Rational& lhs, const Rational& rhs) {
 
   static Rational result;
 
   result = ... ;
 
   return result;
 
}
 
if ((a * b) == (c * d)) {
 
   //当乘积相等时,做适当的相应动作;
 
} else {
 
   //当乘积不等时,做适当的相应动作;
 
}

这样做的问题是,(a * b) == (c * d)永远为true。

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

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

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

当类的构造函数(未声明为explicit)中包含参数时,该参数类型的对象或者数可隐式转换为该对象。如果多个这样的对象之间进行加减乘除,且要让他们全部进行类型转换,需要定义non-member函数(如友元函数)。

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

5. 实现(Implementations)

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

本条款告诉程序员,如果你定义了一个变量且该类型带一个构造函数或析构函数,当程序到达该变量时,你要承受构造成本,而离开作用域时,你要承受析构成本。为了减少这个成本,最好尽可能延后变量定义式的出现时间。

举例说明:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// 此函数太早定义了变量"encrypted"
 
string encryptPassword( const string& password)
 
{
 
   string encrypted;
 
   if (password.length() < MINIMUM_PASSWORD_LENGTH) {
 
    throw logic_error( "Password is too short" );
 
  }
 
  //进行必要的操作,将口令的加密版本放进encrypted之中;
 
   return encrypted;
 
}

如果该函数抛出异常,变量encrypted便不会被使用。较好的做法是将变量”encrypted”的定义放到要用它的前一句或者能够给它初值实参。

条款27:尽量少做转型

本条款论证了为什么要尽量少做类型转换,并告诉读者,如果必须要进行类型转换,有哪些注意事项。

常见的有三种类型转换方式:

(1) C风格:(T)expression

(2) 函数风格:T(expression)

(3) C++ style cast

[1] const_cast(expression) : 移除变量的const属性

[2] dynamic_cast(expression) : 安全向下转型,即:基类指针/引用到派生类指针/引用的转换。如果源和目标类型没有继承/被继承关系,编译器会报错;否则必须在代码里判断返回值是否为NULL来确认转换是否成功。

[3] reinterpret_cast(expression):底层转换

[4] static_cast(expression):强迫隐式转换,如,将non-const对象转换为const对象,将int转换为double类型。

对于这几种类型转换,给出的建议是“

(1) 如果可以,尽量避免转型,特别是在注重效率的代码中避免dynamic_cast

(2) 宁可使用C++ style转型,不要使用旧式转型。前者容易识别出来,而且也比较有分门别类的指掌。

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

本条款告诫程序员,不要在类方法中返回handles(包括references、指针、迭代器)指向对象内部成分,因为这很容易导致空悬、虚吊(dangling)的对象。

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

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

Inline函数可免除函数调用成本,提高程序执行效率,但它也会带来负面影响:(1)增大目标代码的大小,有时候会非常庞大,需要动用虚存,这将大大降低程序执行速度。 (2) inline 函数无法随着程序库的升级而升级。换句话说如果f 是程序库内的一个inline 函数,客户将”f 函数本体”编进其程序中,一旦程序库设计者决定改变f ,所有用到f 的客户端程序都必须重新编译。总之,将大多数inlining 限制在小型、被频繁调用的函数身上才是最明智的选择(根据80-20经验准则,80%的时间花在20%的函数上)。

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

本条款介绍了降低文件间编译依存关系的几种方法。

常见的方法有两种:Handle class和Interface class.

(1) Handle class. main class内含一个指针成员,指向其实现类。这般设计常被称为pimpl idiom (pimpl 是”pointer to implementation” 的缩写,这种class称为“Handle class”。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
#include "Person.h"
 
#include "PersonImpl.h" //我们也必须#include PersonImpl的class 定义式,否则无法调用其成员函数:注意, PersonImpl 有着和Person完全相同的成员函数,两者接口完全相同。
 
Person::Person( const std::string& name , const Date& birthday,
 
const Address& addr)
 
: pImpl( new PersonImpl(name, birthday,  addr))
 
{}
 
std::string Person;;name() const {
 
   return p Impl->name( );
 
}

(2) Interface class. 实际上就是抽象基类

原创文章,转载请注明: 转载自董的博客

本文链接地址: http://dongxicheng.org/cpp/effective-cpp-part1/

你可能感兴趣的:(C/C++,Effective,C++,读书笔记,C++,effective)