目录
一.多态的概念
二.多态的条件
三.对实现多态的条件进行解释
四.override和final
五.三重对比
六.虚函数表和虚函数表指针
七.静态的多态和动态的多态
一.多态的概念
二.多态的条件
什么是虚函数:
在类的成员方法前加上 virtual就表明该函数是一个虚函数,而且只有成员函数才能是虚函数。
什么是虚函数的重写:
在派生类中存在一个和基类中相同的函数名,相同的返回值,相同的参数的函数就被称为基类虚函数的重写(也叫覆盖)。 可以称为虚函数加三同。
这里强调一下,重写是语法层的定义,覆盖是底层原理上的定义,这个概念在后面虚表中有介绍到。
#include <iostream>
using namespace std;
class Person {
public:
virtual void BuyTicket() { cout << "买票-全价" << endl; }
};
class Student : public Person {
public:
virtual void BuyTicket() { cout << "买票-半价" << endl; }
};
void Func(Person& p)
{
p.BuyTicket();
}
int main()
{
Person ps;
Student st;
Func(ps);
Func(st);
return 0;
}
函数的多态调用看的是那个对象来调用,而函数的普通调用看的是调用 这个函数的对象的类型。
因为此时不满足调用重写函数的一定要是基类的引用或指针。
虚函数重写的一些特例:
首先前面提到过,虚函数重写的条件是,虚函数 + 三同。
1.在派生类中重写虚函数时可以不加virtual(但是在使用的时候建议都加上,因为这样会增加代码的可读性,即使不加virtual编译器在处理这块的时候还是会当作虚函数的重写来处理的),基类中的对应的虚函数一定要加上,才能保证多态。
问题:基类中的析构函数可以是虚函数吗?为什么?
基类中的析构函数加virtual后也是虚函数,那么为什么要将基类的析构函数也弄成虚函数呢?
下面看一个示例:
class Person {
public:
virtual void BuyTicket() { cout << "买票-全价" << endl; }
virtual ~Person() { cout << "~Person" << endl; }
};
class Student : public Person {
public:
virtual void BuyTicket() { cout << "买票-半价" << endl; }
virtual ~Student()
{
cout << "~Student()" << endl;
delete[] ptr;
}
protected:
int* ptr = new int[10];
};
int main()
{
Person* p = new Person;
delete p;
p = new Student;
delete p;
return 0;
}
此时如果不重写析构函数就会发生内存泄漏的问题。因为此时p是Person类的指针,所以如果没有虚函数的重写,那么此时就只会去调用基类的析构函数去清理Student类型中Person的部分,而不会去释放Student中自己的部分。
那么通过上面的定义也不满足多态的定义啊,为什么会实现多态呢?
因为类的析构函数会被处理成destructor这个统一的名称。
此时派生类和基类的析构函数就满足了虚函数加同了,然后又是基类指针调用,所以就可以实现多态的调用。
2.协变(在实际开发中并不常用) 理解即可
虚函数在重写时,函数的返回值可以不同,但是要求返回值必须是父子关系的指针和引用。
但是要保证基类中是指针派生类中就是指针,基类中是引用派生类就是引用。
三.对实现多态的条件进行解释
1.为什么一定要是基类的指针或引用?基类对象不可以吗,为什么?
- 因为只有基类的指针或引用才可以做到即可以调用基类又可以调用子类。
- 基类的对象在用派生类赋值时,会发生赋值转换,此时是切片拷贝,即使此时有虚表,也是基类自己的虚表,调用当然还是基类自己的函数。
2. 虚函数重写的本质是,当函数从基类中继承过来时,如果此时要对该函数进行重写,那么此时函数的原型并不会发生改变,只是将该函数的函数体进行重写。
所以也有,普通的函数继承是函数的实现继承,多态继承函数是函数的接口继承。
eg:
此时我们对代码进行更改
运行结果说明此时在派生类中重写的虚函数的this指针依然是派生类类型的指针,不会因为上面的定义而发生改变。this指针是根据类型是什么来形成的。
首先在main函数中定义了B类型的指针变量p,然后new一个B类型的空间让p这个指针变量来保存,然后通过p来调用test函数,而test函数是B类型从基类中继承过来的,B类型中并没有对该虚函数进行重写,所以该函数中this指针依然是基类类型的指针变量,所以此时也就满足了多态调用的第一个条件基类的指针或引用,而在test函数中调用func函数,此时派生类中是对该函数进行重写了的,那么此时两个多态调用的函数的都满足了,多态调用看的是调用该函数的是那个对象,是派生类对象调用那么就去调用派生类对象的的func函数,但是因为虚函数的多态继承是接口继承,所以此时的结果是:
四.override和final
在基类的虚函数中用final修饰表明该函数不能被它的派生类重写,如果重写会报错。
override帮助派生类检查是否完成了重写,如果没有会报错。
如果一个类不想被继承应该如何设计呢:
1.将基类的构造函数定义为私有就可以了。
2.使用final
五.三重对比
重载中函数的参数不同。
只要在继承的体系中派生类和基类的函数名相同就构成重定义,然后具体是否是重写要看基类中的相应函数是否是虚函数。所有会有重写的条件比重定义的条件更加苛刻的说法。
六.虚函数表和虚函数表指针
在64位平台下
这里会产出一个疑问:
为什么会是16字节啊,成员函数不是不在类型实例化时,每次都开辟新的空间吗?此时就引入了虚函数表的概念
当我们将类中的函数声明为虚函数时,那么此时类中就会形成一个虚函数表指针,在这个虚函数表指针中保存的是虚函数表的地址,在虚函数表中保存的都是我们声明为虚函数的函数的地址。
虚函数表其实本质上是一个函数指针数组,在这个数组中保存的是被我们声明为虚函数的函数的地址。
虚函数指针是保证这个函数指针数组的数组名的指针变量。
所以此时也就解释了为什么会是16个字节了,多余的字节用来保存虚表的地址了和内存对齐。
class Person {
public:
virtual void BuyTicket() { cout << "买票-全价" << endl; }
virtual ~Person() { cout << "~Person" << endl; }
};
class Student : public Person {
public:
virtual void BuyTicket() override { cout << "买票-半价" << endl; }
virtual ~Student()
{
cout << "~Student()" << endl;
delete[] ptr;
}
protected:
int* ptr = new int[10];
};
int main()
{
Person p;
Student s;
return 0;
}
所以此时就解释了为什么,在多态调用时,都是基类的引用或指针但是最后的结果却并不相同。
当基类指针或引用指向基类对象时,回去基类对象中的找到虚函数表指针,通过虚函数表指针找到虚函数表,在虚函数表中找到相应的函数的地址完成调用,在通过基类的引用或指针调用派生类时同理。
在虚函数表的内存空间中会在结尾用,nullptr也就是0来表示结尾:(64位平台下)
在不同的平台下可能结果并不相同 。在vs中是这样但是在g++中就会在结尾加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;
};
int main(void)
{
Derive d;
cout << sizeof(d) << endl;
return 0;
}
此时这个代码的输出结果是多少:20(32位)
为什么会是20个字节呢?因为是多继承所以在Derive这个类型中存在Base1子成员又存在Base2子成员 还有它自己的成员,Base1是8个字节Base2同理,然后将Derive中自己的成员是4字节,最后在内存对齐,可得结果是20字节。
因为派生类不会生成自己的虚表所以,在此时的多继承中存在两个虚表。然后在派生类中发生了对两个基类中函数的重写,然后将重写后的函数的地址分别填写到两个虚表中。
但是派生类中新建立的虚函数func3是保存在两个虚表中那个表中呢?
通过测试可知保存在第一个虚表中。
测试代码
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(*FUNC_PTR) ();
void PrintVTable(FUNC_PTR* vTable)
{
// 依次取虚表中的虚函数指针打印并调用。调用就可以看出存的是哪个函数
cout << " 虚表地址>" << vTable << endl;
for (int i = 0; vTable[i] != nullptr; ++i)
{
printf(" 第%d个虚函数地址 :0X%x,->", i, vTable[i]);
FUNC_PTR f = vTable[i];
f();
}
cout << endl;
}
int main(void)
{
Derive d;
cout << sizeof(d) << endl;
PrintVTable((FUNC_PTR*)(*(int*)(&d)));
return 0;
}
但是通过观察我们发现,此时派生类中对Base1和Base2中的func1函数进行重写但是两个虚表中所保存的地址却不相同,但是多态调用后的结果却相同,这是为什么呢。
七.静态的多态和动态的多态
1.静态的多态
静态的多态是指函数的重载,也称编译是的多态,被调用函数的地址在编译的时候就要被确定。
模板属于编译时多态。
2.动态的多态
动态的多态是指通过继承,虚函数的重写,实现的多态,也称为运行时的多态,是程序运行起来后通过虚函数表中记录的函数的地址来找到被调用的函数的。