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、尽可能延后变量定义式的出现时间

只要定义了一个变量其类型带有一个构造函数或析构函数,当程序控制流到达这个变量定义式时就得承受构造成本。 离开作用域时就得承受析构成本 即使变量最终并未被使用。

#include <iostream>
#include <string>

constexpr int MinimumPasswordLength = 10;

std::string encryptPassword(const std::string& password)
{
    using namespace std;
    std::string encrypted;
    if(password.length()<MinimumPasswordLength)
    {
        throw logic_error("Password is too short");
    }
    //..
    return encrypted;
}

int main()
{
    return 0;
}

下面方式更好

#include <iostream>
#include <string>

constexpr int MinimumPasswordLength = 10;

std::string encryptPassword(const std::string& password)
{
    using namespace std;
    if(password.length()<MinimumPasswordLength)
    {
        throw logic_error("Password is too short");
    }
    std::string encrypted;
    //..
    return encrypted;
}

int main()
{
    return 0;
}

例如下面的循环场景

// 方法A
Widget w;
for(int i=0;i<n;i++)
{
 // w=i;
}
// 方法B
for(int i=0;i<n;++i)
{
 Widget w(i);
}

A:1个构造函数+1个析构函数+n个赋值操作 B:n个构造函数+n个构造函数

具体用那种方法,要根据具体情况评估。

27、尽量少做转型动作

回顾C风格的转型

(T)expression // 将expression转型为T
T(expression) // 将expression转型为T

两种形式并无差别,存粹是小括号的摆放位置不同而已。称此为旧式转型 old-style casts.

C++还提供了四种新式转型

const_cast<T>(expression)
dynamic_cast<T>(expression)
reinterpret_cast<T>(expression)
static_cast<T>(expression)
  1. const_cast 通常被用来将对象的常量性转除(castaway the constness)。
  2. dynamic_cast 主要用来执行“安全向下转型”(safe downcasting)。 dynamic_cast调用会进行字符串比较,需要考虑效率成本。
  3. reinterpret_cast 意图执行低级转型,实际动作(及结果)可能取决千编译器,这也就表示它不可移植。
  4. static_cast用来强迫隐式转换。

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

class A
{
public:
    int n1;
    int n2;
};

class B
{
public:
    B()
    {
        a = make_shared<A>();
    }
    inline int &Getn1() const // 这里封装性被破坏,外部可以修改n1
    {
        return a->n1;
    }
    inline int &Getn2() const // 这里封装性被破坏,外部可以修改n2
    {
        return a->n2;
    }

private:
    std::shared_ptr<A> a;
};

虽然Getn1与Getn2有限制const,但是返回值不是const的

class B
{
public:
    B()
    {
        a = make_shared<A>();
    }
    inline const int &Getn1() const
    {
        return a->n1;
    }
    inline const int &Getn2() const
    {
        return a->n2;
    }

private:
    std::shared_ptr<A> a;
};

还有一种是,返回了随时可能被销毁的资源

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

class A
{
public:
    A(int n1, int n2) : n1(n1), n2(n2) {};
    int n1;
    int n2;
};

class B
{
public:
    B(int n1, int n2)
    {
        a = make_shared<A>(n1, n2);
    }
    inline const int &Getn1() const
    {
        return a->n1;
    }
    inline const int &Getn2() const
    {
        return a->n2;
    }

    void destoryA()
    {
        a.reset();
    }

private:
    std::shared_ptr<A> a;
};

int main(int argc, char **argv)
{
    B b(1, 2);
    const int &n1 = b.Getn1();
    const int &n2 = b.Getn2();
    std::cout << n1 << " " << n2 << std::endl; // 1 2
    b.destoryA();

    *const_cast<int *>(&n1) = 100;
    // Segmentation fault (core dumped)
    std::cout << b.Getn1() << std::endl;

    return 0;
}

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

  1. 基本保证

即使函数在执行中抛出异常,程序的状态仍然是有效的(不会造成资源泄漏或数据结构损坏),但可能无法保证状态是原始状态。

示例:资源的简单释放

假设一个函数在处理动态内存分配,尽管抛出异常,也确保释放了已分配的资源。

#include <iostream>
#include <stdexcept>

void basicGuaranteeExample() {
    int* resource = new int[10]; // 动态分配资源

    try {
        // 模拟某些可能抛出异常的操作
        throw std::runtime_error("Something went wrong!");
    } catch (...) {
        // 捕获异常并确保释放资源
        delete[] resource;
        std::cout << "Basic guarantee: Resource cleaned up." << std::endl;
        throw; // 重新抛出异常
    }
}

结果:即使函数抛出异常,动态分配的资源仍然会被释放,程序状态有效。

  1. 强烈保证

函数提供强烈保证:若函数抛出异常,程序的状态会回到调用函数之前的状态,不会有任何更改。 可以通过 copy-and-swap 技术实现。

示例:swap 技术在容器的异常安全中应用

#include <vector>
#include <stdexcept>

class ExceptionSafeVector {
    std::vector<int> data;

public:
    void addElement(int element) {
        // 创建临时副本,保证异常发生时不会修改原数据
        std::vector<int> temp(data);
        temp.push_back(element); // 操作可能抛出异常

        // 若无异常,完成 swap 替换
        data.swap(temp); // 强烈保证:要么成功,要么不修改 data
    }

    void print() const {
        for (int val : data) {
            std::cout << val << " ";
        }
        std::cout << std::endl;
    }
};

int main() {
    ExceptionSafeVector vec;
    try {
        vec.addElement(1);
        vec.addElement(2);
        vec.addElement(3); // 无异常时更新数据
    } catch (...) {
        std::cout << "Strong guarantee: No changes to the original state!" << std::endl;
    }

    vec.print(); // 打印已添加的元素
}

结果:如果 addElement 抛出异常,data 保持调用前的状态。

  1. 不抛异常保证

函数绝对不会抛出异常,通常用于析构函数或一些保证不会失败的操作。

示例:提供强制的 noexcept 保证

#include <iostream>
#include <utility>

class NoThrowClass {
    int* resource;

public:
    NoThrowClass() : resource(new int(42)) {}

    // noexcept 保证函数不会抛出异常
    ~NoThrowClass() noexcept {
        delete resource; // 确保资源被释放,不抛异常
    }

    // noexcept 移动操作:在发生资源转移时,确保不抛异常
    NoThrowClass(NoThrowClass&& other) noexcept : resource(nullptr) {
        resource = other.resource;
        other.resource = nullptr;
    }

    // 赋值操作符 noexcept
    NoThrowClass& operator=(NoThrowClass&& other) noexcept {
        if (this != &other) {
            delete resource; // 释放已有资源
            resource = other.resource;
            other.resource = nullptr;
        }
        return *this;
    }
};

int main() {
    NoThrowClass obj1;
    NoThrowClass obj2 = std::move(obj1); // 不抛异常

    std::cout << "No-throw guarantee: Successful resource transfer without exceptions!" << std::endl;
}

结果:析构函数和移动操作通过 noexcept 保证不会抛出异常。

假设有三个函数 A、B 和 C,它们分别提供不同级别的异常安全保证:

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

inline函数,动作像函数,比宏好用得多,可以调用它们又不需要蒙受函数调用所招致得额外开销。 没有白吃得午餐,inline函数背后得整体观念是,将“对此函数得每一个调用”都以函数本体替换之。一台内存有限得机器上,过度热衷inlining会造成程序体积太大(对可用空间而言)。即使拥有虚内存,inline造成的代码膨胀亦会导致额外得换页行为,降低指令高速缓存装置得击中率,带来效率损失。

class Person
{
public:
    ...
    int age() const { return theAge; } // 一个隐喻的inline申请
    ...
private:
    int theAge;
};

使用inline函数的函数指针,并不是inlined。

inline void f() {...}
void (*pf)() = f;
f(); // 是inline调用
pf(); // 不是inline调用

