《Effective C++》学习笔记一

目录

1. 将C++看作一个语言联邦
2. 尽量使用 const,enum,inline 替换 #define)
3. 尽可能使用 const
4. 确定对象使用之前已被初始化
5. 了解C++默认编写并调用哪些函数
6. 若不想使用编译器自动生成的函数,就明确拒绝
7. 为多态基类声明 virtual 析构函数
8. 别让异常逃离析构函数
9. 不在构造和析构函数调用 virtual 函数
10. 让 operator= 返回一个 reference to *this
11. 在 operator= 中处理自我赋值
12. 复制对象时不要忘记每一部分
13. 以对象管理资源
14. 在资源管理类中小心 copying 行为
15. 在资源管理类中提供对原始资源的访问
16. 成对使用 new 和 delete 时要采取相同形式
17. 以独立语句将 newed 对象置入智能指针
18. 让接口容易被正确使用
19. 设计 class 犹如设计 type
20. 以 pass-by-reference-to-const 替换 pass-by-value
21. 必须返回对象时别返回其reference
22. 将成员变量声明为 private
23. 以 non-member、non-friend 替换 member 函数


1. 将C++看作一个语言联邦

C++的次语言有C,Object-Oriented C++,Template C++

2. 尽量使用const,enum,inline替换 #define

例如代码:

#define MAX_SIZE 100 //MAX_SIZE可能不会进入记号表,导致错误难以追查

建议替换为:

const int max_size = 100

当你class编译期需要一个class常量,若编译器不允许”static整数型常量“完成”int class初值设定“,使用enum。

class Test{
private:
  static const int max_size = 100;
}
// the enum hack做法
class Test{
private:
  enum { max_size = 100 };
}
  • 对于单纯常量,最好以const,enum替换 #defines。
  • 对于类似函数的宏,改用inline函数替换#defines。

3. 尽可能使用const

可以把const用在classes外部修饰global或namespace作用域中的常量,或修饰文件、函数、区块作用域中被声明为static的对象,也可以用它修饰classes内部的static和non-static成员变量。

// 以下两种写法意义相同
void f1(const test* t);
void f2(test const *t);
  • 将某些东西声明为const可帮助编译器侦探出错误用法。
  • 编译器强制实施bitwise constnnss,但你编写程序时应该使用”概念上的常量性“。
  • 当const和non-const成员函数有着实质等价的实现时,让non-const调用const可免于代码重复。

4. 确定对象使用之前已被初始化

C++规定,对象的成员变量初始化动作发生在进入构造函数本体之前。对于class的构造函数使用member initialization list完成初始化操作更可取。
C++对“定义在不同编译单元里的non-local static对象”的初始化相对次序无明确定义,最常见形式就是多个编译单元内的non-local static对象由“模板隐式具现化,implicit template instantiations”形成,不可能决定正确次序。解决办法是:将每个non-local static对象搬到自己的专属函数内。

  • 为内置型对象进行手工初始化,因为C++不能保证初始化它们。
  • class的构造函数使用member initialization list,其排列次序与它们在class中声明的次序相同。
  • 为避免“跨编译单元的初始化次序”问题,请以local static对象替换non-local static对象。

5. 了解C++默认编写并调用哪些函数

对于一个class,如果你没有自己声明,编译器就会为你声明一个拷贝构造函数,拷贝赋值函数和析构函数,甚至是default构造函数。所有这些函数都是public inline的。

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

如果你不希望你的copy构造函数和copy assignment操作符等被外界函数调用,你得自行声明他们为private。

class BaseUncopyable{
protected:
  BaseUncopyable(){}  //允许构造
  ~BaseUncopyable(){}  //允许析构
private:
  BaseUncopyable(const BaseUncopyable&);  //阻止copy
  BaseUncopyable& operator=(const BaseUncopyable&);  //阻止copy assignment
};
// Test对象
class Test: private BaseUncopyable{
 ...
};

只要是member函数或friend函数尝试进行使用copy构造函数,编译器便会拒绝,因为base class中是private的。继承base class不一定得是public,析构函数不一定得是virtual。

  • 为阻止编译器自动提供的机能,可将成员函数声明为private且不予实现,像BaseUncopyable做法一样。

