继承类和组合类的区别:

  • 组合类:是整体和部分的关系,比如说汽车和轮子
  • 继承类:是种属问题,比如说交通工具和汽车
  • 一般来说继承意味着要修改父类,而组合是没有修改能力的。

如果一个问题既能用组合解决也能用继承解决,首选组合,因为语法更加简单一些。

本章内容:

  • 继承与派生的基本概念
  • 单继承与多继承
  • 类成员的访问控制
  • 派生类对象的构造和析构
  • 派生类与基类对象的类型转换
  • 类成员的标识与访问
  • 虚继承

一、基本语法和概念

父类 Base class 子类 Derived class

  • 继承与派生是同一过程从不同的角度看
    • 保持已有类的特性而构造新类的过程称为继承
    • 在已有类的基础上新增自己的特性而产生新类的过程称为派生
  • 被继承的已有类称为基类(或父类)
  • 派生出的新类称为派生类(或子类)
  • 直接参与派生出某类的基类称为直接基类
  • 基类的基类甚至更高层的基类称为间接基类

继承与派生的目的

  • 继承的目的:实现设计与代码的重用。
  • 派生的目的:当新的问题出现,原有程序无法解决(或不能完全解决)时,需要对原有程序进行改造。

单继承时派生类的定义

  • 语法
  1. class 派生类名:继承方式 基类名
  2. {成员声明;}
  1. class Derived: public Base
  2. {public:
  3. Derived ();
  4. ~Derived ();};

多继承时派生类的定义

  • 语法
  1. class 派生类名:继承方式1 基类名1,继承方式2 基类名2,...
  2. {成员声明;}

注意:每一个“继承方式”,只用于限制对紧随其后之基类的继承。

  1. class Derived: public Base1, private Base2
  2. {public:
  3. Derived ();
  4. ~Derived ();};

子类的构成

  1. 吸收基类成员
    默认情况下子类包含父类的全部成员除了构造函数和析构函数
    C++11规定可用using语句继承基类构造函数
  2. 改造基类成员
    子类中与基类同名成员会覆盖基类成员,使其不可见
  3. 添加新的成员

二、继承方式

类的继承与派生 - 图1

不同继承方式的影响主要体现在:

  • 派生类成员对基类成员的访问权限
  • 通过派生类对象对基类成员的访问权限

三种继承方式

  • 公有继承 public
  • 私有继承 private
  • 保护继承 protected

公有继承(public)

可以理解为:protected是专门为子类提供的区分。

  • 继承的访问控制
    • 基类的public和protected成员:访问属性在派生类中保持不变;
    • 基类的private成员:不可直接访问。
  • 访问权限
    • 派生类中的成员函数:可以直接访问基类中的public和protected成员,但不能直接访问基类的private成员;
    • 通过派生类的对象:只能访问public成员。

私有继承(private)

  • 继承的访问控制
    • 基类的publicprotected成员:都以private身份出现在派生类中;
    • 基类的private成员:不可直接访问。
  • 访问权限
    • 派生类中的成员函数:可以直接访问基类中的public和protected成员,但不能直接访问基类的private成员;
    • 通过派生类的对象:不能直接访问从基类继承的任何成员。

保护继承(protected)

  • 继承的访问控制
    • 基类的publicprotected成员:都以protected身份出现在派生类中;
    • 基类的private成员:不可直接访问。
  • 访问权限
    • 派生类中的成员函数:可以直接访问基类中的public和protected成员,但不能直接访问基类的private成员;
    • 通过派生类的对象:不能直接访问从基类继承的任何成员。
  • protected 成员的特点与作用
    • 对建立其所在类对象的模块来说,它与 private 成员的性质相同。
    • 对于其派生类来说,它与 public 成员的性质相同。
    • 既实现了数据隐藏,又方便继承,实现代码重用。
    • 如果派生类有多个基类,也就是多继承时,可以用不同的方式继承每个基类。

三、子类与父类的类型转化