构造函数和析构函数往往是inlining的糟糕候选人。

  1. 构造函数和析构函数通常不是“小而简单”的函数
class Example {
    int a;
    double b;
    std::string c;

public:
    Example() : a(0), b(0.0), c("default") {} // 构造函数看似简单,但实际上初始化了多个成员
};

即使这个构造函数只有一行,实际上它要调用 std::string 的构造函数,还可能涉及动态内存分配。将这样的函数内联会生成大量代码,增加可执行文件的大小(代码膨胀)。

  1. 内联化可能导致代码膨胀(Code Bloat)
  1. 调试和维护的复杂性
  1. 可能影响虚函数行为

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

如A.h内定义了class A, class A中使用了 class B和class C,class B定义在 B.h class C在 C.h,如果 A.h include了B.h C.h,那么修了B.h或者C.h A的实现需要重新编译,因为A.h也发生了变化。所有可以在A.h中使用前置声明。

// A.h
#include "B.h"
#include "C.h"
class A{
public:
    A(B&b,C&c);
};

上面代码依存密切,容易引起编译灾难。

// A.h
class B;
class C;
class A{
public:
   A(B&b,C&c);
};

想要使用某个类型就需要知道分配多少内存,如

int main()
{
    int x; // 定义一个int
    Person p; // 定义一个Person对象,需要include到Person的定义式,不然编译不过
    ...
}

有一种 pointer to implementation的骚操作。

// A.h
class AImpl;
class B;
class C;
class A
{
public:
    A(B&b, C&c);
    // ...
private:
    std::shared_ptr<AImpl> pImpl;
};

这样的设计之下,Person的客户完全与A,B以及A的实现细目分离了。include了A.h的cpp,即使B.h C.h修改了东西,也不会令include A.h的源代码重新编译。

  1. 如果使用 object references或object pointers可以完成任务,就不要使用objects。你可以只靠一个类型声明式就定义出指向该类型的references和pointers;但如果定义某类型的objects,就需要用到该类型的定义式。
  2. 如果能够,尽量以class声明式替换class定义式。
  3. 为声明式和定义式子提供不同的头文件。

注意,当你声明一个函数而它用到某个class时,你并不需要该class的定义;纵使函数以byvalue方式传递该类型的参数(或返回值)亦然:

// A.h
#include "Bfwd.h"
#include "Cfwd.h"
B bfunc();
void showC(C c);
#include "A.h"
#include "AImpl.h"
A::A(const B&b,const C&c,const std::string&str):AImpl(new AImpl(b,c,str))
{}
std::string A::name() const
{
    return pImpl->name();
}

继承与面向对象设计

三种继承方式

  1. public继承表示派生类”is-a”基类,这意味着派生类应该能完全替代基类使用(里氏替换原则 LSP)。
class Animal {
public:
    virtual void makeSound() { std::cout << "某种声音" << std::endl; }
protected:
    int age;
private:
    std::string dna;
};

class Dog : public Animal {
    // Animal的public成员在Dog中仍然是public
    // Animal的protected成员在Dog中仍然是protected
    // Animal的private成员在Dog中不可访问
};
  1. private继承表示”根据某物实现出”,“has-a”,是一种实现技术而非设计关系。它通常用于实现复用,而不是表达类之间的概念关系。
class Engine {
public:
    void start() { std::cout << "引擎启动" << std::endl; }
    void stop() { std::cout << "引擎停止" << std::endl; }
protected:
    void inject_fuel() {}
private:
    void internal_process() {}
};

class Car : private Engine {  // private继承
    // Engine的public和protected成员在Car中变成private
    // Engine的private成员在Car中不可访问
public:
    void start_car() {
        start();  // 可以访问Engine的方法,但只能在Car的内部使用
    }
};

int main() {
    Car car;
    // car.start();  // 错误:Engine的方法对外不可见
    car.start_car(); // 正确:通过Car的公共接口访问
}
  1. protected继承介于public继承(is-a)和private继承(has-a)之间,它表示一种”在实现中是一种”(implemented-in-terms-of)的关系。
class Base {
public:
    void publicFunc() {}
protected:
    void protectedFunc() {}
private:
    void privateFunc() {}
};

class Derived : protected Base {
    // Base的public成员在Derived中变成protected
    // Base的protected成员在Derived中保持protected
    // Base的private成员在Derived中不可访问
};

class Further : public Derived {
    void func() {
        publicFunc();     // 可以访问,因为在Derived中是protected
        protectedFunc();  // 可以访问,因为在Derived中是protected
        // privateFunc(); // 不能访问
    }
};

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

如果你令 class D以public形式继承class B,那么当你看到class D的某个对象时,你便知道它也是个class B。反之不成立。

我来用一个简单的例子来解释”public继承”中的is-a关系。

class Animal {
public:
    virtual void makeSound() {
        std::cout << "动物发出声音" << std::endl;
    }
    
    void eat() {
        std::cout << "动物在进食" << std::endl;
    }
};

class Dog : public Animal {  // Dog "是一个" Animal
public:
    void makeSound() override {  // 重写基类的方法
        std::cout << "汪汪汪!" << std::endl;
    }
    
    void fetchBall() {
        std::cout << "狗狗去捡球" << std::endl;
    }
};

int main() {
    Dog dog;
    Animal* animal_ptr = &dog;  // 这是合法的,因为 Dog "是一个" Animal
    
    // 下面这些操作都是合法的
    dog.makeSound();      // 输出:"汪汪汪!"
    dog.eat();           // 输出:"动物在进食"
    
    animal_ptr->makeSound();  // 输出:"汪汪汪!"
    animal_ptr->eat();       // 输出:"动物在进食"
}

让我解释这个例子:

  1. Animal 是基类,定义了所有动物共有的行为:makeSound()eat()

  2. Dog 通过 public 继承自 Animal,这表明”狗是一个动物”(is-a关系)。

  3. 因为存在 is-a 关系:

这就是为什么说”适用于基类的每一件事情一定也适用于派生类”。因为派生类对象在任何情况下都可以被当作基类对象来使用,这是 public 继承的核心特性。

相反,如果这种关系不成立,就不应该使用 public 继承。例如,“房子有一个门”这种关系就不应该用 public 继承,而应该用组合(composition)来实现,因为门不是房子的一种类型。

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

C++有种现象为名字遮掩。在C++中,当派生类声明了一个与基类同名的函数时,基类中所有同名函数(无论参数如何)都会被遮掩,只要原因有:

名称查找规则:

#include <iostream>

class Base
{
private:
    int x;

public:
    virtual void mf1() = 0;
    virtual void mf1(int);
    virtual void mf2();
    void mf3();
    void mf3(double);
    virtual ~Base();
};

Base::~Base() {}
void Base::mf1(int x)
{
    std::cout << "Base::mf1(int x)" << std::endl;
}
void Base::mf2()
{
    std::cout << "Base::mf2()" << std::endl;
}
void Base::mf3()
{
    std::cout << "Base::mf3()" << std::endl;
}
void Base::mf3(double x)
{
    std::cout << "Base::mf3(double x)" << std::endl;
}

class Derived : public Base
{
public:
    virtual void mf1();
    void mf3();
    void mf4();
};

void Derived::mf1()
{
    std::cout << "Derived::mf1()" << std::endl;
}

void Derived::mf3()
{
    std::cout << "Derived::mf3()" << std::endl;
}

void Derived::mf4()
{
    std::cout << "Derived::mf4()" << std::endl;
}

int main()
{
    Derived d;
    d.mf1(); // Derived::mf1()
    // d.mf1(12); // 错误:Derived::mf1(int) 遮掩了 Base::mf1(int)
    d.mf2(); // Base::mf2()
    d.mf3(); // Derived::mf3()
    // d.mf3(1);// 错误:Derived::mf3遮掩了Base::mf3
    return 0;
}

解决方法

  1. 使用using声明式
