多态
# 引入
多态就是函数调用的多种形态,使用多态能够使得不同的对象去完成同一件事时,产生不同的动作和结果。
在现实生活当中,普通人买票是全价,学生买票是半价,而军人允许优先买票。不同身份的人去买票,所产生的行为是不同的,这就是一种所谓的多态。
# 多态的构成条件
多态是指不同继承关系的类对象,去调用同一函数,产生了不同的行为。在继承中要想构成多态需要满足三个条件:
- 有继承关系
- 必须通过基类的指针或者引用调用虚函数
- 被调用的函数必须是虚函数,且派生类必须对基类的虚函数进行重写
# 虚函数
被virtual修饰的类成员函数被称为虚函数
virtual void buyTicket() {
cout << "普通人全价购票" << endl;
}
2
3
# 虚函数的重写
虚函数的重写也叫做虚函数的覆盖,若子类中有一个和父类完全相同的虚函数(返回值类型相同、函数名相同以及参数列表完全相同,与缺省参数是否相同无关),此时我们称该派生类的虚函数重写了基类的虚函数。
例如,下面测试代码:
class person {
public:
virtual void buyTicket() {
cout << "普通人全价购票" << endl;
}
};
class student : public person {
virtual void buyTicket() override {
cout << "学生半价车票" << endl;
}
};
class soldier : public person {
virtual void buyTicket() override {
cout << "军人优先购票" << endl;
}
};
void func(person& p) {
p.buyTicket();
}
int main() {
person p;
student stu;
soldier sol;
func(stu);
func(sol);
func(p);
// 学生半价车票
// 军人优先购票
// 普通人全价购票
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
33
34
# 重写的例外
协变(基类与派生类虚函数的返回值类型不同)
派生类重写基类虚函数时,与基类虚函数返回值类型不同。即基类虚函数返回基类对象的指针或者引用,派生类虚函数返回派生类对象的指针或者引用,称为协变。 例如:
//基类
class A {};
//子类
class B : public A {};
//基类
class Person {
public:
//返回基类A的指针
virtual A* fun() {
cout << "A* Person::f()" << endl;
return new A;
}
};
//子类
class Student : public Person {
public:
//返回子类B的指针
virtual B* fun() {
cout << "B* Student::f()" << endl;
return new B;
}
};
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
此时,我们通过父类Person的指针调用虚函数fun,父类指针若指向的是父类对象,则调用父类的虚函数,父类指针若指向的是子类对象,则调用子类的虚函数。
int main() {
Person p;
Student st;
//父类指针指向父类对象
Person* ptr1 = &p;
//父类指针指向子类对象
Person* ptr2 = &st;
//父类指针ptr1指向的p是父类对象,调用父类的虚函数
ptr1->fun(); //A* Person::f()
//父类指针ptr2指向的st是子类对象,调用子类的虚函数
ptr2->fun(); //B* Student::f()
return 0;
}
2
3
4
5
6
7
8
9
10
11
12
13
14
析构函数的重写(基类与派生类析构函数的名字不同)
如果基类的析构函数为虚函数,此时派生类析构函数只要定义,无论是否加virtual关键字,都与基类的析构函数构成重写,虽然基类与派生类析构函数名字不同。
//父类
class Person {
public:
virtual ~Person() {
cout << "~Person()" << endl;
}
};
//子类
class Student : public Person {
public:
virtual ~Student() {
cout << "~Student()" << endl;
}
};
2
3
4
5
6
7
8
9
10
11
12
13
14
在这种场景下,若是父类和子类的析构函数没有构成重写就可能会导致内存泄漏,因为此时delete p1和delete p2都是调用的父类的析构函数,而我们所期望的是p1调用父类的析构函数,p2调用子类的析构函数,即我们期望的是一种多态行为。 此时只有父类和子类的析构函数构成了重写,才能使得delete按照我们的预期进行析构函数的调用,才能实现多态。因此,为了避免出现这种情况,比较建议将父类的析构函数定义为虚函数。
# override和final
# override
override:检查派生类虚函数是否重写了基类的某个虚函数,如果没有重写则编译报错。
# final
final: 修饰虚函数,表示该虚函数不能再被重写
# 重载、重写、重定义
- 重载:两个函数名相同,参数不同,并且在同一作用域
- 重写(覆盖):两个函数分别在子类和父类的作用域,函数名、参数、返回值必须相同,并且函数都是虚函数
- 重定义:两个函数分别在子类和父类的作用域,函数名必须相同
# 抽象类
我们发现对于父类的虚函数没有意义,主要在于调用子类的重写函数
因此,我们可以将虚函数写为纯虚函数
,此时该类变为抽象类
语法:virtual 返回值 函数名 (参数)= 0;
特点:抽象无法实例化对象并且如果子类没有重写虚函数则也是抽象类
class person {
public:
virtual void buyTicket(int a = 1) = 0;
};
2
3
4
既然无法实例化对象那么抽象类的意义是什么?
原因:
- 抽象类可以更好的去表示现实世界中,没有实例对象对应的抽象类型,比如:植物、人、动物等。
- 抽象类很好的体现了虚函数的继承是一种接口继承,强制子类去重写纯虚函数,因为子类若是不重写从父类继承下来的纯虚函数,那么子类也是抽象类也不能实例化出对象。
# 多态的原理
# 虚函数表
- 结构体的第一个成员对齐到和结构体变量起始位置偏移量为0的地址处
- 其他成员变量要对齐到对齐数的整数倍的地址处。
- 对齐数=编译器默认对齐数与成员变量大小中的较小值 默认对齐数==8 gcc中没有默认对齐数,则对齐数为成员自身的大小
- 结构体总大小为最大对齐数(结构体中每个成员变量都有⼀个对齐数,所有对齐数中最大的)的整数倍字节
来看下面测试代码:
class Base {
public:
virtual void Func1() {
cout << "Func1()" << endl;
}
private:
int _b = 1;
};
int main() {
cout << sizeof(Base) << endl;
return 0;
}
2
3
4
5
6
7
8
9
10
11
12
13
输出结果为16,原来,Base当中除了_b成员外,实际上还有一个_vfptr指针放在对象的前面/后面(与平台有关)
根据对齐规则,我们可以计算大小为16字节(x86 32位 : 4byte x64(x86-64) 64位: 8byte
)
对象中的这个指针叫做虚函数表指针,简称虚表指针,虚表指针指向一个虚函数表,简称虚表,每一个含有虚函数的类中都至少有一个虚表指针。
虚表就是指针数组,其中存放了指向虚函数的指针,即存放的是虚函数的地址。
class person {
public:
int _a;
virtual void buyTicket() {
cout << "全价车票" << endl;
}
};
class student : public person {
public:
int b;
virtual void buyTicket() override {
cout << "学生半价车票" << endl;
}
};
int main() {
person p;
student s;
cout << sizeof(p) << endl; // 16
cout << sizeof(s) << endl; // 24
return 0;
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
虚表实际上是在构造函数初始化列表阶段进行初始化的,注意虚表当中存的是虚函数的地址不是虚函数,虚函数和普通函数一样,都是存在代码段的,只是他的地址又存到了虚表当中。另外,对象中存的不是虚表而是指向虚表的指针,虚表实际上是存在代码段的。
总结一下,子类的虚表生成步骤如下:
- 先将基类中的虚表内容拷贝一份到派生类的虚表。
- 如果派生类重写了基类中的某个虚函数,则用派生类自己的虚函数地址覆盖虚表中基类的虚函数地址。
- 派生类自己新增加的虚函数按其在派生类中的声明次序增加到派生类虚表的最后。
虚表是初始化的过程:
虚表实际上是在构造函数初始化列表阶段进行初始化的,注意虚表当中存的是虚函数的地址不是虚函数,虚函数和普通函数一样,都是存在代码段的,只是他的地址又存到了虚表当中。另外,对象中存的不是虚表而是指向虚表的指针。
# 总结
多态的原理
构成多态的父类对象和子类对象的成员当中都包含一个虚表指针,这个虚表指针指向一个虚表,虚表当中存储的是该类对应的虚函数地址。因此,当父类指针指向父类对象时,通过父类指针找到虚表指针,然后在虚表当中找到的就是父类当中对应的虚函数;当父类指针指向子类对象时,通过父类指针找到虚表指针,然后在虚表当中找到的就是子类当中对应的虚函数。
内联函数、静态函数、构造函数和析构函数可以成为虚函数吗?
我们知道内联函数是会在调用的地方展开的,也就是说内联函数是没有地址的,但是内联函数是可以定义成虚函数的,当我们把内联函数定义虚函数后,编译器就忽略了该函数的内联属性,这个函数就不再是内联函数了,因为需要将虚函数的地址放到虚表中去。
静态成员函数不能是虚函数,因为静态成员函数没有this指针,使用类型::成员函数的调用方式无法访问虚表,所以静态成员函数无法放进虚表。
构造函数不能是虚函数,因为对象中的虚表指针是在构造函数初始化列表阶段才初始化的
析构函数可以是虚函数,并且最后把基类的析构函数定义成虚函数。若是我们分别new一个父类对象和一个子类对象,并均用父类指针指向它们,当我们使用delete调用析构函数并释放对象空间时,只有当父类的析构函数是虚函数的情况下,才能正确调用父类和子类的析构函数分别对父类和子类对象进行析构,否则当我们使用父类指针delete对象时,只能调用到父类的析构函数。
# from cen