公有派生类对象可以被当作基类的对象使用,反之则不可。

  • 派生类的对象可以隐含转换为基类对象;
  • 派生类的对象可以初始化基类的引用;
  • 派生类的指针可以隐含转换为基类的指针。
  • 通过基类对象名、指针只能使用从基类继承的成员。

观察下面例子,思考虚函数的使用

  1. #include <iostream>
  2. using namespace std;
  3. class Base1 { //基类Base1定义
  4. public:
  5. void display() const {
  6. cout << "Base1::display()" << endl;
  7. }
  8. };
  9. class Base2: public Base1 { //公有派生类Base2定义
  10. public:
  11. void display() const {
  12. cout << "Base2::display()" << endl;
  13. }
  14. };
  15. class Derived: public Base2 { //公有派生类Derived定义
  16. public:
  17. void display() const {
  18. cout << "Derived::display()" << endl;
  19. }
  20. };
  1. void fun(Base1 *ptr) { //参数为指向基类对象的指针
  2. ptr->display(); //"对象指针->成员名"
  3. }
  4. int main() { //主函数
  5. Base1 base1; //声明Base1类对象
  6. Base2 base2; //声明Base2类对象
  7. Derived derived; //声明Derived类对象
  8. fun(&base1); //用Base1对象的指针调用fun函数
  9. fun(&base2); //用Base2对象的指针调用fun函数
  10. fun(&derived); //用Derived对象的指针调用fun函数
  11. return 0;
  12. }
  13. //输出结果发现都是调用的Base1的display //子类退化了

四、子类的构造和析构

  • 默认情况
    • 基类的构造函数不被继承;
    • 派生类需要定义自己的构造函数。
  • C++11规定
    • 可用using语句继承基类构造函数。
      • 只能初始化从基类继承的成员。
      • 子类新增成员可以通过类内初始值进行初始化。
    • 语法形式:
      • using B::B;

建议

如果派生类有自己新增的成员,且需要通过构造函数初始化,则派生类要自定义构造函数

自定义构造函数

  • 派生类新增成员:派生类定义构造函数初始化;
  • 继承来的成员:自动调用基类构造函数进行初始化;
  • 派生类的构造函数需要给基类的构造函数传递参数

4.1 单继承

派生类只有一个直接基类的情况,是单继承。单继承时,派生类的构造函数只需要给一个直接基类构造函数传递参数。

单继承时构造函数的定义语法

  1. 派生类名::派生类名(基类所需的形参,本类成员所需的形参):
  2. 基类名(参数表), 本类成员初始化列表
  3. {
  4. //其他初始化;
  5. };

单继承时的构造函数举例(补7-3)

  1. #include<iostream>
  2. using namespace std;
  3. class B {
  4. public:
  5. B();
  6. B(int i);
  7. ~B();
  8. void print() const;
  9. private:
  10. int b;
  11. };
  12. B::B() {
  13. b=0;
  14. cout << "B's default constructor called." << endl;
  15. }
  16. B::B(int i) {
  17. b=i;
  18. cout << "B's constructor called." << endl;
  19. }
  20. B::~B() {
  21. cout << "B's destructor called." << endl;
  22. }
  23. void B::print() const {
  24. cout << b << endl;
  25. }
  26. class C: public B {
  27. public:
  28. C();
  29. C(int i, int j);
  30. ~C();
  31. void print() const;
  32. private:
  33. int c;
  34. };
  35. C::C() {
  36. c = 0;
  37. cout << "C's default constructor called." << endl;
  38. }
  39. C::C(int i,int j): B(i), c(j){
  40. cout << "C's constructor called." << endl;
  41. }
  42. C::~C() {
  43. cout << "C's destructor called." << endl;
  44. }
  45. void C::print() const {
  46. B::print();
  47. cout << c << endl;
  48. }
  49. int main() {
  50. C obj(5, 6);
  51. obj.print();
  52. return 0;
  53. }

4.2多继承

多继承时,有多个直接基类,如果不继承基类的构造函数,派生类构造函数需要给所有基类构造函数传递参数。我们来看一下语法规定

