继承
继承(inheritance)指的是面向对象程序设计的一种方法,允许一个类沿用另一个类的成员,并在此基础上添加新成员或修改成员函数的实现。
被继承的类称为基类(base class),继承得到的类称为派生类(derived class)
继承的基本操作
虚函数
在基类中,希望指定一个共同的接口,而在派生类中指定这个函数的具体实现,需要定义为虚函数(virtual function)。虚函数使用 virtual 关键字注明。
例如,定义 Quote 类的函数 net_price 为虚函数:
class Quote {
public:
virtual double net_price(std::size_t) const;
};
使用 =0 可以制定纯虚函数:
struct Shape {
virtual void draw() = 0;
}
继承
定义一个派生类为某一个类的继承时,需要在类名后面加上冒号指明:
class BulkQuote: public Quote {
public:
double net_price(std::size_t) const;
}
private:
double discount = 0.0; // 派生类自己的成员变量
派生类继承了基类的所有成员函数和成员变量,但也允许定义自己的成员函数和成员变量。
struct继承时,默认是public继承class继承时,默认是private继承
转换
派生类包含了对应其基类的组成部分,所以也可以把派生类的对象当作基类对象来使用。
隐式类型转换:
Quote item;
BulkQuote bulk_item; // BulkQuote 是 Quote 的继承类
Quote *p = &item;
p = &bulk; // p 可以指向 bulk 的 Quote 部分。
只有 public 继承才允许这种转换。
单继承的转换
单继承时,主流的编译器会把基类的成员放在低地址,继承类的放在高地址。
例如:
class A {
int x;
};
class B: public A {
int y;
};
虚函数表与虚表指针
这部分不属于 C++ 的标准,但是主流编译器通常这样实现。
虚函数表储存了一个类下所有定义的虚函数对应的实现的指针(函数指针),“表”主要体现在一个类会有不止一个虚函数,而虚指针则定义在具体对象中,指向了这个对象的虚函数表。
例
对于一个非常简单的类来说:
class Animal {
virtual void say() {
std::cout << "I am an animal." << std::endl;
}
virtual void eat() {
std::cout << "Eating" << std::endl;
}
};
class Cat: public Animal {
void say() override {
std::cout << "Meow~" << std::endl;
}
};
int main() {
Cat cat();
Animal animal = &cat;
animal.say(); // Meow~
animal.eat(); // Eating
return 0;
}
可以认为,Animal 在类里面定义了一个虚函数表,可以暂时理解为函数指针的数组,大致的结构是:
| 下标 | 指针 |
|---|---|
| 0 | &Animal::say |
| 1 | &Animal::eat |
这里的编号是编译时固定下来的,编译器就能确定所有的函数指针。
而 Cat 继承了 Animal,又覆写了函数 say,虚函数表成为:
| 下标 | 指针 |
|---|---|
| 0 | &Cat::say |
| 1 | &Animal::Cat |
此时,Animal 及其继承类,会维护一个虚表指针(vptr),指向 vtable。在上述的代码中,Animal animal = &cat 定义出来的 animal 对象,就包含了一个指向 Cat::vtable 的 vptr,因此它调用 say 时,仍然使用的是 Cat 的 say 方法。
简单说,使用虚函数表和虚表指针,是这样实现多态的:
- 访问这个虚表的指针指向的对象
- 获得当前函数的编号,利用虚表头指针+编号,得到当前真正实现的函数指针
- 执行函数指针指向的函数
一些疑问和解答
子类如果定义了新的虚函数,转换成基类时,基类的虚表大小其实和子类不一致,会有问题吗?
答案是没有问题,因为新增的虚函数会放在虚表的末端。基类拿到虚函数表的指针后,只需把它当作一个长度更小的数组,基类不知道、也不需要知道后面有没有更多的函数指针。
读完了,有什么想法?
阅读已保存的问答示例,并循着引用回到原文。