使用
概念
由于没有一个较为标准的概念,这里就扼要阐述一下
不同对象在使用相同函数时,具有不同的实现效果。
ps:多态的实现是在继承的基础上。
虚函数
使用关键词 virtual 修饰的成员函数
如上图的 virtual void func()
-
虚函数的作用就是被子类重写
-
而对于析构函数,在使用delete释放资源时,就会使用父子类析构函数。否则就只会调用父类的析构函数。如下图:
细心的朋友可以已经发现,为什么~B()没有加virtual也构成多态呢?其他的成员函数也可以这样吗?
答案是肯定的,这是因为继承的原因,**所以只要父类有virtual关键字,子类的成员函数不加,也可以构成多态。**但是我们建议还是加上,这样增加可读性,更加规范。
特例:
1. 虚析构函数重写(函数名可以不同)
上例有体现
2. 协变(返回值可以不同)
即基类虚函数返回基类对象的指针或者引用(父用父的),派生类虚函数返回派生类对象的指针或者引用时(子用子的),称为协变。
返回值 | 函数名 | 形参列表 | 发生的作用域 | |
---|---|---|---|---|
重载 | \ | 相同 | 参数类型,数量,位置不同 | 同一级作用域 |
重写(覆盖) | 相同(除协变) | 相同(除虚析构函数) | 全相同 | 父子类作用域中 |
隐藏(重定义) | \ | 相同 | \ | 父子类作用域中 |
构成条件
前面铺垫这么多,就是为了引入多态的成立条件。
- 重写虚函数
- 使用父类指针或引用来调用虚函数
结合前面例子我们逐一分析:
这里说明一下,虚函数的重写规则:
派生类中有一个跟基类完全相同的虚函数
(即派生类虚函数与基类虚函数的返回值类型、函数名字、参数列表完全相同)
下面我们来一道题巩固一下所学知识
请问上述的结果是什么?
下面我们来分析一下:
子类对象作为实参传入,由于子类里面没有test()函数,所以使用父类的test()函数,该函数调用func()函数,但是func()函数是虚函数,这里就要考虑子类有没有发生重写,B类中重写了func(),产生多态,而这里的重写只是函数体的覆盖,而形参还是继承下来了,所以打印"B 0"。
抽象类
包含纯虚函数在的类就是,抽象类,也叫接口类
上面这个animal类就是抽象类
- 特点
- 不能实例化对象
- 子类必须实现纯虚函数
ps: 纯虚函数跟虚函数没有任何关系
多态中的对象模型
虚函数表
简称虚表,是用来存储虚函数指针的一个虚函数指针数组。
先来一道题,看看输出结果是什么?
这是基于32位平台的数据
可能会很奇怪,为什么是8呢,不是只有一个 int 的成员变量_b吗,应该是4才对。
其实不然,我们调试发现,对象b中还有一张虚函数表指针(_vfptr),
来看张图加深理解
这就很好理解为什么大小为8,而不是4了。
原理
前文有提到,虚表是用来存储虚函数指针的,也就是说只要是类中的虚函数,虚表都会将他们的地址存储起来。说到这个,我们插入一个题外话:普通成员函数和虚函数的存储地方在哪里?
💡 答案是:都在代码段里,因为虚函数和普通成员函数一样都是函数,只读的代码
所以可不能想当然的认为在虚表里面,那里面只是存储了虚函数的地址而已。
我们言归正传,多态是在继承的基础上进行的,单继承和多继承的多态在虚表方面又有所区别。
单继承虚表
先看下面这段代码,我们带着问题去得出答案
// 此函数用于打印虚表里面的函数个数
typedef void(*VTPtr)();
void print_vtptr(VTPtr table[])
{
for (int i = 0; table[i]; ++i)
{
printf("[%d]->%p\t", i, table[i]);
table[i]();
//VTPtr f = table[i];
//f();
}
cout << endl;
}
void Test()
{
class A
{
virtual void func1() { cout << "A func1()" << endl; }
virtual void func2() { cout << "A func2()" << endl; }
};
class B: public A {
virtual void func1() { cout << "B func1()" << endl; }
virtual void func3() { cout << "B func3()" << endl; }
};
A a;
B b;
// 运行环境32位的可以用下面两行
//print_vtptr((VTPtr*)(*(int*)&a));
//print_vtptr((VTPtr*)(*(int*)&b));
print_vtptr(*(VTPtr**)&a);
print_vtptr(*(VTPtr**)&b);
}
int main()
{
Test();
return 0;
}
❓问题:父类和子类的虚表是分别装有哪些内容?
我们编译可以看到,在监视窗口中,a,b对象的虚表 _vfptr 只有一个函数指针,括号里面都写着 func1 ,那么事实是否如此,a,b对象的虚表中都只存了一个虚函数指针 func1(),显然事实并非如此。
如下图,我们分析了内存,分别对a,b虚表进行内容查看,图中提前给出答案。
✔️ 在vs里面,虚表以nullptr结尾,这点就刚好能让我们利用起来。
下面对上述代码进行测验
至此可以得出答案了
💡子类会对父类的虚表拷贝一份,然后把自己重写的虚函数对父类的进行覆盖,没有重写的就保留下来。
多继承虚表
说完了单继承,我们来谈谈多继承的虚表,看看会有什么不同之处。
// 此函数用于打印虚表里面的函数个数
typedef void(*VTPtr)();
void print_vtptr(VTPtr table[])
{
for (int i = 0; table[i]; ++i)
{
printf("[%d]->%p\t", i, table[i]);
table[i]();
}
cout << endl;
}
void main()
{
class A
{
virtual void func1() { cout << "A func1()" << endl; }
virtual void func2() { cout << "A func2()" << endl; }
};
class B
{
virtual void func1() { cout << "B func1()" << endl; }
virtual void func3() { cout << "B func3()" << endl; }
};
class C:public A, public B
{
virtual void func1() { cout << "C func1()" << endl; }
virtual void func3() { cout << "C func3()" << endl; }
virtual void func4() { cout << "C func4()" << endl; }
};
A a;
B b;
C c;
print_vtptr(*(VTPtr**)&a);
print_vtptr(*(VTPtr**)&b);
print_vtptr(*(VTPtr**)&c);
B* pc = &c; // 第二张虚表的起始位置(利用指针自动偏移)
print_vtptr(*(VTPtr**)pc);
return 0;
}
❓问题:子类有几张虚表?分别装有哪些内容?
从单继承的例子中,我们不难分析得出:对象c应该会有两张虚表,分别拷贝a,b的,然后把重写的虚函数指针在c中的虚表覆盖。而对于c中新增的虚函数,该如何处理呢?我们猜测最大的可能性应该是在两张虚表中都新增虚函数的指针,当然这是一种猜测,还有其他很多种情况,那么具体什么情况,我们接着分析:
从这张截图分析:对象a,b分别有两个虚函数,对象c有三个虚函数,分别重写了 a中的 func1() 和 b中的 func3(),当然也拷贝了a,b两张虚表,因为多态所以在虚表中覆盖了父类原有的函数,最后看到,c对象新增的虚函数 func4() 补充到了第一张虚表的末尾。
💡得出结论:
多继承的虚表就是将父类们的虚表先拷贝过来,然后在进行虚函数的覆盖,最后新增的虚函数就加在第一张虚表上。