「经典研读」Effective C++

#Effective C++

#1 - 让自己习惯C++

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

C & Object-Oriented C++ & Template C++ & STL

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

条款03 - 尽可能使用const

如果 const* 的左边,那么我们不能通过指针去修改变量的值,

vector<int> vec;

// const T*
vector<int>::const_iterator cIte = vec.begin();
*cIte = 10; // no!
++cIte;			// yes!

如果 const* 的右边,那么我们不能修改指针的值,即不能改变指针的指向,

vector<int> vec;

// T* const
const vector<int>::iterator ite = vec.begin();
*ite = 10;	// yes!
++ite;			// no!

const 成员函数,保证不修改成员变量的值,且 const 也可以作为函数签名,其中 const object 只能调用 const 成员函数;non-const object 可以调用 non-const 和 const 的成员函数

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

使用 member initialization list 进行初始化,而不是在 {} 内进行赋值操作

利用 member initialization list 进行初始化,其实就是调用 constructor 进行对象构造并完成赋值操作;而在 {} 内进行赋值操作是分成两步的:首先调用 default constructor 构造对象,然后再调用 copy assignment 进行对象赋值

为了避免“跨编译单元的初始化次序”带来的问题,多以 local static object 替换 non-local static object

static object ,其寿命从被构造出来直到程序结束为止。函数内的 static object 称为 local static object , 其他 static object 称为 non-local static object

#2 - 构造/析构/赋值运算

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

如果我们自己没有声明 Big Tree,那么编译器会替我们声明一个 copy constructor,copy assignment 以及 destructor ,

Widget w1;	// default constructor
Widget w2(w1);	// copy constructor
w1 = w2;		// copy assignment
Widget w3 = w2;	// copy constructor

如果一个 object 还没有被构造出来就被赋值,那么该赋值操作一定是 copy ctor。因为如果有一个新 object 被定义,那么一定会先调用构造函数。连构造都没有完成,何来的赋值?

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

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

  • 如果一个 class 带有任何 virtual 函数,那么该 class 就叫带多态性质的基类( base class )。并且带多态性质的基类应该声明一个 virtual 析构函数
  • 如果 class 的设计目的不是为了作为 base class 使用的话,那么请不要声明 virtual 函数
  • C++明确指出,当子类( derived class )对象经由一个 base class pointer 删除时,如果 base class 没有 virtual 析构函数的话,会导致该对象的 derived 成分并没有被销毁
  • 想要实现 virtual 函数功能,该对象必须携带某些信息用来决定在运行期间哪一个 virtual 函数该被调用。该条信息通常是由一个 vptr 指出,vptr 指向一个由函数指针构成的数组 vtbl
class Point {
  private:
  	int x, y;
  public:
  	Point(int x, int y);
  	~Point();
}

如果 Point class 内含有 virtual 函数,其对象的体积会增大:在32-bit计算机体系结构中将从64bits增大到96bits,即两个 int + virtual 。因此,为 Point 添加一个 vptr 会使其对象至少膨胀50%

并非所有的 base class 的设计目的都是为了多态。例如STL容器和标准的 string ,它们都不允许作为 base class 使用,更别提多态了

讽刺的是,大多数对象的析构函数消耗了巨量的 CPU 时间,仅仅是为了正确地调用其他对象的析构函数,保证内存空间的正确释放!

条款08 - 别让异常逃离析构函数

  • 在构造函数中也应该有 try - catch

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

在构造和析构阶段调用 virtual 函数没有意义!因为这类调用从不下降至 derived class 。如果偏要在构造或析构中调用 virtual 函数,那么流程是这样的:构造一个 derived class object 需要先构造出 base class object ,在构造 base class object 时会调用 derived class object 的 virtual 函数,思考一下都会觉得荒谬!因为 derived class object 的 virtual 函数势必会操作那些 base class 中并未定义的 data member 。析构也一样,只是逆向过程罢了

