Effective C++

改善程序与设计的55个具体做法

让自己习惯 C++

1、视 C++为一个语言联邦

C++是一个多重范型编程语言,支持过程形式、面向对象形式、函数形式、泛型形式、元编程形式,C++高效编程守则视状况而变化,取决于使用C++哪一个部分,在合适的场景选择使用合适的功能

2、尽量以 const、enum、inline 替换 define

C开发中以前都在使用宏定义define,但是往往难以维护而且难以调试

#define ASPECT_RATIO 1.653
//使用const替换
const double AspectRatio = 1.653;

const的底层const与顶层const要知道

const char* const authorName = "Scott Meyers";//字符串用const代替宏
//cpp等推荐使用string,更加抽象
const std::string authorName("Scott Meyers");

对于类内的可以使用静态成员变量

class A{
private:
    static const int Num = 5;//常量声明式
    int scores[Num];
};

如果不使用A::Num的地址,那么只使用声明式即可,如果要用地址则需要定义式在源文件中

const int A::Num;//Num定义,声明时已经提供初值,定义式不再需要提供初值
//如果编译器不支持声明式初始化,则要在定义式提供初值

类内的enum更像define,不能取值

class A
{
public:
    enum
    {
        Num = 4
    };
    int arr[Num];
};

int main(int argc, char **argv)
{
    cout << A::Num << endl; // 4
    return 0;
}

关于define写函数形式的一些问题,尽量使用inline,一般inline函数写在头文件中,因为源文件编译时需要将函数展开

#define MAX(a, b) (a) > (b) ? (a) : (b)

int main(int argc, char **argv)
{
    int n = MAX(1, 2);
    cout << n << endl; // 2
    // int n1 = MAX(n++, ++n); 这种问题容易混乱
    return 0;
}
//尽可能使用inline函数,也可以使用模板进行扩展
//函数也能展开,而且利于开发维护
template <typename T>
inline T mymax(const T &a, const T &b)
{
    return a > b ? a : b;
}

3、尽可能使用 const

const有顶层const(一般为指针不能修改)与底层const(数据不能修改),

char greeting[] = "hello"; 
char *p = greeting;//none-const pointer,non-const data
const char* p =greeting;//non-const pointer,const data
char* const p = greeting;//const pointer,non-const data
const char* const p = greeting;//const pointer,const data

有两种形式的表达的意思是相同的,都是data const

void func(char const *p);
void func(const char*p);

函数返回常量值的作用

class A
{
public:
    A(const int &n) : num(n)
    {
    }
    int num;
    const A operator*(const A &o)
    {
        return A(this->num * o.num);
    }
};

int main(int argc, char **argv)
{
    A a(1);
    A b(2);
    A c = a * b;
    //(a * b) = 3;           // 错误:操作数类型为: const A = int,如果返回的不是const值则不报错
    cout << c.num << endl; // 2
    return 0;
}

const成员函数

class A
{
public:
    static const int num{9};
    char arr[num] = {0};
    const char &operator[](std::size_t position) const
    {
        return arr[position];
    }
    char &operator[](std::size_t position)
    {
        return arr[position];
    }
};

int main(int argc, char **argv)
{
    A a;
    const A b;
    cout << a[0] << endl; // 调用char &A::operator[]
    cout << b[0] << endl; // 调用 const char &A::operator[]
    // b[0] = '1'; 错误
    a[0] = 'a';
    cout << a[0] << endl; // a
    return 0;
}

在const和non-const成员函数中避免重复,可以让non-const调用const成员函数

class A
{
public:
    static const int num{9};
    char arr[num] = {0};
    const char &operator[](std::size_t position) const
    {
        //...
        // ...
        return arr[position];
    }
    char &operator[](std::size_t position)
    {
        return const_cast<char &>(static_cast<const A &>(*this)[position]);
    }
};

4、确定对象被使用前已经被初始化

int n;
cout << n << endl;

会输出什么,大部分都会说0,但是不一定,有随机性,不能相信机器与编译器,加上个初始值不会杀了你

为什么要使用初始化列表,而不是在构造函数内赋值

