阅读《Effective C++》系列

《Effective C++》条款07:为多态基类声明virtual析构函数

这样做主要是为了防止内存泄漏,见我hexo博客。

C++的虚析构函数

《Effective C++》条款11:在operator=中处理“自我赋值”

“自我赋值”发生在对象赋值给自己时:

1
2
3
4
class Widget { ... }
Widget w;
...
w=w;
1
a[i]=a[j]; //潜在的自我赋值,如果i和j有相同的值
1
*px=*py; //潜在的自我赋值,如果px和py恰好指向同一个东西

如果遵循条款13和条款14的忠告,你会运用对象来管理资源,而且你可以确定所谓“资源管理对象”在copy发生时有正确的举措。这种情况下你的赋值操作符或许是“自我赋值安全的”(self-assignment-safe),不需要额外操心。然而如果你尝试自行管理资源(如果你打算写一个用于资源管理的class就得这样做),可能会掉进“在停止使用资源之前意外释放了它”的陷阱。

其实从上面例子来看,似乎没有太大的问题,但假设你简历一个class来保存一个指针指向一块动态分配的位图(bitmap)

1
2
3
4
5
6
class Bitmap { ... }
class Widget {
...
private:
Bitmap* pb; // 指针,指向一个从heap分配而得到的对象
};

对于每次赋值,我们要考虑到资源管理,即可能会写出如下的代码:

1
2
3
4
5
6
7
Widget&
Widget::operator=(const Widget& rhs) //一份不安全的operator=实现版本
{
delete pb;
pb = new Bitmap(*rhs.pb);
return *this;
}

这里自我赋值的问题是,operator=函数内的*this(赋值的目的端)和rhs有可能是同一个对象。果真如此,delete就不只是销毁当前对象的bitmap,它也销毁rhs的bitmap。

可以通过“证同测试”达到“自我赋值”的检验目的:

1
2
3
4
5
6
7
Widget& Widget::operator=(const Widget& rhs)
{
if(this == &rhs) return *this;
delete pb;
pb=new Bitmap(*rhs.pb);
return *this;
}

然而,这个版本不具备“异常安全性”,考虑在“new Bitmap”导致异常,它将得到一个指针指向一块被删除的Bitmap。

解决办法:

  • 在复制pb所指东西之前不删除pb:
1
2
3
4
5
6
7
Widget& Widget::operator=(const Widget& rhs)
{
Bitmap* pOrig=pb;
pb=new Bitmap(*rhs.pb);
delete pOrig;
return *this;
}
  • 使用copy and swap技术
1
2
3
4
5
6
7
8
9
10
11
class Widget {
...
void swap(Widget& rhs);
...
};
Widget& Widget::operator=(const Widget& rhs)
{
Widget temp(rhs);
swap(temp);
return *this;
}
  • 进阶版:利用传值方式,并考虑传值方式造成的副本
1
2
3
4
5
Widget& Widget::operator=(Widget rhs)
{
swap(rhs);
return *this;
}

《Effective C++》条款25:考虑写一个不抛出异常的swap函数

swap动作的典型实现:

1
2
3
4
5
6
7
8
9
namespace std {
template<typename T>
void swap(T& a, T& b)
{
T temp(a);
a=b;
b=temp;
}
}
  • 当std::swap对你的类型效率不高时,提供一个swap成员函数,并确定这个函数不抛出异常。

  • 如果你提供一个member swap,也该提供一个non-member swap用来调用前者,对于classes(而非templates),也请特化 std::swap。

  • 调用swap时应针对std::swap使用using声明式,然后调用swap并且不带任何“命名空间资格修饰”

  • 为“用户定义类型”进行std templates全特化是好的,但千万不要尝试在std内加入某些对std而言全新的东西

《More Effective C++》条款01:指针与引用的区别