多继承时构造函数的定义语法

  1. 派生类名::派生类名(参数表) :
  2. 基类名1(基类1初始化参数表),
  3. 基类名2(基类2初始化参数表),
  4. ...
  5. 基类名n(基类n初始化参数表),
  6. 本类成员初始化列表
  7. {
  8. //其他初始化;
  9. };

派生类与基类的构造函数

  • 当基类有默认构造函数时
    • 派生类构造函数可以不向基类构造函数传递参数。
    • 构造派生类的对象时,基类的默认构造函数将被调用。
  • 如需执行基类中带参数的构造函数
    • 派生类构造函数应为基类构造函数提供参数。

多继承且有对象成员时派生的构造函数定义语法

  1. 派生类名::派生类名(形参表):
  2. 基类名1(参数), 基类名2(参数), ..., 基类名n(参数),
  3. 本类成员(含对象成员)初始化列表
  4. {
  5. //其他初始化
  6. };

4.3 构造函数与析构函数的执行顺序

  1. 调用基类构造函数。
    • 顺序按照它们被继承时声明的顺序(从左向右)。
  2. 对初始化列表中的成员进行初始化。
    • 顺序按照它们在类中定义的顺序
    • 对象成员初始化时自动调用其所属类的构造函数。由初始化列表提供参数。
  3. 执行派生类的构造函数体中的内容。
  4. 析构函数刚好相反

类的继承与派生 - 图2 类的继承与派生 - 图3

4.4派生类复制构造函数

派生类未定义复制构造函数的情况

  • 编译器会在需要时生成一个隐含的复制构造函数;
  • 先调用基类的复制构造函数;
  • 再为派生类新增的成员执行复制。

派生类定义了复制构造函数的情况

  • 一般都要为基类的复制构造函数传递参数。
  • 复制构造函数只能接受一个参数,既用来初始化派生类定义的成员,也将被传递给基类的复制构造函数。
  • 基类的复制构造函数形参类型是基类对象的引用,实参可以是派生类对象的引用
  • 例如: C::C(const C &c1): B(c1) {…}

4.5派生类的析构函数

  • 析构函数不被继承,派生类如果需要,要自行声明析构函数。
  • 声明方法与无继承关系时类的析构函数相同。
  • 不需要显式地调用基类的析构函数,系统会自动隐式调用。
  • 先执行派生类析构函数的函数体,再调用基类的析构函数。

4.6示例

  1. #include <iostream>
  2. using namespace std;
  3. class Base1 {
  4. public:
  5. Base1(int i)
  6. { cout << "Constructing Base1 " << i << endl; }
  7. ~Base1() { cout << "Destructing Base1" << endl; }
  8. };
  9. class Base2 {
  10. public:
  11. Base2(int j)
  12. { cout << "Constructing Base2 " << j << endl; }
  13. ~Base2() { cout << "Destructing Base2" << endl; }
  14. };
  15. class Base3 {
  16. public:
  17. Base3() { cout << "Constructing Base3 *" << endl; }
  18. ~Base3() { cout << "Destructing Base3" << endl; }
  19. };
  20. class Derived: public Base2, public Base1, public Base3 {
  21. public:
  22. Derived(int a, int b, int c, int d): Base1(a), member2(d), member1(c), Base2(b)
  23. { }
  24. private:
  25. Base1 member1;
  26. Base2 member2;
  27. Base3 member3;
  28. };
  29. int main() {
  30. Derived obj(1, 2, 3, 4);
  31. return 0;
  32. }

五、子类成员的标识与访问

5.1 访问从基类继承的成员

作用域限定

当派生类与基类中有相同成员时:若未特别限定,则通过派生类对象使用的是派生类中的同名成员

如要通过派生类对象访问基类中被隐藏的同名成员,应使用基类名和作用域操作符(::)来限定

5.2 二义性问题

如果从不同基类继承了同名成员,但是在派生类中没有定义同名成员,“派生类对象名或引用名.成员名”、“派生类指针->成员名”访问成员存在二义性问题

解决方式:用类名限定,或者在派生类中定义同名成员选择用哪个基类的函数。