derived class 在构造期间,首先构造 base class,然后再构造自身。当构造 base class 时,derived class 成分还未被构造出来,所以 base class 无法调用到 derived class 中的 virtual 函数,也就不能完成动态绑定;同样在析构时因为 derived class 的虚表 vtbl 在析构 base class 前已经被销毁了,所以 base class 在析构中也无法调用到 derived class 中定义的 virtual 函数

class Base {
  public:
  	Base() {
      log();
    }
  
  	~Base() {
      cout << "~";
      log();
    }
  
  	virtual void log() {
      cout << "Base::log()" << endl;
    }
};

class Derived : public Base {
  public:
  	virtual void log() {
      cout << "Derived::log()" << endl;
    }
};

main() {
  Derived d;
}

// output:
Base::log()
~Base::log()

在构造和析构阶段, virtual 函数的性质是没有办法得到体现的!

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

  • 返回 reference 可以进行连等操作!

条款11 - 在operator=中处理"自我赋值"

// copy assignment
String& String::operator=(const String &str) {
  // 检测自我赋值 self assignment
  if(this != &str) {
    // 1. 释放原有空间
    delete[] m_data;
    // 2. 根据str长度分配相应的大小
    m_data = new char[strlen(str.m_data)+1];
    // 3. copy
    strcpy(m_data, str.m_data);
  }
  
  return *this;
}

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

  • Copying 函数( Copying constructor & Copying assignment )应该确保复制 “对象内的所有成员变量” and “所有 base class 成分”
  • derived class 的一般构造函数不用刻意地调用 base class 的构造函数,编译器会自动调用

#3 - 资源管理

条款13 - 以对象管理资源

  • 获得资源后立刻放进管理对象内,并且管理对象要运用析构函数确保资源被释放

auto_ptr 的缺点?

auto_ptr 指向的对象只能被删除一次,如果对象被删除多次,就会出现 “未定义行为” ,所以 auto_ptr 管理的资源必须确保只有一个 auto_ptr 指向它

shared_ptr 的优缺点

“引用计数型智慧指针” ,持续追踪共有多少对象指向某笔资源,并在无人指向它时自动删除该资源。 shared_ptr 在便于管理资源的同时,也带来了内存的开销,它比原始指针大两倍!

  • auto_ptrshared_ptr 都在析构函数内做 delete 而非 delete[] 动作,所以不能用智能指针管理数组对象。可以用 stringvector 来替代数组
  • RAII( Resource Acquisition Is initialization ),“以对象管理资源” 的观念经常被称为 “资源取得时机便是初始化时机”

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

  • 禁止复制,而采用深拷贝
class Lock : private Uncopyable {}

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

shared_ptrauto_ptr 都提供了一个 get 成员函数,用来执行显式转换,获取智能指针内部的原始指针。同时也重载了 operator->operator*

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

  • 如果在 new 表达式中使用[],必须在相应的 delete 表达式中也使用[]

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

// 函数声明
int priority();
void processWidget(shared_ptr<Widget> pw, int priority);

不要考虑下面这种调用形式!它不能通过编译,shared_ptr 构造函数是个 explicit 构造函数,无法进行隐式转换,

// 错误示范
processWidget(new Widget, priority());

下面这种情况是可以通过编译的,但可能引发内存泄露,

processWidget(shared_ptr<Widget>(new Widget), priority());

编译器可能先执行 new Widget ,然后再调用 priority() ,最后再调用 shared_ptr 构造函数。但是万一对 priority() 的调用导致异常呢?那么 new Widget 返回的指针将会遗失,造成内存泄露,

// 正确示范
shared_ptr<Widget> pw(new Widget);
processWidget(pw, priority());

#4 - 设计与声明

条款18 - 让接口容易被正常使用,不易被误用

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

条款20 - 宁以pass-by-reference-to-const替换pass-by-value

但是对于内置类型以及STL迭代器和函数对象,pass-by-value往往比较合适

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

