继承
继承(inheritance)机制是面向对象程序设计使代码可以复用的重要的手段,它允许程序员在保持原有类特性的基础上进行扩展,增加功能,这样产生新的类,称为派生类。
# 引入
例如:老师类和学生类有很多相似之处,如姓名、年龄、性别等;但是也有不同之处,例如老师要备课、讲课,学生要学习、考试等行为。这时我们可以定义出人这一类,包含姓名、性别、年龄、电话等信息,而老师类和学生类继承人类,进而有自己独有的行为。代码如下:
// 人类、学生类、老师类
class person {
private:
string name;
string gender;
int age;
public:
person(string _name,string _gender,int _age) : name(_name),gender(_gender),age(_age) {};
};
class student : public person{
private:
int stuid;
public:
// ...
};
class teacher : public person{
private:
int jobid;
public:
// ...
};
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
# 定义
class son :public father
语法: class 子类名 :继承方式 父类名
继承方式同访问限定符一样,有public
、private
、protected
三种
基类当中被不同访问限定符修饰的成员,以不同的继承方式继承到派生类当中后,该成员最终在派生类当中的访问方式将会发生变化:
类成员/继承方式 | public继承 | protected继承 | private继承 |
---|---|---|---|
基类的public成员 | 派生类的public成员 | 派生类的protected成员 | 派生类的private成员 |
基类的protected成员 | 派生类的protected成员 | 派生类的protected成员 | 派生类的private成员 |
基类的private成员 | 在派生类中不可见 | 在派生类中不可见 | 在派生类中不可见 |
三种访问限定符的权限大小为:public > protected > private,基类成员访问方式的变化规则如下:
- 在基类当中的访问方式为public或protected的成员,在派生类当中的访问方式变为:Min(成员在基类的访问方式,继承方式)。
- 在基类当中的访问方式为private的成员,在派生类当中都是不可见的
# 子类和父类的赋值转换
在程序编写中,我们会遇到以下情况:
Student s;
Person p = s; //派生类对象赋值给基类对象
Person* ptr = &s; //派生类对象赋值给基类指针
Person& ref = s; //派生类对象赋值给基类引用
2
3
4
# 继承中的作用域
在继承体系中的基类和派生类都有独立的作用域。若子类和父类中有同名成员,子类成员将屏蔽父类对同名成员的直接访问,这种情况叫隐藏,也叫重定义
- 访问父类的同名成员:加上作用域
- 访问子类的同名成员:直接访问
特别注意: 代码当中,父类和子类中的同名函数不是构成函数重载,因为函数重载要求两个函数在同一作用域,而同名函数并不在同一作用域。为了避免类似问题,实际在继承体系当中最好不要定义同名的成员
# 子类默认构造函数
下面我们将person,student
两个类更加完善一下,teacher
与student
类似
class person {
private:
string name;
string gender;
int age;
public:
// 构造函数
person(string _name = " ",string _gender = " ",int _age = 0 ) : name(_name),gender(_gender),age(_age) {}
// 拷贝构造函数
person(const person& p) {
name = p.name;
gender = p.gender;
age = p.age;
}
// 赋值运算符重载函数
person& operator=(const person& p) {
if(this != &p) {
name = p.name;
gender = p.gender;
age = p.age;
}
return *this;
}
// 析构函数
~person() {}
};
class student : public person{
private:
int stuid; // 学号
public:
// 构造函数
student(const string& name,string gender,int age,int _stuid) : person(name,gender,age),stuid(_stuid) {}
// 拷贝构造函数
student(const student& s) : person(s), stuid(s.stuid) {}
// 赋值运算符重载函数
student& operator=(const student& s) {
if(this != &s) {
person::operator=(s);
stuid = s.stuid;
}
return *this;
}
// 析构函数
~student() {}
};
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
41
42
43
44
45
46
子类与普通类的默认成员函数的不同之处概括为以下几点:
- 子类的构造函数被调用时,会自动调用父类的构造函数初始化父类的那一部分成员。
- 子类的拷贝构造函数必须调用父类的拷贝构造函数完成父类成员的拷贝构造。
- 子类的赋值运算符重载函数必须调用父类的赋值运算符重载函数完成父类成员的赋值。
- 子类的析构函数会在被调用完成后自动调用父类的析构函数清理父类成员。
- 子类对象初始化时,会先调用父类的构造函数再调用子类的构造函数。
- 子类对象在析构时,会先调用派生类的析构函数再调用基类的析构函数。
- 子类和父类的赋值运算符重载函数因为函数名相同构成隐藏,因此在子类当中调用父类的赋值运算符重载函数时,需要使用父类作用域限定符进行指定调用。
- 由于多态的某些原因,任何类的析构函数名都会被统一处理为
destructor();
。因此,子类和父类的析构函数也会因为函数名相同构成隐藏,若是我们需要在某处调用基类的析构函数,那么就要使用作用域限定符进行指定调用。 - 在子类的拷贝构造函数和operator=当中调用父类的拷贝构造函数和operator=的传参方式是一个切片行为,都是将子类对象直接赋值给父类的引用。
# 友元和静态成员
- 友元关系不能继承,也就是说父类的友元可以访问基类的私有和保护成员,但是不能访问派生类的私有和保护成员;
- 父类当中定义了一个static静态成员变量,则在整个继承体系里面只有一个该静态成员。无论派生出多少个子类,都只有一个static成员实例
# 继承的形式
# 单继承
一个子类只有一个直接父类时称这个继承关系为单继承
class student : public person {
// ...
}
2
3
# 多继承
一个子类有两个或两个以上直接父类时称这个继承关系为多继承
class assitant : public student, public teacher {
// ...
}
2
3
# 菱形继承
# 介绍
正由于C++特殊的多继承方式,使得产生了菱形继承这种结构
从菱形继承的模型构造就可以看出,菱形继承的继承方式存在数据冗余和二义性的问题。
assistant对象是多继承的student和teacher,而student和teacher当中都继承了person,因此student和teacher当中都有name成员,若是直接访问assistant对象的name成员会出现访问不明确的报错。 对于此,我们可以显示指定访问assistant哪个父类的name成员。
a.Student::_name = "张同学";
a.Teacher::_name = "张老师";
2
虽然该方法可以解决二义性的问题,但仍然不能解决数据冗余的问题。因为在assistant的对象在person成员始终会存在两份。
# 虚拟继承
为了解决菱形继承存在的问题,出现了虚拟继承的解决方案, 举一个简单的例子
class A {
public:
int _a;
};
// 2 * int
class B : public A {
public:
int _b;
};
// 2 * int
class C : public A {
public:
int _c;
};
// 5 * int
class D : public B, public C {
public:
int _d;
};
int main() {
D d;
d.B::_a = 1;
d.C::_a = 2;
d._b = 3;
d._c = 4;
d._d = 5;
cout << sizeof(d) << endl; // 20
return 0;
}
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
通过内存窗口,我们可以看到D类对象当中各个成员在内存当中的分布情况如下
d的内存分布:
如果使用B、C虚继承的话,D类成员的内存分布会发生变化,测试代码如下:
#include <iostream>
using namespace std;
class A
{
public:
int _a;
};
class B : virtual public A
{
public:
int _b;
};
class C : virtual public A
{
public:
int _c;
};
class D : public B, public C
{
public:
int _d;
};
int main()
{
D d;
d.B::_a = 1;
d.C::_a = 2;
d._b = 3;
d._c = 4;
d._d = 5;
return 0;
}
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
# 虚继承底层原理
通过观察虚继承的内存分布图,可以发现:
D类对象当中的_a成员被放到了最后,而在原来存放两个_a成员的位置变成了两个指针,这两个指针叫虚基表指针,它们分别指向一个虚基表。
虚基表中包含两个数据,第一个数据是为多态的虚表预留的存偏移量的位置(这里我们不必关心),第二个数据就是当前类对象位置距离公共虚基类的偏移量。
也就是说,这两个指针经过一系列的计算,最终都可以找到成员_a。
总结:菱形虚拟继承是指在菱形继承的腰部使用虚拟继承(virtual)的继承方式,菱形虚拟继承对于D类对象当中重复的A类成员只存储一份,然后采用虚基表指针和虚基表使得D类对象当中继承的B类和C类可以找到自己继承的A类成员,从而解决了数据冗余和二义性的问题。
# from cen