指针与引用看上去完全不同(指针用操作符’*’和’->’,引用使用操作符’.’),但是它们似乎有相同的功能。指针与引用都是让你间接引用其他对象。你如何决定在什么时候使用指针,在什么时候使用引用呢?

  • 任何情况下不能用指向空值的引用,而指针没这样的限制。

    不存在指向空值的引用这个事实意味着使用引用的代码效率比使用指针的要高。因为在使用引用之前不需要测试它的合法性。

  • 以下情况下你应该使用指针,一是你考虑到存在不指向任何对象的可能(在这种情况下,你能够设置指针为空),二是你需要能够在不同的时刻指向不同的对象(在这种情况下,你能改变指针的指向)。

  • 如果总是指向一个对象并且一旦指向一个对象后就不会改变指向,那么你应该使用引用。当你重载某个操作符时,你也应该使用引用。

《More Effective C++》条款06:自增(increment)、自减(decrement)操作符前缀形式与后缀形式的区别

直接从代码层面说明区别,定义一个类

1
2
3
4
5
6
7
8
9
10
11
12
class UPInt { // "unlimited precision int"
  public:
   UPInt& operator++(); // ++ 前缀
   const UPInt operator++(int); // ++ 后缀
   UPInt& operator--(); // -- 前缀
   const UPInt operator--(int); // -- 后缀
   UPInt& operator+=(int); // += 操作符,UPInts
    // 与ints 相运算
   ...
  };

前缀操作的自增是用类似以下的代码:

1
2
3
4
5
6
// 前缀形式:增加然后取回值
  UPInt& UPInt::operator++()
  {
   *this += 1; // 增加
   return *this; // 取回值
  }

而后缀形式,则是如此:

1
2
3
4
5
6
const UPInt UPInt::operator++(int)
  {
   UPInt oldValue = *this; // 取回值
   ++(*this); // 增加
   return oldValue; // 返回被取回的值
  }

后缀的会有个临时对象的产生,效率高低比较明了。

《More Effective C++》条款08:理解各种不同含义的new与delete

一般情况下,new operator=先operator new + 后 placement new,前者用于分配存储空间,后者用于调用构造函数初始化所分配的内存。

《More Effective C++》条款19:理解临时对象的来源

在C++中真正的临时对象是看不见的,它们不出现在你的源代码中。建立一个没有命名的非堆(non-heap)对象会产生临时对象。这种未命名的对象通常在两种条件下产生:为了使函数成功调用而进行隐式类型转换和函数返回对象时。

临时对象是有开销的,所以你应该尽可能地去除它们,然而更重要的是训练自己寻找可能建立临时对象的地方

主要有以下两个地方:

  • 任何时候只要见到常量引用(reference-to-const)参数,就存在建立临时对象而绑定在参数上的可能性

  • 任何时候只要见到函数返回对象,就会有一个临时对象被建立(以后被释放)

《More Effective C++》条款26:限制某个类所能产生对象的数量

据我所知,C++控制类的一些trick主要包括构造函数设置为private+设置static函数调用它们设置友元函数/类

  • 允许建立零个或一个对象

阻止建立某个类的对象,最容易的方法就是把该类的构造函数声明在类的private域

1
2
3
4
5
6
class CantBeInstantiated {
  private:
   CantBeInstantiated();
   CantBeInstantiated(const CantBeInstantiated&);
   ...
  };
  • 这样的限制太强了一些,用友元放松限制:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
class PrintJob; // forward 声明
   // 参见Effective C++条款34
  class Printer {
  public:
   void submitJob(const PrintJob& job);
   void reset();
   void performSelfTest();
   ...
  friend Printer& thePrinter();
  private:
   Printer();
   Printer(const Printer& rhs);
   ...
  };
  Printer& thePrinter()
  {
   static Printer p; // 单个打印机对象
   return p;
  }
  • 用静态函数解除限制
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
class Printer {
  public:
   static Printer& thePrinter();
   ...
  private:
   Printer();
   Printer(const Printer& rhs);
   ...
  };
  Printer& Printer::thePrinter()
  {
   static Printer p;
   return p;
  }
  • 通过静态变量来控制类的数量