class A
{
public:
    A()
    {
        cout << "A()" << endl;
    }
    A(const int &n)
    {
        cout << "A(const int &n)" << endl;
    }
    const A &operator=(const int &n)
    {
        cout << "const A &operator=(const int &n)" << endl;
        return *this;
    }
};

class B
{
public:
    B()
    {
        a = 1; // 这是赋值不是初始化
    }
    A a;
};

int main(int argc, char **argv)
{
    B b;
    // A()
    // const A &operator=(const int &n)
    return 0;
}

如果使用构造函数列表,It’s fucking cool.特别注意的是初始化列表为什么要与在类内声明的顺序相同,这是因为它们构造的现后顺序并不取决于在初始化列表中的顺序而是在类内声明的顺序所以我们写代码直接把二者顺序同步好了。

class B
{
public:
    B() : a(1)
    {
    }
    A a;
};

int main(int argc, char **argv)
{
    B b;
    // A(const int &n)
    return 0;
}

什么是local-static对象和non-local static对象,栈内存与堆内存对象都不是static对象。像全局对象、定义在命名空间作用域内的、在class内的、在函数内的、以及在源文件作用域内的被声明为static的对象。其中在函数内的为local-static其他为non-local static。程序结束时static会被自动销毁,析构函数在main返回前调用

可能有时会使用extern访问在其他源文件定义的对象,如果一个源文件中某个non-local static对象初始化时用到了另一个源文件中的non-local static对象,可能会出现赋值操作右边的变量没有初始化过的情况,因为C++中:对于“定义于不同源文件内的non-local static对象”的初始化次序并无明确定义

//mian.cpp
extern int n;
int n1=n;
//main1.cpp
int n;

怎样解决这一问题,推荐使用local static代替non-local static

//main.cpp
int n1=n();
//main1.cpp
int& n(){
    static int v=100;
    return v;
}

上面例子可能还不清楚看下面这个

//main.cpp
#include <iostream>
#include "main1.h"
#include "main2.h"
using namespace std;

int main(int argc, char **argv)
{
    return 0;
}
//main1.h
#pragma once

class main1
{
public:
    main1();
};
//main1.cpp
#include "main1.h"
#include <iostream>

main1 main1Object;

main1::main1()
{
    std::cout << "main1" << std::endl;
}
//main2.h
#pragma once
#include "main1.h"

class main2
{
private:
    /* data */
public:
    main2(/* args */);
};
//main2.cpp
#include "main2.h"
#include <iostream>

main2 main2Object;

main2::main2(/* args */)
{
    std::cout << "main2" << std::endl;
}

请问main1和main2谁先输出,答案是不确定的,所以总之记住全局变量之间不要互相引用初始化,特别是在不同源文件中的不同全局变量。

gaowanlu@DESKTOP-QDLGRDB:/mnt/c/Users/gaowanlu/Desktop/MyProject/note/testcode$ g++ -c main1.cpp
gaowanlu@DESKTOP-QDLGRDB:/mnt/c/Users/gaowanlu/Desktop/MyProject/note/testcode$ g++ -c main2.cpp
gaowanlu@DESKTOP-QDLGRDB:/mnt/c/Users/gaowanlu/Desktop/MyProject/note/testcode$ g++ -c main.cpp
gaowanlu@DESKTOP-QDLGRDB:/mnt/c/Users/gaowanlu/Desktop/MyProject/note/testcode$ g++ main.o main1.o main2.o -o main.exe
gaowanlu@DESKTOP-QDLGRDB:/mnt/c/Users/gaowanlu/Desktop/MyProject/note/testcode$ ./main.exe
main1
main2
gaowanlu@DESKTOP-QDLGRDB:/mnt/c/Users/gaowanlu/Desktop/MyProject/note/testcode$ g++ main.o main2.o main1.o -o main.exe
gaowanlu@DESKTOP-QDLGRDB:/mnt/c/Users/gaowanlu/Desktop/MyProject/note/testcode$ ./main.exe
main2
main1
gaowanlu@DESKTOP-QDLGRDB:/mnt/c/Users/gaowanlu/Desktop/MyProject/note/testcode$ 

还心存执念,那你循环引用下,初始化肯定有问题吧,n1在main.cpp,n2在main1.cpp,n1用n2初始化,n2用n1初始化。这样虽然能编译,能运行,但它们的初始化确实有问题。

