More Effective-CPP:读书笔记


概述

虽然早在4年前的大二时期就看过并且实践在工程作业里面,但一直没有一个完整的记录,想着别的几本都有了干脆补全一下吧,个人总结,难免会出现一些不太准确的地方,欢迎各位指正。

条款1:仔细区别 pointers 和 references

  • reference 不能为 null。
  • 一般而言,当你需要考虑“不指向任何对象”的可能性时,或是考虑“在不同事件指向不同对象”的能力时,你就应该采用 pointer。
  • 当你知道你需要指向某个东西,而且绝不会改变指向其他东西,或是当你实现一个操作符而其语法需求无法由 pointers 达成,你就应该选择 reference。任何其他时候,请采用 pointers。

条款2:最好使用 C++ 转型操作符

1 static_cast

使用 static_cast<type>(expression)代替 (type)expression

2 const_cast

将某个对象的常量性去除掉

3 dynamic_cast

  • 利用dynamic_cast,将“指向 base class objects 的 pointers 或 references”转型为“指向derived(或 sibling base)class objects 的 pointers 或 references”, 并得知转型是否成功。
  • 如果转型失败,会以一个 null 指针(当转型对象是指针)或一个 exception (当转型对象是 reference)表现出来。

4 reinterpret_cast

最常用用途是转换“函数指针”类型,对应C里面的强制转换

条款3:绝对不要以多态(polymorphically)方式处理数组

class base{
public:
    base(int p1, int p2)
        :p1(p1), p2(p2){};
    int p1, p2;
    virtual void print(){ std::cout << p1 << ", " << p2 << std::endl; }
};

class derived : public base{
public:
    derived(int p1, int p2, int p3)
        : base(p1, p2), p3(p3){};
    int p3;
    void print() override{ std::cout << p1 << ", " << p2 << ", " << p3 << std::endl; }
};

void print(base array[], int n)
{
    for(int i = 0; i < n; ++i)
    {
        std::cout << i << "th item's size is: " << sizeof(array[i]) << std::endl;
        array[i].print();
    }
}

int main()
{
    base    b1(10,  20);
    derived d1(1,   2,  3);
    base    b2(40,  50);
    derived d2(4,   5,  6);
    base array[4] = {d1, b1, d2, b2};

    std::cout << "sizeof(base): " << sizeof(b1) << "\tsizeof(derived): " << sizeof(d1) << std::endl;
    // sizeof(base): 8 sizeof(derived): 12

    print(array, 4);
    // 0th item's size is: 8    1, 2
    // 1th item's size is: 8    10, 20
    // 2th item's size is: 8    4, 5
    // 3th item's size is: 8    40, 50
}

如果你交给printBaseArray函数一个包含derived对象组成的数组,你的编译器就会被误导。这种情况下它仍假设数组中的每一个元素的大小是base的大小,但其实每一个元素的大小是不一样的。

条款4:非必要不提供 default constructor

  • 在一个完美的世界中,凡可以“合理地从无到有生成对象”的 classes,都应该内含 default constructors,“必须有某些外来信息才能生成对象” 的 classes,则不必拥有 default constructors。
  • 在进退维谷的情况下,最后一个考虑点和 virtual base classes 有关。 Virtual base classes 如果缺乏 default constructors,与之合作将会是一种刑法。
  • 添加无意义的 constructors,也会影响 classes 的效率。

条款5:对定制的 “类型转换函数” 保持警觉

下述代码根本的原因在于,在你从未打算也未预期的情况下,此函数可能会被调用,而其结果可能是不正确.不直观的程序行为很难发现调试。

class Rational{
public:
    Rational(int numerator = 1, int denominator = 1);
    operator double() const;
    friend std::ostream &operator<<(std::ostream &os, Rational &r);

    int numerator;
    int denominator;
};

Rational::Rational(int numerator, int denominator) {
     this->numerator = numerator;
     this->denominator = denominator;
}

Rational::operator double() const {
    return static_cast<double>(this->numerator)/static_cast<double>(this->denominator);
}

std::ostream &operator<<(std::ostream &os, Rational &r) {
    os << r.numerator << "/" << r.denominator << std::endl;
    return os;
}

int main()
{
   Rational r(1, 2);
   std::cout << r;                  // 1/2

   double d = 0.5 * r;              // 0.25
   std::cout << d << std::endl;
}

假设你忘了为 Rational 写一个 operator<<,你或许以为上述的 std::cout << r; 不会成功,因为没有适当的 operator<< 可以调用。但是你错了,你的编译器面对上述动作,发现不存在任何 operator<< 可以接受一个 Rational,但它会想尽各种办法(包括找出一系列可接受的隐式类型转换)让函数调用动作成功。即进行了隐式类型转换,导致(非预期)的函数被调用。

