Effective C++ 笔记 (一)
目录

1. Accustoming youself to C++

函数签名式(signature):

  • 官方定义:函数参数
  • ECPP: 函数参数+返回值

C++不明确/未定义/无法预期的行为(undefined behavior)太多。要尽可能避免。

01. View C++ as a federation of languages

如今C++已是多重范型编程语言(multiparadigm programming language),它支持:

  • 过程形式(procedural)
  • 面向对象形式(object-oriented)
  • 函数形式(functional)
  • 范型形式(generic)
  • 元编程形式(metaprogramming)

因此,C++有4种次语言使用方式:

  • C
  • Object-Oriented C++
  • Template C++
  • STL

对于内置类型通常pass-by-value比pass-by-reference更高效。 对于STL的迭代器和仿函数来说,pass-by-value更适用。 其它情况pass-by-reference更好。

02. Prefer consts, enums and inline to #defines

对于单纯常量,最好以const对象或enums替换#define。这样变量名更容易记入到符号表。

#define authorName "Scott Meyers"
const char* const authorName = "Scott Meyers";
const std::string authorName("Scott Meyers");

头文件中class的成员都是声明式,但是class的专属static整型常量static const int可以在头文件中提供初值:

class GamePlayer {
private:
static const int NumTurns = 5;
};

而不是必须在.cpp中提供一个定义式。如果后面代码需要访问专属static整型常量的地址,则这个编译优化trick失效。 常量可能被编译器优化,从而不占用内存空间。ENUM也可能被优化导致非必要的内存分配。

对于形似函数的宏(macros),最好以template inline函数代替#define宏:

#define MAX(a,b) f((a) > (b) ? (a) : (b))
template<typename T>
inline void max(const T &a, const T &b) {
f(a > b ? a : b);
}

03. Use const where possible

只要修饰目标确实没被修改,那么就应该用const声明出来,这可以帮助编译器检测出代码错误。

一个有趣的声明是:

const char *const p;

它说明p指针本身是个常量(指向的地址不会修改),并且p指向的字符串也是常量。

const作用在类的成员函数上很有趣,它保证了函数内部不会修改类成员变量的值,这有很多可探讨的地方。 比如mutable变量可以在const函数中改变。

一个很容易忽视的事实:两个成员函数如果只有常量性(constness)不同,可以被重载。 (我理解因为const成员函数实际上是把隐藏nonconst-this形参改为const-this形参,做为函数的第一个参数,等价于函数参数不同,可以重载)。

04. Make sure that objects are initialized before they're used

对象的数据成员确保在构造函数中初始化。内置类型确保在定义时手工初始化。

要区分类成员的赋值(assignment)与初始化(initialization)。 在类的构造函数中使用成员初始化列表比函数内赋值操作效率更高, 因为前者直接调用拷贝构造,而后者先使用默认构造,再进行赋值操作。 const或reference成员变量一定需要初值,不能被赋值。 一个简单的做法是:总是使用成员初始化列表。

C++中类的初始化次序是:父类先于子类初始化,而类中成员变量总是以其声明的次序初始化

class Derived : public Base {
public:
Derived() {} // 默认调用Base::Base()
Derived(int i, string a, string b) : Base(i),txtb(b),txta(a) {} // 显示调用Base::Base(int),然后初始化txta,最后初始化txtb
private:
string txta;
string txtb;
};

local static指的函数内的静态对象,non-local static指的其它在数据段保存的对象(全局或静态对象)。 对于不同编译单元(每个.c/.cpp文件)内的non-local static对象,初始化的顺序不确定。 因此non-local static之间初始化不能相互依赖。 为避免这个“跨编译单元之初始化次序”问题,可以使用 local static对象替换 non-local static对象,这也是Singleton设计模式使用的方法。


2. Constructors, Destructors and Assignment Operators

05. Know what functions C++ silently writes and calls

C++ 默认会对类自动增加以下函数(但是只有被调用时才会被编译器真正生成):

  • default constructor: 如果没有声明其它构造函数,就是生成
  • destructor: 如果父类的析构函数不是虚函数,就生成non-virtual;如果父类是虚函数,调用父类的
  • copy constructor:没有就生成。
  • copy assignment:合法就生成。

注意The Rule of Three。

这些默认都是public和inline属性。

注意,operator new和构造函数都可以重载(因为可以有任意参数,函数的签名式也有多个),operator delete和析构函数不能重载(因为没参数,函数的签名式唯一)。

copy constructor对于对象每一个non-static成员变量,如果是内置类型,则内存拷贝;如果是用户定义类型,则尝试使用那个类型的拷贝构造。

copy assignment与copy constructor类似,但是如果operator=操作不合法,比如让reference改指不同对象、给const变量赋值,则编译器拒绝生成copy assignment。

06. Explicitly disallow the use of compiler-generated functions you do not want

如果想禁止拷贝构造和赋值操作,两种做法:

  • 把它们声明为private并不予实现。这样即使member函数和friend函数尝试去调用拷贝函数时将产生链接错误。(链接期)
  • 写一个Uncopyable类实现上一条,并private继承它。这样派生类调用拷贝函数时会被编译器拒绝。(编译期)