构造析构赋值运算

5、了解 C++默默编写并调用哪些函数

默认生成这些函数是C++的基础知识,应该问题不大,当程序中使用这些函数时编译器才会生成,如果自己声明了自定义的相关函数则编译器不再自动生成默认的对应函数

class A
{
public:
    A() {}
    ~A() {}
    A(const A &a)
    {
        this->num = a.num;
    }
    A &operator=(const A &a)
    {
        this->num = a.num;
        return *this;
    }
    int num;
};

6、若不想使用编译器自动生成的函数应明确拒绝

//写为private,只声明不定义
class A
{
public:
    A() {}
    ~A() {}

private:
    A(const A &a); // 只声明不定义
    A &operator=(const A &a);
};
//使用delete关键词
class B
{
public:
    B() {}
    ~B() {}
    B(const B &b) = delete;
    B &operator=(const B &b) = delete;
};
int main(int argc, char **argv)
{
    A a;
    A b;
    // a = b; 错误
    return 0;
}

还可以使用Uncopyable基类的方式,在基类进行拷贝构造和赋值时,会先执行基类的相关函数

class A
{
public:
    A() {}
    A(const A &a)
    {
        cout << "A(const A&a)" << endl;
    }
    A &operator=(const A &a)
    {
        cout << "A& operator=(const A&a)" << endl;
        return *this;
    }
    virtual ~A() = default;
};

class B : public A
{
public:
    B() {}
    B(const B &b) : A(b)
    {
        cout << "B(const B&b)" << endl;
    }
    B &operator=(const B &b)
    {
        if (&b != this)
        {
            A::operator=(b);
        }
        cout << "B &operator=(const B &b)" << endl;
        return *this;
    }
    ~B() = default;
};

int main(int argc, char **argv)
{
    B b1;
    B b2 = b1;
    // A(const A&a)
    // B(const B &b)
    return 0;
}

那么就可以写一个Uncopyable基类

class A
{
public:
    A() {}
    virtual ~A() = default;

private:
    A(const A &a);
    A &operator=(const A &a);
};

class B : public A
{
public:
    B() {}
    ~B() = default;
    // 理应当自动生成拷贝构造和赋值操作函数,但是由于不能访问基类部分,所以不能自动生成
};

int main(int argc, char **argv)
{
    B b1;
    // B b2 = b1;
    // 无法引用 函数 "B::B(const B &)" (已隐式声明) -- 它是已删除的函数
    return 0;
}

7、为多态基类声明 virtual 析构函数

先看以下有什么搞人的事情,深入理解此部分要对虚函数表以及C++多态机制有一定了解,下面的代码只执行了基类的析构函数只是释放了基类中buffer的动态内存,而派生类部分内存泄露,这是因为A*a,a被程序认为其对象只是一个A,而不是B,如果将基类析构函数改为virtual的,那么会向下找,找到~B执行,然后再向上执行如果虚函数有定义的话

class A
{
public:
    A() : buffer(new char[10])
    {
    }
    ~A()
    {
        cout << "~A()" << endl;
        delete buffer;
    }

private:
    char *buffer;
};

class B : public A
{
public:
    B() : buffer(new char[10])
    {
    }
    ~B()
    {
        cout << "~B()" << endl;
        delete buffer;
    }

private:
    char *buffer;
};

int main(int argc, char **argv)
{
    A *a = new B;
    delete a;
    //~A()
    return 0;
}

所以要修改为这样,即可

class A
{
public:
    A() : buffer(new char[10])
    {
    }
    virtual ~A()
    {
        cout << "~A()" << endl;
        delete buffer;
    }

private:
    char *buffer;
};

如果想让基类为抽象类,可以改为纯虚函数,与前面不同的时拥有纯虚函数的类为抽象类不允许实例化,纯虚函数不用定义。而虚函数是需要有定义的。

class A
{
public:
    A() {}
    virtual ~A() = 0;
};

A::~A() {}

class B : public A
{
};

int main(int argc, char **argv)
{
    // A a; 错误A为抽象类型
    B b;
    return 0;
}

8、别让异常逃离析构函数

例如以下情况

void freeA()
{
    throw runtime_error("freeA() error");
}

