主页
文章
分类
系列
标签
简历
【C++ Primer(edition 5) 14】继承
发布于: 2021-10-22   更新于: 2021-10-22   收录于: Cpp
文章字数: 596   阅读时间: 3 分钟   阅读量:

一、概述

  • 面向对象程序设计(object-oriented programming)的核心思想是数据抽象继承多态

  • 继承(inheritance):

    • 通过继承联系在一起的类构成一种层次关系。
    • 通常在层次关系的根部有一个基类(base class)。
    • 其他类直接或者简介从基类继承而来,这些继承得到的类成为派生类(derived class)。
    • 基类负责定义在层次关系中所有类共同拥有的成员,而每个派生类定义各自特有的成员。
    • 对于某些函数,基类希望它的派生类自定义适合自己的版本,此时基类就将这些函数声明成虚函数(virtual function)。
    • 派生类必须通过使用类派生列表(class derivation list)明确指出它是从哪个基类继承而来。形式:一个冒号,后面紧跟以逗号分隔的基类列表,每个基类前都可以有访问说明符。class Bulk_quote : public Quote{};
    • 派生类必须在其内部对所有重新定义的虚函数进行声明。可以在函数之前加上virtual关键字,也可以不加。C++11新标准允许派生类显式地注明它将使用哪个成员函数改写基类的虚函数,即在函数的形参列表之后加一个override关键字。
    • 继承是一种强大的属性重用方式,是通向多态的跳板
  • 动态绑定(dynamic binding,又称运行时绑定):

    • 使用同一段代码可以分别处理基类和派生类的对象。
    • 函数的运行版本由实参决定,即在运行时选择函数的版本。

二、定义继承

1
2
3
4
5
6
7
class Base{
 ......
};

class Derived:access-specifier Base{
 .......
};
  • 派生类必须通过类派生列表(class derivation list)明确指出它是从哪个基类继承而来。形式:冒号,后面紧跟以逗号分隔的基类列表,每个基类前面可以有一下三种访问说明符的一个:publicprotectedprivate

  • 派生类中的虚函数: C++11新标准允许派生类显式地注明它将使用哪个成员函数改写基类的虚函数,即在函数的形参列表之后加一个override关键字。

  • 静态成员:如果基类定义了一个基类成员,则在整个继承体系中只存在该成员的唯一定义。

  • 派生类的声明:声明中不包含它的派生列表。

  • 如果使用某个类作为基类, 则该类必须定义而非申明

  • 派生类使用基类的成员:派生类可以访问基类的共有成员和受保护成员

  • C++11新标准提供一种防止继承的方法,在类名后面跟一个关键字final

三、基类和派生类之间细节

1、类型转换与继承

1.1 基于指针和引用对象的类型转换

理解基类和派生类之间的类型抓换是理解C++语言面向对象编程的关键所在。 派生类对象及派生类向基类的类型转换:因为在派生类对象中含有与其基类对应的组成部分,所以能把派生类的对象当成基类对象来使用,而且也能将基类的指针或引用绑定到派生类对象中的基类部分上。

1
2
3
4
5
Quote item;            //基类
Bulk_quote bulk;       //派生类
Quote *p = &item;      
p = &bulk;
Quote &r = bulk;

可以将基类的指针或引用绑定到派生类对象上有一层极为至要的含义:当使用基类的引用(或指针)时,实际上我们并不清楚该引用(或指针)所绑定对象的真实类型。该对象可能是基类的对象,也可能是派生类的对象。

1.2 强制类型转换带来的sliced down问题

对象之间的转换会导致某些对象被切掉(sliced down)。所谓的切掉,就是当派生类和基类之间通过拷贝构造,赋值构造等进行转化时,有一些成员会因为数据成员的不同导致成员丢失,有点像被切掉了。

2、派生类的实例化