class Derived : public Base
{
public:
    using Base::mf1; // 在Base class内名为mf1和mf3的所有东西
    using Base::mf3; // 在Derived作用域内都可看见 并且public
    virtual void mf1();
    void mf3();
    void mf4();
};
  1. 通过作用域解析运算符显式调用
Derived d;
d.Base::mf1(12);
  1. 转交函数
// 不好的方式:using声明在private继承中
class Derived : private Base {
private:
    using Base::mf1;  // 这样无法将mf1暴露为public接口
};

// 好的方式:使用转交函数
class Derived : private Base {
public:
    // 可以选择性地只转发某些重载版本
    void mf1() { Base::mf1(); }  // 显式转发到基类版本
    void mf1(int x) { Base::mf1(x); }  // 可以控制访问级别为public
};

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

函数接口(function interfaces)继承和函数实现(function implementations)继承.

class Shape
{
public:
    virtual void draw() const = 0; // pure-virtual函数使得Shape成为抽象类
    virtual void error(const std::string &msg); // impure-virtual函数,提供缺省实现
    int objectID() const; // non-virtual函数,强制实现
    //...
};

void Shape::error(const std::string &msg)
{
    std::cerr << msg << std::endl;
}

int Shape::objectID() const
{
    return 0;
}

class Rectangle : public Shape
{
    //...
    virtual void draw() const {}
};

class Ellipse : public Shape
{
    //...
    virtual void draw() const {}
};

pure virtual函数有两个最突出的特性:它们必须被任何”继承了它们”的具象class重新声明,而且它们在抽象class中通常没有定义.

声明一个pure virtual函数的目的是为了让derived classes只继承函数接口. 令人意外的是,可以为pure virtual函数提供定义,C++并不会发出怨言,虽然提供了pure virtual函数的定义但是其derived classes仍然要重新override,在derived classes中定义中可以再调用BaseClass的pure virtual的定义实现.

#include <iostream>

class Shape
{
public:
    virtual void draw() const = 0;
    virtual void error(const std::string &msg);
    int objectID() const;
    //...
};

void Shape::draw() const
{
    std::cerr << "Shape::draw" << std::endl;
}

void Shape::error(const std::string &msg)
{
    std::cerr << msg << std::endl;
}

int Shape::objectID() const
{
    return 0;
}

class Rectangle : public Shape
{
    //...
    virtual void draw() const override
    {
        std::cerr << "Rectangle::draw" << std::endl;
    }
};

class Ellipse : public Shape
{
    //...
    virtual void draw() const override
    {
        std::cerr << "Ellipse::draw" << std::endl;
    }
};

class Circle : public Shape
{
    virtual void draw() const override
    {
        Shape::draw();
    }
};

int main()
{
    Rectangle rect;
    Ellipse elli;
    Circle circle;
    Shape *ps = &rect;
    ps->draw();        // Rectangle::draw 虽然draw在Circle中是private的,但是Circle继承了Shape的draw函数,所以可以调用
    ps->Shape::draw(); // Shape::draw
    ps = &elli;
    ps->draw();        // Ellipse::draw
    ps->Shape::draw(); // Shape::draw
    circle.draw();     // 编译错误: 因为draw为class Circle内draw默认为private的, 如果是struct则是public的
    // 为circle的draw函数设置public 则会输出 Shape::draw

    return 0;
}

声明简朴的(非纯)impure virtual函数的目的,是让derived classes继承该函数的接口和缺省实现,也就是选择性实现,而且能够实现多态效果,如果必须要derived classes自己实现就使用纯虚函数.

声明non-virtual函数的目的是为了令derived classes继承函数的接口及一份强制性实现。当然non-virtual可以写出自己的实现但是无法实现多态效果.

#include <iostream>

class Shape
{
public:
    int objectID() const;
    //...
};

int Shape::objectID() const
{
    return 0;
}

class Ellipse : public Shape
{
public:
    // 坚决 绝不重新定义继承而来的non-virtual函数
    // 容易惹祸上身
    int objectID() const
    {
        return 1;
    }
};

int main()
{

    Ellipse elli;
    Shape *ps = &elli;
    std::cout << ps->objectID() << std::endl;  // 0
    std::cout << elli.objectID() << std::endl; // 1

    return 0;
}

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

不一定使用virtual函数才能展现出多态效果。

NVI手法,通过public non-virtual成员函数间接调用private virtual函数,称为 non-virtual interface (NVI)手法。

下面代码可以防止Derived外部调用虚函数doSomethingImpl

#include <iostream>

class Base
{
public:
    // public 非虚函数作为接口
    void doSomething()
    {
        // 前置处理
        preProcess();

        // 调用私有虚函数
        doSomethingImpl();

        // 后置处理
        postProcess();
    }

private:
    // private 虚函数实现具体行为
    virtual void doSomethingImpl() = 0;

    virtual void preProcess() { /* 默认实现 */ }
    virtual void postProcess() { /* 默认实现 */ }
};

class Derived : public Base
{
private:
    // 重写私有虚函数
    virtual void doSomethingImpl() override
    {
        // 派生类的具体实现
        std::cout << "Derived::doSomethingImpl" << std::endl;
    }
};

int main()
{
    Derived d;
    Base &b = d;
    b.doSomething(); // Derived::doSomethingImpl

    return 0;
}

再来个更符合实际的例子

class Document {
public:
    void save() {
        // 统一的保存前检查
        if (!canSave()) {
            throw std::runtime_error("无法保存文档");
        }
        
        // 加锁
        std::lock_guard<std::mutex> lock(mutex_);
        
        // 调用具体的保存实现
        saveImpl();
        
        // 更新保存状态
        lastSaveTime_ = std::time(nullptr);
    }

private:
    virtual void saveImpl() = 0;
    virtual bool canSave() const { return true; }
    
    std::mutex mutex_;
    std::time_t lastSaveTime_;
};

class TextDocument : public Document {
private:
    virtual void saveImpl() override {
        // 实现文本文档的具体保存逻辑
    }
};

NVI手法对publicvirtual函数而言是一个有趣的替代方案, 还有一种方式使用函数指针,由Function Pointers实现 Strategy 模式。

#include <iostream>

class Base;

void doSomethingImpl1(const Base &b)
{
    std::cout << "doSomethingImpl1" << std::endl;
}

void doSomethingImpl2(const Base &b)
{
    std::cout << "doSomethingImpl2" << std::endl;
}

class Base
{
public:
    typedef void (*DoSomethingImplFunc)(const Base &b);

    explicit Base(DoSomethingImplFunc func = doSomethingImpl1) : funcImpl(func)
    {
    }

    // public 非虚函数作为接口
    void doSomething()
    {
        // 前置处理
        preProcess();

        // 调用私有虚函数
        funcImpl(*this);

        // 后置处理
        postProcess();
    }

private:
    virtual void preProcess() { /* 默认实现 */ }
    virtual void postProcess() { /* 默认实现 */ }

private:
    DoSomethingImplFunc funcImpl;
};

class Derived : public Base
{
public:
    explicit Derived(Base::DoSomethingImplFunc func) : Base(func)
    {
    }
};

int main()
{
    Derived d1(&doSomethingImpl1);
    Derived d2(&doSomethingImpl2);
    Base &b1 = d1;
    Base &b2 = d2;
    b1.doSomething(); // doSomethingImpl1
    b2.doSomething(); // doSomethingImpl2
    d1.doSomething(); // doSomethingImpl1
    d2.doSomething(); // doSomethingImpl2
    return 0;
}

可以将上面的 typedef void (*DoSomethingImplFunc)(const Base &b); 改为std::function将会有很多优势。

#include <iostream>
#include <functional>

class Base;

void doSomethingImpl1(const Base &b)
{
    std::cout << "doSomethingImpl1" << std::endl;
}

void doSomethingImpl2(const Base &b)
{
    std::cout << "doSomethingImpl2" << std::endl;
}