class A
{
public:
    A() {}
    ~A()
    {
        try
        {
            freeA();
        }
        catch (...)
        {
            // std::abort();//生成coredump结束
            // 或者处理异常
            //...
        }
    }
};

int main(int argc, char **argv)
{
    A *a = new A;
    delete a;
    return 0;
}

如果外部需要对某些在析构函数内的产生的异常进行操作等,应该提供新的方法,缩减析构函数内容

void freeA()
{
    throw runtime_error("freeA() error");
}

class A
{
public:
    A() {}
    ~A()
    {
        if (!freeAed)
        {
            try
            {
                freeA();
            }
            catch (...)
            {
                // std::abort();//生成coredump结束
                // 或者处理异常
                //...
            }
        }
    }
    void freeA()
    {
        ::freeA();
        freeAed = true;
    }

private:
    bool freeAed = {false};
};

int main(int argc, char **argv)
{
    A *a = new A;
    try
    {
        a->freeA();
    }
    catch (const runtime_error &e)
    {
        cout << e.what() << endl;
    }
    delete a;
    return 0;
}

9、绝不在构造和析构函数过程中调用 virtual 函数

1、构造函数中调用虚函数:

当在基类的构造函数中调用虚函数时,由于派生类的构造函数尚未执行,派生类对象的派生部分还没有被初始化。这意味着在基类构造函数中调用的虚函数将无法正确地访问或使用派生类的成员。此外,派生类中覆盖的虚函数也不会被调用,因为派生类的构造函数尚未执行完毕。

2、析构函数中调用虚函数:

当在基类的析构函数中调用虚函数时,如果正在销毁的对象是一个派生类对象,那么派生类的部分已经被销毁,只剩下基类的部分。此时调用虚函数可能会导致访问已被销毁的派生类成员,从而引发未定义行为。

以下程序是没问题的

class A
{
public:
    A()
    {
        func();
    }
    virtual ~A()
    {
        func();
    }
    virtual void func()
    {
        cout << "A::func" << endl;
    };
};

class B : public A
{
public:
    B()
    {
        func();
    }
    ~B()
    {
        func();
    }
    void func() override
    {
        cout << "B::func" << endl;
    }
};

int main(int argc, char **argv)
{
    B b;
// A::func 此时只有A::func 无B::func
// B::func 此时在执行B构造函数故执行B::func
// B::func 此时在执行B析构函数故执行B::func
// A::func 此时在执行A析构函数只有A::func 无B::func
    return 0;
}

10、令 operator=返回一个 reference to *this

像+=、-=、*=操作符函数可以没有返回值,但是如果想有赋值连锁形式就要返回引用

class A
{
public:
    A()
    {
    }
    virtual ~A()
    {
    }
    void operator=(const A &a)
    {
        cout << "=" << endl;
    }
};

int main(int argc, char **argv)
{
    A a1;
    A a2;
    a1 = a2; //=
    return 0;
}

赋值连锁形式,如果想要支持这种形式就要返回引用

int x1, x2, x3;
x1 = x2 = x3 = 1;
cout << " " << x1 << " " << x2 << " " << x3 << endl; // 1 1 1
//自定义为
A &operator=(const A &a)
{
    cout << "=" << endl;
    return *this;
}

11、在 operator=中处理自我赋值

Object obj;
obj=obj;//这不是有病吗

如何判断与解决此问题呢,或者定义使用std::swap(需要定义swap方法或重写operator=)

class A
{
public:
    virtual ~A()
    {
    }
    A &operator=(const A &a)
    {
        if (this == &a)
        {
            cout << "self" << endl;
            return *this;
        }
        cout << "other" << endl;
        //----------------------------------------------------
        A temp(a); // 临时副本,一面在复制期间a修改了导致数据不一致
        // 赋值操作
        //...
        //----------------------------------------------------
        return *this;
    }
};

int main(int argc, char **argv)
{
    A a;
    a = a; // self
    A a1;
    a = a1; // other
    return 0;
}

12、复制对象时勿忘其每一个成分

可能一开始的业务是这样,但后来加上了isman属性,但是你却忘了加到拷贝构造和赋值函数中,那么这是异常灾难,可能你还找不出来自己错在哪里