解决这个问题,可以采用:

  • 以功能对等的另一个函数取代类型转换操作符
  • 使用关键字 explicit

条款6:区别 increment/decrement 操作符的前置(prefix)和后置(postfix)形式

class UPInt{
public:
    UPInt(int i)                    //提供一个构造函数
        :val(i){};

    UPInt& operator++();            //前置++
    const UPInt operator++(int);    //后置++

    UPInt& operator--();            //前置--
    const UPInt operator--(int);    //后置--

    UPInt& operator+=(const int i); //+=操作符
    //...

    int val;
};

//前置++,返回reference
UPInt &UPInt::operator++() {
    *this += 1;
    return *this;
}

//后置++,返回一个const对象
const UPInt UPInt::operator++(int) {
    UPInt oldValue = *this;
    ++(*this);
    return oldValue;
}

//前置--,返回reference
UPInt &UPInt::operator--() {
    *this += -1;
    return *this;
}

//后置--,返回一个const对象
const UPInt UPInt::operator--(int) {
    UPInt oldValue = *this;
    --(*this);
    return oldValue;
}

//+=操作符,看需求,这里返回一个 reference
UPInt &UPInt::operator+=(const int i) {
    this->val = this->val + i;
    return *this;
}

//这里帮助打印UPint里的val值
void print(const UPInt &up) { std::cout << up.val <

为什么后置++或–返回的是一个const值呢?如果不是一个const值的话,那么下面的动作就是合法的。

    UPInt i(0);
    i++++;

处理用户定制类型时,应尽可能使用前置式 increment,因为它天生体质较佳。

条款7:千万不要重载 &&,|| 和 , 操作符

C++ 对于“真假值表达式”采用所谓的“骤死式”评估方式。意思是一旦该表达式的真假值确定,即使表达式中还有部分尚未检验,整个评估工作仍结束。

如果你决定重载 operator&& 或 operator||,你必须知道,你正在从根本层面改变整个游戏规则,因为从此“函数调用”语义会取代“骤死式”语义。

如果你将 && 或 || 重载,就没有办法提供程序员预期(甚至依赖)的某种行为模式。

如果你没有什么好的理由将某个操作符重载,就不要去做。

条款8:了解各种不同意义的 new 和 delete

new operator

举个例子,当你写出这样的代码,就是使用了所谓的 new operator

string *ps = new string(“Hello World!”);
分配足够的内存,用来放置某类型的对象
调用一个constructor,为上一步中分配的内存中的那个对象设定初始值。
new operator 总是做这两件事,无论如何你不能改变其行为。

operator new

举个例子,函数 operator new 通常声明如下

void *operator new(size_t size);

上述返回值类型是 void*。此函数返回一个指针,直线一块原始的、未设初始值的内存。函数中的 size_t 参数表示需要分配多少内存。你可以将 operator new 重载,加上额外的参数,但第一参数的类型必须总是 size_t。

举个例子

void *rawMemory = operator new(sizeof(string))

这里的 operator new 将返回指针,指向一块足够容纳一个 string 对象的内存。和 malloc 一样 operator new 的唯一任务就是分配内存。

placement new

new(内存地址)([实参]);
class base{
public:
    base(int p1)
        :p1(p1) {};
    int p1;
};

int main()
{
    std::cout << sizeof(base) << std::endl;     // 4
    std::cout << sizeof(char) << std::endl;     // 1
    char *memory = new char[sizeof(base)*12];   // address: 0xdb3278

    base *b1 = new(&memory[0]) base(1000);      // address:0xdb3278    val:e8 03 00 00     03e8    = 1000
    base *b2 = new(&memory[4]) base(2);         // address:0xdb327c    val:02 00 00 00     02      = 2
    base *b3 = new(&memory[8]) base(9999999);   // address:0xdb3280    val:7f 96 98 00     98967f  = 9999999
}

简而言之,如果你有一些分配好的内存,且需要在上面构建对象。那么可以使用 placement new。

分配内存构造
new operator
operator new×
placement new×

至于delete也是同理,不在这过多描述(很少使用)。

条款9:利用 deconstructors 避免泄露资源

  • 使用析构函数
  • 使用智能指针

当然,到了现在一般更喜欢依赖RAII来对资源进行自动管理,也更加优雅。

条款10:在 constructors 内阻止资源泄露

  • C++ 只会析构已构造完成的对象
  • 对于在构造期抛出 exceptions 的对象,C++不会自动清理,所以你必须设计你的 constructors 使它们能够在那种情况下也能自我清理。
  • 一个更好的解答是,接受条款9的忠告,将point data members所指对象视为资源,交给局部对象管理(即使用智能指针)。

条款11:禁止异常(exceptions)流出 destructors 之外

两种情况下 destructor 会被调用

  • 当对象在正常状态下被销毁,也就是当它离开了它的生存空间(scope)或是被明确地删除。
  • 当对象被 exception 处理机制——也就是 exception 传播过程中的 stk-unwinding(栈展开)机制——销毁。

危害

  • 如果控制权基于 exception 的因素离开 destructor,而此时正有另一个 exception 处于作用状态,C++ 会调用 terminate 函数。此函数会将你的程序结束掉——它会立刻动手,甚至不等局部对象被销毁。

全力阻止 exceptions 传出 destructors 的好处:

  • 避免 terminate 函数在 exception 传播过程的栈展开(stack-unwinding)机制中被调用。
  • 协助确保 destructors 完成其应该完成的所有事情。

换句话说,一定要保证析构函数是nonexpection的。

条款12:了解“抛出一个 exception” 与 “传递一个参数” 或 “调用一个虚函数” 之间的差异

函数参数和 exceptions 的传递方式有3种:

  • by value
  • by reference
  • by pointer

区别1:当你调用一个函数,控制器最终会回到调用端(除非函数失败以至于无法返回),但是当你抛出一个 exception ,控制权不会再回到抛出端。而且一个对象被抛出作为 exception 时,总是会发生复制(copy)。

区别2:“抛出exception”比“传递参数”慢。因为“exception objects 必定会造成复制行为”这一事实,所以解释了“抛出exception”常常比“传递参数”慢。复制动作永远是以静态类型为本。

区别3:函数调用过程中将一个临时对象传递给一个 non-const reference 参数是不允许的,但是对 exceptions 则属合法。一个被抛出的对象(必为临时对象)可以简单地用 by reference 的方式捕捉,不需要以 by reference-to-const 的方式捕捉。

区别4:“抛出exception” 比 “传递函数参数”多构造一个“被抛出物”的副本(并于稍后析构),千万不要抛出一个指向局部对象的指针,因为该局部对象会在 exception 传离其 scope 时被销毁,因此 catch 子句会获得一个指向“已被销毁的对象”的指针。这正时“义务性复制(copy)规则”的设计要避免的情况。

区别5:“自变量传递”与“exception 传播”两动作有着互异的做法。

区别6:catch 子句总是依出现顺序做匹配尝试。

总结:“传递对象到函数去,或是以对象调用虚函数”和“将对象抛出成为一个exception”之间,有3个主要差异

  • exception objects 总是会被复制,如果以 by value 方式捕捉,它们甚至被复制两次。至于传递给函数参数的对象则不一定得复制。
  • 被抛出成为 exceptions 的对象,其被允许的类型转换动作,比“被传递到函数去”的对象少。
  • catch 子句以其“出现于源代码的顺序”被编译器检验对比,其中第一个匹配成功者变执行;而当我们以某对象调用一个虚函数,被选中执行的是那个“与对象类型最佳吻合”的函数,不论它是不是源代码所列的第一个。

条款13:以 by reference 方式捕捉 exceptions

  • 避开 exception objects 的切割(slicing)的问题
  • 保留捕捉标准 exceptions 的能力
  • 约束了exception objects 需被复制的次数

条款14:明智运用 exception specifications

告诉编译器函数不引发任何异常。 但是,在 std:c++14 模式下,如果函数确实引发异常,这可能会导致未定义的行为。 因此,建议使用 noexcept 运算符:

条款15:了解异常处理(exception handling)的成本

只要你用上那么一个,也就是说一旦你决定捕捉 exceptions,你就得付出那样的成本。不同的编译器以不同的方法实现 try 语句块,代码大约整体膨胀 5%~10%,执行速度亦下降这个数。

为了将此成本最小化,你应该避免非必要的 try 语句块

条款16:谨记 80-20 法则

软件的证一性能几乎总是由其构成要素(代码)的一小部分决定。

条款17:考虑使用 lazy evaluation(缓式评估)

  • Reference Counting(引用计数):在你真正需要之前,不必着急为某物做一个副本——可避免非必要的对象复制
  • 区分读和写:可区别 operator[]的读和写的动作
  • Lazy Fatching(缓式取出):可避免非必要的数据库读取动作
  • Lazy Expression Evaluation(表达式缓评估):可避免非必要的数值计算动作

总结
如果你的计算是必要的,lazy evaluation 并不会为你的程序节省人和工作或任何时间。只有当“你的软件被要求执行某些计算,而那些计算其实可以避免”的情况下,lazy evaluation 才有用处。

条款18:分期摊还预期的计算成本

简单来说,就是使用缓存。

int findCubicleNumber(const string &employeeName)
{
    typedef map<string, int> CubicleMap;
    static CubicleMap cubes;

    CubicleMap::iterator it = cubes.find(employeeName)  ;

    if(it == cubes.end())
    {
        int cubicle = ...   // 这里逻辑处理

        cubes[employeeName] = cubicle;
        return cubicle;
    }

    return (*it).second;
}

最后一个语句返回 (*it).second 而非传统的 it->second,为什么?答案关系到STL实行的规矩。简单地说,iterator本身是对象,不是指针,所以并不能保证 -> 可施行于 it 身上。但STL明确要求 . 和 * 对 iterators 必须有效,所以 (*it).second 虽然语法上笨拙,却保证能够有效运行。

条款19:了解临时对象的来源

临时对象可能很耗成本,所以你应该尽可能消除它们。这本书写成的时候比较早,到现在有了std::move和移动构造,移动赋值,就转换成了左值和右值的问题。

条款20:协助完成“返回值优化(RVO)

函数返回对象,背后隐藏着 constructor 和 destructor。如果是为了行为正确而不得不这么做,是可以返回一个对象的;否则就不要那么做。

有人企图采用某些方法消除 by-value的返回方式

const Rational* operator* (const Rational& lhs, const Rational& rhs);

Rational a = 1, b = 2;
Rational c = *(a * b);

这样会使得整个调用流程显得不自然,同时调用者也需要手动删除此函数返回的指针,不然会导致资源泄露。

有些人试图返回 references, 于是就有了

//h
const Rational& operator* (const Rational& lhs, const Rational& rhs);

//cpp
const Rational& operator* (const Rational& lhs, const Rational& rhs){
    Rational result(lhs.numerator() * rhs.numerator(), lhs.denominator * rhs.denominator());
    return result;
}

//use
Rational a = 1, b = 2;
Rational c = a * b;

这看起来似乎没有问题,但是当局部变量 result 离开了 const Rational& operator* 之后,就被自动销毁了。所以 const Rational& operator* 实际返回的 reference 指向的是一个不在存活的对象。

条款21:利用重载技术(overload)避免隐式类型转换

假设我们有这么一个结构

class UPInt{
    UPInt();
    UPInt(int value);
    ...
}

UPInt operator+(const UPInt& lhs, const UPInt& rhs);

当我们调用

UPInt upi1, upi2, upi3;
...

upi3 = upi1 + upi2;     // 成功,调用了 UPInt operator+(const UPInt& lhs, const UPInt& rhs);
upi3 = upi1 + 1;        // 成功,生成了临时对象
upi3 = 1 + upi1;        // 成功,生成了临时对象

其中 upi1 + 1 与 1 + upi1 都会进行隐式类型转换,这里会有一点成本。为了避免隐式类型转换带来的开销,我们可以重载 UPInt operator+ 这个函数

UPInt operator+(const UPInt& lhs, const int rhs);
UPInt operator+(const int lhs, const UPInt& rhs);

来消除类型转换。但是我们不能狂热过度写出下面的函数

UPInt operator+(const int lhs, const int rhs);

这会导致可怕的灾难。

条款22:考虑以操作符符合形式(op=)取代其独身形式(op)

到目前为止 C++ 并不考虑在 operator+,operator= 和 operator+= 之间设立任何互动关系。如果你希望这三个操作符都存在并且有着你所期望的互动关系,你必须自己实现。

三个于效率有关的情况需要注意

  1. 一般而言,符合操作符比起对应的独身版本效率高,因为独身版通常必须返回一个新对象,而我们必须因此负担一个临时对象的构造成本和析构成本。至于复合版本则是直接将结果写入其左端自变量,所以不需要产生一个临时对象来放置返回值。
  2. 如果同提供某个操作符的复合形式和独身形式,便允许你的客户在效率与便利性之间做取舍。

下面的两个例子中,第二个虽然更容易理解,但是却比第一个多构造了一个临时对象。

//Good
template<class T>
const T operator+(const T& lhs, const T& rhs)
{ return T(lhs) + rhs; }

//Not Good
template<class T>
const T operator+(const T& lhs, const T& rhs)
{ 
    T result(lhs);
    return result += rhs; 
}

身为一位程序库设计者,你应该为两者都提供。

条款23:考虑使用其他程序库

由于不同的程序库将效率、扩充性、移植性、类型安全性等的不同设计具体化,有时候你可以找找看是否存在另一个功能相近的程序库而其在效率上有较高的设计权重。

条款24:了解虚函数、多重继承、虚基类和运行类型的成本

  1. 虚函数
    当一个虚函数被调用,执行的代码必须对应于“调用者(对象)的动态类型”。大部分编译器使用所谓的 virtual tables 和 virtual table pointers —— 此二者通常被简写为 vtabls 和 vptrs。

虚函数成本:

  • 你必须为每个拥有虚函数的 class 耗费一个 vtable 空间,其大小视虚函数的个数(包括继承而来的)而定。
  • 你必须在每一个拥有虚函数的对象内付出“一个额外指针”的代价。调用一个虚函数的成本,基本上和”通过一个函数指针来调用函数“相同。虚函数本身并不构成性能上的瓶颈。
  • 你事实上废弃了 inlining。虚函数不应该 inlined。因为 inline 意味“在编译期,将调用端的调用动作被调用函数的函数本身取代”,而 virtual 则意味着“等待,知道运行时期才知道哪个函数被调用”。
  1. 多重继承
    多重继承问我导致 virtual base classes(虚拟基类)的需求。
    在 non-virtual base class 的情况下,如果 derived class 在其 base class 有多条继承路径,则此 base class 的 data members 会在每一个 derived class object 体内复制滋生,每一个副本对应 “derived class 和 base class 之间的一条继承路线”。

  2. 虚拟继承
    让base class 成为 virtual,可以消除这样的复制现象,学习资料

  3. RTTI
    RTTI 让我们得以在运行时获得 objects 和 classes 的相关信息,他们被存发在类型为 type_info 的对象内。一个 class 只需要一份 RTTI 信息就好,但是必须有某种办法让其下属的每个对象都能取用它。只有当某种类型拥有至少一个虚函数,才保证我们能够检验该类型对象的动态类型。

性质对象大小增加Class数据量增加Inlining 几率低
虚函数 Virtual Functions
多重继承Multiple Inheritance
虚拟基类 Virtual Base Classes往往如此有时候
运行时期类型辨识RTTI

条款25:将 constructor 和 non-member functions 虚化

constructor 虚化其实不是真正的虚化构造函数,书中所讲不是很好理解,可以参考下面的例子:

class Shape {
public:
  virtual ~Shape() { }                 // A virtual destructor
  virtual void draw() = 0;             // A pure virtual function
  virtual void move() = 0;
  // ...
  virtual Shape* clone()  const = 0;   // Uses the copy constructor
  virtual Shape* create() const = 0;   // Uses the default constructor
};
class Circle : public Shape {
public:
  Circle* clone()  const;   // Covariant Return Types; see below
  Circle* create() const;   // Covariant Return Types; see below
  // ...
};
Circle* Circle::clone()  const { return new Circle(*this); }
Circle* Circle::create() const { return new Circle();      }

通过调用 clone() 或 create()虚函数来间接地调用构造函数与拷贝构造。即虚假的构造函数与真正的构造函数。

而至于non-member functions 的虚化十分容易:写一个虚函数做实际工作,再写一个什么都不做的非虚函数,只负责调用虚函数。

条款26:限制某个class所能产生的对象数量

1.允许零个或一个对象

1.1.零个对象

每当即将产生一个对象,就会有一个 constructor 被调用。阻止某个 class 产出对象的最简单方法就是将其 constructors 声明为 private

class CantBeInstantiated {
private:
    CatBeInstantiated();
    CantBeInstantiated(const CantBeInstantiated&)
    ...
}

1.2.封装对象在函数内

我们可以将对象封装在某个函数内,如此一来只有唯一一个对象被产生.
接下使用打印机的例子来说明。

class PrintJob;

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;           //唯一一个打印机对象
}