class Base
{
public:
    typedef std::function<void(const Base &b)> DoSomethingImplFunc;

    explicit Base(DoSomethingImplFunc func = doSomethingImpl1) : funcImpl(func)
    {
    }

    // public 非虚函数作为接口
    void doSomething()
    {
        // 前置处理
        preProcess();

        // 调用私有虚函数
        funcImpl(*this);

        // 后置处理
        postProcess();
    }

private:
    virtual void preProcess() { /* 默认实现 */ }
    virtual void postProcess() { /* 默认实现 */ }

private:
    DoSomethingImplFunc funcImpl;
};

class Derived : public Base
{
public:
    explicit Derived(Base::DoSomethingImplFunc func) : Base(func)
    {
    }
};

struct StructDoSomethingImpl
{
    void operator()(const Base &b) const
    {
        std::cout << "StructDoSomethingImpl::operator()" << std::endl;
    }
};

class ClassDoSomthingImpl
{
public:
    void Do(const Base &b) const
    {
        std::cout << "ClassDoSomthingImpl" << std::endl;
    }
};

int main()
{
    Derived d1(&doSomethingImpl1);
    Derived d2(&doSomethingImpl2);
    Base &b1 = d1;
    Base &b2 = d2;
    b1.doSomething(); // doSomethingImpl1
    b2.doSomething(); // doSomethingImpl2
    d1.doSomething(); // doSomethingImpl1
    d2.doSomething(); // doSomethingImpl2

    // 使用函数对象
    Derived d3((StructDoSomethingImpl())); // StructDoSomethingImpl::operator()
    d3.doSomething();

    // 使用成员函数
    ClassDoSomthingImpl classDoSomethingImpl;
    Derived d4(std::bind(&ClassDoSomthingImpl::Do, classDoSomethingImpl, std::placeholders::_1));
    d4.doSomething(); // ClassDoSomthingImpl

    return 0;
}

上面基本都是设计模式骚操作。

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

non-virtual函数是静态绑定的,virtual函数是动态绑定的。

#include <iostream>

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

class B : public A
{
public:
    int func()
    {
        std::cout << "B::func" << std::endl;
        return 0;
    }
};

int main()
{
    B bObj;
    A *pA = &bObj;
    B *pB = &bObj;
    pA->func();    // A::func
    pB->func();    // B::func
    pB->A::func(); // A::func
    pB->B::func(); // B::func

    return 0;
}

上面的写法非常模糊,使用的时候还得区别我们调用的到底是基类的定义还是派生类中方法的定义。

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

你只能继承两种函数:virtual和non-virtual函数,重新定义一个继承而来的non-virtual函数永远是错误的。

#include <iostream>

class A
{
public:
    enum class Color
    {
        RED = 0,
        GREEN = 1
    };
    virtual void func(Color color = Color::RED)
    {
        std::cout << "A::func " << (int)color << std::endl;
    }
};

class B : public A
{
public:
    virtual void func(Color color) // 虽然可以指定默认实参但不要这样做 非常混乱 是跟着静态类型走的
    {
        std::cout << "B::func " << (int)color << std::endl;
    }
};

int main()
{
    B bObj;
    A *pA = &bObj;
    B *pB = &bObj;
    pA->func(); // 通过A*调用func有默认值可使用  B::func 0
    // pB->func(); // 通过B*调用func需要提供color实参
    pB->func(B::Color::GREEN); // B::func 1

    return 0;
}

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

  1. 复合(Composition)的两种含义

在应用域(has-a 关系) 这表示一个对象包含另一个对象作为其成员。例如:

// 应用域的复合示例
class Person {
private:
    Address address;    // Person has-a Address
    PhoneNumber phone;  // Person has-a PhoneNumber
};

这里的关系很直观:一个人”有一个”地址和”有一个”电话号码。

在实现域(is-implemented-in-terms-of 关系) 这表示一个类使用另一个类来实现其功能。例如:

// 实现域的复合示例
class Set {
private:
    List<T> list;    // Set is-implemented-in-terms-of List
    // 使用List来实现Set的功能
};
  1. 与public继承的区别

public继承表示 is-a(是一个)关系:

class Animal {
public:
    virtual void makeSound() = 0;
};

class Dog : public Animal {  // Dog is-a Animal
public:
    void makeSound() override { /* 汪汪 */ }
};

主要区别:

  1. 关系类型
  2. 耦合度
  3. 灵活性

使用建议:

  1. 优先使用复合而不是继承
  2. 当确实需要表达”是一个”的关系时才使用继承
  3. 在实现细节上,用复合来实现具体功能
  4. 记住设计原则:“组合优于继承”(Composition over Inheritance)

这样的理解和使用可以帮助我们创建更灵活、更易维护的代码。

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

基类的private virtual是可以被覆写的,并且是动态绑定

// 基类的private virtual是可以被覆写的,并且是动态绑定
#include <iostream>

class A
{
private:
    virtual void func()
    {
        std::cout << "A::func " << std::endl;
    }

public:
    void exe()
    {
        func();
    }
};

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

int main()
{
    B bObj;
    A *pA = &bObj;
    B *pB = &bObj;
    pA->exe(); // B::func
    pB->exe(); // B::func

    return 0;
}

private继承主要有两条规则

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

class Timer
{
public:
    Timer()
    {
        std::cout << "Timer::Timer()" << std::endl;
        auto callback = std::bind(&Timer::OnTick, this);
        callback();
        // 完全可以把callback注册到某处去 这让只要使用派生类对象就好了关注上层业务
    }

protected:
    virtual void OnTick()
    {
        std::cout << "Timer::OnTick " << std::endl;
    }
};

class Widget : private Timer
{
private:
    virtual void OnTick() override
    {
        std::cout << "Widget::OnTick " << std::endl;
        UpdateDisplay();
    }
    void UpdateDisplay()
    {
    }
};

int main()
{
    // 创建对象时完全可以让Timer内进行一些操作将自己挂载到某处
    Widget widgetObject;

    // Timer::Timer()
    // Timer::OnTick

    return 0;
}

能用复合就用复合,下面就是一个骚操作

#include <iostream>

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

class B
{
private:
    class BA : public A
    {
    public:
        virtual void OnTick() override
        {
            std::cout << "BA::A" << std::endl;
        }
    };
    BA bCompositionA;
};

int main()
{
    B bObj;

    return 0;
}

private继承相比组合而言可以进行孔基类优化

// 场景二:空基类优化
class EmptyClass {
    typedef int DataType;  // 只包含类型定义
};

// 使用复合
class BadWidget {
private:
    EmptyClass ec;    // 会占用内存
    int data;         // 4字节
}; // sizeof(BadWidget) > 4

// 使用private继承
class GoodWidget : private EmptyClass {
private:
    int data;         // 4字节
}; // sizeof(GoodWidget) == 4,实现了空基类优化

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

#include <iostream>
#include <functional>

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

class B
{
public:
    bool func()
    {
        std::cout << "B::func" << std::endl;
        return true;
    }
};

class C : public A, public B
{
};

int main()
{
    C c;
    // c.func(); //"C::func" is ambiguous
    c.A::func(); // A::func
    c.B::func(); // B::func
    return 0;
}

菱形多重继承问题

#include <iostream>
#include <functional>

class A
{
public:
    void func()
    {
        std::cout << "A::func " << n++ << std::endl;
    }

private:
    int n{0};
};

class B : public A
{
};

class C : public A
{
};

class D : public B, public C
{
};

int main()
{
    D d;
    // d.func(); // "D::func" is ambiguous
    d.B::func(); // A::func 0
    d.C::func(); // A::func  0
    d.B::func(); // A::func 1
    d.C::func(); // A::func 1
    return 0;
}

上面的d对象其实存在两个int n。使用virtual继承

#include <iostream>
#include <functional>

class A
{
public:
    void func()
    {
        std::cout << "A::func " << n++ << std::endl;
    }

private:
    int n{0};
};