class A
{
public:
    A() {}
    A(const A &a) : num(a.num)
    {
    }
    A &operator=(const A &a)
    {
        this->num = a.num;
    }
    int num;
    //bool isman;
};

还有更恐怖的风险,在存在继承时,你可能忘记了基类部分,所以千万不能忘记

class A
{
public:
    A() {}
    virtual ~A(){};
    A(const A &a) : num(a.num)
    {
    }
    A &operator=(const A &a)
    {
        this->num = a.num;
        return *this;
    }
    int num;
};

class B : public A
{
public:
    B() : A()
    {
    }
    ~B() {}
    B(const B &b) : A(b), priority(b.priority) // 不要忘记
    {
    }
    B &operator=(const B &b)
    {
        A::operator=(b); // 不要忘记
        this->priority = b.priority;
        return *this;
    }
    int priority;
};

资源管理

13、以对象管理资源

下面的就是风险较大的情况

void func()
{
    int *ptr = new int(5);
    // ...
    // ... 做许多事情,中间可能会return,措施delete执行
    delete ptr;
}

14、在资源管理类中小心 copying 行为

请记住:

  1. 复制RAII对象必须一并复制它所管理的资源,所以资源的copying行为决定RAII对象的copying行为。
  2. 普遍常见的RAII class copying行为是:抑制copying、施行引用计数法。

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

#include <iostream>
using namespace std;

class RAII
{
public:
    RAII(int *ptr)
    {
        m_ptr = ptr;
    }
    ~RAII()
    {
        if (m_ptr)
        {
            delete m_ptr;
        }
    }

    int *get()
    {
        return m_ptr;
    }

    operator int()
    {
        if (m_ptr)
        {
            return *m_ptr;
        }
        return 0;
    }

    operator int *()
    {
        return m_ptr;
    }

private:
    int *m_ptr{nullptr};
};

int main(int argc, char **argv)
{
    RAII ptr(new int(9));
    *ptr.get() = 323;
    std::cout << (*ptr.get()) << std::endl; // 323

    int *resource = ptr;
    std::cout << *resource << std::endl; // 323

    return 0;
}

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

#include <iostream>
using namespace std;

int main(int argc, char **argv)
{
    int *arr = new int[100];
    int *ptr = new int;

    delete[] arr;
    delete ptr;

    return 0;
}

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

这句话什么意思呢?

void function(RAII raii, int i)
{
    // do something
}

function(RAII(new int(1)), otherFunction(199));

这样可能存在内存泄露的风险,因为可能出现下面的情况

  1. 执行new int
  2. 调用otherFunction
  3. 调用RAII的构造函数

万一中间调用otherFunction出现异常就完蛋了,所以请遵守规则,独立创建RAII

RAII raii(new int(1));
function(raii, otherFunction(199));

设计与声明

18、让接口容易被正确使用,不易被误用

设计一个组件,那么设计外部接口是非常重要的,正常使用的情况下,还应该做到不易用错,例如

class Date
{
public:
    Date(int month, int day, int year);
};

外部调用Date很容易将三个参数写错,或者写反,造成目的与实际效果不同,很难排查。上面的例子,可以为每类参数设计一个类,如

struct Day
{
    explicit Day(int d) : val(d)
    {
    }
    int val;
}
struct Month()
{
    static Month Jan() { return Month(1); }
    ...
};
struct Year()...
class Date
{
public:
    Date(const Month& month, const Day& day, const Year& year);
};

例如工厂函数

Investment* createInvestment();
void getRidOfInvestment(Investment);

这样很容易内存泄露,所以要使用智能指针

std::shared_ptr<Investment> createInvestment();

关于使用智能指针则可以为智能指针指定销毁函数,解决跨DLL问题(例如再某个申请的内存指针地址传到另一个DLL使用了另一个DLL的delete,我们尽可能遵循那个DLL申请的内存则由那个DLL的销毁函数进行释放)

std::shared_ptr<Investment> createInvestment()
{
    Investment *ptr = new Investment;
    std::shared_ptr<Investment> ret(ptr, [](Investment *ptr) -> void
                                    { delete ptr; });
    return ret;
}

请记住

