Effective CPP

1. 让自己习惯C++

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

  • C++以C为基础
  • Object-Oriented C++。面向对象编程
  • Template C++。泛型编程
  • STL。template程序库,各部件紧密配合


2:尽量以const,enum,inline替换#define

  • 写#define宏时,为所有实参加上小括号,避免歧义
  • 有了 consts 、 enums 和 inlines ,我们对预处理器(特别是 #define) 的需求降低了,但并非完全消除。 #include 仍然是必需品,而 #ifdef/#ifndef 也继续扮演控制编译的重要角色(如头文件的防卫式声明)。目前还不到预处理器全面引退的时候,但你应该明确地给予它更长更频繁的假期

3:尽可能使用const

  • 只要“某值保持不变” 是事实,就应该用const说出来

4:确定对象被使用前已先被初始化

  • 确保每一个构造函数都将对象的每一个成员初始化
  • C++ 对”定义于不同的编译单元内的 non-local static对象”的初始化相对次序并无明确定义,所以将non-local static对象搬到自己的专属函数内(该对象在函数内声明为static)

2. 构造/析构/赋值运算

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

  • 默默写了构造函数,拷贝构造函数,拷贝赋值函数,析构函数
  • 如果在类中自己声明了一个构造函数,则编译器不再为它创建default构造函数。如果自己没有写拷贝构造函数和拷贝赋值函数,则编译器会为其创建(仅限于生出的代码是合法的)
    • 如c++不允许“让reference改指向不同对象”,不允许更改const成员。这时p=s的赋值就是错误的
    • p的nameValue引用已经和newDog绑定,s同理。所以不能p=s
    • p的objectValue是const的,不能随便改。所以不能p=s


  • 如果某个 base classes 将 copy assignment 操作符声明为 private ,编译器将拒绝为其 derived classes 生成一个 copy
    assignment 操作符

6:若不想使用编译器自动生成的函数,就该明确拒绝

有些东西是独一无二的,如房子HomeForSale,这时并不希望它可以被copy

  • 如果你不声明 copy构造函数或 copy assignment 操作符,编译器可能为你产出一份,于是你的 class 支持 copying。如果你声明它们,你的 class还是支持 copying。 但这里的目标却是要阻止 copying!

  • 解决方法1:将成员函数声明为 private 而且故意不实现它们(只声明了但没有实现,当然不可以copy啦)

  • 解决方法2:只要将 copy构造函数和 copy assignment操作符声明为 private 就可以办到,但不是在 HomeForSale 自身,而是在一个专门为了阻止 copying 动作而设计的 base class 内

    • 子类如果写自己的拷贝函数,则需要完整地把父类部分也实现(见条款12)。由于父类部分包含在子类中,而父类的copy函数是私有的,因此子类无法实现父类部分的copy,所以编译器也不会允许子类的拷贝
  • HomeForSale类继承了基类。HomeForSale类在建造自己的copy构造和copy assignment函数函数时,需要把基类部分也实现。然而基类的copy构造和copy assignment函数是私有的,不能为HomeForSale类所用,因此他自己也无法copy构造和copy assignment
    • copy constructor of ‘HomeForSale’ is implicitly deleted because base class ‘Uncopyable’ has an inaccessible copy constructor
//如果把Uncopyable类的private注释取消,则编译器允许HomeForSale h3(h1)、h2=h1等操作

#include <iostream>

using namespace std;

class Uncopyable {
//    允许子类对象构造和析构
protected:
    Uncopyable() {};
    ~Uncopyable() {};

//    但阻止子类对象copy
private:
    Uncopyable(const Uncopyable &);
    Uncopyable & operator=(const Uncopyable &);

};

class HomeForSale : private Uncopyable {

};

int main() {
    HomeForSale h1;

//   不允许拷贝和赋值
//    HomeForSale h2=h1;
//    HomeForSale h3(h1);
}

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

  • “给base Class一个virtual析构函数”这个规则只适用于带多态性质的base class身上(Base* pBase = new Derived)。这种base class的设计目的是为了用来“通过base class接口处理 derived class 对象”。
    • 比如有一个表示形状的Shape类,这个类中有一个virtual的area()函数,用来计算形状的面积。Shape类有两个子类:矩形Rectangle类和三角形Triangle类,这两个子类中分别实现了各自的计算面积函数area()
    • 对客户端而言,它只需要关注base class的接口即可,用这个接口即可处理相应的 derived class 对象。根据基类指针指向的不同对象 (Base* pBase = new Rectangle或者Base* pBase = new Triangle),自动调用各自的面积计算函数
  • 因为Base* pBase = new Derived是把一个派生类对象的地址赋值给了基类指针,在销毁时,也需要完整地销毁这个对象。
    • 如果基类的析构函数设为virtual,在销毁时,由于多态的性质,会先调用派生类的析构函数,然后调用基类的析构函数,实现了完全销毁
    • 如果基类的析构函数不是virtual,则在销毁时,只调用了基类的析构函数,派生类部分无法被销毁。所以属于部分销毁,出现了内存泄漏,这种做法是不可取的 ~
    • 所以如果一个基类中有virtual的函数,说明这个基类带有多态性质,会出现基类指针指向派生类对象的情况。**为了达到完全销毁,应该将基类的析构函数设为virtual

**

  • 任何class 只要带有 virtual 函数(多态基类表现:它希望子类根据各自情况去实现该函数)都几乎确定应该也有一个virtual析构函数
  • 如果 class 不含 virtual 函数,通常表示它并不意图被用做一个base class。当 class不企图被当作base class,就不要把析构函数设为virtual(如条款6的Uncopyable基类)
  • 欲实现出 virtual 函数,对象必须携带某些信息,主要用来在运行期决定哪一个virtual 函数该被调用。这份信息通常是由一个所谓 vptr (virtual table pointer) 指针指出。 vptr 指向一个由函数指针构成的数组,称为 vtbl ( virtual table) ;每一个带有virtual函数的 class 都有一个相应的 vtbl。当对象调用某一virtual 函数,实际被调用的函数取决于该对象的 vptr 所指的那个vtbl一一编译器在其中寻找适当的函数指针

图片来自侯捷老师讲义_高级编程下_P51

  • 通过在基类中将析构函数定义成虚函数才能确保执行正确的析构函数版本
    • 现在有一个基类Base,和一个派生类Derived:public Base,用一个基类的指针指向派生类的对象:Base* pBase = new Derived。在delete pBase时,如果Base的析构函数为虚,则会先调用Derived的析构函数,再调用Base的析构函数(像拆包裹一样,先拆外面的,再拆里面的)。如果Base的析构函数非虚,则只会调用Base的析构函数,造成“局部销毁”对象,导致资源泄露
      c++ primer P556

示例:

#include <iostream>

using namespace std;

class a
{
public:
    virtual ~a()
    {
        cout<<"delete a"<<endl;
    };
};

class b : public a
{
    ~b()
    {
        cout<<"delete b"<<endl;
    };
};


int main()
{
    a *pa = new b;
    delete pa;

    return 0;
}
//结果输出
//delete b
//delete a

// 删除掉a类中的virtual 修饰这样只会调用a类的析构函数,造成局部销毁,导致资源泄露
// 结果输出
//delete a
  • polymorphic (带多态性质的) base classes 应该声明一个 virtual 析构函数。如果 class 带有任何 virtual 函数,它就应该拥有一个 virtual 析构函数
  • Classes 的设计目的如果不是作为 base classes 使用,或不是为了具备多态性> (polymorphically) ,就不该声明 virtual 析构函数

8:别让异常逃离析构函数(待)

  • 构函数绝对不要吐出异常。如果一个被析构函数调用的函数可能抛出异常,析构函数应该捕捉任何异常,然后吞下它们(不传播)或结束程序
  • 如果客户需要对某个操作函数运行期间抛出的异常做出反应,那么 class 应该提供一个普通函数(而非在析构函数中)执行该操作

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

  • 由于base class构造函数的执行更早于derived class 构造函数,当 base class 构造函数执行时 derived class 的成员变量尚未初始化。这会导致不明确行为。”要求使用对象内部尚未初始化的成分”非常危险,所以 C++ 不让你走这条路
  • 唯一能够避免此问题的做法就是:确定你的构造函数和析构函数都没有(在对象被创建和被销毁期间)调用virtual 函数,而它们调用的所有函数也都服从同一约束
  • 如果想在每次子类继承基类时,都能有适合于该子类的打印函数被调用,则将该函数设为non-virtual,然后要求 derived class 构造函数传递必要信息给 Base类的构造函数,而后那个构造函数便可安全地调用 non-virual 信息。
    • 换句话说,由于你无法使用 virtual 函数从 base classes 向下调用,在构造期间,你可以藉由”令 derived classes 将必要的构造信息向上传递至 base class 构造函数”替换之而加以弥补

示例1:

#include<iostream>
#include<string>
#include <vector>

using namespace std;

class Base {
public:
    Base(const string &logInfo);

    void printInfo(const string &logInfo) const;
};

Base::Base(const string &logInfo) {
    printInfo(logInfo);
}

void Base::printInfo(const string &logInfo) const {
    cout << logInfo << endl;
}

class Derived1 : Base { ;
public:
    Derived1(int a_1) : a1(a_1), Base("This is int information") {}

private:
    int a1;
};

class Derived2 : Base {
public:
    Derived2(double b_1) : b1(b_1), Base("This is double information") {};
private:
    double b1;
};

int main() {
    Derived1 derived1(1);// This is int information
    Derived2 derived2(1.1);// This is double information
}

示例2(与本条款对应的代码):

#include<iostream>
#include<string>
#include <vector>

using namespace std;

class Transaction {
public:
    explicit Transaction(const string &logInfo);

    void logTransaction(const string &logInfo) const;
};

//基类的构造函数内调用了logTransaction()函数,用来记录log信息
Transaction::Transaction(const string &logInfo) {
    logTransaction(logInfo);
}

//打印出log信息
void Transaction::logTransaction(const string &logInfo) const {
    cout << logInfo << endl;
}

class BuyTransaction : public Transaction {
public:
//    子类的构造函数
//    子类继承了父类,在构造函数中,也要完整地初始化子类的基类part
//    利用辅助函数将子类的信息传递到父类的构造函数中
    BuyTransaction() : Transaction(createLogString(parameters)) {};
private:
    static string parameters;

    static string createLogString(string parameters) {
        return parameters;
    };
};
//在类外和函数外初始化static类型的的private变量
string BuyTransaction::parameters = "logInformation";

int main() {

    BuyTransaction b;//logInformation
//    子类对象调用了父类的non-virtual函数
    b.logTransaction("Derived object call Base non-virtual function");//Derived object call Base non-virtual function

}

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

  • 为了实现”连锁赋值”,赋值操作符必须返回一个reference指向操作符的左侧实参
  • 适用于所有赋值相关运算,如+=等

以Base类为例,_返回类型是个reference_,指向当前对象

// 
Base& operator=(const Base& rhs){
    ...
    return *this;// 返回左侧对象
    ...
}

11:在operator=中处理“自我赋值”

  • 如果某段代码操作 pointers 或 references 而它们被用来”指向多个相同类型的对象”,就需考虑这些对象是否为同一个

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

当编写一个copy(包括拷贝构造和拷贝赋值)函数时,一定要

  • 复制所有local成员变量
  • 调用所有base class内的适当的copy函数

写派生类的构造函数/拷贝构造函数/拷贝赋值操作时,一定要完善基类部分

示例:

#include<iostream>
#include<string>
#include <vector>

using namespace std;

class Person {
public:
//    构造函数
    Person(string _name, int _age) : name(_name), age(_age) {
        cout << "Person construction function" << endl;
    }

//    拷贝构造
    Person(const Person &p1) : name(p1.name), age(p1.age) {
        cout << "Person copy constructor" << endl;
    }

//    拷贝赋值
    Person &operator=(const Person &p1) {
        cout << "Person copy assignment operator" << endl;
        name = p1.name;
        age = p1.age;
        return *this;
    }

    const string &getName() const {
        return name;
    }

    int getAge() const {
        return age;
    }

private:
    string name;
    int age;
};

class Student : public Person {
public:
//    构造函数
    Student(string _name, int _age, string _ID) : Person(_name, _age), ID(_ID) {
        cout << "Student construction function" << endl;
    }

//    拷贝赋值
    Student(const Student &s1) : Person(s1), ID(s1.ID) {
        cout << "Student copy constructor" << endl;
    }

//    拷贝赋值
    Student &operator=(const Student &s1) {
        cout << "Student copy assignment operator" << endl;
        Person::operator=(s1);
        ID = s1.ID;
        return *this;
    }

    const string &getId() const {
        return ID;
    }

private:
    string ID;
};

int main() {
    Student s1("Amy", 18, "aaa123");
    cout << "s1 information: " << s1.getName() << "\t" << s1.getAge() << "\t" << s1.getId() << endl;
    cout << "==========" << endl;
    //    使用拷贝构造函数
    Student s2(s1);
    cout << "s2 information: " << s2.getName() << "\t" << s2.getAge() << "\t" << s2.getId() << endl;
    cout << "==========" << endl;
//    使用拷贝赋值操作
    Student s3("Tom", 20, "aaa456");
    s3 = s1;
    cout << "s3 information: " << s3.getName() << "\t" << s3.getAge() << "\t" << s3.getId() << endl;
}

// 结果为:
// Person construction function
// Student construction function
// s1 information: Amy    18    aaa123
// ==========
// Person copy constructor
// Student copy constructor
// s2 information: Amy    18    aaa123
// ==========
// Person construction function
// Student construction function
// Student copy assignment operator
// Person copy assignment operator
// s3 information: Amy    18    aaa123

4. 设计与声明

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

19:设计class犹如设计type




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

  • 如果传值的话,实际上传递的是实参的副本(这是一个新对象),这些副本由对象的copy构造函数产生,因此在使用时需要调用很多次构造和析构函数,成本比较高
  • 如果传引用的话,因为没有任何新对象产生,所以无需调用构造和析构函数,成本较低。为了告诉调用者:你不要改变我的对象,因此需要在前面加const

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

  • 所谓reference 只是个名称,代表某个既有对象。任何时候看到一个 reference 声明式,你都应该立刻问自己,它的另一个名称是什么?因为它一定是某物的另一个名称
    • 如果一个对象是local对象,而该local对象在函数退出前就被销毁了。这时如果返回该对象的reference的话,很危险,属于无定义行为(如果函数返回指针指向一个local对象,也是一样)


22:将成员变量声明为private


23:宁以non-mmber,non-friend替换member函数

  • 如果某些东西被封装,它就不再可见。愈多东西被封装,愈少人可以看到它。而愈少人看到它,我们就有愈大的弹性去变化它,因为我们的改变仅仅直接影响看到改变的那些人事物
  • 愈少代码可以看到数据(也就是访问它) ,愈多的数据可被封装,而我们也就愈能自由地改变对象数据,例如改变成员变量的数量、类型等
  • non-mmber,non-friend会有较好的封装性,因为它并不增加”能够访问class 内之 private 成分”的函数数量
  • 在 C++,比较自然的做法是让都使用的函数成为一个 non-member 函数并且位于和类所在的同一个 namespace (命名空间)内

示例:

P.S., 如果既有头文件.h,又有源文件.cpp,就不要在头文件中实现函数。否则会导致multiple definition

  • 报错的原因是虽然头文件指定只编译一次,但每次编译时,都编译了一次,最后链接的时候自然就报错了
    • 解决方法一:头文件只声明,函数主体内容放到.c/.cpp中
    • 解决方法二:头文件的函数加上关键字inline(仅适用于简短的函数),直接把函数编译进去,自然不会冲突。

与类相关东西全部放在同一个namespace (Stuff)中
(1) 设计一个WebBrowser类

  • 这个类中有个clearBrowser函数,浏览器的其他模块(如书签模块)也会用到该函数
  • 本着更好封装性的原则,clearBrowser函数设为non-member类型,并将其放在和WebBrowser类所在的同一个namespace中

Bookmarks.h文件

#ifndef P1_2_WEBBROWSER_H
#define P1_2_WEBBROWSER_H
#include <iostream>
using namespace std;
namespace Stuff {
    class WebBrowser {
    };

    void clearBrowser(WebBrowser &wb);
}
#endif //P1_2_WEBBROWSER_H

Bookmarks.cpp文件

#include "WebBrowser.h"
void Stuff::clearBrowser(WebBrowser &wb) {
    cout << "Clear Browser" << endl;
}

(2) 设计一个Bookmarks类,该类需要对WebBrowser类进行书签操作
Bookmarks.h文件

#ifndef P1_2_BOOKMARKS_H
#define P1_2_BOOKMARKS_H

#include <iostream>
#include "WebBrowser.h"

using namespace std;
namespace Stuff {
    class Bookmarks {
    public:
        void bookmarksFunc(WebBrowser& wb);
    };
}
#endif //P1_2_BOOKMARKS_H

Bookmarks.cpp文件

#include "Bookmarks.h"
void Stuff::Bookmarks::bookmarksFunc(Stuff::WebBrowser &wb) {
    cout<<"Operate bookmarks "<<endl;
    clearBrowser(wb);
}

(3) 使用

  • WebBrowser的对象可以使用non-member函数
  • 与WebBrowser类相关的Bookmarks模块也可以使用WebBrowser类及non-member函数

main.cpp文件

#include "WebBrowser.h"
#include "Bookmarks.h"

int main() {
    Stuff::WebBrowser w;
    clearBrowser(w);
    cout<<"======"<<endl;
    Stuff::Bookmarks b;
    b.bookmarksFunc(w);

    return 0;
}
//Clear Browser
//======
//Operate bookmarks
//Clear Browser

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

如果需要为某个函数的所有参数进行类型转换,则这个函数必须是个non-member。在下面的例子中,实现有理数的相乘

  • (正确示例)采用non-member函数,确保左边的数据left hand side(lhs)和右边的数据right hand side(rhs)都能实现隐式类型转换
  • (错误示例)如果用成员函数实现乘法,则const Rational operator*(const Rational &rhs)函数中,相乘操作采用this->n*=rhs.numerator();this->d*=rhs.denominator();,这样只能实现rhs的隐式类型转换,对lhs束手无策,所以无法做类似于2*oneFourth的操作

正确示例:2需要类型隐式转换,将operator函数写成non-member(注:该函数里的*两个参数是Rational类型的成员引用

#include <iostream>
using namespace std;
class Rational {
public:
    Rational(int numerator = 0, int denominator = 1);// 分子分母分别为0,1
    // 分子和分母的访问函数
    int numerator() const;
    int denominator() const;

private:
    int n;
    int d;
};

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

int Rational::numerator() const { return n; }

int Rational::denominator() const { return d; }

// 将乘法操作写在non-member函数中
const Rational operator*(const Rational &lhs, const Rational &rhs) {
    return Rational(lhs.numerator() * rhs.numerator(), lhs.denominator() * rhs.denominator());
}

int main(){
    Rational oneFourth(1,4); // 1/4
    Rational result=oneFourth*2;

    Rational result_=2*oneFourth;
    cout<<result.numerator()<<"\t"<<result.denominator()<<endl;// 2 4
    cout<<result_.numerator()<<"\t"<<result_.denominator()<<endl;// 2 4
}

错误示例:operator属于类内成员函数,不可以编译`result=2oneFourth;`

#include <iostream>
using namespace std;
class Rational {
public:
    Rational(int numerator = 0, int denominator = 1);// 分子分母分别为0,1
    int numerator() const;
    int denominator() const;
    const Rational operator*(const Rational &rhs){
        this->n*=rhs.numerator();
        this->d*=rhs.denominator();
        return *this;
    }
private:
    int n;
    int d;
};
Rational::Rational(int numerator, int denominator) : n(numerator), d(denominator) {}
int Rational::numerator() const { return n; }
int Rational::denominator() const { return d; }

int main(){
    Rational oneFourth(1,4); // 1/4
    Rational result;
    result=oneFourth*2; //可以编译通过,2被隐式转换为Rational对象
//    result=2*oneFourth; //操作符实现不了重载,不可以编译通过
    cout<<result.numerator()<<"\t"<<result.denominator()<<endl;
}

25:考虑写出一个不抛弃异常的swap函数(待)

5. 实现

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


27:尽量少做转型动作

effective c++ P117

  • 优良的 C++ 代码很少使用转型,但若说要完全摆脱它们又太过不切实际

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

29:为“异常安全”而努力是值得的(待)

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

  • inline 只是对编译器的一个申请,不是强制命令。这项申请可以隐喻提出,也可以明确提出。隐喻方式是将函数定义于class 定义式内,明确声明 inline 函数的做法则是在其定义式前加上关键字iline
  • Inline 函数通常一定被置于头文件内,因为大多数建置环境(build environments)在编译过程中进行 inlining,而为了将一个”函数调用”替换为”被调用函数的本体”,编译器必须知道那个函数长什么样子(Inlining 在大多数 C++ 程序中是编译期行为)
    • Templates 通常也被置于头文件内,因为它一旦被使用,编译器为了将它具现化,需要知道它长什么样子
  • 一个表面上看似 inline 的函数是否真是 inline,取决于你的建置环境,主要取决于编译器

    31:将文件间的编译依存关系降至最低(深入)



6. 继承与面向对象设计

32:确定你的public继承塑模出is-a关系

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

  • 只要(变量或函数的)名称相同(不管类型是否相同、不管函数是否有不同参数、不管是不是virtual函数),Derived都会把Base中的相关内容遮掩。要避免!
  • 实在要用的话,以如下为例,若要用Base中的内容的话,需要在Derived中加入using Base::mf1;using Base::mf3;,或者在main函数中调用时使用d.Base::mf1(x);
#include <iostream>
using namespace std;

class Base{
private:
    int x;
public:
    virtual void mf1()=0;
    virtual void mf1(int){
        cout<<"Base:virtual void mf1(int)"<<endl;
    }
    void mf3(){
        cout<<"Base:void mf3()"<<endl;
    }
    void mf3(double){
        cout<<"Base:void mf3(double)"<<endl;
    }
};

class Derived:public Base{
public:
//    加这两行,让Base类的mf1和mf3的所有东西在Derived类内可见
//    否则,Derived中的mf1和mf3会遮盖Base相关内容,(即便有不同的参数,无论是否是virtual函数)
    using Base::mf1;
    using Base::mf3;
    virtual void mf1(){
        cout<<"Derived:virtual void mf1()"<<endl;
    }
    void mf3(){
        cout<<"Derived:void mf3()"<<endl;
    }
};

int main(){
    Derived d;
    int x=1;
    d.mf1();
    d.mf1(x);
    d.mf3();
    d.mf3(x);
}
//结果为:
//Derived:virtual void mf1()
//Base:virtual void mf1(int)
//Derived:void mf3()
//Base:void mf3(double)

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

函数接口继承(pure virtual)和实现继承(virtual ,non-virtual)

  • pure virtual 函数只具体指定接口继承:
    • 父类:子类啊,你一定要把这个函数override(覆写)啊
  • 简朴的(非纯) impure virtual 函数具体指定接口继承及缺省实现继承:
    • 父类:子类啊,你要不想override,就(默认)用父类的;要想自己override,就用你自己(子类)的。俗称动态绑定
  • non-virtual 函数具体指定接口继承以及强制性实现继承:
    • 父类:子类啊,你自己别费力气override了,就老老实实用父类的(子类即便override了也没用。因为这个函数早就在编译时和父类绑定好了,调用时仍用的是父类的函数)。俗称静态绑定

35:考虑virtual函数以外的其他选择(待)

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

如果继承类重新定义了基类的non-virtual函数,则设计便出现了矛盾

  • virtual函数(唯一应该覆写的东西)可以实现动态绑定(p3->virtualFunc();
  • non-virtual函数是静态绑定。即便基类指针指向派生类的对象,通过该指针调用的non-virtial函数永远是基类的版本(p3->nonVirtualFunc();
#include<iostream>
#include<string>
#include <vector>
using namespace std;

class Base{
public:
    virtual void virtualFunc(){
        cout<<"This is a virtual base function"<<endl;
    }
    void nonVirtualFunc(){
        cout<<"This is a non-virtual base function"<<endl;
    }
};
class Derived:public Base{
public:
    void virtualFunc(){
        cout<<"This is a virtual derived function"<<endl;
    }
    void nonVirtualFunc(){
        cout<<"This is a non-virtual derived function"<<endl;
    }
};
int main(){    
////// 常规地,各自调用各自的函数
    Base b1;
    Base *p1= &b1;
    p1->virtualFunc();//this is a virtual base function
    p1->nonVirtualFunc();//This is a non-virtual base function

    Derived d1;
    Derived *p2=&d1;
    p2->virtualFunc();//This is a virtual derived function
    p2->nonVirtualFunc();//This is a non-virtual derived function
//////////////

    Base *p3=&d1;
    // 动态绑定,实现了正确调用
    p3->virtualFunc();// This is a virtual derived function
    // 错误调用,通过基类指针调用的non-virtual函数永远是基类的版本,即使该指针指向派生类对象
    // non-virtual函数是静态绑定,因此:
    // 即便派生类重新定义了继承而来的non-virtual函数,也无法调用Derived::nonVirtualFunc()
    p3->nonVirtualFunc();//This is a non-virtual base function
}

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

根据36条,我们讨论的是:继承带有缺省参数值的virtual函数

静态绑定又名前期绑定,动态绑定又名后期绑定

  • virtual函数系动态绑定,而缺省参数值却是静态绑定。重新定义缺省参数值会出现矛盾

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

  • “public 继承”带有 is-a (是一种)的意义。复合也有它自己的意义。实际上它有两个意义。复合意味 has-a(有一个,“一个类里面的成员是另一个类,如Person类中有一个成员为Address类型的对象”)或 is-implemented-in也rms-of(根据某物实现出,“为了实现这个类,需要复用(reuse)到另一个类的一部分功能,如Set类需要用到List类的一部分,但又不能完全都用”)
  • 当复合发生于应用域内的对象之间,表现出 has-a 的关系;当它发生于实现域内则是表现 is-implemented-in-terms-of 的关系

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

  • Private 继承意味 is-implemented-in-terms of (根据某物实现出)。它通常比复合(composition) 的级别低。但是当 derived class 需要访问 proteted base class 的成员,或需要重新定义继承而来的virtual 函数时,这么设计是合理的
    • 如果Derived类继承了Base类,你的用意是为了采用Base类内已经备妥的某些特性。private继承意味只有实现部分被继承,接口部分应略去
    • 尽可能使用复合,必要时才使用private继承
  • 和复合(composition) 不同, private 继承可以造成 empty base 最优化。这对致力于”对象只寸最小化”的程序库开发者而言,可能很重要

P.S.,public 继承 / private 继承 / protected 继承详解及区别

  • 对于公有继承方式
    (1) 基类成员对其对象的可见性:  
    公有成员可见,其他不可见。这里保护成员同于私有成员。
      
    (2) 基类成员对派生类的可见性:  
    公有成员和保护成员可见,而私有成员不可见。这里保护成员同于公有成员。
      
    (3) 基类成员对派生类对象的可见性:  
    公有成员可见,其他成员不可见。  

所以,在公有继承时,派生类的对象可以访问基类中的公有成员;派生类的成员函数可以访问基类中的公有成员和保护成员。这里,一定要区分清楚派生类的对象和派生类中的成员函数对基类的访问是不同的

  • 对于私有继承方式
    (1) 基类成员对其对象的可见性:  
    公有成员可见,其他成员不可见。  
    (2) 基类成员对派生类的可见性:  
    公有成员和保护成员是可见的,而私有成员是不可见的。  
    (3) 基类成员对派生类对象的可见性:  
    所有成员都是不可见的。  
    所以,在私有继承时,基类的成员只能由直接派生类访问,而无法再往下继承。
// 一个小示例
#include <iostream>
using namespace std;

class Base {
public:
    void f1() {
        cout << "Base:void f1()" << endl;
    }
};

class Derived : private Base {
//    私有继承:派生类可以调用基类的f1函数
public:
    Derived() {
        f1();
    }
};

int main() {
    Derived d; // Base:void f1()
//    私有继承:派生类的对象不能调用基类的f1对象
//    d.f1();// 不可以这样用
}
  • 对于保护继承方式
      这种继承方式与私有继承方式的情况相同。两者的区别仅在于对派生类的成员而言,对基类成员有不同的可见性。  上述所说的可见性也就是可访问性。关于可访问性还有另的一种说法。这种规则中,称派生类的对象对基类访问为水平访问,称派生类的派生类对基类的访问为垂直访问。

  • 一般规则
    公有继承时,水平访问和垂直访问对基类中的公有成员不受限制;  
    私有继承时,水平访问和垂直访问对基类中的公有成员也不能访问;  
    保护继承时,对于垂直访问同于公有继承,对于水平访问同于私有继承。  
    对于基类中的私有成员,只能被基类中的成员函数和友元函数所访问,不能被其他的函数访问。  

基类与派生类的关系: 任何一个类都可以派生出一个新类,派生类也可以再派生出新类,因此,基类和派生类是相对而言的。

  • 基类与派生类之间的关系
  1. 派生类是基类的具体化
      类的层次通常反映了客观世界中某种真实的模型。在这种情况下,不难看出:基类是对若干个派生类的抽象,而派生类是基类的具体化。基类抽取了它的派生类的公共特征,而派生类通过增加行为将抽象类变为某种有用的类型。

  2. 派生类是基类定义的延续
      先定义一个抽象基类,该基类中有些操作并未实现。然后定义非抽象的派生类,实现抽象基类中定义的操作。例如,虚函数就属此类情况。这时,派生类是抽象的基类的实现,即可看成是基类定义的延续。这也是派生类的一种常用方法。

  3. 派生类是基类的组合
      在多继承时,一个派生类有多于一个的基类,这时派生类将是所有基类行为的组合。  派生类将其本身与基类区别开来的方法是添加数据成员和成员函数。因此,继承的机制将使得在创建新类时,只需说明新类与已有类的区别,从而大量原有的程序代码都可以复用,所以有人称类是 “可复用的软件构件”。

(1)子类对父类成员的访问权限跟如何继承没有任何关系,“子类可以访问父类的 public 和 protected 成员,不可以访问父类的 private 成员”——这句话对任何一种继承都是成立的。
(2)继承修饰符影响着谁可以知道 “继承” 这件事。public 继承大家都知道,有点像 “法定继承人”,因此,任何代码都可以把子类的引用(或指针)直接转换为父类。也因为这个原因,public 继承常用来表达设计中所谓的“is-a” 关系。private 继承则有点像 “私生子”,除了子类自己,没有人知道这层关系,也因此,除了子类自己的代码之外,没有其它人知道自己还有个父亲,于是也就没有其它人可以做相应的类型转换。为此,私有继承常用于表达非“is-a” 的关系,这种情况下子类只是借用父类的某些实现细节。protected 继承则有点特殊,外界同样不知道这层关系,但家族内部的子孙们可以知道,有点像 “自家知道就行了,不许外扬” 的意思,于是子孙们是可以做这种向上转型,其它代码则不可以。因为这种特殊性,protected 继承在实际中用得很少。
(3)还需要补充一点,由于 “继承关系” 的可见性受到了影响,那么继承来的财产的可见性也必然受到影响。比如一个成员变量或成员函数,在父类中本来是 public 的,被某个子类 protected 继承之后,对子类来讲,这个成员就相当于 protected 成员了——继承是继承到了,但权限变了。

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

7. 模板与泛型编程

41:了解隐式接口和编译器多态



42:了解typename的双重意义(待)

C++ 并不总是把 class 和 typename 视为等价。有时候你一定得使用typename
typename使用示例

typedef typename说明(待)

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

基类:超人类(含有很多特异的功能,如一目千行),超人类(可能)有个全特化的版本(性能少一些,比如不能一目千行)
子类在继承了超人类后,产生了一个新类:Student。
Student类想使用一目千行的功能。编译器说:你继承的超人类有可能被特化,这个特化版本可能没有一目千行的功能,所以我才不去找一目千行的功能呢。
Student类说:好吧,我(用this-> 或using 声明式的方式)告诉你,我继承的这个超人类有这个功能
然后,Student类的对象可以调用一目千行的功能了。
而通过全特化版本确定的Student类就没有这个功能

  • 模板全特化
    • class 定义式最前头的”template<>”表示这是个特化版的 MsgSender template. 在 template 实参是 CompanyZ 时被使用。这是所谓的模板全特化 (total template specialization) : template MsgSender针对类型 CompanyZ特化了,而且其特化是全面性的,也就是说一旦类型参数被定义为 CompanyZ. 再没有其他template 参数可供变化
      //模板全特化:一个特别版本的CompanyInfo,只有cat(),而没有dog()
      template<> // 特化版的CompanyInfo template,在template实参是CompanyZ时被使用
      class CompanyInfo<CompanyZ> {
      '''
      }
  • 更具体的标题:(LoggingCompanyInfo)学习处理模板化基类(CompanyInfo)内的名称(dogInfo()catInfo()函数)
    • LoggingCompanyInfo类继承了CompanyInfo类。而在子类中,需要用到基类的dogInfo函数,但基类的Company是一个template参数。这时编译器没办法知道CompanyInfo是否有个dogInfo函数
    • 编译器知道base class templates有可能被特化,而那个特化版本可能不提供和一般性 template 相同的接口。因此它往往拒绝在templatizedbase classes (模板化基类,本例的 CompanyInfo) 内寻找继承而来的名称(本例的dogInfo()
      • 比如特化版本的CompanyInfo中就没有dogInfo()函数
    • 解决方法1:在base class 函数调用动作之前加上”this->” (this->dogInfo(info);
class LoggingCompanyInfo : public CompanyInfo<Company> {
public:
    void logDogInfo(string &info) {
        ...
        this->dogInfo(info);// 调用基类CompanyInfo的dogInfo函数;如果 Company == CompanyZ,这个函数不存在
        ...
    }

    void logCatInfo(string &info) {
        ...
        this->catInfo(info);// 调用基类CompanyInfo的catInfo函数
        ...
    }
};
  • 解决方法2:使用声明式。告诉编译器,请它假设dogInfo()函数位于base class内 (using CompanyInfo<Company>::dogInfo;
class LoggingCompanyInfo : public CompanyInfo<Company> {
public:
    using CompanyInfo<Company>::dogInfo;
    using CompanyInfo<Company>::catInfo;
    void logDogInfo(string &info) {
        ...
        dogInfo(info);// 调用基类CompanyInfo的dogInfo函数;如果 Company == CompanyZ,这个函数不存在
        ...
    }

    void logCatInfo(string &info) {
        ...
        catInfo(info);// 调用基类CompanyInfo的catInfo函数
        ...
    }
};

可在 derived class templates 内通过”this->” 指涉 base class templates 内的成员名称,或藉由一个明白写出的 “base class 资格修饰符”完成。

完整示例:

  • 公司类
    • 公司A卖A品种的dog和cat;
    • 公司B卖B品种的dog和cat(不会用到,只是说明有很多类公司);
    • 公司Z卖Z品种的cat;
  • 信息类(表示公司的广告语)
    • CompanyInfo类用于传送公司广告语信息(可传dog和cat)
    • 一个特别版本的CompanyInfo(模板全特化), 在template实参是CompanyZ时被使用: 只传Z公司的cat
    • 一个包含log信息的LoggingCompanyInfo类,继承了CompanyInfo类
      • 可调用基类CompanyInfo的dogInfo函数;如果 Company == CompanyZ,这个函数不存在
      • 调用基类CompanyInfo的catInfo函数
#include <iostream>
#include <algorithm>

using namespace std;

// A公司类,负责dog和cat
class CompanyA {
public:
    void dog(const string &msg);

    void cat(const string &msg);
};

void CompanyA::dog(const string &msg) {
    cout << "CompanyA::dog(): " << msg << endl;
}

void CompanyA::cat(const string &msg) {
    cout << "CompanyA::cat(): " << msg << endl;
}

// B公司类(不会用到,只是说明company可能有好多类)
class CompanyB {
public:
    void dog(const string &msg);

    void cat(const string &msg);
};

void CompanyB::dog(const string &msg) {
    cout << "CompanyB::dog(): " << msg << endl;
}

void CompanyB::cat(const string &msg) {
    cout << "CompanyB::cat(): " << msg << endl;
}

// Z公司类。与上面两类不同的是,它只负责cat
class CompanyZ {
public:
    void cat(const string &msg);
};

void CompanyZ::cat(const string &msg) {
    cout << "CompanyZ::cat(): " << msg << endl;
}


//类模板
//CompanyInfo负责为公司传送信息
template<typename Company> // 现在还不确定传送哪个公司,先写成模板,Company表示公司类
class CompanyInfo {
public:
    void dogInfo(string &info) {
        string msg(info);
        Company c;
        c.dog(msg); //调用公司的dog函数
    }

    void catInfo(string &info) {
        string msg(info);
        Company c;
        c.cat(msg); // 调用公司的cat函数
    }
};

//模板全特化:一个特别版本的CompanyInfo,只有cat(),而没有dog()
template<> // 特化版的CompanyInfo template,在template实参是CompanyZ时被使用
class CompanyInfo<CompanyZ> {
public:
    void catInfo(string &info) {
        string msg(info);
        CompanyZ z;
        z.cat(msg);
    }
};

// 一个包含log信息的LoggingCompanyInfo类,继承了CompanyInfo类
template<typename Company>
class LoggingCompanyInfo : public CompanyInfo<Company> {
public:
    void logDogInfo(string &info) {
        cout << "Before logDogInfo" << endl;
        this->dogInfo(info);// 调用基类CompanyInfo的dogInfo函数;如果 Company == CompanyZ,这个函数不存在
        cout << "After logDogInfo" << endl;
    }

    void logCatInfo(string &info) {
        cout << "Before logCatInfo" << endl;
        this->catInfo(info);// 调用基类CompanyInfo的catInfo函数
        cout << "After logCatInfo" << endl;
    }
};

int main() {
//    对公司A进行操作
    CompanyA a;
    CompanyB b; // 不会用到。只是说明有多类公司
//    cout << "Use CompanyInfo (company A)" << endl;
    CompanyInfo<CompanyA> ms; //表示CompanyInfo类中用到的公司类为CompanyA
    string info("Best A dogs");
    ms.dogInfo(info);
    cout << "======" << endl;

//    cout << "Use LoggingCompanyInfo (company A)" << endl;
    LoggingCompanyInfo<CompanyA> lms; //包含log信息的LoggingCompanyInfo类,类中用到公司类CompanyA
    string logInfo("Best A dogs (add logging)");
    lms.logDogInfo(logInfo);
    cout << "======" << endl;


//    cout << "Use CompanyInfo (company Z)" << endl;
    CompanyZ z;
    CompanyInfo<CompanyZ> msz; //
    string infoZ("Best Z cats");
    msz.catInfo(infoZ);
    cout << "======" << endl;


//    cout << "Use LoggingCompanyInfo (company Z)" << endl;
    LoggingCompanyInfo<CompanyZ> lmsz;
    string logInfoZ("Best Z cats (add logging)");
//    lmsz.logDogInfo(logInfoZ);  这句话会报错,因为没有Z公司不用logDogInfo
    lmsz.logCatInfo(logInfoZ);
}
// output:
// CompanyA::dog(): Best A dogs
// ======
// Before logDogInfo
// CompanyA::dog(): Best A dogs (add logging)
// After logDogInfo
// ======
// CompanyZ::cat(): Best Z cats
// ======
// Before logCatInfo
// CompanyZ::cat(): Best Z cats (add logging)
// After logCatInfo

44:将与参数无关的代码抽离

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



   转载规则


《Effective CPP》 M 采用 知识共享署名 4.0 国际许可协议 进行许可。
 上一篇
第零章:一个方法团灭 nSum 问题 第零章:一个方法团灭 nSum 问题
labuladong一个方法团灭 nSum 问题 1. 两数之和方法1:利用字典去重 利用字典保存遍历过程变量,若满足条件,则返回结果 class Solution(object): def twoSum(self, nums,
2020-07-16
下一篇 
第四章:如何判断回文链表 第四章:如何判断回文链表
labuladong如何判断回文链表 234. 回文链表方法1:链表转列表,双指针逼近将链表转化为列表,然后利用左右双指针技巧,从两端到中间逼近 # Definition for singly-linked list. # class Li
2020-07-13
  目录