C++多态
Posted ℳℓ白ℳℓ夜ℳℓ
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了C++多态相关的知识,希望对你有一定的参考价值。
C++多态
多态的概念
多态的概念:通俗来说,就是多种形态,具体点就是去完成某个行为,当不同的对象去完成时会产生出不同的状态
举个例子:比如说买票,普通人是全价买,学生是半价,退伍军人是优先。
多态的定义与实现
多态的构成条件与虚函数
多态很重要的前提就是先继承。
并且要去用基类的指针或者是引用去调用虚函数
被调用的函数必须是虚函数,且派生类必须对基类的虚函数进行重写。
#include<iostream>
using namespace std;
class Person
public:
virtual void BuyTicket() cout << "买票-全价" << endl; //成员函数前面加一个virtual就成为虚函数
;
class Student:public Person
public:
//这里是虚函数的 重写/覆盖
virtual void BuyTicket() cout << "买票-半价" << endl; //条件是三同:返回值和函数名还有参数相同
;
int main()
Person s1;
Student s2;
Person* p = &s1;
p->BuyTicket();
p = &s2;
p->BuyTicket();
return 0;
这里也叫做多态调用。
之前的调用都是普通调用,一直都和对象的类型有关。
多态调用是跟指向的对象有关。
如果改成普通调用就是类型是谁就去调用谁的成员函数,多态调用就是指向的对象是谁就去调用谁的虚函数。
虚函数的重写
子类虚函数可以不加virtual:
#include<iostream>
using namespace std;
class Person
public:
virtual void BuyTicket() cout << "买票-全价" << endl;
;
class Student:public Person
public:
//这里是虚函数的 重写/覆盖
void BuyTicket() cout << "买票-半价" << endl; //只要三同,子类不加virtual也是虚函数
int main()
Person s1;
Student s2;
Person* p = &s1;
p->BuyTicket();
p = &s2;
p->BuyTicket();
return 0;
不过这里建议都加上virtual。
协变:
三同中,返回值可以不同,但是要求返回值必须是一个父子类关系的指针或者引用。
#include<iostream>
using namespace std;
class Person
public:
virtual Person* BuyTicket() cout << "买票-全价" << endl; return this;
;
class Student:public Person
public:
virtual Student* BuyTicket() cout << "买票-半价" << endl; return this;
;
int main()
Person s1;
Student s2;
Person* p = &s1;
p->BuyTicket();
p = &s2;
p->BuyTicket();
return 0;
正常运行。
析构函数的重写
#include<iostream>
using namespace std;
class A
public:
~A()
cout << "delete s1" << endl;
delete[] s1;
protected:
int* s1 = new int[20];
;
class B :public A
public:
~B()
cout << "delete s2" << endl;
delete[] s2;
protected:
int* s2 = new int[20];
;
int main()
A a;
B b;
return 0;
目前看来确实没什么问题,都是正常调用,来看看如下的情况:
这里导致了内存泄漏,因为析构函数不是虚函数,只能完成普通调用,所以最好在析构面前加一个virtual。
这下子就可以了。
其实子类不加virtual这里更合适,更方便。
所以在实现父类的时候,最好无脑的给析构函数加virtual。
C++11 override 和 final
final:
如何实现一个不被继承的类?
C++11提供了一个关键字,类定义的时候加final:
如果放在父类的某个虚函数后面就是不让这个虚函数被重写。
但是这个情况很少见。
override:
检查派生类虚函数是否重写了基类某个虚函数,如果没有重写编译报错。
重载、覆盖(重写)、隐藏(重定义)的对比
抽象类
在虚函数的后面写上 =0 ,则这个函数为纯虚函数。包含纯虚函数的类叫做抽象类(也叫接口类),抽象类不能实例化出对象。派生类继承后也不能实例化出对象,只有重写纯虚函数,派生
类才能实例化出对象。纯虚函数规范了派生类必须重写,另外纯虚函数更体现出了接口继承。
#include <iostream>
using namespace std;
class A//抽象类
public:
virtual void add() = 0 ;//纯虚函数
;
class B:public A
public:
void add()
;
int main()
B s;//但是A仍然不能实例化
return 0;
这就是说给某个函数必须进行重写。
抽象类一般用于,比如说车,他是一个概念,但是他有自行车,电动车,跑车等等,然后还被分为好多的品牌,所以车必须要分类出来。
接口继承和实现继承:
普通函数的继承是一种实现继承,派生类继承了基类函数,可以使用函数,继承的是函数的实现。虚函数的继承是一种接口继承,派生类继承的是基类虚函数的接口,目的是为了重写,达成多态,继承的是接口。所以如果不实现多态,不要把函数定义成虚函数。
下面程序输出什么?
#include <iostream>
using namespace std;
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;
首先,创建的是子类对象,子类对象去调用虚函数test(),然后里面是调用func(),这里要注意,是一个多态调用,因为test成员函数是属于A类的,调用func函数是通过this指针去调用(就算是test函数被子类继承了,内部的this指针也不会被更换,还是A类的this指针),并且func函数也进行了重写,在main函数中调用的也是子类对象,所以走向的是B类中的func函数。
这里最让我们疑惑的就是为什么是1不是0,这里就涉及到了只继承接口,所以val的缺省值还是1。
但是子类的缺省参数并不是一点用处都没有,当普通调用的时候这个缺省参数就可以使用了。
再看一个程序:选哪个?
#include <iostream>
using namespace std;
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;
A:p1 == p2 == p3
B:p1 < p2 < p3
C:p1 == p3 != p2
D:p1 != p2 != p3
数据模型大概是这样的:
所以选C。
这里注意一下:其实继承的对象在内存里是从下面开始放,因为下面是低地址,上面是是高地址,我们经常能看到一个数组,用数组名+n就能到对应的位置,这就是为什么从低地址放的原因,加就代表要到高地址。
多态原理
虚函数表
先来研究一下这个类的大小:32位环境下
#include<iostream>
using namespace std;
class Base
public:
virtual void Func1()
cout << "Func1()" << endl;
private:
int _b = 1;
;
int main()
cout << sizeof(Base) << endl;
return 0;
这里明明只有一个成员变量,之前说过成员函数并不在类中,可是为什么结果是8呢?
这里多出来了一个_vfptr,这个叫做虚表/虚函数表,里面储存的是虚函数的地址。
#include<iostream>
using namespace std;
class Base
public:
virtual void Func1()
cout << "Func1()" << endl;
virtual void Func2()
cout << "Func2()" << endl;
void Func3()
cout << "Func3()" << endl;
private:
int _b = 1;
;
int main()
cout << sizeof(Base) << endl;
Base a;
return 0;
原理与动静态绑定
多态的原理一定跟虚表有着千丝万缕的联系。
再来看看完成重写有什么区别;
#include<iostream>
using namespace std;
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;
这里虚表也变了,之前重写也可以叫做覆盖,这里就是覆盖的部分。
其实重写只是语法上的,继承了父类的接口,重写了实现部分。覆盖就是覆盖了父类继承过来重写的虚函数的地址。
那么我们这样调用试一下:
多态调用更长。
这里差别就在于,根本不在乎是指向哪里,因为有虚表的存在,如果指向父类就去父类的虚表中找,如果指向子类就去子类的虚表中找。
在汇编当中eax里面存的就是虚表指针数组。
1.静态绑定又称为前期绑定(早绑定),在程序编译期间确定了程序的行为,也称为静态多态,比如:函数重载。
2.动态绑定又称后期绑定(晚绑定),是在程序运行期间,根据具体拿到的类型确定程序的具体行为,调用具体的函数,也称为动态多态。
那么虚表是放在哪一个位置呢?
打印出来的地址和常量区非常接近,所以是在常量区。
单继承与多继承关系的虚函数列表
单继承的虚函数表
#include <iostream>
using namespace std;
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;
;
int main()
Base b;
Derive d;
return 0;
在VS当中其实并不能看到虚表当中所有的虚函数,这时VS编译器的一个优化,也可以看作是一个BUG。
这个时候我们可以用内存窗口去看。
这里也将func3和func4的函数地址给显示出来,顺便说一下,在VS编译器下,虚表是以空指针结尾的。
但是这样看有些麻烦,我们想个办法给他打印出来。
#include <iostream>
using namespace std;
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(*p)();
void PrintVFTbale(p vft[])//打印虚表
for (int i = 0; vft[i]; i++)
printf("[%d]:%p->", i, vft[i]);//打印虚表当中每个数组的内容,也就是每个虚函数的地址
vft[i]();//调用对应的函数
int main()
Base b;
Derive d;
PrintVFTbale((p*)(*(int*)&b));//将虚表的地址传过去
PrintVFTbale((p*)(*(int*)&d));
return 0;
这里还可以改进,因为有时候是64位和32位,到时候64位就是取头8个字节了。
其实只需要将里面的变成二级指针就行了(任何类型的二级指针都可以),因为二级指针是储存一级指针的,解引用之后再去看解引用多大时,剩下的就是一级指针,一级指针就可以根据平台位数变化了,到时候就对应了64位和32位的平台大小了。
多继承的虚函数表
#include <iostream>
using namespace std;
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(*VFPTR) ();
typedef void(*p)();
void PrintVFTbale(p vft[])//打印虚表
for (int i = 0; vft[i] != nullptr; i++)
printf("[%d]:%p->", i, vft[i]);
vft[i]();
int main()
Base1 b1;
Base2 b2;
PrintVFTbale((p*)(*(void**)&b1));
PrintVFTbale((p*)(*(void**)&b2