Primer Plus 类继承

派生一个类

使用公有派生,基类的公有成员将成为派生类的公有成员,基类的私有部分也将成为派生类的一部分,但只能通过基类的公有和保护方法访问

1
2
3
class RatedPlayer : public TableTennisPlayer {

};

创建派生类时,基类先被创建,派生类过期时,先析构派生类,再析构基类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
#include <iostream>

class Base {
public:
Base() {
std::cout << "Base" << '\n';
}
~Base() {
std::cout << "~Base" << '\n';
}
};

class Derived : public Base {
public:
Derived() {
std::cout << "Derived" << '\n';
}
~Derived() {
std::cout << "~Derived" << '\n';
}
};

int main() {
Derived derived;
// Output:
// Base
// Derived
// ~Derived
// ~Base
return 0;
}

派生类的构造函数总是调用一个基类构造函数,可以使用初始化列表语法指明要使用的基类构造函数,否则将使用默认的基类构造函数

1
2
3
derived::derived(t1 x, t2 y) : base(x, y) {

}

基类指针可以指向派生类对象,基类引用也可以引用派生类对象,但只能用于调用基类方法

继承是is-a关系

多态公有继承

如果要在派生类中重新定义基类的方法,通常应将基类方法声明为虚的,这样,程序将根据对象类型选择方法版本,而不是根据指针或引用的类型

虚析构函数同样,根据对象类型选择调用对应的虚构函数,而不是根据指针或引用的类型,确保了析构函数序列的正确

静态联编和动态联编

函数名联编:将源代码中的函数调用解释为执行特定的函数代码块

静态(早期)联编:在编译过程中进行的联编

virtual关键字导致使用哪一个函数在编译时无法确定,所以编译器必须生成能够在程序运行时选择正确的虚方法的代码,这是动态(晚期)联编

编译器对非虚方法使用静态联编,对虚方法使用动态联编

为什么默认静态联编

效率更高,由C++的指导原则不要为不使用的特性付出代价,因此在需要虚函数时再考虑动态联编

虚函数的工作原理

有虚函数的类有一个虚函数表,比如基类有一个虚函数表,派生类有另一个虚函数表

每个对象都有一个隐藏成员,是一个指向虚函数表的指针

虚函数表里存了所有为类对象声明的虚函数的地址

如果派生类重新定义了基类虚函数,虚函数表里存的是新函数的地址

如果派生类没有重新定义基类虚函数,虚函数表里存的是函数原始版本的地址

如果派生类新定义了一个虚函数,虚函数表里会增加这个函数的地址

虚函数表存在只读数据段里

注意事项

构造函数不能是虚的

析构函数应该是虚函数,除非类不作为基类

友元不能是虚函数

派生类重新定义将隐藏基类方法

1
2
3
4
5
6
7
8
9
10
11
12
13
// 重新定义继承的方法不是重载,无论参数列表是否相同
class A {
public:
virtual void f(int x) const;
};
class B : public A {
public:
virtual void f() const;
}

B test;
test.f(); // 合法
test.f(5); // 不合法

如果重新定义继承的方法,且返回类型是基类引用或指针,则可以修改为指向派生类的引用或指针

1
2
3
4
5
6
7
8
9
// 返回类型协变
class A {
public:
virtual A & f(int x);
};
class B : public A {
public:
virtual B & f(int x);
}

访问控制:protected

protected和private的区别只在派生类中体现,派生类的成员可以直接访问基类的保护成员

抽象基类(ABC)

纯虚函数提供未实现的函数

1
virtual double Area() const = 0; // 纯虚函数的结尾处为 =0

当类声明中包含纯虚函数时,不能创建该类的对象

ABC描述的是至少使用一个纯虚函数的接口

可以把ABC看作是一种必须实施的接口