这里有三个点值得注意

  • Printer classconstructors 属性 private,可以压制对象的诞生。
  • 全局函数 thePrinter 被声明在此 class 的一个 friend,致使 thePrinter 不受 private constructors 的约束。
  • thePrinter 内含一个 static Printer 对象,意思只有一个 Printer 对象被产生出来。

在使用的时候,只需要调用 thePrinter(). 就可以

1.2.消除firend

以上方的例子为例,我们可以让 thePrinter 成为 Printer 的一个 static member function,消除 friend 的必要性。我们就能获得接下来的代码

class Printer {
public:
static Printer& thePrinter();
    ...

private:
    Printer();
    Printer(const Printer& rhs);
    ...
};

Printer& Printer::thePrinter()
{
    static Printer p;
    return p;
}

现在用户调用 Printer时,会显得冗长

Printer::thePrinter().reset();

1.3.使用namespace

另一个做法是把 PrinterthePrinter 从全局空间移走,放进一个 namespace 内。我们就可以得到以下代码

namespace PrintingStuff{
    class Printer {         //这个class 位于 PrintingStuff namespace 内
    public:
        void submitJob(const PrintJob& job);
        void reset();
        void performSelfTest();
        ...

    friend Printer& thePrinter();

    private:
        Printer();
        Printer(const Printer& rhs);
        ...
    };

