一.多态是什么
多态是在不同继承关系的类对象,去调用同一函数,产生了不同的行为。
对于多态,不同的对象传过去,会调用不同的函数;
即多态调用看的是指向的对象。
//A,B类中的func函数是个多态
class A
{
public:
virtual void func()
{
cout << "A->func" << endl;
}
};
class B :public A
{
public:
virtual void func()
{
cout << "B->func" << endl;
}
};
动态绑定和静态绑定
多态分为两种:
1.静态绑定,也称为静态多态,是在程序编译阶段确定的,例如:函数重载和模板;
2.动态绑定,也称为动态多态,是在程序运行阶段确定的,根据具体拿到的类型确定程序的 具体行为,调用具体的函数。
二.虚函数
虚函数:即被virtual修饰的类成员函数称为虚函数;
虚函数一般是存在代码段(常量区)的,可能不同的编译器会不一样。
纯虚函数与抽象类
在虚函数的后面写上 =0 ,则这个函数为纯虚函数。包含纯虚函数的类叫做抽象类(也叫接口
类),抽象类不能实例化出对象。派生类继承后也不能实例化出对象,只有重写纯虚函数,派生
类才能实例化出对象。
抽象类的作用:抽象类强制重写了虚函数,另外抽象类体现出了接口继承关系。
接口继承和实现继承
实现继承:普通函数的继承是一种实现继承;
接口继承:虚函数的继承是一种接口继承,如果不实现多态,不要把函数定义成虚函数。
虚函数与静态成员函数
静态成员函数没有this指针,使用类型::成员函数的调用方式无法访问虚函数表,所以静态成员函数无法放进虚函数表,即静态成员函数不能设置成虚函数。
虚函数与 inline 函数
inline函数可以设置成虚函数,不过编译器就忽略inline属性,这个函数就不再是inline,因为虚函数要放到虚表中去。
三.多态的条件
虚函数重写(覆盖)条件
a.是虚函数,即要有 virtual ;
b.虚函数满足三同(返回值,函数名,参数列表相同)即构成重写;
例外:
a.派生类可以不加 virtual ,因为派生类已经继承了基类的 virtual;
b.协变(基类虚函数返回基类对象的指针或者引用,派生类虚函数返回派生类对象 的指针或者引用时,称为协变)
构成条件
1.调用的函数是重写的虚函数;
2.必须通过基类的指针或者引用调用虚函数。
重写析构函数
其实编译后析构函数的名称统一处理成destructor,此时析构函数的函数名相同,参数列表也相同,再加上 virtual 修饰,此时就重写了基类和派生类中的析构函数,即构成了多态。
结论
析构函数建议设置成虚函数,因为有时可能利用多态方式通过基类指针调用子类析构函 数,尤其是父类的析构函数强力建议设置为虚函数,这样动态释放父类指针所指的子类 对象时,能够达到析构的多态。
重载,重定义(隐藏)与重写
重载:在同一作用域,函数名相同,返回值可以不同,参数列表必须不同;
重定义(隐藏):在不同的作用域,一个在基类,一个在派生类,只要函数名相同就构成重定义;
重写:1.在不同的作用域,一个在基类,一个在派生类;
2.都必须是虚函数;
3.满足三同(函数名,返回值,参数列表相同(协变除外));
总结
1.重写比重定义的条件更加严苛;
2.两个基类和派生类的同名函数,不是重定义就是重写;
class A
{
public:
virtual void func() //func函数重写
{
cout << "A->func" << endl;
}
};
class B :public A
{
public:
virtual void func()
{
cout << "B->func" << endl;
}
};
void test(A& a) //基类引用
{
a.func();
}
int main()
{
A a;
B b;
test(a); //传A的对象,调用A类的函数
test(b); //传B的对象,调用B类的函数
return 0;
}
override 和 final
override:检查派生类虚函数是否重写了基类某个虚函数,如果没有重写编译报错;
final:修饰虚函数,表示该虚函数不能再被重写
class A
{
public:
virtual void func1() final {} //func1不能被重写
virtual void func2() {}
};
class B:public A
{
public:
virtual void func2() override {} //检查 func2 是否正确重写
};
四.多态的原理
虚函数表
其实基类和派生类的虚函数都会被放进虚函数表(简称虚表)里,类实例化出对象后会生成一个指针(_vfptr),指向虚函数表,其实虚函数表就是一个函数指针数组,里面存着虚函数的地址,一般情况这个数组最后面放了一个nullptr。
同一个类的对象共享一个虚表。
打印虚表
因为虚表指针一般存在对象的前4个字节(64位则为前8个字节),我们可以通过强制类型转换拿到这个虚表指针。
typedef void (*FUNC_PTR) (); //重定义函数指针
void print(FUNC_PTR* table)
{
for (size_t i = 0; table[i] != nullptr; i++) //需要注意每次运行时最好重新生成以下解决方
//案,因为多次编译vs可能就没有这个nullptr了
{
printf("%p : ", table[i]);
FUNC_PTR f = table[i];
f(); //调用函数指针所指向的函数
}
cout << endl;
}
class A
{
public:
virtual void func1()
{
cout << "A->func1" << endl;
}
virtual void func2()
{
cout << "A->func2" << endl;
}
};
int main()
{
A a;
int vft1 = *((int*)&a);
print((FUNC_PTR*)vft1);
return 0;
}
观察打印结果我们可以发现,打印的就是对象里的所有虚函数。
虚表生成
虚表指针其实是在初始化列表阶段初始化的,所以构造函数不能设置成虚函数;
虚表生成:
a.先将基类中的虚表内容拷贝一份到派生类虚表中 ;
b.如果派生类重写了基类中某个虚函数,用派生类自己的虚函数覆盖虚表中基类的虚函 数;
c.派生类自己新增加的虚函数按其在派生类中的声明次序增加到派生类虚表的最后;
🐬🤖本篇文章到此就结束了, 若有错误或是建议的话,欢迎小伙伴们指出;🕊️👻
😄😆希望小伙伴们能支持支持博主啊,你们的支持对我很重要哦;🥰🤩
😍😁谢谢你的阅读。😸😼