解密C++多态属性
Posted AllenSquirrel
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了解密C++多态属性相关的知识,希望对你有一定的参考价值。
解密C++特性下的多态属性
-
多态
什么是多态?
多态就是在不同继承关系的类对象,去调用同一个函数,产生不同的行为
实现多态的前提条件:
- 多态属性产生在继承过程中
- 必须有虚函数(virtual关键字)
- 调用虚函数的类型必须是指针或引用
- 虚函数需要被重写
- 一般是通过父类指针或引用进行调用虚函数
根据以下代码测试可以发现,以上5点均被涉及,感兴趣可以仔细看一下代码,仔细体会5点实现条件
#include<iostream>
using namespace std;
class person {
public:
virtual void tickets()
{
cout << "全价票" << endl;
}
};
class student :public person
{
public:
virtual void tickets()
{
cout << "半价票" << endl;
}
};
void fun(person& pt)//父类引用调用
{
pt.tickets();
}
void test()
{
person p;
student stu;
fun(p);//传入父类引用,调用父类虚函数
fun(stu);//传入子类引用,调用子类虚函数
}
int main()
{
test();
return 0;
}
测试结果:
、
总结:多态:看所指向的对象 非多态:看调用的类型
主要区分:虚函数是virtual+函数 虚函数重写要求:函数名,参数列表,返回值和父类虚函数完全相同
返回值可以不是同一个类型,但必须是有继承关系的指针或引用,这种称之为协变
#include<iostream>
using namespace std;
class A {
};
class B :public A
{
};
class person {
public:
virtual A* tickets()
{
cout << "全价票" << endl;
return new A;
}
};
class student :public person
{
public:
virtual B* tickets()
{
cout << "半价票" << endl;
return new B;
}
};
void fun(person& pt)//父类引用调用
{
pt.tickets();//协变也构成虚函数
}
void test()
{
person p;
student stu;
fun(p);//传入父类引用,调用父类虚函数
fun(stu);//传入子类引用,调用子类虚函数
}
int main()
{
test();
return 0;
}
-
析构函数的多态应用
#include<iostream>
using namespace std;
class person {
public:
void tickets()
{
cout << "全价票" << endl;
}
~person()
{
cout << "~person" << endl;
}
};
class student :public person
{
public:
void tickets()
{
cout << "半价票" << endl;
}
~student()
{
if (_name)
{
delete[] _name;
cout << "delete" << endl;
}
cout << "~student" << endl;
}
private:
char *_name = new char[100];
};
void test()
{
person* p = new student;
delete p;//父类指针,调用父类析构
}
int main()
{
test();
return 0;
}
根据上述测试结果,在子类中开辟资源空间,并自定义析构函数释放资源,通过父类指针指向子类对象时,delete父类指针时,仅调用父类析构函数,并没有调用子类析构函数,对此,采用虚函数方式实现多态,指针指向子类对象则调用子类析构函数
一般情况下,会将析构函数定义为虚函数,产生多态行为,此时小伙伴可能会有疑问:子类和父类的析构函数并不同名,不满足产生多态的条件
事实上,子类析构函数经过编译器编译处理后,其实在底层二者是同名的,满足多态产生的条件
注:overide关键字 子类虚函数后,用于检查是否重写,如果没有重写基类虚函数则报错
final关键字,基类虚函数后,用于说明此虚函数在子类中不可被重写
-
抽象类和纯虚函数
纯虚函数:就是在基类中的一个函数接口,没有函数结构体,将其=0
抽象类:包含纯虚函数,且抽象类不能实例化,
.抽象类的派生类如果不实现纯虚函数,它也是抽象类
class A {
//定义纯虚函数
virtual void fun() = 0;
};
class B :public A
{
virtual void fun()
{
cout <<"B:fun()" << endl;
}
};
class C :public A
{
virtual void fun()
{
cout << "C:fun()" << endl;
}
};
class D :public A
{
};
-
重载、覆盖(重写)、隐藏(重定义)的对比
-
虚函数表
虚函数表本质是一个存虚函数指针的指针数组,数组每一个元素都存放一个指针,这个数组最后面放了一个nullptr
__vfptr为虚函数表指针,指向虚函数表首地址,则__vfptr为一个二级指针(为对象前四个字节内容即为虚表指针地址9)
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;
};
void test()
{
Base b;
Derive d;
}
int main()
{
test();
return 0;
}
根据上述测试结果发现:
- 子类会继承父类的虚表,fun1基类定义为虚函数,子类并重写fun1,覆盖导致在虚函数表地址变化,fun2基类定义为虚函数,子类没有重写,在虚表中地址没有变化
- 子类重写的虚函数,对应的重写虚函数地址会覆盖掉虚表中对应虚表指针
- 只有虚函数,才会将函数地址放在虚表中
- 虚表存放在代码段,虚表指针存放在对象中
根据对虚函数表的理解,我们也就明白,为什么多态要看所指向的对象?
就是因为,虚表指针存放在对象中,子类和父类虚表不同,继承过程父类虚表会继承,同时虚表指针此时指向子类虚表,部分发生重写,导致继承的虚函数地址改变,所以通过虚函数表指针找到对应虚函数是重写覆盖后的地址,也就是说,父类指针指向子类对象,调用子类重写的虚函数。
总结起来四个步骤:
- 从对象中获取虚表指针
- 通过虚表指针找到虚表
- 从虚表中找到虚函数地址
- 执行虚函数的指令
-
动态绑定与静态绑定
1. 静态绑定又称为前期绑定(早绑定),在程序编译期间确定了程序的行为,也称为静态多态,比如:函数重载,模板
2. 动态绑定又称后期绑定(晚绑定),是在程序运行期间,根据具体拿到的类型确定程序的具体行为,调用具体的函数,也称为动态多态。
-
单继承与多继承的虚函数表
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 (*vfptr)();
void printvftable(vfptr vtable[])
{
cout << "虚表地址:" << vtable << endl;
vfptr* fptr = vtable;
while (*fptr != nullptr)
{
(*fptr)();//不为空有虚函数存在 则执行虚函数
++fptr;
}
}
void test()
{
Base b;
Derive d;
cout << "Base: " << endl;
vfptr* vtable = (vfptr*)(*(int*)&b);
printvftable(vtable);
cout << "Derive: " << endl;
vtable = (vfptr*)(*(int*)&d);
printvftable(vtable);
}
int main()
{
test();
return 0;
}
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)();
void printvftable(vfptr vtable[])
{
cout << "虚表地址:" << vtable << endl;
vfptr* fptr = vtable;
while (*fptr != nullptr)
{
(*fptr)();//不为空有虚函数存在 则执行虚函数
++fptr;
}
}
void test()
{
Base1 b1;
Base2 b2;
Derive d;
cout << "Base1: " << endl;
vfptr* vtable = (vfptr*)(*(int*)&b1);
printvftable(vtable);
cout << "Base2: " << endl;
vtable = (vfptr*)(*(int*)&b2);
printvftable(vtable);
cout << "Derive第一个虚表(base1): " << endl;
vtable = (vfptr*)(*(int*)&d);
printvftable(vtable);
cout << "Derive第二个虚表(base2): " << endl;
vtable = (vfptr*)(*(int*)(char*)(&d+sizeof(Base1)));
printvftable(vtable);
}
int main()
{
test();
return 0;
}
根据上图,多继承关系下,两个虚表并非连续,存在一定的偏移量,这个偏移量即为继承的第一个父类大小sizeof(Base1)
子类新定义的虚函数,其函数指针存放在第一个虚表中
以上是关于解密C++多态属性的主要内容,如果未能解决你的问题,请参考以下文章