多态(polymorphism)是面向对象三大特征之一。同一行为,通过不同的子类,可以体现出来的不同的形态。举个最经典的例子就是不同的人去买票,对于这个相同的行为,学生买学生票,军人免票,成人买成人票,不同的类别执行不同的动作而呈现出来的形态叫多态。
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;
}
};
a.被调用的函数必须是虚函数,子类要实现对父类虚函数的重写(三同:参数,返回值,函数名相同)
b.必须得用父类指针或引用去调用,传值不行
a.三同之中返回值可以不同,但必须是父子关系的指针或引用(协变)
b.父类用virtue函数后,子类的重写可以不加virtue(接口继承)
ps:普通函数的继承是一种实现继承,而虚函数的继承是一种接口继承,派生类继承的是基类虚函数的接口,目的是为了重写,达成多态
重载:两函数在同一个作用域,函数名相同,参数不同,返回值无要求
重写(覆盖):两个函数分别在派生类和基类的作用域,函数名、参数、返回类型相同(协变除外),两个函数必须是虚函数
重定义(隐藏):两个基类和派生类的同名函数不构成重写就是重定义
在虚函数的后面写上 =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();
}
抽象类不能实例化出对象,一般一个类型在现实中没有对应的实体,我们就可以将它定义为抽象类。主要目的就是让子类去重写,更体现出接口继承
1.如果父类和子类有虚函数的实现,父类和子类对象中都有一个虚函数表指针指向虚表,当构成多态时,便会去虚表中找到虚函数的地址。
2.虚函数表本质是一个存虚函数指针的指针数组,一般情况这个数组最后面放了一个nullptr。类的虚函数表是一块连续的内存,每个内存单元中记录一个JMP指令的地址
3.派生类的虚表生成:a.先将基类中的虚表内容拷贝一份到派生类虚表中 b.如果派生
类重写了基类中某个虚函数,用派生类自己的虚函数覆盖虚表中基类的虚函数 c.派生类自己
新增加的虚函数按其在派生类中的声明次序增加到派生类虚表的最后
4.注意
虚表存的是虚函数指针,不是虚函数,虚函数和普通函数一样的,都是存在代码段的,只是
他的指针又存到了虚表中。另外实例出的对象中存的不是虚表,存的是虚表指针。那么虚表存在哪的呢?实际我们去验证一下会发现vs下是存在代码段(常量区)的
5.虚表什么时候产生? --编译阶段生成的
6.虚表指针是在构造函数的初始化列表时初始化的
总结:每个有虚函数的类有虚表,类的实例对象有虚表指针(一般是前4个字节),虚函数存在于代码段
1. 静态绑定又称为前期绑定(早绑定),在程序编译期间确定了程序的行为,也称为静态多态,
比如:函数重载,模板
2. 动态绑定又称后期绑定(晚绑定),是在程序运行期间,根据具体拿到的类型确定程序的具体
行为,调用具体的函数,也称为动态多态。
3.在运行期间,通过传递不同类的对象,编译器选择调用不同类的虚函数:编译期间,编译器主要检测代码是否违反语法规则,此时无法知道基类的指针或者引用到底引用那个类的对象,也就无法知道调用哪个类的虚函数。在程序运行时,才知道具体指向那个类的对象,然后通过虚表调用对应的虚函数,从而实现多态。
总结:普通调用是编译时确定函数地址,而多态调用是在运行时通过查虚函数表确定函数地址
由于在vs下虚表中最后以nullptr结尾(g++下不是),我们可以通过for循环遍历拿到虚表指针并打印出来
typedef void(*VF_PTR)();
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;
};
void PrintVFTable(VF_PTR table[])
{
for (int i = 0; table[i] != nullptr; i++)
{
printf("%p\n", table[i]);
VF_PTR f = table[i];
f();
}
}
int main()
{
Base b;
Derive d;
/*PrintVFTable((VF_PTR*)(*(int*)&b));
PrintVFTable((VF_PTR*)(*(int*)&d));*/
PrintVFTable((*(VF_PTR**)&b));
PrintVFTable((*(VF_PTR**)&d));
return 0;
}
运行结果:
自己可以理解下这里的参数传递,非常的细!
1.多继承派生类的未重写的虚函数放在第一个继承基类部分的虚函数表中
2.为何地址不同
第一个先call寄存器找到jmp的地址,jmp到func1的地址执行func1
第二个先jmp到伪func1(ptr2中的func1),再修正ecx(存的是this指针,减的是一个base1的大小),指向ptr1的func1,再jmp到func1执行。
换句话说因为先继承的base1所以base2要修正,如果先继承base2,则base1需要修正。
(1)虚函数在类中声明和类外定义的时候,virtual关键字只在声明时加上,而不能加在在类外实现上
(2)静态成员不可以是虚函数。因为静态成员函数没有this指针,使用类型::成员函数 的调用方式无法访问虚函数表,所以静态成员函数无法放进虚函数表。
(3)友元函数不属于成员函数,不能成为虚函数
(4)静态成员函数就不能设置为虚函数(原因:静态成员函数与具体对象无关,属于整个类,核心关键是没有隐藏的this指针,可以通过类名::成员函数名 直接调用,此时没有this无法拿到虚表,就无法实现多态,因此不能设置为虚函数)
(5)析构函数建议设置成虚函数,因为有时可能利用多态方式通过基类指针调用子类析构函数(尤其是父类的析构函数强力建议设置为虚函数,这样动态释放父类指针所指的子类对象时,能够达到析构的多态)
1.inline可以是虚函数吗?
可以,不过编译器就忽略inline属性,这个函数就不再是inline,因为虚函数要放到虚表中去。
2.静态成员可以是虚函数吗?答:不能,因为静态成员函数没有this指针,使用类型::成员函数
的调用方式无法访问虚函数表,所以静态成员函数无法放进虚函数表。
3. 构造函数可以是虚函数吗?答:不能,因为对象中的虚函数表指针是在构造函数初始化列表
阶段才初始化的。
4. 析构函数可以是虚函数吗?什么场景下析构函数是虚函数?答:可以,并且最好把基类的析
构函数定义成虚函数。
5. 对象访问普通函数快还是虚函数更快?答:首先如果是普通对象,是一样快的。如果是指针
对象或者是引用对象,则调用的普通函数快,因为构成多态,运行时调用虚函数需要到虚函
数表中去查找。
6. 虚函数表是在什么阶段生成的,存在哪的?答:虚函数表是在编译阶段就生成的,一般情况
下存在代码段(常量区)的。
7. C++菱形继承的问题?虚继承的原理?答:注意这里不要把虚函数表和虚基表搞混了。
8. 什么是抽象类?抽象类的作用?答:参考(3.抽象类)。抽象类强制重写了虚函数,另外抽
象类体现出了接口继承关系。
以上就是本文的全部内容,点个赞再走吧