// 错误示范
const Rational& operator*(const Rational& lhs, const Rational& rhs) {
  Rational ret(lhs.n * rhs.n, lhs.d * rhs.d);
  return ret;
}

返回一个 reference 指向本地对象 ret ,这样是不行的,因为 local 对象在函数退出前就被销毁了,

// 可以,但可能内存泄露
const Rational& operator*(const Rational& lhs, const Rational& rhs) {
  Rational *ret = new Rational(lhs.n * rhs.n, lhs.d * rhs.d);
  return *ret;
}

带来的问题就是谁该对被你 new 出来的对象进行 delete 呢?

// 可以,但不线程安全
const Rational& operator*(const Rational& lhs, const Rational& rhs) {
	static Rational ret(lhs.n * rhs.n, lhs.d * rhs.d);
  return ret;
}

用 static local 变量会带来两大弊端,其一就是多线程不安全(没有加锁),其二如下,

bool operator==(const Ration& lhs, const Rational& rhs);
Rational a, b, c, d;
if((a*b) == (c*d)) {}
// 等价于
if(operator==(operator*(a, b), operator*(c, d))) {}

if内的判断结果永远为 true ,因为( a * b )会修改 ret 的值,( c * d )又会修改 ret 的值,所以程序始终在做 ret==ret 的判断,当然为 true ,

// 正确示范
inline const Rational operator*(const Ration& lhs, const Ration &rhs) {
  return Rational(lhs.n * rhs.n, lhs.d * rhs.d);
}

在有些时候必须承受 operator* 返回值的构造和析构成本,只要这样才能保证正确

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

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

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

// member函数
class Rational {
  public:
  	...
  	const Rational operator*(const Rational& rhs) const;
};

ret = oneHalf * 2;	// 可
ret = 2 * oneHalf;	// 错误

编译器会翻译成,

ret = oneHalf.operator*(2);		// 很好,调用member函数
ret = 2.operator*(oneHalf);		// 在内置类型中可没有这种类型转换

所以如果需要为某个函数的所有参数(包括被this指针所指的那个隐喻参数)进行类型转换,那么这个函数必须是个 non-member ,

// non-member函数
class Rational {
   ...
};
const Rational operator* (const Rational& lhs, const Rational& rhs)
{
   return Rational(lhs*rhs);
}
ret = oneHalf * 2;   // 可
ret = 2 * oneHalf;   // 可!

太多 C++ 程序员假设,一个 “与某class” 相关的函数要不是 member 要不就是 friend 。无论何时都应该尽可能避免使用 friend 函数,因为就像真实世界一样,朋友带来的麻烦往往多过其价值

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

// std::swap的典型实现
namespace std {
  template<typename T> 
  void swap(T& a, T& b) {
    T tmp(a);
    a = b;
    b = tmp;
  }
};

C++标准提供的 swap 函数可以完成交换操作,但在某些情况下效率不高,比如 class 内含指针

其中最主要的就是 “以指针指向一个对象,内含真正数据” 那种类型,叫做 “pimpl” 手法( “pointer to implementation” ),

// pImpl
class WidgetImpl {
  public:
	  ...
  private:
  	int a, b, c;			// 可能有许多数据
  	vector<double> v;	// 意味着复制时间很长
  	...
};

class Widget {
  public:
  	Widget(const Widget& rhs);
  	// 复制Widget时,令它复制其WidgetImpl对象
  	Widget& operator=(const Widget& rhs) {
      ...
      *pImpl = *(rhs.pImpl);
      ...
    }
	  ...
  private:
  	WidgetImpl* pImpl;
};

可以针对 Widget 类进行特化提高其 swap 速度,

// class特化swap
class Widget {
  public:
  	...
    void swap(Widget& other) {
      using std::swap;
      // 若要置换Widget就置换其pImpl指针
      swap(pImpl, other.pImpl);
    }
};

