继承作为面向对象的三大特色之一,可以极大的提高代码的复用性,但同时继承也有很多知识点值得整理。

C++ 的继承机制 链接到标题

首先,在面向对象编程过程中的继承代表的是“是”关系(IS-A),而并非“有”关系(HAS-A)。

其次,C++ 是支持多重继承的。

继承语法 链接到标题

class 派生类名: 继承访问控制 基类名 {
    派生类新增的定义
}

继承访问控制有public、protected、private(缺省时)三种。

派生类的构成 链接到标题

img1

基类继承来的数据所占空间一定是在派生类新增数据所占空间的上方的,这是为了方便实现多态。

任何的静态数据都是单独储存的,不会储存在派生类或是基类对象的空间中。

成员控制访问 类自身 派生类 其他类/程序代码
public 可访问 可访问 可访问
protected 可访问 可访问 不可访问
private 可访问 不可访问 不可访问
基类成员访问控制 继承访问控制 在派生类中的访问控制
public public public
protected protected
private 不可访问
public protected protected
protected protected
private 不可访问
public private private
protected private
private 不可访问

访问控制规则 链接到标题

  1. 基类成员函数访问基类成员:都可访问
  2. 派生类成员函数访问基类成员:private 成员不可访问
  3. 在基类/派生类外访问基类成员:public 成员可访问
  4. 基类成员函数访问派生类成员:都不可访问
  5. 派生类成员函数访问新增成员:都可访问
  6. 派生类外访问派生类成员:依照访问控制访问,public 可访问

私有继承会导致孙类无法访问祖父类的任何成员

常用共用继承

派生类的构造函数 链接到标题

派生类的构造顺序:

  1. 调用基类构造函数(存在多个基类则依照基类的出现顺序调用)
  2. 调用成员对象类构造函数
  3. 调用派生类构造函数

多重继承的基类构造函数是按声明时基类出现先后顺序调用的。

派生类的析构顺序:

  1. 调用派生类析构函数
  2. 调用成员对象类析构函数
  3. 调用基类析构函数(存在多个基类则依照基类的出现逆序调用)

多重继承的基类析构函数是按声明时基类出现先后顺序逆向调用的。

语法 链接到标题

显式调用基类构造函数(有子对象):

派生类构造函数名(总参数列表): 基类构造函数名(参数列表), 子对象名(参数列表) {
    派生类中新增数据成员初始化语句
}

隐式调用基类构造函数(有子对象):

派生类构造函数名(总参数列表): 子对象名(参数列表) {
    派生类中新增数据成员初始化语句
}

此时会调用基类default/无参/参数带默认值的构造函数。

无子对象就只需删除子对象那部分即可。

多重继承的情况下在派生类内调用基类函数 链接到标题

因为存在同名覆盖,所以直接调用一个基类与派生类同名的函数时只会调用派生类的。

如果存在多个基类,且它们存在同名成员,则直接访问这样的成员会造成两义性错误。

因此,需要在调用基类的函数前加 基类名::

虚基类 链接到标题

img2

如果图中所有继承都是普通继承的话,例如:

class A {
public:
    int b;
};
class B: public A {};
class C: public A {};
class D: public A {};
class E: public B, public C, public D {
public:
    int fun() {
        std::cout << "fun";
    }
};
E e;

此时直接通过 e.b 来访问 b 成员,或是通过 e.A::b 来访问 b 成员都是存在二义性的,而且前文用作用域来区别也不可以,此时就需要引入虚继承了。

语法 链接到标题

class B: virtual public A {};

我们利用这个技术将上面的错误例子稍作修改:

class A {
public:
    int b;
};
class B: virtual public A {};
class C: virtual public A {};
class D: virtual public A {};
class E: public B, public C, public D {
public:
    int fun() {
        std::cout << "fun";
    }
};
E e;

然后我们发现直接通过 e.b 来访问 b 成员,或是通过 e.A::b 来访问 b 成员都没问题了。

访问声明 链接到标题

如果继承的权限控制并不能满足派生的需求,例如 public 继承时想仅仅留下一部分的 public 的基类成员的外部访问权限,或是 privateprotected 继承时向开放一些基类成员给外部访问,则需要使用访问声明来控制访问权限。

语法 链接到标题

基类名::成员名

例如:

class Base {
public:
    void setID(int);
private:
    int ID;
}

class Derived: private Base {
public:
    Base::setID;
}

此时 Dervied 的对象就可以在类外调用 setID 函数了。

但基类的私有成员不能通过访问声明来修改派生类的访问权限。

对于重载函数的访问声明将对所有的同名重载函数有效,所以需要慎重考虑。

重写基类成员函数 链接到标题

如果基类已有的成员函数的功能不能满足派生类的需求,可以在派生类中重写该函数来覆盖基类的函数以实现需求。

要求 链接到标题

  1. 派生类的新函数与被覆盖的函数必须有同样的 返回值类型(或是协变)函数名参数列表(包括参数类型、个数和顺序)
  2. 被覆盖的函数要被 virtual 修饰,不能被 static 修饰。
  3. 派生类的新函数不能被 static 修饰。
  4. 两个函数可以有不同的权限修饰符。

协变:子类的对象可以称为父类对象的协变。反之成为逆变。

抽象函数 链接到标题

如果基类的某一函数必须存在,但继承的子类也必须重写来满足需求的,可以在基类中将其声明为抽象函数(纯虚函数)。

class Base {
public:
    virtual void fun() = 0;
}

此时的基类便成为了抽象类,它将无法被实例为一个对象,只能被其他类继承,且继承后必须重写它的抽象函数。

重定义 链接到标题

如果基类的函数与派生类的函数名相同,派生类的函数会像上文说的那样覆盖基类的函数,但此时仍可以通过域限定符来调用基类被覆盖的函数。