    Printer& thePrinter()   这个函数也位于 PrintingStuff namespace{
        static Printer p;
        return p;           //唯一一个打印机对象
    }
}

有了这个 namespace,用户就能使用完全限定名来取用 thePrinter:

using PrintingStuff::theprinter;

thePrinter().reset();
...

在此代码实现中,又两个精细的地方值得探讨。

  • 形成唯一一个 Printer 对象的,是函数中的 static 对象,而非 class 中的 static 对象。
    C++ 的一个设计哲学基础是你不应该为你并不使用的东西付出任何代价。
    function static的初始时机:在该函数第一次被调用时。
    class static 则不一定在什么时候初始化。
  • 函数的 static 对象与 inlining 的互动。
Printer& thePrinter()
{
 static Printer p;
 return p;
}

如果上方的函数被声明为 inline,那么你的程序可能会拥有多份该 static 对象的副本。因为 inline 意味着编译器应该将每一个调用动作以函数本身取代。
千万不要产生内含 local static 对象的 inline non-member functions

1.4.使用抛出异常提示产生了过多的对象

我们继续改进我们的 Printer,给定一个函数来抛出一个类型为 TooManyObjectsexception

class Printer{
public:
    class TooManyObjects{};

    Printer();
    ~Printer();
    ...

private:
    static size_t numberObjects;
    Printer(cosnt Printer& rhs);    //我们限制只有一个打印机,所以绝不允许复制行为,所以放在private区
}