namespace std {
  template<>
  void swap<Widget>(Widget& lhs, Widget& rhs) {
    // 若要置换Widget,调用其swap成员函数即可
    lhs.swap(rhs);
  }
};

#5 - 实现

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

1个构造函数 + 1个析构函数 + n个赋值操作,

// 方法A: 定义于循环外
Widget w;
for(int i=0; i<n; ++i) {
   w = ...;
   ...
}

n个构造函数 + n个析构函数,

// 方法B: 定义于循环内
for(int i=0; i<n; ++i) {
   Widget w;
   ...
}

如果 class 的赋值成本低于构造 + 析构成本, 方法 A 更好, 尤其是在 n n n 很大的情况下

条款27 - 尽量少做转型动作

  • 如果可以,尽量避免转型动作,特别是 dynamic_cast ,因为 dynamic_cast 的许多实现版本执行速度相当慢
  • 宁可用 C+±style 转型,不要使用旧式转型
  • 常见转型:const_cast , dynamic_cast , reinterpret_cast , static_cast

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

  • 对于一个 class 来说,应该避免返回 class 自身的私有数据;如果要返回请用 const 修饰

条款29 - 为“异常安全”而努力是值得的

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

  • inline函数背后的整体观念是,将 “对此函数的每一个调用” 都以函数本体替换之,这样做可能增加你的目标代码大小。所以在项目中如果是修改 inline 函数,那么必须的重新编译;如果是non-inline函数,程序库采取动态链接,也就不需做太多修改了
  • 大部分编译器拒绝将太过复杂(例如带有循环或递归)的函数inling, 而所有对 virtual 函数的调用也不能 inlining 。virtual 意味 “等待, 直到运行期才确定调用哪个函数” ,而 inline 意味 “执行前,先将调用动作替换为被调用函数的本体。”

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

  • 相依于声明式,不要相依于定义式
    如果使用 object pointers( Handle classes )可以完成任务,就不要使用 objects ,即 main class 内含有一个指针成员,指向其实现类( Impl ),这样设计成为 pimpl idiom ( pimpl,pointer to implementation )

#6 - 继承与面向对象设计

条款32 - 确定你的public继承塑膜出is-a关系

条款33 - 避免遮掩继承而来的名称

  • derived classes 内的名称会遮掩 base classes 内的名称,尽量避免重名
  • 如果使用 public 继承而又不继承那些重载函数,就是违反 base 和 derived classes 之间的 is-a 关系

条款34: 区分接口继承和实现继承

  • 要求 derived class 必须重写的函数请声明为 pure virtual
  • derived class 可以重写也可以用 base class 定义的函数请声明为 virtual
  • 如果不希望 derived class 重写该函数请声明为 non-virtual

条款35 - 考虑virtual函数以外的其他选择

条款36 - 绝不重新定义继承而来的non-virtual函数

条款37 - 绝不重新定义继承而来的缺省参数值

条款38 - 通过复合塑膜出has-a或“根据某物实现出”

  • 多用组合,少用继承

条款39 - 明智而审慎地使用private继承

  • 如果 classes 之间的继承关系是 private ,编译器不会自动将一个 derived class 对象转换为一个 base class 对象
  • 由 private base class 继承而来的所有成员,在 derived class 中都会变成 private 属性,纵使它们在 base class 中原本是 protected 或 public 属性

条款40 - 明智而审慎地使用多继承

  • virtual 继承会增加大小、速度、初始化(及赋值)复杂度等等成本。如果 virtual base classes 不带任何数据, 将是很有实用价值的

#7 - 模版与范型编程

条款41 - 了解隐式接口和编译器多态

条款42 - 了解typename的双重意义

  • typename的作用设计模版类型时让定义不再模棱两可

#8 - 杂项讨论

条款53 - 不要轻忽编译器的警告

条款54 - 让自己熟悉包括TR1在内的标准程序库

条款55 - 让自己熟悉Boost

  • 2
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值