class B : virtual public A
{
};

class C : virtual public A
{
};

class D : public B, public C
{
};

int main()
{
    D d;
    d.func();    // A::func 0
    d.B::func(); // A::func 1
    d.C::func(); // A::func 2
    d.B::func(); // A::func 3
    d.C::func(); // A::func 4
    return 0;
}

下面就是非常烂的行为

#include <iostream>
#include <functional>

class A
{
public:
    void func()
    {
        std::cout << "A::func " << n++ << std::endl;
    }
    virtual void exe()
    {
        std::cout << "A::exe" << std::endl;
    }

private:
    int n{0};
};

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

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

class D : public B, public C
{
public:
    virtual void exe() override
    {
        std::cout << "D::exe" << std::endl;
    }
};

int main()
{
    D d;
    d.exe(); // D::exe
    C *c = &d;
    c->exe(); // D::exe
    B *b = &d;
    b->exe(); // D::exe
    A *a = &d;
    a->exe(); // D::exe
    return 0;
}

模板与泛型编程

C++ template机制自身是一部完整的图灵机,它可以被用来计算任何可计算的值,于是导出了模板元编程,创造出 在C++编译器内执行并于编译完成时停止执行的 程序。

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

面向对象编程世界总是以显式接口和运行期多态解决问题。

#include <iostream>
using namespace std;

template <typename T>
void doProcessing(T &w)
{
    if (w.size() > 10 && w != someNastyWidget)
    {
        T temp(w);
        temp.normalize();
        temp.swap(w);
    }
}

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

w必须支持哪一种接口,看起来类型T好像必须支持size,normalize,swap成员函数,copy构造函数,不等比较。 这一组表达式(对此template而言必须有效编译)便是T必须支持的一组隐式接口。

凡涉及w的任何函数调用,例如operator >和 operator!=,有可能造成template具现化,使这些调用得以成功。这样的具现行为发生在编译期。 “以不同的template参数具现化function templates“会导致调用不同的函数,这便是所谓的编译期多态(compile-timepolymorphism)。

42、了解 typename 的双重意义

template<class T> class Widget; // 使用 class
template<typename T> class Widget; // 使用 typename

上面的用法,class与typename意义完全相同。但是C++并不总是把class和typename视为等价,有时候一定得使用typename。

template<typename C>
void print2nd(const C& container)
{
    C::const_iterator* x;
    // ...
}

上面代码片段,在我们知道C是什么之前,没有任何办法可以知道C::const_iterator是否为一个类型。编译器遇见这种情况会假设为非类型。直接告诉编译器就能解决。

template<typename C>
void print2nd(const C& container)
{
    typename C::const_iterator iter(container.begin());
    // ...
}
template<typename C>
void f(const C& container, typename C::iterator iter)
{
}

但是是存在例外得,在基类列表和成员初值列中不能使用 typename

#include <iostream>

template <typename T>
class Base
{
public:
    Base() {}
    class Nested
    {
    public:
        Nested() {}
    };
};

template <typename T>
class Derived : public Base<T>::Nested
{ // 不能使用 typename 作为基类修饰符
public:
    Derived() : Base<T>::Nested()
    { // 不能使用 typename 作为成员初值列修饰符
        // ... 其他代码 ...
        typename Base<T>::Nested n; // 必须使用 typename 作为成员修饰符

        typedef typename Base<T>::Nested NestedType; // 必须使用 typename 作为类型修饰符
        NestedType n2;
    }
};

int main()
{
    Derived<int> d;
    return 0;
}

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

  1. 使用this->指针

通过this->明确指示成员属于基类模板,this是派生类的指针,this->可以用来访问基类模板的成员。

#include <iostream>
using namespace std;

template <typename T>
class Base
{
public:
    Base() { cout << "Base()" << endl; }
    virtual ~Base() { cout << "~Base()" << endl; }
    void display()
    {
        cout << "value = " << value << endl;
    }

protected:
    T value = 42;
};

template <typename T>
class Derived : public Base<T>
{
public:
    Derived() { cout << "Derived()" << endl; }
    ~Derived() { cout << "~Derived()" << endl; }
    void show()
    {
        this->display();
        // display();
        // error: there are no arguments to ‘display’ that depend on a template parameter, so a declaration of ‘display’ must be available [-fpermissive]
    }
};

int main(int argc, char **argv)
{
    Derived<int> d;
    d.show();
    return 0;
}

在派生类中使用using也是可以的

template <typename T>
class Derived : public Base<T>
{
public:
    Derived() { cout << "Derived()" << endl; }
    ~Derived() { cout << "~Derived()" << endl; }
    using Base<T>::display;
    void show()
    {
        display();
    }
};
  1. 使用基类资格修饰符

通过明确写出基类的名称来访问基类模板的成员。

template <typename T>
class Derived : public Base<T>
{
public:
    Derived() { cout << "Derived()" << endl; }
    ~Derived() { cout << "~Derived()" << endl; }
    void show()
    {
        Base<T>::display();
    }
};

使用this->是一种更通用的方式,尤其是在派生类中需要频繁访问基类成员时,使用基类资格修饰符可以显式地表明成员来自基类,但代码可能显得冗长。

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

背景

模板在 C++ 中非常强大,但它们也可能导致代码膨胀,因为每个模板实例化都会生成一份独立的代码。如果模板中包含了与模板参数无关的代码,这些代码会被重复生成,导致不必要的代码冗余。

核心思想

将与模板参数无关的代码从模板中分离出来,放到非模板的普通函数或类中。这样可以避免重复生成这些代码,从而减少代码膨胀。

示例

假设我们有一个模板类,其中有一些与模板参数无关的逻辑:

#include <string>
#include <iostream>

template<typename T>
class Printer {
public:
    void print(const T& value) const {
        std::cout << "Value: " << value << std::endl; // 与模板参数无关 这里的字符串常量其实是占据空间的
    }
};

在这个例子中,std::cout << "Value: " 是与模板参数无关的代码,但它会在每个模板实例化中重复生成。

改进方法

我们可以将与模板参数无关的部分抽离到一个非模板的辅助函数中:

#include <string>
#include <iostream>

// 非模板辅助函数
void printImpl(const std::string& value) {
    std::cout << "Value: " << value << std::endl;
}

template<typename T>
class Printer {
public:
    void print(const T& value) const {
        printImpl(std::to_string(value)); // 调用非模板函数
    }
};

通过这种方式,printImpl 只会生成一份代码,而不会因为模板实例化而重复生成。

优势

减少代码膨胀:与模板参数无关的代码只会生成一次。

提高可读性和可维护性:将通用逻辑集中在一个地方,便于修改和调试。

分离关注点:模板只负责与模板参数相关的逻辑,非模板代码处理通用逻辑。

非类型模板参数(如整数、指针等)也会导致代码膨胀,因为每个不同的非类型参数都会实例化一个新的模板版本。可以通过将非类型模板参数替换为函数参数或类的成员变量来避免这种膨胀。

// 非类型模板参数导致代码膨胀
template<int N>
class Array {
public:
    void printSize() const {
        std::cout << "Size: " << N << std::endl;
    }
};

// 改进:将非类型模板参数替换为成员变量
class Array {
public:
    Array(int n) : size(n) {}
    void printSize() const {
        std::cout << "Size: " << size << std::endl;
    }
private:
    int size;
};
// 通过这种方式,Array 类只会生成一份代码,而不需要为每个不同的 N 生成新的模板实例。

模板类型参数(如 int、float 等)也会导致代码膨胀,尤其是当多个类型的二进制表示相同时(例如 int 和 unsigned int 在某些平台上可能有相同的二进制表示)。可以通过共享实现代码来减少膨胀。

// 原始模板,可能导致代码膨胀
template<typename T>
class Calculator {
public:
    T add(T a, T b) {
        return a + b;
    }
};

