#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_ptr
和shared_ptr
都在析构函数内做delete
而非delete[]
动作,所以不能用智能指针管理数组对象。可以用string
和vector
来替代数组- RAII( Resource Acquisition Is initialization ),“以对象管理资源” 的观念经常被称为 “资源取得时机便是初始化时机”
条款14 - 在资源管理类中小心copying行为
- 禁止复制,而采用深拷贝
class Lock : private Uncopyable {}
条款15 - 在资源管理类中提供对原始资源的访问
shared_ptr
和 auto_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的作用设计模版类型时让定义不再模棱两可