计算机

继承

继承(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 方法。

简单说,使用虚函数表和虚表指针,是这样实现多态的:

  1. 访问这个虚表的指针指向的对象
  2. 获得当前函数的编号,利用虚表头指针+编号,得到当前真正实现的函数指针
  3. 执行函数指针指向的函数

一些疑问和解答

子类如果定义了新的虚函数,转换成基类时,基类的虚表大小其实和子类不一致,会有问题吗?

答案是没有问题,因为新增的虚函数会放在虚表的末端。基类拿到虚函数表的指针后,只需把它当作一个长度更小的数组,基类不知道、也不需要知道后面有没有更多的函数指针。

BEYOND THE ARTICLE

读完了,有什么想法?

阅读已保存的问答示例,并循着引用回到原文。

查看公开问答示例
— 本文完 — 返回文章列表