文章目录
- 思维导图
- 一、多态的概念
- 二、多态的定义及其实现
- 1.多态的构成条件
- 2.虚函数
- 3.虚函数的重写
- 不构成多态的情况展示
- 4.虚函数重写的两个例外
- 4.1 协变
- 4.2析构函数的重写
- 5.C++11 override和final
- 5.1. final
- 5.2.override
- 三、抽象类
- 1.概念
- 2、对比纯虚函数与override
- 3.接口继承和实现继承
- 四、多态的原理
- 1.虚函数表指针
- 2.虚函数表
- 3.多态的原理
- 4.再次理解多态构成的条件
- 5.多态的易错知识点
- 6.虚函数,虚表,虚表指针存储的位置
- 7.动态绑定与静态绑定
- 五、单继承和多继承关系的虚函数表
- 1、 单继承中的虚函数表
- 2、 多继承中的虚函数表
- 六、继承和多态习题练习
- 1.概念考察
- 2.问答题
思维导图
一、多态的概念
通俗来说,就是多种形态,具体点就是去完成某个行为,当不同的对象去完成时会产生出不同的状态。
在C++中有两种多态性,一种是静态的多态、一种是动态的多态
-
静态的多态:函数重载,看起来调用同一个函数却有不同的行为。
静态:原理是编译时实现。 -
动态的多态:一个父类的引用或指针去调用同一个函数,传递不同的对象,会调用不同的函数。
动态:原理是运行时实现。
二、多态的定义及其实现
1.多态的构成条件
在继承中要构成多态还有两个条件:
- 必须通过基类的指针或者引用调用虚函数。
- 被调用的函数必须是虚函数,且派生类必须对基类的虚函数进行重写。
2.虚函数
虚函数:即被virtual修饰的类成员函数称为虚函数。
一旦定义了虚函数,该基类的派生类中同名函数也自动成为了虚函数。也就是说在派生类中有一个和基类同名的函数,只要基类加了virtual修饰,派生类不加virtual修饰也是虚函数。
虚函数只能是类中的一个成员函数,不能是静态成员或普通函数。
注意:我们在继承中为了解决数据冗余和二义性的问题,需要用到虚拟继承,关键字也是virtual,和多态中的virtual是没有关系的。
class Person {
public:
virtual void BuyTicket() { cout << "买票-全价" << endl; }
};
3.虚函数的重写
虚函数的重写也叫做覆盖:子类中有一个与父类完全相同的虚函数,他们两个的虚函数满足三同:返回值,函数名,参数列表完全相同,则称子类的虚函数重写了父类的虚函数。
通过对虚函数的重写,就能够实现多态:
class Person {
public:
virtual void BuyTicket() { cout << "买票-全价" << endl; }
};
class Student : public Person {
public:
virtual void BuyTicket() { cout << "买票-半价" << endl; }
};
void Func(Person& p)
{
p.BuyTicket(); //使用父类的引用去调用虚函数
}
int main()
{
Person ps;
Student st;
Func(ps);
Func(st);//这行传参会完成切片
return 0;
}
这里就完成了我们的多态,如果传参传的是父类对象,就会调用父类的函数;如果传参传的是子类对象,就会调用子类的函数。
本质:不同的人做相同的事情,结果不同!
注意:
-
- 虚函数的重写允许,两个都是虚函数或者父类是虚函数,再满足三同,就构成重写。
-
- 在重写父类虚函数时,子类的虚函数在不加virtual关键字时,虽然也可以构成重写,因为继承后,父类的虚函数被继承下来了,在子类中依旧保持虚函数的属性,其实这个是C++不是很规范的地方,当然我们建议两个都写上virtual,虽然子类没写virtual,但是他是先继承了父类的虚函数的属性,再完成重写,那么他也算是虚函数。
- 在重写父类虚函数时,子类的虚函数在不加virtual关键字时,虽然也可以构成重写,因为继承后,父类的虚函数被继承下来了,在子类中依旧保持虚函数的属性,其实这个是C++不是很规范的地方,当然我们建议两个都写上virtual,虽然子类没写virtual,但是他是先继承了父类的虚函数的属性,再完成重写,那么他也算是虚函数。
不构成多态的情况展示
1️⃣构成多态,跟p类型没有关系,传参传的是哪个类型的对象,调用的就是哪个类型对象中的虚函数——(跟对象有关)
2️⃣不构成多态,调用的就是p类型函数——(跟类型有关)
1、使用父类的对象调用,不构成多态,只会调用父类对象的函数
2、不加virtual也不会构成多态
3、虚函数没有重写,因为两个虚函数的参数列表不同,不构成多态
4.虚函数重写的两个例外
4.1 协变
父类与子类的虚函数构成重写,返回值有个例外(返回值可以不同),即父类虚函数返回父类对象的指针或者引用,子类虚函数返回子类对象的指针或者引用,称为协变。
class Person {
public:
virtual Person* BuyTicket() { cout << "买票-全价" << endl; return nullptr; }
};
class Student : public Person {
public:
virtual Student* BuyTicket() { cout << "买票-半价" << endl; return nullptr; }
};
4.2析构函数的重写
析构函数是虚函数,是否构成重写?——构成
因为析构函数名被编译器特殊处理成了destructor
class Person {
public:
virtual ~Person() { cout << "~Person()" << endl; }
};
class Student : public Person {
public:
virtual ~Student() { cout << "~Student()" << endl; }
};
int main()
{
Person p;
Student s;
return 0;
}
重点:
下面再来看看这个代码:
class Person {
public:
~Person() { cout << "~Person()" << endl; }
};
class Student : public Person {
public:
~Student() { cout << "~Student()" << endl; }
};
int main()
{
Person* p1=new Person;
Person* p2 = new Student;
delete p1;
delete p2;
return 0;
}
解释:这两个函数并没有构成多态(因为没有用virtual修饰),我们析构的时候,想要父类调用父类的析构,而子类调用子类的析构,而并没有构成多态,就会跟类型有关,p1 p2都是person类型的,都会去调用父类的析构函数。万一子类对象中有些动态开辟的资源,没有被释放,就会很危险!
解决:构成多态即可解决这个问题!重写里面的虚函数,加上virtual关键字:
5.C++11 override和final
从上面可以看出,C++对函数重写的要求比较严格,但是有些情况下由于疏忽,可能会导致函数名字母次序写反而无法构成重写,而这种错误在编译期间是不会报出的,只有在程序运行时没有得到预期结果才来debug会得不偿失,因此:C++11提供了
override
和final
两个关键字,可以帮助用户检测是否重写。
5.1. final
修饰类:限制类的继承
修饰虚函数,表示该虚函数不能被重写
例如:如何设计一个类无法被继承?
可以父类的构造函数私有(private)
class A
{
private:
A(int a)
:_a(a)
{}
public:
static A CreateObj(int a=10)
{
return A(20);//创建一个对象出来
}
protected:
int _a = 10;
};
// 间接限制,子类的构造函数无法调用父类的构造函数初始化,没办法实例化出对象,因为是private私有权限
class B :public A
{
};
int main()
{
A aa=A::CreateObj(100);
// A类的构造函数是私有的,类内能用,但是在类外却不能实例化
// 所以调用一个公共的函数接口,来实例化
return 0;
}
以上的方法太过复杂,C++11新增加了一个语法,final关键字
// 直接限制,该类无法被继承
class A final
{
protected:
int _a = 10;
};
class B :public A
{
};
限制虚函数的重写
class C
{
public:
virtual void f() final
{
cout << "C::f()" << endl;
}
};
class D :public C
{
public:
virtual void f()
{
cout << "D::f()" << endl;
}// 无法重写final函数
};
5.2.override
放在子类重写的虚函数的后面,检查是否完成重写,没有重写就报错
class Car {
public:
virtual void Drive() {}
};
class Benz :public Car {
public:
virtual void Drive() override { cout << "Benz-舒适" << endl; }
};
三、抽象类
1.概念
在虚函数的后面写上
= 0
,则这个函数为纯虚函数。包含纯虚函数的类叫做抽象类(也叫接口类),抽象类不能实例化出对象。
//抽象类
class Car
{
public:
virtual void Drive() = 0;//纯虚函数
};
int main()
{
Car c;//抽象类不能实例化出对象
return 0;
}
派生类继承后也不能实例化出对象,只有重写纯虚函数,派生类才能实例化出对象。纯虚函数规范了派生类必须重写,另外纯虚函数更体现出了接口继承。
class Car
{
public:
//纯虚函数一般只声明,不实现(可以实现,但没有价值,因为不能实例化出对象,可以定义指针或引用)
virtual void Drive() = 0;
};
class Benz :public Car
{
public:
virtual void Drive()
{
cout << "Benz-舒适" << endl;
}
};
class BMW :public Car
{
public:
virtual void Drive()
{
cout << "BMW-操控" << endl;
}
};
int main()
{
//派生类只有重写了虚函数才能实例化出对象
Benz b1;
BMV b2;
//通过基类的指针去调用不同对象的函数
Car* pBenz = new Benz;
pBenz->Drive();
Car* pBMW = new BMW;
pBMW->Drive();
}
2、对比纯虚函数与override
- 纯虚函数的类,本质上强制子类去完成虚函数的重写
- override只是语法上检查是否完成重写
3.接口继承和实现继承
- 普通函数的继承是一种实现继承(声明和定义全部继承),派生类继承了基类函数,可以使用函数,继承的是函数的实现。
- 虚函数的继承是一种接口继承(声明完全一样),派生类继承的是基类虚函数的接口,目的是为了重写,达成多态,继承的是接口。
- 所以如果不实现多态,不要把函数定义成虚函数。
四、多态的原理
1.虚函数表指针
这里常考一道笔试题:sizeof(Base)是多少?
class Base
{
public:
virtual void Func1()
{
cout << "Func1()" << endl;
}
private:
int _b = 1;
char c = 'A';
};
int main()
{
cout << sizeof(Base) << endl;// 12
return 0;
}
可能刚看到这个题目的时候,都会觉得答案是8个字节,但是我们在打印后却发现是12个字节;这是为什么呢?
因为有了虚函数,这个对象里面就多了一个成员,虚函数表指针__vfptr。简称虚表指针——virtual function pointer
2.虚函数表
虚函数表指针指向的这个表就是虚函数表,本质是一个函数指针数组,表中存储的是虚函数的地址
注:一个含有虚函数的类中,都至少有一个虚函数表指针,因为虚函数的地址要被放到虚函数表中。
如下:含有多个虚函数
我们通过下面的程序来进行分析虚表到底是什么:
class Base
{
public:
virtual void Func1() { cout << "Base::Func1()" << endl; }
virtual void Func2() { cout << "Base::Func2()" << endl; }
void Func3() { cout << "Base::Func3()" << endl; }
private:
int _b = 1;
};
class Derive : public Base
{
public:
virtual void Func1() { cout << "Derive::Func1()" << endl; }
private:
int _d = 2;
};
int main()
{
Base b;
Derive d;
return 0;
}
通过观察和测试,我们发现了以下几点问题:
- 派生类对象d中也有一个虚表指针,d对象由两部分构成,一部分是父类继承下来的成员,另一部分是自己的成员。
- 基类b对象和派生类d对象虚表是不一样的(如果没重写且没有新增就一样了),这里我们发现Func1完成了重写,所以d的虚表中存的是重写的Derive::Func1,所以虚函数的重写也叫作覆盖,覆盖就是指虚表中虚函数的覆盖。重写是语法的叫法,覆盖是原理层的叫法。
- 另外Func2继承下来后是虚函数,所以放进了虚表,Func3也继承下来了,但是不是虚函数,所以不会放进虚表。
- 虚函数表本质是一个存虚函数指针的指针数组,一般情况这个数组最后面放了一nullptr。
总结一下派生类的虚表生成:
- a.先将基类中的虚表内容拷贝一份到派生类虚表中;
- b.如果派生类重写了基类中某个虚函数,用派生类自己的虚函数覆盖虚表中基类的虚函数;
- c.派生类自己新增加的虚函数按其在派生类中的声明次序增加到派生类虚表的最后。
3.多态的原理
未实现多态时:
class Base
{
public:
virtual void Func1() { cout << "Base::Func1()" << endl; }
private:
int _b = 1;
};
class Derive : public Base
{
private:
int _d = 2;
};
int main()
{
Base b;
Derive d;
Base* p1 = &b;
p1->Func1();
p1 = &d;
p1->Func1();
return 0;
}
实现了多态:
class Base
{
public:
virtual void Func1()
{ cout << "Base::Func1()" << endl; }
virtual void Func2()
{ cout << "Base::Func2()" << endl; }
void Func3()
{ cout << "Base::Func3()" << endl; }
private:
int _b = 1;
};
class Derive : public Base
{
public:
virtual void Func1()
{ cout << "Derive::Func1()" << endl; }
private:
int _d = 2;
};
int main()
{
Base b;
Derive d;
Base* p1 = &b;
p1->Func1();
p1 = &d;
p1->Func1();
return 0;
}
注意:虚表指针实在构造函数的初始化列表由系统自动进行创建的。
多态原理总结:
基类的指针或者引用,调用谁,就去谁的虚表中找到对应的位置的虚函数,就实现了对应的多态的功能。
如上述所示:
传递的是父类对象,就直接去父类虚表中寻找该虚函数的地址
传递的是子类对象,先会完成切片操作,相当于现在是子类中父类那部分对象的别名,再去子类中继承自父类中去寻找该类中重写的虚函数的地址,进而完成调用,实现多态。
4.再次理解多态构成的条件
为什么必须要是父类的指针或引用来调用虚函数,为什么不能是对象调用?
传入的是对象:就是传入的值,是值拷贝
我们再来看看对象的引用r1和对象本身p,切片过后内存布局,如下图所示:
我们注意到,引用的r1其实就是Johnson中继承自分类那部分对象的别名,里面虚表是一样的
对象切片的时候,我们会把值(注意_a变量)给拷贝过去,不会把子类的虚表指针给赋值过去,如果赋值成功,父类的虚表指针会指向子类的虚表,那么多态就不可能实现了。
5.多态的易错知识点
1、同类型的对象,虚表指针是相同的,指向同一张虚表
如下所示,三个同类型的person对象都指向同一张虚表。
2、普通函数和虚函数的存储位置是否一样?
答案:一样的!都是在代码段
只不过虚函数要把地址存一份到虚表中,方便实现多态
注意:如果子类的虚函数是私有的,那么也是能够实现多态的。
从语法上来看,重写是一种接口继承,继承父类的接口,重写函数体内容,编译器检查不出来,因为是运行时,去p指向对象的虚表中,找到虚函数的地址,所以私有的限制不起作用。
6.虚函数,虚表,虚表指针存储的位置
-
虚函数:和普通函数一样在代码段,只不过虚函数要把地址存一份到虚表中,方便实现多态
-
虚表:在编译阶段生成,存放在代码段(常量区)
-
虚表指针:存放在对象的前四个字节(32位平台下),有些平台可能会放到对象的最后面,这个跟平台有关
7.动态绑定与静态绑定
- 静态绑定又称为前期绑定(早绑定),在程序编译期间确定了程序的行为,也称为静态多态,比如:函数重载
- 动态绑定又称后期绑定(晚绑定),是在程序运行期间,根据具体拿到的类型确定程序的具体行为,调用具体的函数,也称为动态多态。
五、单继承和多继承关系的虚函数表
1、 单继承中的虚函数表
class Base {
public:
virtual void func1() { cout << "Base::func1" << endl; }
virtual void func2() { cout << "Base::func2" << endl; }
private:
int a;
};
class Derive :public Base {
public:
virtual void func1() { cout << "Derive::func1" << endl; }
virtual void func3() { cout << "Derive::func3" << endl; }
virtual void func4() { cout << "Derive::func4" << endl; }
private:
int b;
};
typedef void(*VF_PTR)(); // 等价于 typedef void(*)() VF_PTR;函数指针比较特别
void PrintVTable(VF_PTR* table)// void PrintVTable(VF_PTR _table[])
{
for (int i = 0; table[i] != nullptr; i++)
{
printf("vft[%d]:%p\n",i,table[i]);
}
}
int main()
{
Base b;
PrintVTable((VF_PTR*)(*(int*)&b));
Derive d;
return 0;
}
思路:取出b对象的头4bytes,就是虚表的指针
- 先取b的地址,强制类型转换为int*的指针
- 在解引用取值,就取到了b对象头4bytes,这个值就是指向虚表的指针
- 再强转成VF_PTR*,因为虚表就是一个存放VF_PTR虚函数指针类型的数组
2、 多继承中的虚函数表
// 多继承中的虚表
class Base1 {
public:
virtual void func1() { cout << "Base1::func1" << endl; }
virtual void func2() { cout << "Base1::func2" << endl; }
private:
int b1;
};
class Base2 {
public:
virtual void func1() { cout << "Base2::func1" << endl; }
virtual void func2() { cout << "Base2::func2" << endl; }
private:
int b2;
};
class Derive : public Base1, public Base2 {
public:
virtual void func1() { cout << "Derive::func1" << endl; }
virtual void func3() { cout << "Derive::func3" << endl; }
private:
int d1;
};
typedef void(*VF_PTR)(); // 等价于 typedef void(*)() VF_PTR;函数指针比较特别
void PrintVTable(VF_PTR* table)// void PrintVTable(VF_PTR _table[])
{
for (int i = 0; table[i] != nullptr; i++)
{
printf("vft[%d]:%p\n",i,table[i]);
}
}
int main()
{
Base1 b1;
Base2 b2;
Derive d;
PrintVTable((VF_PTR*)(*(void**)&d));
PrintVTable((VF_PTR*)(*(void**)((char*)&d+sizeof(Base1))));
return 0;
}
六、继承和多态习题练习
1.概念考察
- 下面程序输出结果是什么? ( A )
A:class A class B class C class D
B:class D class B class C class A
C:class D class C class B class A
D:class A class C class B class D
#include<iostream>
using namespace std;
class A {
public:
A(char* s) { cout << s << endl; }
~A() {}
};
class B :virtual public A {
public:
B(char* s1, char* s2) :A(s1) { cout << s2 << endl; }
};
class C :virtual public A
{
public:
C(char* s1, char* s2) :A(s1) { cout << s2 << endl; }
};
class D :public B, public C
{
public:
D(char* s1, char* s2, char* s3, char* s4) :B(s1, s2), C(s1, s3), A(s1)
{
cout << s4 << endl;
}
};
int main()
{
D* p = new D("class A", "class B", "class C", "class D");
delete p;
return 0;
}
解释:上述代码首先会构造A(s1),因为如果A(s1)最后再构造就会产生歧义了。然后的构造顺序就是继承的顺序,自己的D最后再构造
- 多继承中指针偏移问题?下面说法正确的是( C )
A:p1 == p2 == p3 B:p1 < p2 < p3
C:p1 == p3 != p2 D:p1 != p2 != p3
class Base1 { public: int _b1; };
class Base2 { public: int _b2; };
class Derive : public Base1, public Base2 { public: int _d; };
int main()
{
Derive d;
Base1* p1 = &d;
Base2* p2 = &d;
Derive* p3 = &d;
return 0;
}
图解:
- 以下程序输出结果是什么( B )
A: A->0 B: B->1 C: A->1
D: B->0 E: 编译出错 F: 以上都不正确
class A
{
public:
virtual void func(int val = 1) { std::cout << "A->" << val << std::endl; }
virtual void test() { func(); }
};
class B : public A
{
public:
void func(int val = 0) { std::cout << "B->" << val << std::endl; }
};
int main(int argc, char* argv[])
{
B* p = new B;
p->test();
return 0;
}
图解:
2.问答题
1. 什么是多态?
a、通俗的讲,多态就是多种不同的对象做同一件事会有不同的状态。多态有两种静态多态和动态多态。
b、静态多态例如函数重载,在编译阶段编译器根据函数名的修饰规则就确定下来要执行的的函数。
c、动态多态是在运行时才确定下来的状态。是以继承为基础的,当子类继承了父类的虚函数,并且完成了重写就构成了多态,当我们使用父类对象指针或者引用去调用虚函数,根据调用对象的不同,,调用的虚函数就不同,就会产生不同的状态
什么是重载、重写(覆盖)、重定义(隐藏)?
a、重载:两个函数在同一个作用域,且函数名相同,函数的参数个数,参数类型,类型的顺序不同
b、重写(覆盖):两个函数在不同的作用域,一个在父类,一个在子类。两个函数的返回值,函数名,参数列表相同(协变除外)。且两个函数都是虚函数
c、重定义(隐藏):在继承中,在子类中当出现和父类同名的变量和同名函数时候,子类对象会将父类的成员屏蔽,优先访问子类的成员,这就叫做隐藏。
多态的实现原理?
当子类继承了父类的虚函数并完成了重写就构成了多态。底层是有一个虚函数表指针,指向一张虚表(本质是函数指针数组),虚表中存 的是各自类中虚函数的地址(不是真正的地址,可以理解为间接地址)。当一个父类的指针或者引用来调用虚函数的时候,看调用的对象是谁,是父类就去父类的虚表中找对应的虚函数,调用的是子类就去子类的虚表中去找对用的虚函数的
inline函数可以是虚函数吗?
可以,不过编译器就忽略inline属性,因为内敛函数没有地址,忽略后这个函数就不再是inline,因为虚函数要放到虚表中去。
静态成员可以是虚函数吗?
不能,因为静态成员函数没有this指针,使用类型::成员函数的调用方式无法访问虚函数表,所以静态成员函数无法放进虚函数表。
注意:静态成员是属于整个类的,不属于某个对象,调用方式也不是通过this指针,虽然可以使用对象调用,但是不是通过this指针。并且这种调用不常用。一般都是使用类型::成员函数的方式。
构造函数可以是虚函数吗?
不能,因为对象中的虚函数表指针是在构造函数初始化列表阶段才初始化的。
析构函数可以是虚函数吗?什么场景下析构函数是虚函数?
可以,并且最好把基类的析构函数定义成虚函数。当我们在子类中也有动态开辟的内存需要释放时,就需要将虚构函数写为虚函数。
对象访问普通函数快还是虚函数更快?
首先如果是普通对象,是一样快的。如果是指针对象或者是引用对象,则调用的普通函数快,因为构成多态,运行时调用虚函数需要到虚函数表中去查找。
虚函数表是在什么阶段生成的,存在哪的?
虚函数表是在编译阶段就生成的,一般情况下存在代码段(常量区)的。
C++菱形继承的问题?虚继承的原理?
菱形继承的问题是子类对象当中会有两份父类的成员,因此会导致数据冗余和二义性的问题。
采用虚继承即可解决。假设B和C继承A,D继承了B和C,在B和C继承处使用virtual修饰。在D对象中只会存储一份A类,这个A类的成员同时属于B和C。如果B和C需要访问A的成员,会到自己的虚基表中查找偏移量,通过自身地址+偏移量即可找到A类的成员。
什么是抽象类?抽象类的作用?
包含纯虚函数的类就是抽象类,抽象类不能定义对象,且抽象类要求子类在继承后必须重写虚函数,否则子类对象也无法定义对象,体现了接口继承