19、设计 class 犹如设计 type

设计新的class应该带着“语言设计者当初设计语言内置类型时”一样的严谨来讨论class的设计。

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

#include <iostream>
using namespace std;

class A
{
public:
    // big content...
};

void dosomething(const A &a)
{
}

int main(int argc, char **argv)
{
    A a;
    dosomething(a);
    return 0;
}

切割问题

#include <iostream>
using namespace std;

class A
{
public:
    virtual void run()
    {
        std::cout << "A::run" << std::endl;
    }
};

class B : public A
{
public:
    void run() override
    {
        std::cout << "A::run" << std::endl;
    }
};

int main(int argc, char **argv)
{
    A a;
    // B *b_ptr = dynamic_cast<B *>(&a); // 会报错
    B *b_ptr = (B *)&a; // 不会报错
    b_ptr->run();       // 产生切割问题 输出A::run
    B b_instance;
    A a_copy_from_b = b_instance; // 产生切割问题 只拷贝了基类部分
    a_copy_from_b.run();          // A::run
    return 0;
}

21、必须返回对象时,别妄想返回其 reference

如下面的场景返回值类型就挺好的

#include <iostream>
using namespace std;

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

class Rational
{
public:
    Rational(int numberator = 0, int denominator = 1);

private:
    int n, d;
    friend const Rational operator*(const Rational &lhs, const Rational &rhs);
};

Rational::Rational(int numberator, int denominator) : n(numberator), d(denominator)
{
}

// 比较好的方式
const Rational operator*(const Rational &lhs, const Rational &rhs)
{
    return Rational(lhs.n * rhs.n, lhs.d * rhs.d);
}

int main(int argc, char **argv)
{
    return 0;
}

下面返回了local stack 的引用,则会core

#include <iostream>
using namespace std;

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

class Rational
{
public:
    Rational(int numberator = 0, int denominator = 1);

private:
    int n, d;
    friend const Rational &operator*(const Rational &lhs, const Rational &rhs);
};

Rational::Rational(int numberator, int denominator) : n(numberator), d(denominator)
{
}

const Rational &operator*(const Rational &lhs, const Rational &rhs)
{
    Rational ret(lhs.n * rhs.n, lhs.d * rhs.d);
    return ret;
}

int main(int argc, char **argv)
{
    Rational r1, r2;
    Rational r3 = r1 * r2;
    return 0;
}

返回local static的引用,也会存在一定问题

#include <iostream>
using namespace std;

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

class Rational
{
public:
    Rational(int numberator = 0, int denominator = 1);
    bool operator==(const Rational &other) const
    {
        return this->n == other.n && this->d == other.d;
    }

private:
    int n,
        d;
    friend const Rational &operator*(const Rational &lhs, const Rational &rhs);
};

Rational::Rational(int numberator, int denominator) : n(numberator), d(denominator)
{
}

// 比较好的方式
const Rational &operator*(const Rational &lhs, const Rational &rhs)
{
    static Rational ret;
    ret.n = lhs.n * rhs.n;
    ret.d = lhs.d * rhs.d;
    return ret;
}

int main(int argc, char **argv)
{
    Rational r1(1, 2), r2(3, 4);
    Rational r3(5, 6), r4(7, 9);
    std::cout << boolalpha << ((r1 * r2) == (r3 * r4)) << std::endl; // 总是返回true因为比较的是同一个Rational对象当然总是相等
    return 0;
}

22、将成员变量声明为 private

假设我们有一个public成员变量,而我们最终取消了它。多少代码可能会被破坏呢?所有使用它的客户代码都会被破坏,而那是一个不可知的大量。因此public成员变量完全没有封装性。

假设我们有一个protected成员变量,而我们最终取消了它,有多少代码被破坏? 所有使用它的derived classes都会被破坏,那往往也是个不可知的大量。

因此,protected成员变量就像public成员变量一样缺乏封装性,

因为在这两种情况下,如果成员变量被改变,都会有不可预知的大量代码受到破坏。 一旦你将一个成员变量声明为public或protected而客户开始使用它,就很难改变那个成员变量所涉及的一切。太多代码需要重写、重新测试、重新编写文档、重新编译。