7. 为多态基类声明virtual析构函数

为防止”局部销毁对象“造成资源泄露,应该给base class一个virtual析构函数。如果一个class不被当做base class,令其析构函数为virtual是不好的,因为如果Point class内含有virtual函数,其对象的体积会增加。
欲实现virtual函数,对象必须携带某些信息用来在运行期决定哪一个virtual函数被调用,这些信息由vptr(virtual table pointer)指针指出。vptr指向一个由函数指针构成的数组vtbl(virtual table),每一个带有virtual函数的class都有一个对应的vtbl。当对象调用某一virtual函数时,实际被调用的函数取决于该对象的vptr所指的vtbl。
拒绝继承标准容器或其它带有non-virtual析构函数的class。

  • polymorphic(多态性质的)base classes应该声明一个virtual析构函数。
  • classes设计的目的不是base class,或不是为了具备多态性,就不该声明virtual析构函数。

8. 别让异常逃离析构函数

class DBConn{
public:
  ~DBConn(){  //确保db总能被关闭。但是一旦调用产生异常,析构函数就会传播该异常
    db.close();
  }
}

避免的办法1:

DBConn::~DBConn{
  tr{ db.close(); }
  catch(...){ 
  //记下close调用失败
  std::abort(); }
}

避免的办法2:

DBConn::~DBConn{
  try{ db.close(); }
  catch(...){ //记下close调用失败 }
}

以上两种做法都无法对“导致close抛出的异常”做出反应。解决办法是使用DBConn接口

class DBConn{
public:
  void close(){
    db.close();
    closed = true;
  }
  ~DBConn(){
    if( !closed ){
      try{db.close(); } //关闭连接
      catch(...){ //记下close调用失败 }
    }
  }
private:
  bool closed;
};
  • 不要让析构函数抛出异常。如果一个被析构函数调用的函数可能抛出异常,析构函数应该捕捉他们并不让他们继续传播。
  • 如果需要对某个函数运行期抛出的异常做回应,则class应提供一个普通函数(而不是析构函数)执行该操作。

9. 不在构造和析构函数调用virtual函数

base class构造函数执行早于derived class构造函数(base class构造期间virtual函数不会降到derived class层)。析构函数也是如此。

  • 在构造和析构期间不要调用virtual函数,因为这类调用从不下降至derived class。

10. 让operator=返回一个reference to *this

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

class Test{
public:
  Test& operator=(const Test& t){
    ...
    return *this;
  }
}
  • 让assignment操作符返回一个reference to *this

11. 在operator=中处理自我赋值

自我赋值是对象被赋值给自己,如下:

x = x;
a[i] = a[j] //潜在自我赋值(i=j)
*px = *py //潜在自我赋值(px和py相同)

但自我赋值操作并不总是安全的。一般做法是:

...operator=(const Test& t){ //不安全的operator=
  delete p;
  p = new Bitmap(*t.p);
  return *this;
// 传统使用 if(this == &t)return *this;判断
// 改进使用
 Bitmap *org = p;
 p = new Bitmap(*t.p); 
 delete p;
 return *this;
}
  • 确保当对象自我赋值时operator=有良好行为。
  • 确定任何函数如果操作一个以上对象,而其中多个对象是同一个对象时,其行为仍然正确。

12. 复制对象时不要忘记每一部分

为derived class写copying函数时需要小心地复制其base class成分。那些成分是private,无法直接访问它们,你应该让derived class的copying函数调用相应的base class函数。让copy assignment操作符调用copy构造函数是不合理的,因为就像试图构造一个已经存在的对象;让copy构造函数调用copy assignment操作符也同样无意义。

  • copying函数应该确保复制对象内的所有成员变量以及所有base class成分。
  • 不要以某个copying函数实现另一个copying函数,应该将共同操作部分放进第三个函数中,由两个copying函数共同调用。

13. 以对象管理资源

以对象管理资源的两个关键想法是:获得资源后立刻放进管理对象内,“以对象管理资源”也被称为“资源取得时机就是初始化时机”(RAII);管理对象运用析构函数确保资源被释放。
auto_ptr是个pointerr-like对象,也即是智能指针,其析构函数自动对其所指对象调用delete,一定要注意别让多个auto_ptr同时指向同一对象。

  • 为防止资源泄露,请使用RAII对象,他们在构造函数中获得资源并在析构函数中释放资源。
  • 两个常使用的RAII classes分别是tr1::shared_ptr和auto_ptr。前者通常是较好选择,因为其copy行为比较直观。若选择auto_ptr,复制动作会使它指向null。

14. 在资源管理类中小心copying行为

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

  • 复制RAII(resource acquisition is initialization)对象必须要一并复制它所管理的资源,所以资源的copying行为决定RAII对象的copying行为。
  • 常见的RAII class copying行为是:抑制copying、采用引用计数法(reference counting)。不过其它行为也可能被实现。

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

RAII classes并不是为了封装某物而存在,而是为了确保资源释放能被执行。

  • APIs往往要求访问原始资源,所以每一个RAII class应该提供一个取得他所管理的资源的方法。
  • 对原始资源的访问可能通过显式转换或隐式转换,一般而言显示转换比较安全,但隐式转换对客户更方便。

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

考虑以下代码有什么错误:

std::string* stringArray = new std::string[100];
...
delete stringArray;  // 这只能删除一个对象

当使用new生成一个对象,一是内存被分配出来,二是此内存会有一个或多个构造函数被调用。同样执行delete时也有一个或多个析构函数被调用,然后才释放内存。delete时需要注意即将被删除的内存中有多少个对象?也就是有多少个析构函数还会被执行。

  • 如果在new中使用[]创建对象,delete时也要使用[](delete [] stringArray),如果new时没使用则delete时也不要使用。

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

假设有两个函数用来揭示处理程序的优先权,另一个函数用来在某动态分配所得的Widget上进行某些带有优先权的处理:

int priority();
void processWidget(std::tr1::shared_ptr<Widget> pw, int priority);
// 调用processWidget
processWidget(new Widget, priority());  //这样无法进行隐式转换,不能通过编译
processWidget(std::tr1::shared_ptr<Widget>(new Widget), priority());  //使用类型转换,能编译通过,却可能资源泄漏

std::tr1::shared_ptr(new Widget)由两部分组成:1.执行 new Widget 表达式。2.调用 tr1::shared_ptr构造函数。在调用processWidget之前,编译器先做:1.调用priority。2.执行 new Widget。3.调用tr1::shared_ptr构造函数。但如果1和2执行顺序交换,并且对priority的调用导致异常,则导致资源泄漏。
避免这类问题应该:1.创建Widget。2.将它置入智能指针内,然后把指针传递给processWidget:

std::tr1::shared_ptr<Widget> pw(new Widget); //采用智能指针
processWidget(pw, priority()); //这样则不会资源泄漏
  • 以独立语句将newed对象存储于智能指针内。若不这样可能导致难以察觉的资源泄漏。

18. 让接口容易被正确使用

假如有一个用来表现日期的class设计构造函数:

class Date{
public:
  Date(int month, int day, int year);
}

使用者很容易犯下两个错:1.以错误的次序传参。2.传递一个无效的值。
为了预防这样的问题可以使用外覆类型(wrapper types)来改造:

struct Day{
  explicit Day(int d):val(d){}
  int val;
}
struct Month{
explicit Month(int m):val(m){}
int val;
}
struct Year{
explicit Year(int y):val(y){}
int val;
}
//
class Date{
public:
  Date(const Month& m, const Day& d, const Year& y);
}
//
Date d(30, 3, 2019);  // error
Date d(Day(30), Month(12), Year(2019));  //error
Date d(Month(12), Day(30), Year(2019));  //ok
  • 好的接口容易被正确使用。
  • 促进正确使用的方法包括接口一致性,以及内置类型的行为兼容。
  • 防止误用的方法包括建立新的类型、限制类型操作,束缚对象值等。
  • tr1::shared_ptr支持定制型删除器(custom deleter)。可防范DLL问题,可被用来自动解除互斥锁等。

19. 设计class犹如设计type

如何设计高效的classes,你必须考虑如下问题:

  1. 新type的对象应该如何被创建和销毁?
  • 这会影响到你的class的构造函数和析构函数以及内存分配函数和释放函数的设计。
  1. 对象的初始化和对象的赋值该有什么样的差别?
  • 这决定你的构造函数和赋值操作符的行为以及其间的差距。别混淆了初始化和赋值,因为它们对应于不同的函数调用。
  1. 新type的对象如果被passed bu value,意味着什么?
  • 记住,copy构造函数用来定义一个type的pass-by-value该如何实现。
  1. 什么是新type的“合法值”?
  • 对class的成员变量而言,通常只有某些数值集是有效的。那些数值决定了你的class必须要维护的约束条件,即你的成员函数(特别是构造函数、赋值操作符和setter函数)必须进行的错误检查。它也影响了函数抛出的异常以及函数异常明细列(exception specification)。
  1. 你的新type需要配合某个继承图系吗?
  • 如果你允许其他classes继承你的class,那会影响你所声明的函数,尤其是析构函数是否是virtual。
  1. 你的新type需要什么样的转换?
  • 如果你只允许explicit构造函数存在,就得写出专门负责执行转换的函数,且不得为类型转换操作符(type conversion operators)或non-explicit-argument构造函数。
  1. 什么样的操作符和函数对此新的type而言是合理的?
  • 这个问题答案决定你将为你的class声明哪些函数。其中某些该是member函数,某些则否。
  1. 什么样的标准函数应该驳回?
  • 那些正是你必须声明为private的。
  1. 谁该取用新type的成员?
  • 这可帮你决定哪个成员为public,protected,private,哪个classes或function应该是friends,以及将他们嵌套于另一个之内是否合理。
  1. 什么是新type的“未声明接口”?
  • 它对异常安全性,效率以及资源运用提供何种保障?你在这方面提供的保证将为你的class实现代码加上相应的约束条件。
  1. 你的新type有多么一般化?
  • 或许你并非定义一个新的type,而是定义一整个types家族,所有你该定义一个新的class template。说不定单纯定义一个或多个non-member函数或templates,更能达到目标。

20. 以pass-by-reference-to-const替换pass-by-value

缺省情况下C++以by value方式传递对象至函数,但pass by reference to const没有任何构造函数或析构函数被调用,没有任何对象被调用所有效率更高。

  • 尽量以pass-by-reference-to-const替换pass-by-value,前者通常更高效并且避免slicing problem。
  • 但并不适合内置类型,以及STL的迭代器和函数对象。对他们而言pass-by-value更适当。

21. 必须返回对象时别返回其reference

如果在stack空间创建一个local变量,并且你函数返回一个reference指向该local对象,将导致”无定义行为“,同样在heap上也会导致operator*使用者不能获取到reference背后的指针而资源泄漏。

  • 绝不要返回pointer或reference指向一个local stack对象,或返回reference指向一个heap-allocated对象,或返回pointer或reference指向一个local static对象而有可能需要多个这样的对象。

22. 将成员变量声明为private

  • 请将成员变量声明为private。protected并不比public更具封装性。

23. 以non-member、non-friend替换member函数

假如有个class用来表示浏览器,其中一个函数用于清除数据:

class WebBrowser{
public:
  void clearCache();
  void clearHistory();
  ...
}

比较自然的做法是:

namespace WebBrowserStuff{
  class WebBrowser{ ... }
  void clearBrowser(WebBrowser& b); // 其中clearBrowser为一个non-member函数调用member方法。
  ...
}
  • 以non-member, non-friend函数替换member函数,这样做可以增加封装性包装弹性和可扩充性。

References:

  • 《Effective C++》改善程序与设计的55个具体做法,第三版
原创文章 38 获赞 13 访问量 4039

猜你喜欢

转载自blog.csdn.net/qq_36287943/article/details/103655021
今日推荐