1
2
3
4
5
6
7
8
9
10
11
12
13
class Printer {
  public:
   class TooManyObjects{}; // 当需要的对象过多时
    // 就使用这个异常类
   Printer();
   ~Printer();
   ...
  private:
   static size_t numObjects;
   Printer(const Printer& rhs); // 这里只能有一个printer,
    // 所以不允许拷贝
  }; // (参见Effective C++ 条款27)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
 // Obligatory definition of the class static
  size_t Printer::numObjects = 0;
  Printer::Printer()
  {
   if (numObjects >= 1) {
    throw TooManyObjects();
   }
   继续运行正常的构造函数;
   ++numObjects;
  }
  Printer::~Printer()
  {
   进行正常的析构函数处理;
   --numObjects;
  }

《More Effective C++》条款27:要求或禁止在堆中产生对象

系统自动分配的内存是栈内存,是由系统自动分配、释放。程序员通过new或malloc操作开辟的内存,是堆内存,由程序员通过代码进行分配、释放

有以上的条件,我们知道,禁止在堆中产生对象,即限制new的功能;要求只在堆中产生对象,即限制系统对象的实例化

  • 要求只在堆中产生对象

让我们先从必须在堆中建立对象开始说起。为了执行这种限制,你必须找到一种方法禁止以调用“new”以外的其它手段建立对象。这很容易做到。非堆对象(non-heap object)在定义它的地方被自动构造,在生存时间结束时自动被释放,所以只要禁止使用隐式的构造函数和析构函数,就可以实现这种限制。

把这些调用变得不合法的一种最直接的方法是把构造函数和析构函数声明为private。这样做副作用太大。没有理由让这两个函数都是private。最好让析构函数成为private,让构造函数成为public。处理过程与条款26相似,你可以引进一个专用的伪析构函数,用来访问真正的析构函数。客户端调用伪析构函数释放他们建立的对象。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
class UPNumber {
  public:
   UPNumber();
   UPNumber(int initValue);
   UPNumber(double initValue);
   UPNumber(const UPNumber& rhs);
   // 伪析构函数 (一个const 成员函数, 因为
   // 即使是const对象也能被释放。)
   void destroy() const { delete this; }
   ...
  private:
   ~UPNumber();
  };
  • 禁止在堆中产生对象
1
2
3
4
5
6
class UPNumber {
  private:
   static void *operator new(size_t size);
   static void operator delete(void *ptr);
   ...
  };

《More Effective C++》条款28:灵巧(smart)指针

灵巧指针是一种外观和行为都被设计成与内建指针相类似的对象,不过它能提供更多的功能。它们有许多应用的领域,包括资源管理(参见条款9、10、25和31)和重复代码任务的自动化(参见条款17和29)

当你使用灵巧指针替代C++的内建指针(也就是dumb pointer),你就能控制下面这些方面的指针的行为:

  构造和析构。你可以决定建立灵巧指针时应该怎么做。通常赋给灵巧指针缺省值0,避免出现令人头疼的未初始化的指针。当指向某一对象的最后一个灵巧指针被释放时,一些灵巧指针负责删除它们指向的对象。这样做对防止资源泄漏很有帮助。

  拷贝和赋值。你能对拷贝灵巧指针或设计灵巧指针的赋值操作进行控制。对于一些类型的灵巧指针来说,期望的行为是自动拷贝它们所指向的对象或用对这些对象进行赋值操作,也就是进行deep copy(深层拷贝)。对于其它的一些灵巧指针来说,仅仅拷贝指针本身或对指针进行赋值操作。还有一部分类型的灵巧指针根本就不允许这些操作。无论你认为应该如何去做,灵巧指针始终受你的控制。

大多数灵巧指针模板看起来都象这样:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
template<class T> //灵巧指针对象模板
  class SmartPtr {
  public:
   SmartPtr(T* realPtr = 0); // 建立一个灵巧指针
    // 指向dumb pointer所指的
   // 对象。未初始化的指针
    // 缺省值为0(null)
   SmartPtr(const SmartPtr& rhs); // 拷贝一个灵巧指针
   ~SmartPtr(); // 释放灵巧指针
   // make an assignment to a smart ptr
   SmartPtr& operator=(const SmartPtr& rhs);
   T* operator->() const; // dereference一个灵巧指针
    // 以访问所指对象的成员
   T& operator*() const; // dereference 灵巧指针
  private:
   T *pointee; // 灵巧指针所指的对象
  };