从封装的角度观之,其实只有两种访问权限:private(提供封装)和其他(不提供封装)。

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

class WebBrowser
{
public:
    // ...
    void clearCache();
    void clearHistory();
    void removeCookies();
    void clearEveryhing()
    {
        clearCache();
        clearHistory();
        removeCookies();
    }
    // ...
};

不如写为

namespace WebBrowserStuff
{
    class WebBrowser
    {
    public:
        // ...
        void clearCache();
        void clearHistory();
        void removeCookies();
        // ...
    };

    void clearBrowser(WebBrowser &wb)
    {
        wb.clearCache();
        wb.clearHistory();
        wb.removeCookies();
    }
}

还可以根据功能划分与重要成都写到不同的头文件中

// webbrowser.h
namespace WebBrowserStuff
{
 class WebBrowser{...};
 // ... 核心机能,如几乎所有客户端需要的non-member函数
}
// webbrowserbookmarks.h
namespace WebBrowserStuff
{
 // ... 与书签相关的便利函数
}
// 头文件 webbrowsercookies.h
namspace WebBrowserStuff
{
 // ... 与cookie相关的便利函数
}

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

只看描述是很难理解的,看代码的例子,就好很多。

class Rational
{
public:
 Rational(int numerator = 0,
    int denominator = 1); // 构造函数刻意不位explicit 允许int-to-Rational隐式转换
 int numerator() const;
 int denominator() const;
 
private:
 ...
};

自定义乘法运算符

class Rational
{
public:
 ...
 const Rational operator* (const Rational& rhs) const;
};
Rational oneEighth(1, 8);
Rational oneHalf(1, 2);
Rational result = oneHalf * oneEighth;
result = result * oneEighth;
result = oneHalf.operator*(2);
result = 2.operator*(oneHalf); // 错误
result = operator*(2, oneHalf); // 错误

上面的oneHalf.operator*(2)相当于做了隐式转换

const Rational temp(2);
result = oneHalf * temp; // Rational 构造函数是非explicit的

想要支持混合式算术运算,让operator*成为一个non-member函数,允许编译器在每一个实参上执行隐式类型转换:

class Rational
{
 ... // 不包括operator*
};
// 定义为non-member函数
const Rational operator*(const Rational& lhs, const Rational& rhs)
{
 return Rational(lhs.numberator() * rhs.numberator(), lhs.denominator() * rhs.denominator());
}
Rational oneFourth(1, 4);
Rational result;
result = oneFourth * 2;
result = 2 * oneFourth;

25、考虑写出一个不抛异常的 swap 函数

所谓swap(置换)两个对象值,意思是将两对象的值彼此赋予对方。缺省情况下swap动作可由标准程序库提供的swap算法完成。

namespace std
{
    template <typename T>
    void swap(T &a, T &b)
    {
        T temp(a);
        a = b;
        b = temp;
    }
}

只要类型T支持copying(通过拷贝构造函数和拷贝赋值操作符完成)。

#include <iostream>
#include <vector>
using namespace std;

class AImpl
{
public:
    int a, b, c;
    std::vector<int> vec;
};

class A
{
public:
    A()
    {
        implPtr = new AImpl;
    }
    ~A()
    {
        if (implPtr)
        {
            delete implPtr;
        }
    }
    A(const A &a)
    {
  implPtr = new AImpl(*a.implPtr);
    }
    A &operator=(const A &a)
    {
        // 深拷贝 不然默认只拷贝地址有问题
        *implPtr = *a.implPtr;
        return *this;
    }
    AImpl *implPtr;
};

int main(int argc, char **argv)
{
    A a1;
    A a2 = a1;
    return 0;
}

如上面的例子,因为需要拷贝我们必须写深拷贝,不然默认进行地址拷贝会出问题。但是使用std::swap时就显得有些鸡肋,明明只交换二者的 implPtr存储的地址即可,却使用的拷贝。但是我们对std::swap针对A进行特化。

namespace std
{
    template <>
    void swap<A>(A &a, A &b)
    {
        swap(a.implPtr, b.implPtr);
    }
}

上面可以实现,是因为implPtr属性是public的,如果为私有的时应该怎么做

#include <iostream>
#include <vector>
using namespace std;