size_t Printer::numberObjects = 0;

Printer::Printer()
{
    if(numObjects >= 1)    
    {
        throw TooManyObjects();
    }

    // 这里处理一般的构造
    ++numObjects;
}

Printer::~Printer()
{
    // 这里处理析构
    --numObjects;
}

这个非常简单直观。

2.不同的对象构造状态

2.1.继承问题

假设我们有一台彩色打印机

class ColorPrinter: public Printer{
    ...
};

当我们调用下面的代码时

Printer p;
ColorPrinter cp;

我们其实构造了两个Printer对象,这个时候就会有 TooManyObjects exception 被抛出。

2.2.对象包含问题

当我们有对象包含Printer时,就会出现这样的代码

class Machine{      //这是一个机器,处理打印、传真等功能
private:
    Printer p;      // 针对打印功能
    FaxMachine f;   // 针对传真功能
    ...
}

// 这里调用
Machine m1;         // 没有问题
Machine m2;         // 抛出 TooManyObjects exception

可以看到,当我们构造 m2 的时候,就出现问题了,因为此时 Printer 对象位于较大对象当中

2.3.阻止继承

为了阻止上述的继承导致的问题,我们可以通过把 constructors 变为 private 来实现禁止派生。

3.允许对象生生灭灭

到这里我们已经能得到一个较好的版本了,可以限制对象生成的数量。

