Skip to content

Latest commit

 

History

History
397 lines (271 loc) · 13.2 KB

effective-cpp-reading-note-1.md

File metadata and controls

397 lines (271 loc) · 13.2 KB

Effective C++ 1

C++ Advanced - Note: const、enum、inline;先初始化再使用变量;smart pointer 智能指针;virtual 虚函数;Destructor 析构函数;self-assignment 自赋值;new 和 delete;若不想使用编译器自动生成的函数,就该明确拒绝。


  • Created on 2014-05

基础名词解释

  • STL : Standard Template Library
  • TR1 : 一份RFC的规范,描述加入C++标准程序库的诸多新机能。
  • Boost : 一个网站,一个开源的C++程序库。大多数TR1的机能以它的工作为基础

条款2:尽量以const,enum,inline代替#define

perfer consts, enums, and inlines to #defines.

宏语言定义的变量名,如 #define ASPECT_RATIO 1.653中的ASPECT_RATIO,不会进入 symbol table,对于编译器不可见。

  • Extension:符号表,一种用于语言翻译器(例如编译器和解释器)中的数据结构。
  • 在符号表中,程序源代码中的每个标识符都和它的声明或使用信息绑定在一起,比如其数据类型、作用域以及内存地址。
  • 如常数表、变量名表、数组名表、过程名表、标号表等等,统称为符号表。

enum hack

  • 在类内,声明 enum{OneConstNum = 5}
  • 用以声明一个常数。而且取enum量的地址,但对const常量可取地址。

条款3:尽量使用const

use const whenever possible

基础:

char str[] = "test";
char *p = str;               // non-const ptr, non-const data
const char *p = str;      // non-const ptr, const data
char * const p = str;     // const ptr, non-const data
const char * const p = str; // const ptr, const data
char const *p = str;     // the same as "const char *p = str;"

迭代器:(注意,与前面常识有悖)

std::vector<int> vec;
...
const std::vector<int>::iterator iter = vec.begin();     // iter的作用像个 T* const
*iter = 10;     // 没错,改变的只是它所指向的内容
++iter;          // 出错,iter本身是const
...
std::vector<int>::const_iterator cIter = vec.begin();     // cIter的作用像个 const T*
*iter = 11;     // 出错,*cIter是const
++iter;          // 没错,改变cIter

常函数const不能改变任何成员变量,static变量除外。

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

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

make sure that objects are initialized before they're used.

赋值和初始化不同:

  • 用初始化列表来初始化自定义类型,而且效率更高
  • 如果初始化的是built-in类型,则效率与赋值一样;
  • 在构造函数内是赋值。
OneClass::OneClass()
     :theName(),
      theAddress(),
      thePhones(),
      num(0)
{...}
  • 假如一个自定义类型的成员变量,还可以这样使用nothing"()",去调用其default constructor。
  • 成员变量初始化顺序:其类内声明的顺序,不关于初始化列表的顺序。
  • 所以,初始化列表的顺序最好和其类内声明顺序一样。

编译单元

  • 产生单一目标文件(single object file)的那些源码
  • 单一源码文件加上其所include的头文件。

local static对象:

  • 函数内的static对象

不同的编译单元内的non-local static对象的初始化顺序无明确定义。

  • 解决方法:用local static 代替non-local static,即是使用Singleton单例模式。

条款5:C++默默编写并调用哪些函数

know what functions C++ silently writes and calls.

  • 默认构造函数
  • 复制构造函数
  • 析构函数
  • 赋值操作符

编译器产生的析构函数是non-virtual的。

编译器产生的copy构造函数和copy assignment操作符的版本,只是将non-static成员变量拷贝至目标对象而已。

如果你有声明了一个构造函数(无论有无参数),编译器便不会给你创建default的的构造函数了。

若base class的copy assignment操作符被声明为private,那么编译器会拒绝为其derived class生成一个copy assignment操作符。

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

  • 使用private声明它们,且不给出具体的定义。
  • 当客户企图拷贝时,编译器会阻止你;
  • 假如是友元或者成员函数试图拷贝的话,
  • 因为它们没有具体定义,linker链接器就会组织它们。

可以定义这样一个基类

class Uncopyable{
protected:
     Uncopyable(){}
     ~Uncopyable(){}
private:
     Uncopyable(const Uncopyable&);
     Uncopyable& operator=(const Uncopyable&);
}

class DerivedClass: private Uncopyable{
     ...
};

这样,帮助重用,而且其子类就都不能调用复制构造函数和复制赋值操作符了。

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

declare destructors virtual in polymorphic base classes.

一个基类指针指向子类对象, 当这个对象析构的时候,如果基类destructor非virtual, 那么只会调用基类的destructor,而子类的则没有被调用, 导致部分销毁对象,内存泄漏。

基类有virtual函数,其子类就必须有相关的函数实现,即使其不重写。

如果一个class不打算成为基类,就不要声明virtual函数。

要实现virtual函数,必须携带vptr(virtual table pointer)虚函数表指针 vptr指向一个有函数指针组成的数组,成为vtbl(virtual table)虚函数表。 每一个带虚函数的class都带有一个相应的virtual table。

当调用虚函数,取决于该对象的vptr虚函数指针所指的virtual table虚函数表, 编译器在其中寻找适当的函数指针。

而且这个虚指针增加了对象的大小,32位系统增加一个4Bytes的vptr,64位则增加8Bytes。 所以类内至少有一个virtual函数,才去声明虚的destructor。

尽量不要集成STL中不含有virtual函数的容器。

总结

  • 带有polymorphic多态性质的base classes应该声明virtual destructor。
  • 或它带有任何virtual函数,它就该有虚析构函数。
  • 若一个class不作为base class使用,或不是为了多态,就别声明虚析构函数。

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

