本节目标:
1. 多态的概念
2. 多态的定义及实现
3. 抽象类
4. 多态的原理
5. 单继承和多继承关系中的虚函数表
6. 继承和多态常见的面试问题
一,多态的概念
多态的概念:通俗来说,就是多种形态,具体点就是去完成某个行为,当不同的对象去完成时会 产生出不同的状态。
再举个栗子: 最近为了争夺在线支付市场,支付宝年底经常会做诱人的扫红包-支付-给奖励金的 活动。那么大家想想为什么有人扫的红包又大又新鲜8块、10块...,而有人扫的红包都是1毛,5 毛....。其实这背后也是一个多态行为。支付宝首先会分析你的账户数据,比如你是新用户、比如 你没有经常支付宝支付等等,那么你需要被鼓励使用支付宝,那么就你扫码金额 = random()%99;比如你经常使用支付宝支付或者支付宝账户中常年没钱,那么就不需要太鼓励你 去使用支付宝,那么就你扫码金额 = random()%1;总结一下:同样是扫码动作,不同的用户扫 得到的不一样的红包,这也是一种多态行为。
二,多态的定义及实现
1,多态构成的条件
多态是在不同继承关系的类对象,去调用同一函数,产生了不同的行为。比如Student继承了 Person。Person对象买票全价,Student对象买票半价。
那么在继承中要构成多态还有两个条件:
1. 必须通过基类的指针或者引用调用虚函数
2. 被调用的函数必须是虚函数,且派生类必须对基类的虚函数进行重写
class A
{
public:
void func()
{}
protected:
int _a;
};
class B : public A
{
public:
void f()
{
func();
_a++;
}
protected:
int _b;
};
class Person {
public:
virtual A* BuyTicket()
{
cout << "Person买票-全价" << endl;
return nullptr;
}
virtual ~Person()
{
cout << "~Person()" << endl;
}
};
class Student : public Person {
public:
virtual B* BuyTicket()
{
cout << "Student买票-半价" << endl;
return nullptr;
}
/*注意:在重写基类虚函数时,派生类的虚函数在不加virtual关键字时,虽然也可以构成重写(因为继承后基类的虚函数被继承下来了在派生类依旧保持虚函数属性),但是该种写法不是很规范,不建议这样使用*/
/*void BuyTicket() { cout << "买票-半价" << endl; }*/
~Student()
{
cout << "~Student()" << endl;
}
};
2,虚函数
虚函数就是被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; }
/*注意:在重写基类虚函数时,派生类的虚函数在不加virtual关键字时,虽然也可以构成重写(因
为继承后基类的虚函数被继承下来了在派生类依旧保持虚函数属性),但是该种写法不是很规范,不建议
这样使用*/
/*void BuyTicket() { cout << "买票-半价" << endl; }*/
};
void Func(Person& p)
{ p.BuyTicket(); }
int main()
{
Person ps;
Student st;
Func(ps);
Func(st);
return 0;
}
列外1:
返回值可以不同,必须是父子类的指针或者引用
例外2:
子类可以不加virtual,因为子类的是父类的重写
例外3:
只有父子类析构函数都加virtual,才能各自调各自的析构
4,普通调用和多态调用
普通调用:
调用的函数类型是谁,就调用这个类型的函数。
void Func(char p)
{
BuyTicket(p);
}
多态调用:调用指针指向的对象,指向父类调用父类函数,指向子类调用子类函数。
void Func(Person& p)
{
p.BuyTicket();
}
多态传参指针和引用区别:
指针:
void Func(Person* p)
{
p->BuyTicket();
}
int main()
{
Person ps;
Student st;
Func(&ps);
Func(&st);
return 0;
}
引用:
void Func(Person& p)
{
p.BuyTicket();
}
int main()
{
Person ps;
Student st;
Func(ps);
Func(st);
return 0;
}
5,协变(基类和派生类虚函数返回值类型不同)
派生类重写基类虚函数时,与基类虚函数的返回值类型不同.即基类虚函数返回基类对象的指针或引用,派生类虚函数返回派生类对象的指针或者引用时,称为协变.
class preson {
virtual preson* show() {
cout << "preson" << endl;
}
};
class student : public preson {
virtual student* show() {
cout << "student" << endl;
}
};
6,函数重写重定义和重载
重写要求最高,如果不是重写就是重定义
7,例题
1,首先在调用test()函数的时候,其中的this指针是A的,
2,this—>func()是一个多态调用,这个func是调用子类的
3,他两个参数相同,构成多态
思路:
首先test()调用A*指针,得到val参数1,然后调用子类B的func()函数,故最后打印值就是b——>1,选B。
8,final和override
从上面可以看出,C++对函数重写的要求比较严格,但是有些情况下由于疏忽,可能会导致函数 名字母次序写反而无法构成重载,而这种错误在编译期间是不会报出的,只有在程序运行时没有 得到预期结果才来debug会得不偿失,因此:C++11提供了override和final两个关键字,可以帮 助用户检测是否重写。
1. final:修饰虚函数,表示该虚函数不能再被重写
class Car
{
public:
virtual void Drive() final {}
};
class Benz :public Car
{
public:
virtual void Drive() {cout << "Benz-舒适" << endl;}
};
2. override: 检查派生类虚函数是否重写了基类某个虚函数,如果没有重写编译报错。
class Car{
public:
virtual void Drive(){}
};
class Benz :public Car {
public:
virtual void Drive() override {cout << "Benz-舒适" << endl;}
};
三,抽象类
在虚函数的后面写上 =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;
}
};
void Test()
{
Car* pBenz = new Benz;
pBenz->Drive();
Car* pBMW = new BMW;
pBMW->Drive();
}
tips:
1,当把car变成抽象类后,实例化car为一个变量,如car c是会报错的,但是如果是指针car* ,不会报错,这是语法规定。
2,他的子类也无法实例化对象,如Benz a也不行
常用方法:
当有多个子类对象时,就可以通过这种方式指哪打哪,这就是利用了指针可以实例化纯虚函数。
实际上纯虚函数有两个目的:
1,强制子类/派生类去重写
2,如定义一个职业,然后去重写各个种类,医生 警察等,职业是虚拟的对象,所以大量具体化时需要用到纯虚函数。
1,接口继承和实现继承
普通函数的继承是一种实现继承,派生类继承了基类函数,可以使用函数,继承的是函数的实 现。虚函数的继承是一种接口继承,派生类继承的是基类虚函数的接口,目的是为了重写,达成 多态,继承的是接口。所以如果不实现多态,不要把函数定义成虚函数。
2,趁热打铁
一道笔试题:
试着分析输出是多少?
通过观察测试我们发现b对象是8bytes,除了_b成员,还多一个__vfptr放在对象的前面(注意有些 平台可能会放到对象的最后面,这个跟平台有关),对象中的这个指针我们叫做虚函数表指针(v代 表virtual,f代表function)。一个含有虚函数的类中都至少都有一个虚函数表指针,因为虚函数 的地址要被放到虚函数表中,虚函数表也简称虚表,虚表指针占4个字节。
而这些函数是存放在代码段的,只是指向他们的指针单独拿出来,放到虚函数表里面了
实际上他是一个虚函数数组指针
3,父子类虚函数表
父类生成的虚函数会被子类继承拷贝下来,然后子类会将虚表重写一份。
这里父子都有的buytecket都在,但是父类有的func()函数子类没有,原封不动的继承拷贝下来没有做任何改动。
普通调用和多态调用的区别:
所以为什么直接调用不能多态调用?
因为对象调用时首先子类的值会对父类的值拷贝构造过来,但是虚表指针不能拷贝,虚函数表指针拷贝过去会有很多问题。比如父类把虚函数表拷贝给子类,最后多态调用时不知道是调用父类还是子类了。父类对象不一定能保证调用父类虚函数。
甚至会导致父类对象调用子类的虚表。
四,虚函数表的存放
虚函数存在哪,虚函数表存在哪?
1,虚函数的地址
虚函数和普通函数一样存在了,都是存在代码段,同时把虚函数指针存了一份到虚函数表。
验证地址:
class Base {
public:
virtual void func1() { cout << "Base::func1" << endl; }
virtual void func2() { cout << "Base::func2" << endl; }
private:
int a;
};
void func()
{
cout << "void func()" << endl;
}
int main()
{
Base b1;
Base b2;
static int a = 0;
int b = 0;
int* p1 = new int;
const char* p2 = "hello world";
printf("静态区:%p\n", &a);
printf("栈:%p\n", &b);
printf("堆:%p\n", p1);
printf("代码段:%p\n", p2);
printf("虚表:%p\n", *((int*)&b1));
printf("虚函数地址:%p\n", &Base::func1);
printf("普通函数地址:%p\n", func);
2,虚函数一定会放在类的虚函数表吗
func1被重写,func2被继承,那derive类原有的func3和func4呢?其实这是编译器处理过了,故意隐藏了两个函数的地址,实际上是存在的,要对监视窗口保持怀疑态度。
那只能通过原始的方式——直接打印虚表来查看
代码实现:
难度知识点:函数指针数组的typedef,详见博客typedef用法,函数指针_c语言函数指针 typedef-CSDN博客
typedef void (*VFUNC)();//函数指针数组的typedef
//void PrintVFT(VFUNC a[])
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; }
void func5() { cout << "Derive::func5" << endl; }
private:
int b;
};
class X :public Derive {
public:
virtual void func3() { cout << "X::func3" << endl; }
};
void PrintVFT(VFUNC* a)
{
for (size_t i = 0; a[i] != 0; i++)
{
printf("[%d]:%p->", i, a[i]);
VFUNC f = a[i];
f();
//(*f)();
}
printf("\n");
}
int main()
{
void (*f1)();
VFUNC f2;
cout << sizeof(long long) << endl;
Base b;
PrintVFT((VFUNC*)(*((long long*)&b)));//在64位下强转为了取前8个地址得到虚函数表地址
Derive d;
X x;
// PrintVFT((VFUNC*)&d);
PrintVFT((VFUNC*)(*((long long*)&d)));
PrintVFT((VFUNC*)(*((long long*)&x)));
return 0;
}
故实际上是会放进虚表的。
3,多继承问题
当继承俩父类他会有几个虚表呢
派生类的虚函数基本不放进虚函数表,大概符合这种形式。
代码实现:
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;
};
派生类的虚函数基本不会往虚函数表里放,因为切片也不好切。
打印base1比较方便,但是这里如何获取base2的数据呢?
有两两种方式:
(1)直接通过找到子类虚表首地址然后取到第一个字节位置,直接跳过base1地址来获得base2的首地址,再强转int类型获得base2虚表地址。
(2)直接用指针指向base2的首地址,强转int类型取前四个字节为虚表地址,这种方法也是比较推荐的一种。
总结:
由此可以看出子类继承了多少个父类就有几张虚表,两张虚表都会存放公有函数,子类自己的私有函数会存放在第一张虚表里。
而这里又产生了一个问题,这里一共重写了两次func1,一个base1的一个base2的,明明是重写的为什么地址不一样?
验证代码:
他们的指针偏移量不同,一个是p1指向的地址,一个是p2指向的地址:
根据汇编指令,就能得到以下的逻辑,base1经过几次jump的方法和base2jump的方法不一样,base2在其中一次jump中还让地址减8到base1的首地址,目的是为了访问整个derive。所以隐藏的this指针应该指向首地址,而base1不需要这么麻烦的原因是他的指针本来就是指向首地址。
4,菱形继承问题
class A
{
public:
virtual void func1()
{
cout << "A::func1" << endl;
}
public:
int _a;
};
//class B : public A
class B : virtual public A
{
public:
virtual void func1()
{
cout << "B::func1" << endl;
}
virtual void func3()
{
cout << "B::func3" << endl;
}
public:
int _b;
};
//class C : public A
class C : virtual public A
{
public:
virtual void func1()
{
cout << "C::func1" << endl;
}
virtual void func5()
{
cout << "C::func5" << endl;
}
public:
int _c;
};
class D : public B, public C
{
public:
virtual void func1()
{
cout << "D::func1" << endl;
}
virtual void func2()
{
cout << "D::func2" << endl;
}
public:
int _d = 1;
};
int main()
{
D d;
d.B::_a = 1;
d.C::_a = 2;
d._b = 3;
d._c = 4;
d._d = 5;
return 0;
}
问一:d中有几张虚表?
答:两张,几个父类几个虚表
问二:菱形虚拟继承有几张虚表?
答:两张,d自己单独一张虚表,b,c共一份虚表
问三:上图情况d有几张虚表
答:三张,这个情况很复杂
五,自主检验
1. 什么是多态?
答:
静态的多态:函数重载
动态多态:
(1),父类的指针或者引用调用虚函数。
(2),虚函数的重写,指向谁调用谁
2. 什么是重载、重写(覆盖)、重定义(隐藏)?答:参上
3. 多态的实现原理?答:参考本节课件内容
4. inline函数可以是虚函数吗?
答:可以,不过编译器就忽略inline属性,这个函数就不再是 inline,因为虚函数要放到虚表中去。
5. 静态成员可以是虚函数吗?
答:不能,因为静态成员(全局)函数没有this指针,使用类型::成员函数 的调用方式无法访问虚函数表,所以静态成员函数无法放进虚函数表。
6. 构造函数可以是虚函数吗?
答:不能,因为对象中的虚函数表指针是在构造函数初始化列表 阶段才初始化的。虚函数多态调用时,要到虚表找,但是虚函数指针都还没初始化
7. 析构函数可以是虚函数吗?什么场景下析构函数是虚函数?
答:可以,并且最好把基类的析 构函数定义成虚函数。这场景只有析构函数构成重写,delete时,构成多态,才能正确调用子类析构函数
8. 对象访问普通函数快还是虚函数更快?
答:首先如果是普通对象,是一样快的。如果是指针 对象或者是引用对象,则调用的普通函数快,因为构成多态,运行时调用虚函数需要到虚函 数表中去查找。
9. 虚函数表是在什么阶段生成的,存在哪的?
答:虚函数表是在编译阶段就生成的,一般情况 下存在代码段(常量区)的。
10. C++菱形继承的问题?虚继承的原理?
答:参上
11. 什么是抽象类?抽象类的作用?
答:参考(3.抽象类)。抽象类强制重写了虚函数,另外抽 象类体现出了接口继承关系。
本节内容到此结束!