多继承时的二义性和冗余问题

  1. #include <iostream>
  2. using namespace std;
  3. class Base0 { //定义基类Base0
  4. public:
  5. int var0;
  6. void fun0() { cout << "Member of Base0" << endl; }
  7. };
  8. class Base1: public Base0 { //定义派生类Base1
  9. public: //新增外部接口
  10. int var1;
  11. };
  12. class Base2: public Base0 { //定义派生类Base2
  13. public: //新增外部接口
  14. int var2;
  15. };
  16. // 例7-7 多继承时的二义性和冗余问题
  17. class Derived: public Base1, public Base2 {
  18. public:
  19. int var;
  20. void fun()
  21. { cout << "Member of Derived" << endl; }
  22. };
  23. int main() { //程序主函数
  24. Derived d;
  25. d.Base1::var0 = 2;
  26. d.Base1::fun0();
  27. d.Base2::var0 = 3;
  28. d.Base2::fun0();
  29. return 0;
  30. }

1235390283852681216.png

这里就构成了菱形继承
解决方法:虚基类

5.3 虚基类

把公共基类当作虚基类。当派生类从多个基类派生,而这些基类有共同基类,则在访问此共同基类中的成员时,将产生冗余,并有可能因冗余带来不一致性。

  • 虚基类声明
    • 以virtual说明基类继承方式
    • 例:class B1 : virtual public B
  • 作用
    • 主要用来解决多继承时可能发生的对同一基类继承多次而产生的二义性问题
    • 为最远的派生类提供唯一的基类成员,而不重复产生多次复制

类的继承与派生 - 图5注意:在第一级继承时就要将共同基类设计为虚基类。

例7-8 虚基类举例

1235393518416039936.png

  1. #include <iostream>
  2. using namespace std;
  3. class Base0 {
  4. public:
  5. int var0;
  6. void fun0() { cout << "Member of Base0" << endl; }
  7. };
  8. class Base1: virtual public Base0 {
  9. public:
  10. int var1;
  11. };
  12. class Base2: virtual public Base0 {
  13. public:
  14. int var2;
  15. };
  16. class Derived: public Base1, public Base2 {
  17. //定义派生类Derived
  18. public:
  19. int var;
  20. void fun() {
  21. cout << "Member of Derived" << endl;
  22. }
  23. };
  24. int main() {
  25. Derived d;
  26. d.var0 = 2; //直接访问虚基类的数据成员
  27. d.fun0(); //直接访问虚基类的函数成员
  28. return 0;
  29. }

虚基类及其派生类构造函数

  • 建立对象时所指定的类称为最远派生类
  • 虚基类的成员是由最远派生类的构造函数通过调用虚基类的构造函数进行初始化的。
  • 在整个继承结构中,直接或间接继承虚基类的所有派生类,都必须在构造函数的成员初始化表中为虚基类的构造函数列出参数。如果未列出,则表示调用该虚基类的默认构造函数。(用不着用得着,都得为它赋值)
  • 在建立对象时,只有最远派生类的构造函数调用虚基类的构造函数,其他类对虚基类构造函数的调用被忽略

有虚基类时的构造函数举例

  1. #include <iostream>
  2. using namespace std;
  3. class Base0 {
  4. public:
  5. Base0(int var) : var0(var) { }
  6. int var0;
  7. void fun0() { cout << "Member of Base0" << endl; }
  8. };
  9. class Base1: virtual public Base0 {
  10. public:
  11. Base1(int var) : Base0(var) { }
  12. int var1;
  13. };
  14. class Base2: virtual public Base0 {
  15. public:
  16. Base2(int var) : Base0(var) { }
  17. int var2;
  18. };
  19. class Derived: public Base1, public Base2 {
  20. public:
  21. Derived(int var) : Base0(var), Base1(var), Base2(var)
  22. { }
  23. int var;
  24. void fun()
  25. { cout << "Member of Derived" << endl; }
  26. };
  27. int main() { //程序主函数
  28. Derived d(1);
  29. d.var0 = 2; //直接访问虚基类的数据成员
  30. d.fun0(); //直接访问虚基类的函数成员
  31. return 0;
  32. }