07. Declare destructors virtual in ploymorphic base classes

在object的生命周期中,创建object必须通过真实类型的构造函数,而释放object可以通过基类类型的指针释放。 此时派生类的析构函数必须声明为virtual。 在这种情况下,基类类型指针够绑定的是派生类析构函数而不是基类析构函数。

C++并没有提供类似Java的final classes那样"禁止派生"的机制。 因此C++并不会拒绝继承“带有non-virtual析构函数”的类。

标准string和STL容器不能作为base classes使用,更不能用作多态。

因此:

  • polymorphic base classes 应该声明virtual destructor
  • non-base classes and non-polymorphic base classes不该声明virtual destructor

08. Prevent exceptions from leaving destructors

析构函数不要抛出异常,而是在析构函数里处理异常:

  • catch异常,log 异常,正常返回。
  • catch异常,std::abort(),程序结束。

更好的设计是把析构函数中可能抛异常的部分单独拿到一个新的函数中,供用户调用和处理异常。

09. Never call virtual functions during construction or destruction

相当重要)这条和对象的生命周期有关。 在class构造或析构期间,virtual函数不是virtual函数,不能调用derived class的virtual函数。

derived class对象在base class构造期间是base class类型。 virtual函数,RTTI, dynamic_cast, typeid都会这样看它。 析构也一样。在多重继承环境下更加复杂。

10. Have assignment operators return a reference to *this

在赋值operator=或与之相关的运算符如operator+=返回*this的引用,便于x=y=z这样的等式连续赋值:

Widget& operator+=(const Widget& rhs)
{
...
return *this;
}

11. Handle assignment to self in operator=

operator=操作符要考虑以下两种情况:

  • 来源对象和目标对象是同一对象
  • new新对象时产生异常的处理

可以使用copy and swap(COW)技术:

T& operator=(const T& t) {
T temp(t);               // copy
swap(*this, temp);       // swap, better than "new T"
return *this;
}

// better
T& operator(T t) {
swap(*this, t);
return *this;
}

12. Copy all parts of an object

拷贝操作时应考虑所有成员变量和基类成员:

Derived::Derived(const Derived &d)
:Base(d),
p(d.p)
{
}

Derived& operator=(const Derived &d)
{
Base::operator=(d);   // importent
p = d.p;
return *this;
}

赋值构造函数和拷贝构造函数也不能相互调用。


3. Resource Management

管理需要手动释放的资源,包括:

  • 堆上内存资源
  • 文件描述符 file descriptors
  • 互斥锁 mutex lock
  • 字体
  • 数据库连接
  • 网络socket

因此可以设计资源管理类(resource-managing classes)

13. Use objects to manage resources

资源在构造函数中获得(手动),在析构函数中释放(离开作用域时自动)。 这就是著名的RAII(Resource Acquisition Is Initialization)方法,资源取得时机便是初始化时机。

为了确保对象在离开函数后被正常delete,可以依靠栈中自动析构的对象去做delete。 比如使用:

  • 智能指针std::auto_ptr<CLASS>,但复制动作会使原智能指针指向null (已被废弃,使用boost::scoped_ptr代替)
  • 引用计数型智慧指针boost::shared_ptr(是一种简单的GC,无法打破环状引用,要配合boost::weak_ptr解决环状引用)

但这种指针不能指向数组。数组的替代方案是Boost库中boost::scoped_arrayboost::shared_array

14. Think carefully about copying behavior in resource-managing classes

RAII对象在初始化时获取资源。

这样资源的复制行为要小心。可选项有两种:

  • 禁止复制:把赋值成员函数和拷贝构造函数声明为private
  • 浅拷贝,引用计数:如std::shared_ptr那样,并且可以自定义释放函数。
  • 深拷贝:拷贝所有数据成员。
  • ~~转移引用:如std::auto_ptr那样~~

注意在未定义拷贝的情况下,编译器自动产生的拷贝构造函数和赋值成员函数。

15. Provide access to raw resources in resource-managing classes

RAII class可以提供函数满足对原始资源(raw resources)访问的需求,并提供转换。 转换分为显示转换和隐式转换,如class Font转换为class FontHandle:

// 显示转换
class Font {
public:
...
FontHandle get() const {return f;}
};


// 隐式转换(不推荐,破坏接口清晰性)
class Font {
public:
...
operator FontHandle() const {return f;}   // Font => FontHandle
};

显示转换并不是破坏了封装,因为这里本身指的RAII对象,其目的就是分配资源。

16. Use the same form in corresponding uses of new and delete

在new中使用[],也必须在delete中使用[]。

因此不要对数组做typedef,因为这样分不清释放时是用delete还是delete[]。

17. Store newed objects in smart pointers in standalone statements

不要把new语句作为实参。 new语句可能会抛出异常(如内存不足)。这样把new语句作为实参时,如果new或者其它实参产生异常, 讲不能获取到new的结果,从而造成memory leak。

反过来说,在函数调用语句中要注意参数传入时有没有可能产生异常。 并且注意函数参数构造的次序是不确定的。

发表评论