// 改进:通过类型萃取(type traits)共享实现
template<typename T>
class CalculatorImpl {
public:
    T add(T a, T b) {
        return a + b;
    }
};

template<typename T>
class Calculator : public CalculatorImpl<typename std::conditional<std::is_integral<T>::value, int, double>::type> 
{
};

// 改进的核心思想:
// 类型萃取(type traits):

// 使用 std::conditional 和 std::is_integral 来判断模板参数 T 的类型特性。
// 如果 T 是整数类型(如 int、unsigned int),则统一使用 int。
// 如果 T 是浮点类型(如 float、double),则统一使用 double。
// 共享实现:

// 将实际的实现逻辑(add 函数)放在 CalculatorImpl 中。
// Calculator 根据 T 的类型特性选择一个共享的实现(int 或 double)。

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

如何使用成员函数模板来实现“接受所有兼容类型”的函数,解释为什么在某些情况下仍需要显式声明普通的拷贝构造函数和拷贝赋值操作符。

核心内容总结:

  1. 成员函数模板的作用:
  2. 需要显式声明普通的拷贝构造函数和拷贝赋值操作符:

使用成员函数模板实现泛化构造和赋值

#include <iostream>
#include <string>

class MyClass {
public:
    // 普通构造函数
    MyClass(const std::string& str) : data(str) {}

    // 泛化的拷贝构造函数(成员函数模板)
    template<typename T>
    MyClass(const T& other) : data(std::to_string(other)) {
        std::cout << "Generic constructor called with: " << data << std::endl;
    }

    // 泛化的赋值操作符(成员函数模板)
    template<typename T>
    MyClass& operator=(const T& other) {
        data = std::to_string(other);
        std::cout << "Generic assignment operator called with: " << data << std::endl;
        return *this;
    }

    // 显式声明普通的拷贝构造函数
    MyClass(const MyClass& other) : data(other.data) {
        std::cout << "Copy constructor called" << std::endl;
    }

    // 显式声明普通的拷贝赋值操作符
    MyClass& operator=(const MyClass& other) {
        if (this != &other) {
            data = other.data;
            std::cout << "Copy assignment operator called" << std::endl;
        }
        return *this;
    }

    void print() const {
        std::cout << "Data: " << data << std::endl;
    }

private:
    std::string data;
};

示例代码的使用

int main() {
    MyClass obj1("Hello"); // 普通构造函数
    obj1.print();

    MyClass obj2 = 42; // 泛化的构造函数
    obj2.print();

    obj1 = 3.14; // 泛化的赋值操作符
    obj1.print();

    MyClass obj3 = obj1; // 普通拷贝构造函数
    obj3.print();

    obj2 = obj3; // 普通拷贝赋值操作符
    obj2.print();

    return 0;
}
// 输出结果
// Data: Hello
// Generic constructor called with: 42
// Data: 42
// Generic assignment operator called with: 3.140000
// Data: 3.140000
// Copy constructor called
// Data: 3.140000
// Copy assignment operator called
// Data: 3.140000

代码解析

  1. 泛化的构造函数和赋值操作符:
  2. 普通的拷贝构造函数和赋值操作符:
  3. 为什么需要显式声明普通的拷贝构造和赋值操作符:

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

核心思想

  1. 问题背景
  2. 解决方案

示例代码

  1. 使用成员函数模板的问题

假设我们有一个简单的模板类 Rational,表示有理数:

template<typename T>
class Rational {
public:
    Rational(const T& numerator = 0, const T& denominator = 1)
        : num(numerator), den(denominator) {}

    // 成员函数模板,用于支持加法
    template<typename U>
    Rational<T> operator+(const Rational<U>& other) const {
        return Rational<T>(
            num * other.den + other.num * den,
            den * other.den
        );
    }

private:
    T num, den;
};

问题:

如果尝试将一个 Rational<int> 与一个 Rational<double> 相加,代码可能会失败,因为成员函数模板不会触发隐式类型转换。 例如

Rational<int> r1(1, 2);
Rational<double> r2(3.5, 4.5);

// 编译器可能无法解析 r1 + r2
auto result = r1 + r2; // 错误:无法找到合适的 operator+

这是因为 r1 的类型是 Rational<int>,而 r2 的类型是 Rational<double>。编译器不会自动将 r1 转换为 Rational<double> 或 r2 转换为 Rational<int>

改进:使用非成员函数模板

为了解决上述问题,可以将 operator+ 定义为非成员函数模板:

template<typename T>
class Rational {
public:
    Rational(const T& numerator = 0, const T& denominator = 1)
        : num(numerator), den(denominator) {}

    // 声明友元函数模板
    template<typename U, typename V>
    friend Rational<U> operator+(const Rational<U>& lhs, const Rational<V>& rhs);

private:
    T num, den;
};

// 非成员函数模板
template<typename U, typename V>
Rational<U> operator+(const Rational<U>& lhs, const Rational<V>& rhs) {
    return Rational<U>(
        lhs.num * rhs.den + rhs.num * lhs.den,
        lhs.den * rhs.den
    );
}

int main() {
    Rational<int> r1(1, 2);
    Rational<double> r2(3.5, 4.5);

    // 使用非成员函数模板,支持隐式类型转换
    auto result = r1 + r2;

    // 输出结果
    // 注意:这里 result 的类型是 Rational<int>,因为模板参数 U 是 int
    return 0;
}

优势:

为什么非成员函数模板更好?

  1. 成员函数模板的局限性:
  2. 非成员函数模板的优势:

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

核心思想

  1. 什么是 Traits Classes:
  2. Traits Classes 的作用:
  3. 为什么需要 Traits Classes:

实例代码

  1. 基本的 Traits Classes 示例

以下是一个简单的 Traits Classes 示例,用于判断一个类型是否是指针类型:

// 默认模板:假设类型不是指针
template<typename T>
struct IsPointer {
    static const bool value = false;
};

// 特化版本:如果是指针类型,返回 true
template<typename T>
struct IsPointer<T*> {
    static const bool value = true;
};

// 使用示例
#include <iostream>

int main() {
    std::cout << IsPointer<int>::value << std::endl;    // 输出 0(false)
    std::cout << IsPointer<int*>::value << std::endl;  // 输出 1(true)
    return 0;
}

解释

  1. 结合函数重载实现编译期类型判断

Traits classes 可以与函数重载结合,在编译期根据类型选择不同的实现。例如:

#include <iostream>

// Traits Classes:判断类型是否是指针
template<typename T>
struct IsPointer {
    static const bool value = false;
};

template<typename T>
struct IsPointer<T*> {
    static const bool value = true;
};

// 重载函数:针对指针类型
template<typename T>
void process(T* ptr) {
    std::cout << "Processing a pointer" << std::endl;
}

// 重载函数:针对非指针类型
template<typename T>
void process(T value) {
    std::cout << "Processing a non-pointer" << std::endl;
}

// 使用示例
int main() {
    int x = 42;
    int* p = &x;

    process(x);  // 调用非指针版本
    process(p);  // 调用指针版本

    return 0;
}

解释

  1. 使用 traits classes模拟编译期的if else

Traits classes 还可以结合模板特化和 SFINAE(Substitution Failure Is Not An Error)技术,在编译期实现类似 if-else 的逻辑。例如:

#include <iostream>

// Traits Classes:判断类型是否是指针
template<typename T>
struct IsPointer {
    static const bool value = false;
};

template<typename T>
struct IsPointer<T*> {
    static const bool value = true;
};

// 编译期 if-else:选择不同的实现
template<typename T>
void process(T value) {
    // if constexpr 是 C++17 引入的特性,用于在编译期执行条件判断。
    if constexpr (IsPointer<T>::value) {
        std::cout << "Processing a pointer" << std::endl;
    } else {
        std::cout << "Processing a non-pointer" << std::endl;
    }
}

// 使用示例
int main() {
    int x = 42;
    int* p = &x;

    process(x);  // 输出 "Processing a non-pointer"
    process(p);  // 输出 "Processing a pointer"

    return 0;
}