2.1 合成构造函数、赋值函数、析构函数、拷贝控制函数

  • 基类或派生类的合成拷贝控制成员的行为和其他合成的构造函数、赋值运算符或析构函数类似:他们对类本身的成员依次进行初始化、赋值或销毁的操作。此外,这些合成的成员还负责使用直接基类中对应的操作对一个对象的直接基类部分进行初始化、赋值或销毁的操作。合成析构函数也有同样的规则。无论基类成员是合成的版本还是自定义的版本都没有影响,唯一的要求就是相应的成员可以访问并且未被删除。[[继承#五、访问控制与继承|访问控制与继承]]
  • 如果就是不能访问或者被删除
    • 如果基类的默认拷贝构造、构造、拷贝赋值或析构函数是被删除的,则对应的派生类成员也是被删除的。
    • 如果基类有一个不可访问或者删除掉的析构函数,则派生类的默认和拷贝构造函数将被删除
    • 因为基类缺少移动操作会阻止派生类拥有自己的合成移动操作,所以当我们需要执行移动操作时首先应该在基类中进行定义
  • 派生类析构函数:派生类析构函数先执行,然后执行基类的析构函数。析构函数只负责销毁派生类自己分配的资源。

2.2 初始化之-构造函数

  • 派生类构造函数:派生类必须使用基类的构造函数去初始化它的基类部分。
  • C++11新标准中,派生类可以重用其直接基类定义的构造函数。
  • 如果在初始化派生类对象,可以通过初始化列表给基类传递参数,从而调用基类的重载构造函数
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
class Base
public:
	Base(int SomeNumber) // overloaded constructor
	{
	// Do something with SomeNumber
	}
}
Class Derived: public Base
{
public:
	Derived(): Base(25) // instantiate class Base with argument 25
	 {
	 	// derived class constructor code
	 }
};
  • 如果基类的构造函数含有默认实参,这些实参并不会被继承,相反会获得多个继承的构造函数,每个构造函数分别省略掉一个含有默认实参的形参。

2.3 初始化之-拷贝构造函数

当派生类定义了拷贝或移动操作时,该操作负责拷贝或移动包括基类部分成员在内的整个对象。通常使用对应的基类构造函数初始化对象的基类部分,不同于构造函数默认使用基类的默认构造函数,当使用拷贝控制成员和移动控制成员时,需要显式的调用基类的相应成员函数

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
class Base ( /* ... */ };
class D: public Base (
public:
//默认情况下,基类的默认构造函数初始化对象的基类部分
//要想使用拷贝或移动构造函数,我们必须在构造函数初始依列表中
//显式地调用该构造函数
D(const D& d):Base(d)	// 拷贝基类成员
/* D的成员的初始依*/ { /* ... */ }
D(D&& d):Base (std: :move (d))	// 移动基类成员
/* D的成员的初始值*/ { /* ... */ )
)

2.4 初始化之-赋值函数

  • 赋值运算符也是相似的情况
1
2
3
4
5
D &D::operator=(const D &rhs) {
Base::operator= (rhs); // 为基类部分赋值
//按照过去的方式为派生类的成员赋值
//的情处理自赋值及释放已有资源等情况 
return *this;

先调用基类的赋值运算符,然后再完成派生类的赋值操作,无论基类的赋值运算符是由编译器合成的还是自定义的都无关紧要

2.5 初始之-拷贝赋值运算符

2.6 初始化之-析构函数

  • 基类通常应该定义一个虚析构函数,这样我们就能动态分配继承体系中的对象了。通常都应该定义一个虚析构函数,即使该函数不执行任何实际操作也是如此。
  • 如果基类的析构函数不是虚函数,则delete一个指向派生类对象的基类指针将产生未定义的行为。
  • 虚析构函数将阻止合成移动操作。

2.7 构造与析构顺序

构造顺序,先基类后派生 析构顺序,先派生后基类

四、继承中的类作用域

1、如何查找类中的名字

  • 每个类定义自己的作用域,在这个作用域内我们定义类的成员。当存在继承关系时,派生类的作用域嵌套在其基类的作用域之内。如果一个名字在派生类的作用域内无法正确解析,则编译器将继续在外层的基类作用域中寻找该名字的定义。
  • 在编译时进行名字查找 :
    • 一个对象引用或指针的静态类型决定了该对象的哪些成员是可见的,即使与动态类型可能不一致,但是我们能够使用哪些成员仍然是由静态类型决定的。
  • 派生类的成员将隐藏同名的基类成员。也可以通过作用域运算符来使用隐藏的成员。

2、覆盖基类中的成员

  • 声明在内层作用域的函数,并不会存在申明在外层作用域的函数, 因此定义派生类中的函数不会重载基类中的成员,会隐藏基类中的成员。除了覆盖继承而来的虚函数之外,派生类最好不要重用其他定义在基类中的名字。

3、使用基类中的成员

使用域解析运算符:: 来调用基类的方法也可以抑制[[继承#三、虚函数与动态绑定|动态绑定]]

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
class Carp:public Fish
{
public:
	Carp():Fish(true){}

	 void Swim()
	 {
		 cout<<"Carp swims real slow"<<endl;
		Fish::Swim();
	 }

}
  • 覆盖重载的函数 : 和其他函数一样,成员函数无论是否是虚函数都能被重载。派生类可以覆盖重载函数的0个或多个实例。有时一个类仅需要覆盖重载集合中的一些而非全部函数一种好的解决方案,就是为重载的成员提供一条using声明语句,这样我们就可以无需覆盖基类中的每一个重载版本。
  • using Disc_quote::Disc_quote;,注明了要继承Disc_quote的构造函数。如果派生类有自己的数据成员,则这些成员将被默认初始化。
  • using申明不会改变构造函数的访问级别
  • 大多数情况下使用using后,派生类会继承所有基类的构造函数,但是有两种例外,一是派生类可以继承一部分构造函数,而定义自己的版本,如果派生类定义的构造函数与基类的构造函数具有相同的参数列表,则该构造函数将不会被继承。二是默认拷贝和移动构造函数不会被继承。

五 、访问控制与继承

1、对于公有继承(public)方式

基类的public和protected成员的访问属性在派生类中保持不变,但基类的private成员不可直接派生类中访问(可通过调用基类中访问属性为公有或保护的成员函数来访问基类中的私有成员)。即派生类中的成员函数可以直接访问基类中的public和protected成员,但不能直接访问基类的private成员。 基类成员对基类对象的可见性为:公有成员可见(或者说可访问),保护成员和私有成员不可见(或者说不可访问)。 基类成员对派生类的可见性为:基类的公有成员和保护成员可见,基类的私有成员不可见。 基类成员对派生类对象的可见性为:基类的公有成员可见,保护成员和私有成员不可见,即通过派生类的对象只能访问基类的public成员。 所以,在公有继承时,派生类的对象可以直接访问基类中的公有成员,派生类的成员函数可以直接访问基类中的公有成员和保护成员。

2、对于私有继承(private)方式

基类的public和protected成员都以private身份出现在派生类中,但基类的private成员同样是不可直接访问的。 基类成员对基类对象的可见性为:公有成员可见,保护成员和私有成员不可见。 基类成员对派生类的可见性为:基类的公有成员和保护成员可见,基类的私有成员不可见。(经过私有继承之后,所有基类的成员都成为了派生类的私有成员或不可直接访问的成员,如果用此派生类进一步向下派生子类的话,基类的全部成员就无法在这个派生类的子类中被直接访问) 基类成员对派生类对象的可见性为:基类的公有成员、保护成员和私有成员均是不可见的,即通过通过派生类的对象不能直接访问基类中的任何成员。 所以,在私有继承时,派生类的对象无法直接访问基类中的任何成员,派生类的成员函数则依然可以直接访问基类中的公有成员和保护成员。另外,基类的公有和保护成员只能由直接派生类继承,而无法再向下继承。

3、对于保护继承(protected)方式

基类的public和protected成员都以protected身份出现在派生类中,基类的private成员也同样是不可直接访问的。 基类成员对基类对象的可见性为:公有成员可见,保护成员和私有成员不可见。 基类成员对派生类的可见性为:基类的公有成员和保护成员可见,基类的私有成员不可见。(比较私有继承和保护继承可以看出,实际上在直接派生类中,所有成员的访问属性都是完全相同的。但是,如果派生类作为新的基类继续派生时,二者的区别就出现了。) 基类成员对派生类对象的可见性为:基类的公有成员、保护成员和私有成员均是不可见的,即通过通过派生类的对象不能直接访问基类中的任何成员。 因此,保护继承既与私有继承有相似的地方也有与公有继承相似的地方。对派生类的对象来说,它与私有继承方式的性质相同。而对于其派生类来说,它又与公有继承方式的性质相同。这样做既实现了数据隐藏,又方便继承,实现代码重用。 ![[Pasted image 20230119014703.png]]

1
2
3
4
5
6
7
8
class Parent{
public:
	...
private:
	...
protected:
	...
};

2. 派生类的继承方式说明符

class Child : public Parent {};
class Child : protected Parent {};
class Child : private Parent {};

public 继承方式:

  • 基类中的 public 成员在派生类中仍为 public
  • 基类中的 protected 成员在派生类中仍为 protected
  • 基类中的 private 成员在派生类中被继承下来,但是不可访问

protected 继承方式:

  • 基类中的 public 成员在派生类中变为 protected 属性;
  • 基类中的 protected 成员在派生类中变为 protected 属性;
  • 基类中的 private 成员在派生类中被继承下来,但是仍不可访问

private 继承方式:

  • 基类中的 public 成员在派生类中变为 private 属性;
  • 基类中的 protected 成员在派生类中变为 private 属性;
  • 基类中的 private 成员在派生类中被继承下来,但是仍不可访问

可以看出,三种继承方式不会影响派生类成员对基类成员的访问权限,无论哪种继承方式,派生类中仍然只能访问基类中的 public 和 protected 成员,不能访问 private 成员;

  • 基类的 private 成员不能在派生类中使用,并没有说基类的 private 成员不能被继承。实际上,基类的 private 成员是能够被继承的,并且(成员变量)会占用派生类对象的内存,它只是在派生类中不可见,导致无法使用罢了。private 成员的这种特性,能够很好的对派生类隐藏基类的实现,以体现面向对象的封装性。

继承方式影响的是 类实例对象对类成员的访问权限:

  • 例如,如果派生类的继承方式是 private,则基类对象可以访问的 public 属性的类成员,派生类对象便不能访问了,因为派生类中该对象变成了 private 属性;
  • 由于 private 和 protected 继承方式会改变基类成员在派生类中的访问权限,导致继承关系复杂,所以实际开发中我们一般使用 public。

总结:继承方式决定了基类成员在派生类中的可见性,但不影响派生类对基类成员的访问权限

3. 改变访问权限

  • 使用 using 关键字可以改变基类成员在派生类中的访问权限,例如将 public 改为 private、将 protected 改为 public。

  • 注意:using 只能改变基类中 public 和 protected 成员的访问权限,不能改变 private 成员的访问权限,因为基类中 private 成员在派生类中是不可见的,根本不能使用,所以基类中的 private 成员在派生类中无论如何都不能访问。

 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
32
33
34
35
36
37
38
39
40
#include<iostream>
using namespace std;

//基类People
class People {
public:
    void show();
protected:
    char *m_name;
    int m_age;
};
void People::show() {
    cout << m_name << "的年龄是" << m_age << endl;
}

//派生类Student
class Student : public People {
public:
    void learning();
public:
    using People::m_name;  //将protected改为public
    using People::m_age;  //将protected改为public
    float m_score;
private:
    using People::show;  //将public改为private
};
void Student::learning() {
    cout << "我是" << m_name << ",今年" << m_age << "岁,这次考了" << m_score << "分!" << endl;
}

int main() {
    Student stu;
    stu.m_name = "小明";
    stu.m_age = 16;
    stu.m_score = 99.5f;
    stu.show();  //compile error
    stu.learning();

    return 0;
}
  • Student 类中,show 函数被改为了 private 权限,则 Student 类的对象就不能调用 show 函数了。

六、多继承