class Printer{
public:
    class TooManyObjects{};

    static Printer *MakePrinter();;
    ~Printer();

    void submitJob(cibst PrintJob &job);    // 这里是一些外部调用的接口
    ...

private:
    static size_t::numObjects;              // 用于记录已经生成的Printer对象
    const size_t Printer::maxObject = 10;   // 用于限制最大的对象数量

    Printer();                              // 我们不允许继承,所以放置再private区
    Printer(cosnt Printer& rhs);            // 我们不允许直接调用拷贝构造,所以放在private区
    ...
}

Printer::Printer()
{
    if(numObjects >= maxObjects)
    {
        throw TooManyObjects();
    }

    // 这里处理一般的构造
    ++numObjects;
}

Printer::Printer(cosnt Printer& rhs)
{ 
    // 这里处理和默认构造函数一致
}

Printer::~Printer()
{
    // 这里处理析构
    --numObjects;
}

Printer *Printer::makePrinter()
{ return new Printer; }

Printer *Printer::MakePrinter(const Printer& rhs)
{ return new Printer(rhs)}

4.一个用来计算对象个数的 Base Class

接下来我们使用 template 来实现

template<class BeingCounted>
class Counted{
public:
    class TooManyObjects{};
    static int objectCount() { return numObjects; }
protected:
    Counted();
    Counted(const Counted &rhs);
    ~Counted() { --numObjects; }

private:
    static int numObjects;
    static const size_t maxObjects;
    void init();                    // 用以避免 ctor 码重复出现
};

template<class BeingCounted>
Counted<BeingCounted>::Counted() { init(); }

template<class BeingCounted>
Counted<BeingCounted>::Counted(const Counted<BeingCounted>&) { init(); }

template<class BeingCounted>
void Counted<BeingCounted>::init()
{
    if (numObjects >= maxObjects) throw TooManyObjects();
    ++numObjects;
}