Traits Classes的实现方式

  1. 模板和模板特化:
  2. 静态成员变量:
  3. 结合 SFINAE 和 if constexpr:

优势

  1. 编译期类型信息:
  2. 灵活性:
  3. 类型安全:

48、认识 template 元编程

核心思想

  1. 什么是模板元编程 (TMP):
  2. TMP 的优势:
  3. TMP 的用途:

示例代码:

  1. TMP实现编译期计算

以下是一个简单的TMP示例,用于计算编译期的阶乘

// TMP 实现阶乘计算
template<int N>
struct Factorial {
    static const int value = N * Factorial<N - 1>::value;
};

// 特化模板:递归终止条件
template<>
struct Factorial<0> {
    static const int value = 1;
};

// 使用示例
#include <iostream>

int main() {
    std::cout << "Factorial of 5: " << Factorial<5>::value << std::endl; // 输出 120
    return 0;
}

解释:

  1. TMP用于策略组合

TMP可以根据不同的策略组合生成定制化代码

// 策略类
struct PolicyA {
    static void execute() {
        std::cout << "Policy A executed" << std::endl;
    }
};

struct PolicyB {
    static void execute() {
        std::cout << "Policy B executed" << std::endl;
    }
};

// TMP 组合策略
template<typename Policy>
class Strategy {
public:
    void run() {
        Policy::execute();
    }
};

// 使用示例
int main() {
    Strategy<PolicyA> strategyA;
    strategyA.run(); // 输出 "Policy A executed"

    Strategy<PolicyB> strategyB;
    strategyB.run(); // 输出 "Policy B executed"

    return 0;
}

解释

  1. TMP避免生成不合适的代码

TMP可以在编译期检测类型特征,避免生成对某些类型无效的代码

#include <type_traits>

// TMP 检测类型是否为指针
template<typename T>
struct IsPointer {
    static const bool value = false;
};

template<typename T>
struct IsPointer<T*> {
    static const bool value = true;
};

// TMP 避免生成不适合的代码
template<typename T>
void process(T value) {
    static_assert(!IsPointer<T>::value, "Pointers are not allowed!");
    std::cout << "Processing non-pointer type" << std::endl;
}

// 使用示例
int main() {
    int x = 42;
    process(x); // 正常编译

    int* p = &x;
    // process(p); // 编译错误:Pointers are not allowed!

    return 0;
}

解释

TMP的局限性

  1. 复杂性:
  2. 编译时间:
  3. 错误信息:

定制 new 和 delete

  1. 为什么定制 new 和 delete:
  2. 如何定制 new 和 delete:
  3. 重载的形式:
  4. 需要注意的问题:

49、了解 new-handler 的行为

核心内容

  1. 什么是 new-handler:
  2. new-handler 的作用:
  3. 如何设置 new-handler:
  4. new-handler 的行为:

示例代码

  1. 设置 new-handler
#include <iostream>
#include <new> // std::set_new_handler

// 自定义 new-handler
void myNewHandler()
{
    std::cerr << "Memory allocation failed! Attempting to recover..." << std::endl;
    // 尝试释放资源或记录日志
    // 如果无法恢复,可以终止程序
    std::abort();
}

int main()
{
    // 设置全局 new-handler
    std::set_new_handler(myNewHandler);

    try
    {
        // 尝试分配大量内存,导致分配失败
        int *p = new int[1000000000000];
        if (p)
        {
            delete[] p;
        }
    }
    catch (const std::bad_alloc &e)
    {
        std::cerr << "Caught exception: " << e.what() << std::endl;
    }

    return 0;
}

// 输出
// Memory allocation failed! Attempting to recover...
// Aborted (core dumped)
  1. 自定义类的 new-handler

可以为特性的类设置自己的 new-handler,而不影响全局的new-handler,多线程程序绝非下面这样简单的

#include <iostream>
#include <new>

class MyClass {
public:
    // 设置类专属的 new-handler
    static void myClassNewHandler() {
        std::cerr << "MyClass memory allocation failed!" << std::endl;
        std::abort();
    }

    // 重载 operator new
    static void* operator new(size_t size) {
        std::set_new_handler(myClassNewHandler); // 设置专属 new-handler
        void* ptr = ::operator new(size);       // 调用全局 operator new
        std::set_new_handler(nullptr);          // 恢复全局 new-handler
        return ptr;
    }
};

int main() {
    try {
        MyClass* obj = new MyClass[1000000000000]; // 分配失败时调用类的 new-handler
        delete[] obj;
    } catch (const std::bad_alloc& e) {
        std::cerr << "Caught exception: " << e.what() << std::endl;
    }

    return 0;
}

// MyClass memory allocation failed!

注意事项

  1. new-handler 的局限性:
  2. new-handler 的恢复:
  3. 异常安全:
  4. 不要滥用 new-handler:

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

核心内容

  1. 默认情况下,尽量使用标准的 new 和 delete:
  2. 替换 new 和 delete 的合理时机:
    1. 性能优化:
      • 当程序频繁分配和释放小对象时,默认的内存分配器可能效率较低。
      • 可以通过对象池(object pool)或自定义内存分配器来优化性能。
    2. 内存对齐:
      • 某些硬件或算法需要特定的内存对齐(如 SIMD 指令)。
      • 自定义 new 和 delete 可以确保分配的内存满足对齐要求。
    3. 调试和内存管理:
      • 用于检测内存泄漏、非法访问或双重释放。
      • 自定义 new 和 delete 可以记录内存分配和释放的详细信息,帮助调试。
    4. 特殊用途:
      • 在嵌入式系统或实时系统中,可能需要从特定的内存区域分配内存。
      • 自定义 new 和 delete 可以满足这些特殊需求。
  3. 替换的粒度:
  4. 替换时的注意事项:

示例代码

  1. 类级别的 new 和 delete 替换
#include <iostream>
#include <cstdlib>

class MyClass
{
public:
    // 自定义 operator new
    void *operator new(size_t size)
    {
        std::cout << "Custom new for MyClass, size: " << size << std::endl;
        return std::malloc(size);
    }

    // 自定义 operator delete
    void operator delete(void *ptr)
    {
        std::cout << "Custom delete for MyClass" << std::endl;
        std::free(ptr);
    }
};

int main()
{
    MyClass *obj = new MyClass(); // 调用自定义的 new
    delete obj;                   // 调用自定义的 delete
    return 0;
}

// 输出
// Custom new for MyClass, size: 1
// Custom delete for MyClass
  1. 全局替换 new 和 delete
#include <iostream>
#include <cstdlib>

// 全局替换 operator new
void* operator new(size_t size) {
    std::cout << "Global new, size: " << size << std::endl;
    return std::malloc(size);
}

// 全局替换 operator delete
void operator delete(void* ptr) noexcept {
    std::cout << "Global delete" << std::endl;
    std::free(ptr);
}

int main() {
    int* p = new int(42); // 调用全局 new
    delete p;             // 调用全局 delete
    return 0;
}

// 输出
// Global new, size: 4
// Global delete
  1. 特定场景替换

在某些特定场景下,可以为特定的需求自定义为new和delete,例如使用内存池优化小对象的分配。

#include <cstdlib>
#include <iostream>
#include <vector>

class MemoryPool {
    static const std::size_t POOL_SIZE = 1024; // 内存池大小
    char pool[POOL_SIZE];                     // 内存池
    std::vector<void*> freeBlocks;            // 空闲块列表

public:
    MemoryPool() {
        for (std::size_t i = 0; i < POOL_SIZE; i += sizeof(void*)) {
            freeBlocks.push_back(&pool[i]);
        }
    }

    void* allocate() {
        if (freeBlocks.empty()) {
            throw std::bad_alloc();
        }
        void* block = freeBlocks.back();
        freeBlocks.pop_back();
        return block;
    }