class A;
class AImpl;
void swap(A &a, A &b) noexcept;

class AImpl
{
public:
    int a, b, c;
    std::vector<int> vec;
};

class A
{
public:
    A()
    {
        implPtr = new AImpl;
    }
    ~A()
    {
        if (implPtr)
        {
            delete implPtr;
        }
    }
    A(const A &a)
    {
  implPtr = new AImpl(*a.implPtr);
    }
    A &operator=(const A &a)
    {
        // 深拷贝 不然默认只拷贝地址有问题
        *implPtr = *a.implPtr;
        return *this;
    }

public:
    friend void swap(A &a, A &b) noexcept;
    void swap(A &b) noexcept
    {
        ::swap(*this, b);
    }

private:
    AImpl *implPtr;
};

void swap(A &a, A &b) noexcept
{
    std::cout << "my swap" << std::endl;
    std::swap(a.implPtr, b.implPtr);
}

namespace std
{
    template <>
    void swap<A>(A &a, A &b) noexcept
    {
        ::swap(a, b);
    }
}

int main(int argc, char **argv)
{
    A a1;
    A a2 = a1;
    std::swap(a1, a2); // my swap
    swap(a1, a2);      // my swap
    return 0;
}

如果A是一个模板类是,情况则有些麻烦。在C++中,模板的特例化不能放在std命名空间中,除非标准库特意允许。因为直接在std命名空间中特例化会导致不确定行为。

#include <iostream>
#include <vector>
using namespace std;

namespace ASpace
{

    template <typename T>
    class A;

    class AImpl;

    template <typename T>
    void swap(A<T> &a, A<T> &b) noexcept;

    class AImpl
    {
    public:
        int a, b, c;
        std::vector<int> vec;
    };

    template <typename T>
    class A
    {
    public:
        A()
        {
            implPtr = new AImpl;
        }
        ~A()
        {
            delete implPtr;
        }
        A(const A &a)
        {
            implPtr = new AImpl(*a.implPtr);
        }
        A &operator=(const A &a)
        {
            if (this == &a)
            {
                return *this; // nothing todo
            }
            // 深拷贝 不然默认只拷贝地址有问题
            *implPtr = *a.implPtr;
            return *this;
        }

    public:
        friend void swap<>(A<T> &a, A<T> &b) noexcept;

        void swap(A &b) noexcept
        {
            std::swap(implPtr, b.implPtr);
        }

    private:
        AImpl *implPtr;
    };

    template <typename T>
    void swap(A<T> &a, A<T> &b) noexcept
    {
        std::cout << "my swap" << std::endl;
        a.swap(b);
    }

} // ASpace

int main(int argc, char **argv)
{
    ASpace::A<ASpace::AImpl> a1;
    ASpace::A<ASpace::AImpl> a2 = a1;
    swap(a1, a2); // my swap // 触发(argument-dependentlookup或Koenig lookup法则)
    // std::swap(a1,a2);//error

    {
        using std::swap;
        swap(a1, a2); // ADL(Argument Dependent Lookup,参数依赖查找) 调用ASpace::swap
    }
    return 0;
}

实现

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

27、尽量少做转型动作

28、避免返回 handles 指向对象内部成分

29、为异常安全而努力是值得的

30、透彻了解 inlining 的里里外外

31、将文件间的编译依存关系降到最低

继承与面向对象设计

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

33、避免遮掩继承而来的名称

34、区分接口继承和实现继承

35、考虑 virtual 函数以外的其他选择

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

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

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

39、明智而审慎地使用 private 继承

40、明智而审慎地使用多重继承

模板与泛型编程

41、了解隐式接口和编译期多态

42、了解 typename 的双重意义

43、学习处理模板化基类内的名称

44、将于参数无关的代码抽离 templates

45、运用成员函数模板接受所有兼容类型

46、需要类型转换时请为模板定义非成员函数

47、请使用 traits classes 表现类型信息

48、认识 template 编程

定制 new 和 delete

49、了解 new-handler 的行为

50、了解 new 和 delete 的合理替换时机

51、编写 new 和 delete 时需固守常规

52、写了 placement new 也要写 placement delete

杂项讨论

53、不要轻易忽略编译器警告

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

55、让自己熟悉 Boost