目录标题
- 为什么会有多态
- 什么是虚函数的重写
- 多态的定义
- 特殊的重写
- 重载,覆盖(重写),隐藏(重定义)的对比
- final和override
- 抽象类
- 多态的原理
- 验证虚表所在额度位置
- 多继承的多态原理
- 菱形虚拟继承
- 多态的一些小点
为什么会有多态
大家在平时的生活中肯定买过火车票,购买火车票的时候一般会遇到一些特有的福利,比如说作为保家卫国的军人他们在买票的时候就能够享受优先买篇和优惠买票的权力,其次就是学生买票,学生作为刚步入社会的群体他们在一些方面并没有成熟,所以在买票的时候就可以享受到优惠买票的权力但享受不到优先买票的权力,剩下得的就是广大群众他们在买票的时候就享受不了任何的权力,既不能优先买也不能优惠买票,那么上述的例子就是一个多态的过程,在我们的生活中多态的例子还有很多比如说在大家扫码支付的时候就会有一个扫码领红包的功能,这个红包不同人领取的就是不同的情况,比如说有些人他平时不怎么使用支付宝,但是每次使用支付宝支付的金额就非常的大,每次支付就是成百上千,那么这种人在扫支付宝红包的时候一般金额就很大每次扫都是几十块,而像我这种穷屌丝天天用支付宝付款一天付个10几次但是每次付款的金额都是几块钱最大也就是10几块钱,那么像我这样的人扫支付宝的红包,那每次扫就是几分钱了不起也就几毛钱,那么这也是一个多态的情况,所谓的多态就是不同的人做同一件事却有着不同的过程不同的结果,这就是多态要,比如说普通人和军人买同一张火车票,那军人的购买路径购买窗口购买价格就和普通人是完全不一样的,希望大家能够理解,看到这里想必大家应该已经知道了什么是多态,以及为什么会有多态,那么接下来我们就要看看什么是虚函数的重写。
什么是虚函数的重写
对于虚这个词我们第一次接触是在继承的那一个章节,我们说在继承方法的前面加上一个virtual这样继承的方式就变成了虚拟继承,虚拟继承可以解决菱形继承所带来的数据冗余,和访问数据二义性的问题,在继承方法的前面加上virtual就是虚拟继承,那么虚函数就是在函数的声明前面加上virtual就可以变成虚函数,虚函数一般只存在于类中比如说下面的代码:
#include<iostream>
using namespace std;
class tmp
{
public:
virtual void A()
{
cout << "我是父类" << endl;
}
private:
int x = 10;;
};
那么这个函数A就是一个虚函数,好我们知道了什么是虚函数,那虚函数的重写(也可以叫做虚函数的覆盖)就是:派生类中有一个跟基类完全相同的虚函数(既派生类函数与基类虚函数的返回值类型,函数名字,参数列表完全相同),称子类的虚函数重写了基类的虚函数。比如说下面的这段代码:
class tmp_father
{
public:
virtual void A()
{
cout << "我是父类" << endl;
}
private:
int x = 10;;
};
class tmp_child :public tmp_father
{
public:
virtual void A()
{
cout << "我是子类" << endl;
}
private:
int y = 20;
};
子类也有一个名为A的虚函数,并且这两个虚函数的返回值和参数都相同,所以我们就称子类名为A的虚函数和父类名为A的虚函数构成虚函数的重写。
多态的定义
知道了什么是虚函数的重写那么多态的定义就能很好的给大家进行讲解,首先构成多态得有两个条件:
第一个:
被调用的函数必须是虚函数,且子类必须对父类的虚函数进行重写。那这里我们就可以举一个买票的例子,父类是普通人然后有两个子类分别为学生和军人,类里面都有一个名为buy_ticket的虚函数,由于普通人没有任何优惠所以在父类的buy_ticket函数里面就可以直接打印不能优先买票,不能享受优惠买票,比如说下面的代码:
class person
{
public:
virtual void buy_ticket()
{
cout << "你是一名普通人" << endl;
cout << "不能优先买票" << endl;
cout << "不能优惠买票" << endl;
}
private:
};
军人类和学生类都得继承person类并且在类中也有一个名为buy_ticket的函数,并且为了保证虚函数重写,这个函数的参数和返回值得跟父类的buy_ticket函数的参数和返回值相同,那这里我们的代码就如下:
class student :public person
{
public:
virtual void buy_ticket()
{
cout << "你是一名学生" << endl;
cout << "不能优先买票" << endl;
cout << "可以优惠买票" << endl;
}
private:
};
class soldier :public person
{
public:
virtual void buy_ticket()
{
cout << "你是一名军人" << endl;
cout << "可以优先买票" << endl;
cout << "可以优惠买票" << endl;
}
private:
};
这样我们就完成了多态的第一个条件。
第二个:
要想实现多态必须通过父类的指针或者引用调用虚函数比如说下面的代码:
int main()
{
person person_tmp;
student stundet_tmp;
soldier soldier_tmp;
person* p = &person_tmp;
p->buy_ticket();
p = &stundet_tmp;
p->buy_ticket();
p = &soldier_tmp;
p->buy_ticket();
return 0;
}
这段代码的运行结果如下:
可以看到这里是同一个类型的指针指向不同类型的对象调用同名的函数,但是执行的内容确实不一样的,那么这就是实现多态的步骤,首先得有虚函数重写,然后再用父类的指针或者引用来调用虚函数这样就可以实现多态,这里要注意的一点就是必须得通过父类来调用虚函数,因为不管父类还是子类赋值给父类的指针或者引用时都会发生切片,这样两者都可以都可以正常调用虚函数,而子类的指针或者引用来进行调用如果是传过来一个父类的指针或者引用是会报错无法正常的运行下去的。这里大家要注意的一点就是多态的调用跟指针和引用指向的对象有关,指针要是指向student的话就执行student里面的重写虚函数,指针要是指向soldier的话就执行soldier里面的重写虚函数,而普通函数的调用则和指针的类型有关跟指向的对象是没有关系的,指针的类型是父类就算指针指向的是student或者soldier类调用的函数还是父类里面的函数,比如说将上面虚函数的virtual全部都删除,然后再执行上面的代码就可以看到下面的运行结果:
那么这就是多态调用的一个点希望大家能够理解。
特殊的重写
第一个
子类的函数不写virtual也可以实现多态,原因是在子类中就算不写父类中的函数,通过继承在子类中也会有该函数的声明和实现,而我们自己写的函数加上virtual也可以实现重写,但是这里的重写,重写的是函数的实现函数的声明仍然是父类的所以依然可以构成重写。比如说下面的代码:
class A
{
public:
virtual void tmp()
{
cout << "我是父类的函数" << endl;
}
private:
};
class B :public A
{
public:
void tmp()
{
cout << "我是子类的函数" << endl;
}
private:
};
这里的子类函数 没有加virtual但是通过下面的测试代码大家可以发现这里依然可以实现重写:
void test1()
{
A a;
B b;
A* ap;
ap = &a;
ap->tmp();
ap = &b;
ap->tmp();
}
这段代码的运行结果如下:
虽然在实现多态的时候子类可以不加virtual,但是我们还是建议父类和子类虚函数全部都加上virtual。
第二个
协变(基类与派生类虚函数的返回值类型可以不同)派生类重写基类函数时与基类虚函数的返回值类型不同,即基类虚函数返回基类的指针或者引用,派生类虚函数返回派生类或者基类的指针或者引用时称为协变。简单的说就是三同中返回值可以不同,但是这里的返回值必须是一个父子类关系的指针或者引用,一旦有一个函数的返回值不同,则所有的重写函数都得返回父子类指针,这里返回不仅仅只返回自己类的指针或者引用,还可以返回起来类型的指针或者引用,这里了解一下就行,生活中用到的很少,比如说下面的代码:
class A {};
class B : public A {};
class Person
{
public:
virtual A* f()
{
return new A;
}
};
class Student : public Person
{
public:
virtual B* f()
{
return new B;
}
};
通过下面的测试用例可以看到上面的代码是没有问题的:
void test1()
{
Person a;
Student b;
Person* ap;
ap = &a;
ap->f();
ap = &b;
ap->f();
}
代码的运行结果如下;
上面说到一旦有一个类返回了其他父子类的指针,那么其他类也得返回父子类的指针,比如说下面的代码:
class A {};
class B : public A {};
class Person
{
public:
virtual A* f()
{
return new A;
}
};
class Student : public Person
{
public:
virtual void f()
{
}
};
这段代码是编译不过去的,会报出下面的错误:
并且父子类在返回其他类的父子指针时,这两个指针指向的类也得构成父子关系,不能说子类返回的指针确实指向子类,父类返回的指针确实指向父类,但是这两个类根本就没有关系,比如说下面的代码:
class A {};
class B : public A {};
class C {};
class D : public C {};
class Person
{
public:
virtual A* f()
{
return new A;
}
};
class Student : public Person
{
public:
virtual D f()
{
return new D;
}
};
这段代码运行一下就会报出错误:
那么这就是协变的概念,用的地方不多希望大家能够理解。
第三个
析构函数的重写(基类与派生类析构函数的名字不同)如果基类的析构函数为虚函数,此时派生类析构函数只要定义,无论是否加virtual关键字都与基类的析构函数构成重写,虽然基类与派生类的析构函数名字不同,这有点违背重写的规则,但是编译器会对析构函数的名称做出特殊的处理,编译之后析构函数的名字统一变成了destruct,比如说下面的代码:
class Person {
public:
~Person()
{
cout << "~Person()" << endl;
cout << _p << endl;
delete[] _p;
}
protected:
int* _p = new int[10];
};
class Student : public Person {
public:
~Student()
{
cout << "~Student()" << endl;
cout << _p << endl;
delete[] _p;
}
protected:
int* _p = new int[10];
};
int main()
{
Person* p1 = new Person;
Person* p2 = new Student;
delete p1;
delete p2;
return 0;
}
在实现析构函数的时候可以无脑给析构函数加上virtual,因为delete函数会分为两部:首先是调用operator delete(ptr)来释放申请的空间,在空间被释放的时候又会根据指针来调用析构函数来进一步释放空间,当析构函数不加virtual的时候delete p1 和delete p2的底层实现为operator delete(ptr),这里会把指针做为参数进行传递,如果不加virtual的话就是普通调用这时候调用的函数跟指针的类型有关,传过来的是person类型的指针就调用person的析构函数,所以这里就会有出现问题p1和p2虽然指向的对象不同但是类型是相同都为父类的指针,delete函数会根据传过来的指针来调用对应的析构函数,这里指针类型的都是person*所以都会调用父类的析构函数这就会导致子类中的父类数据被释放掉了,但是子类独有的那份数据没有被释放掉,所以就会导致内存泄漏,比如说上面的代码运行结果如下:
但是加了virtual就不会出现这样的问题,因为virtual函数的调用并不是根据指针的类型来调用的函数,而是根据指针指向的对象来调用对应的函数,子类和父类的析构名全部都会被改成destruct,所以delete(ptr)并不是通过ptr调用student或者person的析构函数,而是ptr直接调用destruct函数,ptr指向父类就调用父类的destruct,ptr指向子类就调用子类的destruct,这也是一个多态的情况,所以编译器为了实现析构函数的多态调用,就把析构函数的名字统一改成destruct。
重载,覆盖(重写),隐藏(重定义)的对比
当两个名字相同但是参数和返回值不同的函数在同一个作用域里面的话就会构成重载,这里有个很关键的点就是同一个作用域,如果说两个名字相同的函数,但是不在同一个作用域的话他们依然是不会构成重载的,比如说下面的func函数就构成了重载:
class tmp
{
public:
void func(int x)
{
cout << "x的值为:" << x << endl;
}
void func(double x)
{
cout << "x的值为: " << x << endl;
}
};
如果两个同名函数分别在派生类和基类的话就构成隐藏(重定义),比如说下面的代码:
class A
{
public:
void func(int x)
{}
};
class B
{
public:
int func(int x ,int y)
{
return x + y;
}
};
上面的func函数就构成重写,如果两个虚函数的函数名相同,参数相同,返回值相同(协变除外)并且处于基类和派生类的话就构成重写,比如说下面的代码:
class A
{
public:
virtual int func(int x,int y)
{
return x;
}
};
class B
{
public:
virtual int func(int x ,int y)
{
return x + y;
}
};
这里的两个func函数就构成了函数的重写。
final和override
在之前的学习中我们知道通过将构造函数私有化可以防止该类被其他的函数继承,比如说下面的这段代码:
class A
{
public:
int x;
int y;
private:
A(int _x= 10, int _y= 20)
{
x = _x;
y = _y;
}
};
class B :public A
{
public:
B(int x, int y)
:x1(x)
,y1(y)
{}
private:
int x1;
int y1;
};
通过这样的方法我们就无法实例化出来B的对象,因为在B的初始化列表里面得调用A的构造函数,但是A的构造函数是私有的B无法访问所以就无法创建出来B对象,所以A类就无法被继承,如果我们创建B类的话就会报下面的错误:
同样的道理将私有类的析构函数也能够在一定程度上阻止该类被继承,比如说下面的代码:
class A
{
public:
A(int _x = 10, int _y = 20)
{
x = _x;
y = _y;
}
int x;
int y;
private:
~A()
{}
};
这时我们在创建一个B类的话就会报出下面的错误:
上面的两个方法都是通过私有成员函数来实现的不能重写,c++自己提供也提供了一个方法就是定义类的时候在类名的后面加上final就可以让这个类无法被继承,比如说下面的代码:
class A final
{
public:
A(int _x = 10, int _y = 20)
{
x = _x;
y = _y;
}
int x;
int y;
~A()
{}
private:
};
这时再创建一个B对象就会报出下面的错误:
那么这就是final的作用。override的功能就是判断函数是否发生了重写,这个是编译是报错,用法就是在函数定义的括号后面添加override,如果没有完成重写就会报错,如果该函数实现重写了就不会报错,比如说下面的这段代码:
class A
{
public:
void func(int x, int y)
{
}
private:
};
class B :public A
{
public:
void func(int y) override
{
}
private:
};
由于func函数没有实现重写并且后面加上了override,所以这段代码在编译的时候就报错了:
如果实现了重写并加上了override的话还是不会报错的比如说下面的代码:
class A
{
public:
virtual void func(int x, int y)
{
}
private:
};
class B :public A
{
public:
virtual void func(int x,int y) override
{
}
private:
};
代码的运行结果如下:
那么这就是override的用法希望大家能够理解。
抽象类
在虚函数的后面写上=0,则这个函数为纯虚函数。包含纯虚函数的类叫做抽象类(也叫接口类),抽象类不能实例化出对象。派生类继承后也不能实例化出对象,只有重写纯虚函数之后派生类才能实例化出对象,纯虚函数规范了派生类必须重写,比如说下面的代码:
class A
{
public:
virtual void func(int x, int y) = 0;
private:
};
class B :public A
{
public:
private:
};
这时我要是想通过实例化出来B的对象的话就编译不过去:
说B无法实例化出来对象,但是我们将父类的纯虚函数进行重写的话就会就又可以正常地运行了,纯虚函数更体现了接口继承。这里提一下什么事接口继承什么是实现继承,普通的继承都是实现继承,虚函数的继承就是接口接口继承,也就是将函数的声明继承下来了这里可以看个题
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()
{
B*p=new B;
P->test();
}
test在A类里面,虽然这个函数被继承到B类里面,但依然可以是通过A类的this指针调用func函数,但是这里是B类型的指正调用test函数,所以this指针虽然是A类型,但是他指向的是一个B对象,所以this指针在调用func函数的时候会指向B类型的func,因为虚函数在继承的时候是接口继承,所以函数的声明是A类的,所以打印的结果就是B->0,当我们把test函数放到B类中时我们会发现这里调用执行的结果是B->1因为这里是子函数类型的指正调用的函数不符合重写的条件,所以是B->1,看了这个题大家应该能够更好的理解多态本质是就是一个接口继承,哎那这是不是就说明了一点既然多态是接口继承的话那子函数是不是可以不写virtual呢?答案是确实是可以的,但是我们平时写的时候主要为了方便我们自己查看,所以大家还是添加一下为好。
多态的原理
在了解多态的原理之前我们先来看看下面这段代码:
class E
{
public:
void func()
{}
private:
int x;
};
int main()
{
cout << sizeof(E) << endl;
return 0;
}
根据之前的学习我们知道类中函数是没有存储在对象里面的,而是存储在一个公共的位置,所以类E的大小实际上就是整型变量x的大小也就是4
我们将上面的代码进行修,将类中的普通函数修改成为虚函数的话整体的大小会发生变化吗?这时候有些小伙伴就会想肯定不会发生变化嘛,虚函数不也是函数嘛,函数就不会存储只能对象里面,所以E类创建出对象的大小依然是4,但是再运行一下上面的代码就会发现打印的结果变成了8:
那这里我们就得创建出来一个对象通过调试的方法查看对象里面的内容才能发现其中的问题,比如说下面的图片:
我们发现类对象里面除了整型变量x以外还有一个名为_vfptr的指针,指针的大小刚好也是4个字节,所以我们上面的代码打印出来就是8个字节,那么这个_vfptr(virtual function ptr)指针指向了一个表,这个表可以叫虚函数表或者虚表,这个表里面装的是虚函数的地址,父进程有虚表那么继承父进程的子进程也会有个虚表,子类的虚表相当于是将父类的虚表进行一下拷贝,如果子类重写哪些函数的话就会将对应位置的虚函数地址覆盖成重写之后的函数,所以重写的另外一个名字可以叫做覆盖,普通调用可以看成是编译时/静态绑定,多态调用则可以看成是运行时/动态绑定,多态调用的时候父类的指针要么指向父类的本体,要么就是指向子类对象的一部分,但是不管他们指向哪里其中都包含一个虚表,不同类的虚表的内容是不一样的,当父类的指针或者引用指向不同类对象的时候,实际上这些指针也就指向了不同类的虚表,虚表的内容是不一样的,当你要调用一个虚函数的时候指针就会去不同类的虚表里面进行查找从而执行该函数的内容,这里大家要注意的一点就是只有虚函数会进入虚表,普通函数是不会进入虚表的。监视有时候看不到完整的部分,比如说下面这段代码:
class E
{
public:
virtual void func1(){cout << "我是父类的func1函数" << endl;}
virtual void func2(){cout << "我是父类的func2函数" << endl;}
virtual void func3(){cout << "我是父类的func3函数" << endl;}
virtual void func4(){cout << "我是父类的func4函数" << endl;}
private:
int x= 10;
};
class F :public E
{
public:
virtual void func1(){cout << "我是子类的func1函数" << endl; }
virtual void func2(){cout << "我是子类的func2函数" << endl;}
virtual void func5(){cout << "我是子类的func5函数" << endl;}
virtual void func6(){cout << "我是子类的func6函数" << endl;}
private:
int y = 10;
};
这时我们通过调试来查看内容时就会发现子函数的有些虚函数不在表里面:
子函数的func5和func6函数不在这个虚表里面,难道他们不是虚函数吗?肯定是的,但是编译器在这里进行一系列的骚操作导致我们看不到这两个函数的地址,所以这时候就得在内存上查看虚表额度内容,大家来看看下面的图片:
虚表是以空指针作为表的结尾,那么这里大家就可以看到虚表的内容中确实含有6个地址,并且前四个时继承的父类的函数并对前两个继承重写的,最后两个就是子类独有的虚函数。
验证虚表所在额度位置
通过上面的代码想必大家能够知道多态的原理是什么以及虚表是什么,那这里就有个问题:虚表存储在内存中的哪个位置呢?那要想知道这个问题我们得先打印出来虚表的地址,如果一个类中含有一个虚函数,那么这个类中就会会还有一个_vfptr指针,如果是单继承的话这个指针就存放在类中的开头,所以我们就可以创建一个类,通过取地址的方式得到这个类的起始地址,这个地址的类型肯定是类指针类型,所以我们得通过强制类型转换的方式将这个地址改成int类型,这样解引用的话就能够一下子获取4个字节的内容*((int*)&tmp)
,然后我们就得到了头部的_vfptr指针,这个时候就可以通过打印的方式得到这个指针指向的地址,因为此时地址的类型是int所以打印的时候会将地址看成整型进行打印,所以我们还得对这个地址进行类型转换使其变成void*类型,这个时候打印的内容就变成了指针指向的内容不会进行任何的转换(void*)*((int *)&tmp)
,那么这个比如说下面的代码,就可以打印出来虚表的地址,并且得到虚表的地址之后我们还可以访问虚表的内容,并打印出来虚表中各种虚函数的地址,比如说下面的代码:
int main()
{
E TMP;
cout << "虚表的地址为:" << (void*)(*((int*)(&TMP))) << endl;
return 0;
}
这段代码运行的结果如下:
并且通过调试也可以看到指针里面的内容也确实为上图所示
通过上面的讲述大家已经知道了如何打印虚表的地址,那我们怎么知道这个地址在内存的哪一个模块呢?方法很简单我们之间在各个模块中创建一个变量并将这个变量的地址打印出来进行一下对比,它离哪个模块最近不就属于哪个模块了嘛,比如说下面的代码:
int main()
{
E TMP;
cout << "虚表的地址为:" << (void*)(*((int*)(&TMP))) << endl;
int x = 10;
cout << "栈区的地址大致为:" << (void*)&x << endl;
int* p = new int;
cout << "堆区的地址大致为:" << (void*)p << endl;
static int b = 10;
cout << "静态区的地址大致为:" << (void*)b << endl;
const char* sp = "hello world";
cout << "代码区/常量区大致为:" << (void*)sp << endl;
return 0;
}
这段代码的运行结果为:
我们可以看到虚表的地址和代码区的地址相隔是最近的,所以我们就可以推断除虚表存储的地方是代码区/常量区,并且通过这个方法我们还可以用来验证一个东西就是不同类中的虚表是不一样的比如说下面的代码:
int main()
{
E TMP;
F TMP1;
cout << "E的虚表的地址为:" << (void*)(*((int*)(&TMP))) << endl;
cout << "F的虚表的地址为:" << (void*)(*((int*)(&TMP1))) << endl;
return 0;
}
这段代码的运行结果如下:
那么看到了这里我们就可以通过上面的知识来尝试着打印虚表的内容,这里就通过一个函数来实现,因为类中的函数的参数和返回值都是一样的,所以就可以创建一个函数指针,打印函数的参数就是一个函数指针数组,因为虚表是以空指针结尾,所以在函数体里面就可以通过while循环来进行打印,while循环结束的条件就是数组的内容为空,那么这里的代码就如下:
typedef void(*VFPTR)();
void print(VFPTR vft[])
{
int i = 0;
while (vft[i])
{
cout << (void*)vft[i] << endl;
++i;
}
}
我们可以通过下面的代码来进行测试:
int main()
{
E TMP_E;
F TMP_F;
cout << "TMP_E的虚表内容为:" << endl;
print((VFPTR*)(*((void**)(&TMP_E))));
cout << "TMP_F的虚表内容为:" << endl;
print((VFPTR*)(*((void**)(&TMP_F))));
return 0;
}
TMP_E的内容为:
TMP_F的内容为:
打印的内容为:
经过对比我们就可以确定我们写的程序是正确的,那么接下来我们就要使用上面的print函数来查看多继承的原理。
多继承的多态原理
如果是多继承,那么子类中就会存在多个虚表(前提是父类得有虚表),如果对应的父类没有虚表的话,那么这子类也不会有虚表,虚表一开始就已经被创建好了,只不过是在构造函数里面进行初始化,这里我们就可以对printf函数和父子类进行改造,使其打印地址的时候能够顺便打印出函数名来,那这里的代码就如下:
class E
{
public:
virtual void func1(){cout << "我是父类的func1函数:" ;}
virtual void func2(){cout << "我是父类的func2函数:" ;}
private:
int x= 10;
};
class F
{
public:
virtual void func1() { cout << "我是第二个父类的func1函数:" ; }
virtual void func4() { cout << "我是第二个父类的func4函数:" ; }
private:
int y = 10;
};
class G :public E, public F
{
public:
virtual void func1() { cout << "我是子类的func1函数:"; }
virtual void func6() { cout << "我是子类的func6函数:"; }
private:
int z;
};
typedef void(*VFPTR)();
void print(VFPTR vft[])
{
int i = 0;
while (vft[i])
{
(vft[i]());
cout<< (void*)vft[i] << endl;
++i;
}
}
通过调试可以看到类G创建出来的对象中还有两个虚表指针:
并且还可以看到虚表在对象创建出来之前就已经创建好了,只不过没有被初始化,将其初始化之后就成为了这样:
因为继承的顺序是先继承E再继承F,所以在子类中E的虚表指针就在F的虚表指针前面,由于子类中对func1函数进行了重写,所以两张虚表中的func1函数都被重写了那这里就有个问题,子类的虚函数放到哪个虚表呢?通过调试肯定是看不出来的因为这里没有显示,所以这里我们就可以通过print函数来打印一下子类中两个虚表的内容,那么这里有个问题,第二个虚表如何打印呢?通过观察可以看到第一个父类的之后紧接着的就是第二个父类的数据,而第二个父类的数据的开始就是虚表,所以我们可以取头部位置的地址,然后加上第一个父类的数据便可以获取第二个父类数据的起始地址,也就是第二个虚表指针的内容,那这里的代码就如下:
int main()
{
G TMP_G;
print((VFPTR*)*(void**)&TMP_G);
cout<<endl;
print((VFPTR*)(*(void**)((char*)&TMP_G+sizeof(E))));
return 0;
}
当然我们也可以使用切片的方式来打印第二个虚表的内容,比如说下面的代码:
int main()
{
G TMP_G;
print((VFPTR*)*(void**)&TMP_G);
cout << endl;
F* ptr = &TMP_G;
print((VFPTR*)(*(void**)(ptr)));
return 0;
}
执行的结果如下:
我们可以看到当子进程有多个虚表且子进程自己也有虚函数的时候,会将子进程的虚函数的地址放到子类的第一个虚表里面,那么这就是多继承的多态的原理,希望大家能够理解,子类有几个虚表取决于继承的父类有几个虚表,子类的虚函数会放到父类的第一个虚表里面。
菱形虚拟继承
菱形继承中子类的虚函数和多继承中子类的虚函数是没有什么区别的,比如说下面的代码:
class a
{
public:
virtual void func1()
{}
int _a;
};
class b:public a
{
public:
virtual void func1()
{}
int _b;
};
class c :public a
{
public:
virtual void func1()
{}
int _c;
};
class d :public b, public c
{
public:
virtual void func1()
{}
int _d;
};
int main()
{
d tmp;
tmp.b::_a = 10;
tmp.c::_a = 20;
tmp._b = 30;
tmp._c = 40;
tmp._d = 50;
return 0;
}
b类和c类都是继承的a类,a类有虚函数表所以b类和c类都有一个虚函数表并在表中对func1函数进行了重写,d类继承了b类和c类,所以在d类中有两个虚表指针指向着两个不同的虚表,我们可以通过调试在内存中进行查看内存的情况:
这就是菱形继承没有什么理解难度,那菱形虚拟继承呢?我们说虚拟继承存在的目的就是解决继承所带来的二义性和数据冗余的问题,那如果我们将上面的继承方式改成虚拟继承内存中的数据分布会变成什么样呢?首先两个类都有的_a肯定会变成一份,其次a中的func函数也会变成一份,并且这个func函数只能由子类d来进行重写,如果子类d不对共同的func函数进行重写的话编译器是会报错的,这里大家应该很好理解对吧,之前不是虚拟进程的时候有两个虚表,两个虚表就有两个func函数这时的func函数由各个父类进行重写,但是这时是虚拟菱形进程两个父类共有一个func函数,所以就只能由子类d的func函数进行重写,比如说下面的代码:
class a
{
public:
virtual void func1()
{}
int _a;
};
class b:virtual public a
{
public:
virtual void func1()
{}
int _b;
};
class c :virtual public a
{
public:
virtual void func1()
{}
int _c;
};
class d :public b, public c
{
public:
int _d;
};
由于这里菱形虚拟继承,在子类d中没有对虚函数进行重写所以就会报出下面这样的错误:
接下来我们来看看A的物理结构
再往下走
这时就可以看到只有一份_a,而上面的菱形继承有两份_a接着往下走就可以看到b类数据存储的位置
再往下走就可以看到c类存储数据的位置:
再往下走就可以看到d类存储数据的位置:
那这里就有一个问题00B6FD30储存的东西是什么呢?很明显真相只有一个就是a类的虚表:
也就是说菱形虚拟继承相对于菱形继承内存上最大的区别就是多了一个公共父类a的虚表指针,并且虚拟继承中的虚基表指针依然是存在的
对于c类也是同样的道理:
这里的b类和c类都只有一个函数,而且还是对父类a的func函数进行重写,那如果我们在b和c类中添加一些独有的虚函数,那d类中的数据分布又是什么样的呢?我们来看看下面这段代码:
class a
{
public:
virtual void func1(){}
int _a =10;
};
class b:virtual public a
{
public:
virtual void func1(){}
virtual void func2(){}
int _b=20;
};
class c :virtual public a
{
public:
virtual void func1(){}
virtual void func3(){}
int _c=30;
};
class d :public b, public c
{
public:
virtual void func1(){}
int _d=40;
};
这时再查看d中的内存分布就变成了这样:
因为父类b和c中有了属于自己的虚函数,所以在b和c的数据中就存在了属于自己的虚表指针,所以菱形虚拟继承这个时候就会存在三个虚表指针,两个虚基表指针,其中虚表指针在前面,虚基表指针在后面,这里大家可以自行调试查看一下。那么看到这里想必大家应该已经知道了虚拟菱形继承的原理,那么最后我们来看看多态的一些小点。
多态的一些小点
第一点:
虚函数可以用inline进行修饰,但是编译器会自动的忽略这个属性,因为调用被inline修饰的函数会自动的在被调用的地方替换成对应的函数实现,而多态中的虚函数,得运行到被调用的地方才能知道到哪个虚表中查找对应的函数,无法做到编译时就替换,所以编译器会自动的忽略inline属性,如果是一个普通调用则可以保持inline属性。
第二点:
为什么父类的对象不能实现多态,因为指针和引用只不过是别名,在赋值的时候不会改变虚表,但是对象在赋值的时候会改变虚表,所以不能实现多态
第三点:
静态成员函数不能是虚函树
第四点:
构造函数不能是虚函数,因为虚函数得放进虚表,但是虚表是在构造函数中才被初始化的,所以把构造函数放到虚表里面就会发生矛盾。