prevent exceptions from leaving destructors.

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

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

C++不欢迎在析构函数中抛出异常。

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

never call virtual function during construction or destruction

在子类的构造函数运行时,子类自身类型被解析为基类, 调用的virtual函数是基类的,而不是子类的; 析构函数一样,子类自身类型被解析为基类……

解决方法: 最好将初始化代码,另外放在一个init初始化函数内。

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

have assignment operators return a reference to *this.

x = y = z;//连锁赋值

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

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

handle assignment to self in operator=.

赋值操作一般会先释放掉左值,再给左值赋值, 假如左值右值是同一个对象,会导致“在停止使用资源之前意外释放了它”! 所以

Widget& Widget::operator=(const Widget &rhs){
     if(this == &rhs) return *this; // 证同测试 identity test
     ...
}

还有其它方法的!

确保对象自我赋值时operator=有良好的行为。 有关技术包括:1.证同测试;2.copy-and-swap;3.精心周到的语句顺序。

2.copy-and-swap;

Widget tmp(rhs);
swap(temp); // 交换*this和rhs的数据
return *this;

3.精心周到的语句顺序。

Bitmap *pOrig = pb;
this.pb = new Bitmap(*rhs.pd); // 先创建复本
delete pOrig; // 再delete原本
return *this;

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

copy all parts of an object

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

PriorityCustomer::PriorityCustomer(const PriorityCustomer& rhs)
     : Customer(rhs),
      priority(rhs.priority){
     ...
}

PriorityCustomer& PriorityCustomer::operator=(const PriorityCustomer& rhs){
     Customer::operator=(rhs);
     priority = rhs.priority; // 其它成员变量的赋值
     return *this;
}

条款13:以对象管理资源

use objects to manage resources.

Investment *pInv = createInvestment();
... // 中间的代码可能抛出异常,可能return,
     // 导致最后无法运行到delete那一行,内存泄漏
delete pInv;

可以把资源放到对象里面,利用析构函数自动调用的机制,确保资源释放的问题。 例如智能指针shared_ptr、unique_ptr。

std::auto_ptr<Investment> pInv(createInvestment());

关键:

  1. 获得资源后立即放进管理对象内。这个观念被称为“资源取得时机便是初始化时机”。 (Resource Acquisition Is Initialization——RAII) 2.管理对象运用析构函数确保资源被释放。 若资源释放动作可能导致抛出异常,看条款8怎么处理。(另外使用一个普通函数进行该操作)

auto_ptr和tr1::shared_ptr、unique_ptr等都在其析构函数内做delete而不是delete[], 意味着别将动态分配的array数组交给智能指针!会内存泄漏。

C++并没有特别针对“动态分配数组”而设计类似的智能指针, vector、string可以取代动态分配而得的数组。 boost::scored_array和boost::shared_array classes,就提供了以上你想要的内容, 可是还没有采纳入C++标准库中。

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

think carefully about copying behaviour in resource-managing classes.

RAII对象被复制,应该怎么处理,有两种方式:

  1. 禁止复制。因为这样并不合理。
  2. 使用类似于shared_ptr的引用计数(reference-count)。

而且要注意:

  1. 深拷贝底部资源
  2. 或者转移底部资源的拥有权,如unique_ptr

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

provide access to raw resource in resource-managing classes.

  1. APIs往往要求访问原始资源raw resources,所以每个RAII class应该提供一个get()方法,
  2. 对原始资源的访问可能经由显式转换或隐式转换。

一般隐式转换较方便,显式转换较安全。

class Font(){
     ...
     FontHandle f; // 原始资源
     operator FontHandle() const // 隐式转换
     { return f; }
}

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

use the same form in corresponding uses of new and delete.

避免 typedef std::string AddressLines[4]; // 忘了typedef的用法自己查查!

因为当你 std::string* pal = new AddressLines;

别人不知道该用 delete pal; 还是 delete[] pal;(这个才正确)。

new时,使用了[],必须在相应的delete表达式中也试用[]。

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

store newed objects in smart pointers in standalone statments.

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

processWidget(std::tr1::shared_ptr(new Widget), priority()); 不同编译器以何种顺序执行:

  • A. new Widget
  • B. tr1::shared_ptr的构造函数
  • C. 调用priority()

假如以ACB顺序执行,“调用priority()”时可能抛出异常, 导致new出的Widget没有及时放入智能指针, 还是导致内存泄漏了……

所以分开写成这样: std::tr1::shared_ptr pw(new Widget); processWidget(pw, priority());

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

make interface easy to use correctly and hard to use incorrectly.

函数调用的时候,参数的顺序可能出错,所以可以通过导入相应的类型预防。 如:

Date(const Month& m, const Day& d);
// 而非直接用int指代月份和日,还可以用enum
Investment* createInvestment();
// 这样要求用户记得delete,而且不超过1次

不能将责任推给智能指针,因为用户还是可能忘记使用。所以

std::tr1::shared_ptr<Investment> createInvestment();

强行返回智能指针,先发制人,要求客户使用智能指针。

智能指针可以指定删除器,而非总是使用delete。 std::tr1::shared_ptr pInv(0, getRidOfInvestment); getRidOfInvestment是作为删除器的函数名(函数指针)。 上例并不够好,0只是个int,而非空指针,而智能指针坚持要一个指针,所以

std::tr1::shared_ptr<Investment> pInv(static_cast<Investment*>(0), getRidOfInvestment);

阻止误用的办法:建立新类型、限制类型上的操作,束缚对象值,以及消除客户的资源管理责任。 tr1::shared_ptr支持定制删除器 (custom deleter)。 可以防范DLL问题,可被用来自动解除互斥锁(见条款14,原书)。