// 下面我们要使用上面的模板,实现一个只能构造 10 个对象的打印机

const size_t Counted<Printer>::maxObjects = 10;

class Printer : private Counted<Printer> {
public:
    // pseudo-constructors
    static Printer *makePrinter();
    static Printer *makePrinter(const Printer& rhs)
    ~Printer();

    void submitJob(const PrintJob& job);
    void reset();
    void performSelfTest();
    ...

    using Counted<Printer>::objectCount;
    using Counted<Printer>::TooManyObjects;
private:
    Printer();
    Printer(const Printer &rhs);
} 

条款27:要求(或禁止)对象产生于 heap 中

要求对象产生于 heap 之中Heap-Based Objects

只要限制 destructorconstructors 的运用,便可阻止 non-heap object 的诞生。但是他同时也妨碍了继承(inheritance)和包含(containment

禁止对象产生于 heap

首先我们需要知道有三种情况下,对象可能被产生于 heap

  1. 对象被直接实例化
  2. 对象被实例化为 derived class objects 内的 “base class 成分”
  3. 对象被内嵌于其他对象之中

简单来说,可以直接设置 operator newoperator deleteprivate 即可

class UPNumber{
private:
    static void *operator new(size_t size);
    static void operator delete(void *ptr);
    ...
}

如果你也像禁止“由 UPNumber 对象所组成的数组” 位于 heap 内,可以将 operator new[]operator delete[] 亦声明为 private。当然现在更推荐直接delete掉。

条款28:Smart Pointers(智能指针)

这里的智能指针比较早,包括了auto_ptr,可以单独了解,本书内容有点过时了。

条款29:Reference counting(引用计数)

总体意思需要结合上一个条款中谈到的 smart ptr 来实现引用计数。有几个方面需要考虑

  • 需要有一个结构体即存储引用次数,也要存储数据
  • 上述需要生成在堆中,通过指针访问地址
  • 正确且自动处理引用增加及减少情况
    • 构造函数(包括拷贝构造等)
    • 析构函数
    • 赋值
  • 修改对象数据时需要调整引用及分享权限(也就是 copy on write
  • 避免内存泄漏

条款30:Proxy classes(替身类、代理类)

简单来说,如果我们有一个 string a = "123"; 此时我们想用 a[0] 取得 '1'。然而我们这里取得'1'之后,我们会有以下一种行为

  1. 只读,此时我们不需要修改 reference count
  2. 写入,此时我们需要修改 reference count

所以我们这里返回的时候可以不返回一个 char 而是返回一个结构体,且只要这个结构体能够转换为char就行了。这个和之前所讲的 条款17:缓式评估 有相同的实现思路,可以折回去参考一下。

条款31:让函数根据一个以上的对象类型来决定如何虚化

这里指出了一个情况,例如我们有三种物体,且都继承GameObject

  • SpaceShip 飞船
  • SpaceStation 空间站
  • Asteroid 陨石

不同的物体会相撞,且会产生不同的结果。例如飞船和空间站相撞,飞船能进入到空间站内;飞船和陨石相撞,两者都会摧毁。
这个时候,我们需要一个方法,传入任意俩个GameObject都可以处理。

void processCollision(GameObject& object1, GameObject& object2)

书中讨论了一套方法,是一个不错的方法,但是感觉还不是很完美。目前就整理一下代码,记录下来。

#include <iostream>
#include <memory>
#include <string>
#include <map>

class GameObject {
public:
    virtual ~GameObject() {} //基类里面有虚函数,派生类继承后,使用typeid().name才能取得对应的class name

};
class SpaceShip : public GameObject {};
class SpaceStation : public GameObject {};
class Asteroid : public GameObject {};

//匿名namespace
namespace {
    using std::string;
    using std::map;
    using std::make_pair;
    using std::pair;
    using std::cout;
    using std::endl;

    void shipAsteroid(GameObject& spaceShip, GameObject& asteroid) { cout << "spaceShip collide with asteroid" << endl; };
    void shipStation(GameObject& spaceShip, GameObject& spaceStation) { cout << "spaceShip collide with spaceStation" << endl; };
    void asteroidStation(GameObject& asteroid, GameObject& spaceStation) { cout << "asteroid collide with spaceStation" << endl; };

    void asteroidShip(GameObject& asteroid, GameObject& spaceShip) { shipAsteroid(spaceShip, asteroid); };
    void stationShip(GameObject& spaceStation, GameObject& spaceShip) { shipStation(spaceShip, spaceStation); };
    void stationAsteroid(GameObject& spaceStation, GameObject& asteroid) { asteroidStation(asteroid, spaceStation); };
}

//碰撞map
class CollisionMap {
public:
    //这里使用单例
    static CollisionMap* theCollisionMap() {
        static CollisionMap CM;
        return &CM;
    };

    typedef void (*HitFunctionPtr)(GameObject&, GameObject&);

    //这里添加新的碰撞处理函数,成对处理
    void addEntry(const string& type1, const string& type2, HitFunctionPtr collisionFunction)
    {
        if (collisionMap.find(std::make_pair(type1, type2)) != collisionMap.end()) return;

        //成对添加
        collisionMap[std::make_pair(type1, type2)] = collisionFunction;
        collisionMap[std::make_pair(type2, type1)] = collisionFunction;
    }

    //这里移除碰撞函数
    void removeEntry(const string& type1, const string& type2) {
        if (collisionMap.find(std::make_pair(type1, type2)) != collisionMap.end()) return;

        //成对移除
        collisionMap.erase(std::make_pair(type1, type2));
        collisionMap.erase(std::make_pair(type2, type1));
    }

    //查找有没有对应的碰撞函数
    HitFunctionPtr lookup(const string& class1, const string& class2) {
        HitMap::iterator it = collisionMap.find(make_pair(class1, class2));
        if (it == collisionMap.end()) return 0;

        return (*it).second;
    }
private:
    typedef map<pair<string, string>, HitFunctionPtr> HitMap;
    HitMap collisionMap;

    CollisionMap() { initializeCollisionMap(); };
    CollisionMap(const CollisionMap&);

    // 这里可以内部初始化,也可以改为一个函数,来注册一下函数
    void initializeCollisionMap() {
        collisionMap.clear();

        addEntry("class SpaceShip", "class Asteroid", &shipAsteroid);
        addEntry("class SpaceShip", "class SpaceStation", &shipStation);
        // ...
    }
};

//匿名namespace
namespace
{
    //这里处理碰撞,会查找碰撞map,如果有函数就执行,没有的话就抛出异常
    void processCollision(GameObject& object1, GameObject& object2) {  
        CollisionMap* CM = CollisionMap::theCollisionMap();

        CollisionMap::HitFunctionPtr phf = CM->lookup(typeid(object1).name(), typeid(object2).name());
        if (phf) phf(object1, object2);
        else cout << "UnkowCollision! " << typeid(object1).name() << " - " << typeid(object2).name() << endl;
    }
}

int main() {
    SpaceShip spaceShip;
    Asteroid asteroid;
    SpaceStation spaceStation;

    processCollision(spaceShip, asteroid);  //spaceShip collide with asteroid
    processCollision(asteroid, spaceShip);  //UnkowCollision! class Asteroid - class SpaceShip
    processCollision(spaceShip, spaceStation); //spaceShip collide with spaceStation
    processCollision(asteroid, spaceStation); //UnkowCollision! class Asteroid - class SpaceStation

    return 0;
}

条款32:在未来时态下发展程序

对于未来式思维,作者希望我们多考虑一些东西:

  • 提供玩真的class —— 即使某些部分目前用不到。当心的需求进来,你不太需要回头去修改那些 classes
  • 设计你的接口,使有利于共同的操作行为,阻止共同的错误。让这些 classes 轻易地被正确运用,难以被错误运用。
  • 尽量使你都代码一般化(泛化),除非有不良的巨大后果。

但是注意在之前的effective c++中提到的,过早优化是性能恶化之源。

条款33:将非尾端类(non-leaf classes) 设计为 抽象类(abstract classes)

继承体系中的 non-leaf(非尾端)类应该使抽象类。如果 使用外界供应的程序库,你或许可以对其法则做点变通;单如果代码完全在你掌控之下,坚持这个法则,可以为你带来许多好处,并提升整个软件的可靠度、健壮度、精巧度、扩充度。

当然了,现在的设计思路一般都是组合优于继承,继承能干的组合就能够解决。

条款34:如何在同一程序中结合 C++ 和 C

  • Name Mangling(名命重整)
  • Statics 的初始化
  • 动态内存分配
  • 数据结构的兼容性

并指明了以下守则

  • 确定你的 C++ 和 C 编译器产出兼容的目标文件(object files)。
  • 将双方都使用的函数声明为 extern "C"
  • 如果可能,尽可能在 C++ 中撰写 main
  • 总是以 delete 删除 new 返回的内存:总是以 free 释放 malloc 放回的内存。
  • 将两个语言间的“数据结构传递”限制于 C 所能了解的形式;C++ structs 如果内含非虚函数,但是不受此限。

条款35:让自己习惯于标准 C++ 语言

拥抱新的c++标准。


文章作者: JoyTsing
版权声明: 本博客所有文章除特別声明外,均采用 CC BY 4.0 许可协议。转载请注明来源 JoyTsing !
评论
  目录