    void deallocate(void* ptr) {
        freeBlocks.push_back(ptr);
    }
};

class PooledClass {
    static MemoryPool pool; // 静态内存池

public:
    // 重载 new 操作符
    void* operator new(std::size_t size) {
        std::cout << "PooledClass new: allocating " << size << " bytes from pool\n";
        return pool.allocate();
    }

    // 重载 delete 操作符
    void operator delete(void* ptr) noexcept {
        std::cout << "PooledClass delete: deallocating memory to pool\n";
        pool.deallocate(ptr);
    }
};

// 定义静态内存池
MemoryPool PooledClass::pool;

int main() {
    PooledClass* obj1 = new PooledClass; // 从内存池分配
    PooledClass* obj2 = new PooledClass; // 从内存池分配

    delete obj1; // 释放到内存池
    delete obj2; // 释放到内存池

    return 0;
}

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

核心内容总结

  1. 为什么需要遵循常规?
  1. 自定义 new 和 delete 的常规规则

规则 1:成对重载 new 和 delete 如果你重载了 new,也必须重载对应的 delete。 如果你重载了数组版本的 new[],也必须重载对应的 delete[]

#include <cstdlib>
#include <iostream>

class MyClass {
public:
    // 重载 new
    void* operator new(std::size_t size) {
        std::cout << "Custom new: allocating " << size << " bytes\n";
        return std::malloc(size);
    }

    // 重载 delete
    void operator delete(void* ptr) noexcept {
        std::cout << "Custom delete\n";
        std::free(ptr);
    }

    // 重载 new[]
    void* operator new[](std::size_t size) {
        std::cout << "Custom new[]: allocating " << size << " bytes\n";
        return std::malloc(size);
    }

    // 重载 delete[]
    void operator delete[](void* ptr) noexcept {
        std::cout << "Custom delete[]\n";
        std::free(ptr);
    }
};

int main() {
    MyClass* obj = new MyClass;       // 调用自定义 new
    delete obj;                       // 调用自定义 delete

    MyClass* arr = new MyClass[5];    // 调用自定义 new[]
    delete[] arr;                     // 调用自定义 delete[]

    return 0;
}

// Custom new: allocating 1 bytes
// Custom delete
// Custom new[]: allocating 20 bytes
// Custom delete[]

为什么需要成对重载? 如果只重载了 new 而没有重载 delete,释放内存时会调用默认的 delete,可能导致未定义行为。 同样,如果只重载了 new[] 而没有重载 delete[],会导致内存泄漏或崩溃。

规则 2:确保 new 和 delete 的行为一致 自定义的 new 和 delete 必须使用相同的内存分配和释放机制。 如果 new 使用了 malloc 分配内存,那么 delete 必须使用 free 释放内存。 * 如果 new 使用了自定义的内存池,那么 delete 也必须释放到同一个内存池。

class MyClass {
public:
    static void* operator new(std::size_t size) {
        std::cout << "Allocating memory using malloc\n";
        return std::malloc(size); // 使用 malloc 分配内存
    }

    static void operator delete(void* ptr) noexcept {
        std::cout << "Freeing memory using free\n";
        std::free(ptr); // 使用 free 释放内存
    }
};

为什么需要行为一致? * 如果 new 和 delete 使用不同的内存管理机制,可能导致内存泄漏或崩溃。

规则 3:处理内存分配失败 自定义的 new 操作符必须在内存分配失败时抛出 std::bad_alloc 异常。 不能返回 nullptr,因为这会违反 C++ 的内存分配语义。

#include <new> // std::bad_alloc

class MyClass {
public:
    static void* operator new(std::size_t size) {
        void* ptr = std::malloc(size);
        if (!ptr) {
            throw std::bad_alloc(); // 内存分配失败时抛出异常
        }
        return ptr;
    }

    static void operator delete(void* ptr) noexcept {
        std::free(ptr);
    }
};

为什么需要抛出异常? 如果 new 返回 nullptr,调用者可能不会检查返回值,从而导致程序崩溃。 抛出 std::bad_alloc 是 C++ 的标准行为,符合开发者的预期。

规则 4:避免隐藏默认的 new 和 delete 如果你重载了类级别的 new 和 delete,不要隐藏全局的 new 和 delete。 如果需要调用全局的 new 和 delete,可以使用 ::new 和 ::delete。

class MyClass {
public:
    static void* operator new(std::size_t size) {
        std::cout << "Custom new\n";
        return ::operator new(size); // 调用全局 new
    }

    static void operator delete(void* ptr) noexcept {
        std::cout << "Custom delete\n";
        ::operator delete(ptr); // 调用全局 delete
    }
};

规则 5:为对齐分配提供支持(C++17 及以上) * 如果需要支持对齐分配(aligned allocation),必须重载对齐版本的 new 和 delete。

#include <cstdlib>
#include <iostream>
#include <new>

class MyClass {
public:
    static void* operator new(std::size_t size, std::align_val_t alignment) {
        std::cout << "Aligned new: alignment = " << static_cast<std::size_t>(alignment) << "\n";
        return std::aligned_alloc(static_cast<std::size_t>(alignment), size);
    }

    static void operator delete(void* ptr, std::align_val_t alignment) noexcept {
        std::cout << "Aligned delete\n";
        std::free(ptr);
    }
};

52、写了 placement new 也要写 placement delete

  1. 什么是placement new?
void* operator new(std::size_t size, void* location);
  1. 为什么需要定义 placement delete?

正确的实现

#include <iostream>
#include <cstdlib>

class MyClass {
public:
    // Placement new
    void* operator new(std::size_t size, void* location) {
        std::cout << "Placement new called\n";
        return location; // 返回指定的内存地址
    }

    // Placement delete
    void operator delete(void* ptr, void* location) noexcept {
        std::cout << "Placement delete called\n";
        // 不需要释放内存,因为内存是由调用者管理的
    }

    MyClass() {
        std::cout << "Constructor called\n";
        throw std::runtime_error("Error in constructor"); // 模拟异常
    }

    ~MyClass() {
        std::cout << "Destructor called\n";
    }
};

int main() {
    char buffer[sizeof(MyClass)]; // 预分配内存

    try {
        MyClass* obj = new (buffer) MyClass; // 使用 placement new
    } catch (const std::exception& e) {
        std::cout << "Exception caught: " << e.what() << "\n";
    }

    return 0;
}

// Placement new called
// Constructor called
// Placement delete called
// Exception caught: Error in constructor

解释:

  1. 如果没有定义 placement delete 会发生什么?

如果没有定义对应的 placement delete,当构造函数抛出异常时:

  1. 规则总结

成对定义 placement new 和 placement delete: 如果定义了 placement new,也必须定义对应的 placement delete。 如果定义了数组版本的 placement new[],也必须定义对应的 placement delete[]。

placement delete 的参数必须匹配 placement new: * 如果 placement new 接受额外的参数(如内存地址),placement delete 也必须接受相同的参数。

placement delete 不需要释放内存: * placement delete 通常不负责释放内存,因为内存是由调用者管理的。

示例:数组版本的placement new和delete

#include <iostream>
#include <cstdlib>

class MyClass {
public:
    // Placement new[]
    void* operator new[](std::size_t size, void* location) {
        std::cout << "Placement new[] called\n";
        return location;
    }

    // Placement delete[]
    void operator delete[](void* ptr, void* location) noexcept {
        std::cout << "Placement delete[] called\n";
    }
};

int main() {
    char buffer[sizeof(MyClass) * 3]; // 预分配内存

    MyClass* arr = new (buffer) MyClass[3]; // 使用 placement new[]
    // 注意:此处不会调用构造函数,因为 placement new[] 不会自动调用构造函数。

    // 如果需要手动调用构造函数,可以使用 placement new 的单个版本。

    return 0;
}

杂项讨论

  1. 不要轻易忽略编译器警告
  2. 让自己熟悉 Boost, 例如网络模块