虚函数和多态关系密切,只有掌握好了虚函数才能更好的理解多态
虚函数调用
函数的调用有以下两种形式:
直接调用:在反汇编中是call + 地址的形式,在硬编码中是E8 + 地址的形式
间接调用:在反汇编中是call + [.....]的形式,在硬编码中是FF + 地址的形式。其中[.....]中是一个地址,call + [.....]指的是将[.....]中地址的数据作为地址进行调用
现我们观察如下程序,该程序定义了一个类,类中有普通函数和虚函数,我们在main函数中通过对象去调用这两个函数
class Person
{
public:
void method1()
{
printf("method1\n");
}
virtual void method2() //虚函数
{
printf("method2\n");
}
};
int main(int argc, char* argv[])
{
Person person;
person.method1();
person.method2();
return 0;
}
我们进入反汇编观察函数调用的情况:
我们发现,虚函数和普通函数调用的情况是一致的。
我们已知ecx中存储的是this指针的地址,我们定义的类中并没有定义成员变量,但是this指针却指向了ebp – 4,这是一个奇怪的现象,我们将在之后的内容中讲解
现我们通过指针去调用两个函数,观察是否有所不同
class Person
{
public:
void method1()
{
printf("method1\n");
}
virtual void method2()
{
printf("method2\n");
}
};
int main(int argc, char* argv[])
{
Person person;
Person* p = &person; //用指针的方式调用
p->method1();
p->method2();
return 0;
}
我们进入反汇编观察
我们可以发现,这时,在调用虚函数的时候和调用普通函数就有了区别。调用虚函数(第一个call)是间接调用(FF)。注意最后两行是调用堆栈平衡的代码,不要搞混了
纯虚函数
春旭函数的特点:
1.将成员函数声明为virtual,此时该函数为虚函数
2.该函数没有函数体,函数后跟=0
接下来我们对此进行一个举例
class Base
{
public:
virtual int Plus() = 0;//该函数为纯虚函数
}
抽象类
抽象类有这几种特点:
1.包含纯虚函数的类,就是抽象类;
2.抽象类可以包含普通的函数;
3.抽象类不能实例化,即创建对象。
我们可以把抽象类看作是对子类的一种约束,或者认为其(抽象类)就是定义一种标准。
举例:淘宝,有很多店铺,虽然每个店铺卖的东西都不一样,但是他们同样都可以下单、评论、购物车,也就是说他们都遵守了这种标准规则。也就是说淘宝就是一个作为抽象类的父类,其有很多成员:购物车、评论、商品展示区等等。但是淘宝都有定义实现这些成员,而是交给开淘宝店的人(子类)根据父类成员去定义实现。
现在我们实际应用纯虚函数
如此演示下来,我们不难发现,纯虚函数就是给一个模板。子类想要实现纯虚函数要求的相同的功能,比如函数名和父类的纯虚函数一样,这样的话,实际就是方便类的统一管理
虚函数表的引入
我们更新以下代码。在新的代码中我们为Person定义了两个成员变量,并在main()函数中查看该类的大小
class Person
{
int x;
int y;
public:
void method1()
{
printf("method1\n");
}
virtual void method2()
{
printf("method2\n");
}
};
int main(int argc, char* argv[])
{
Person person;
printf("%d",sizeof(person)); //12
return 0;
}
程序运行以后打印了12,因此我们得知,这个类大小是12。而我们定义的类中只有两个int类型的成员变量,理应只有8字节大小,这时多出来的4字节大小自然跟虚函数有关。
我们现在该类中定义两个虚函数,观察是否该类多出来8字节大小
class Person
{
int x;
int y;
public:
virtual void method1()
{
printf("method1\n");
}
virtual void method2()
{
printf("method2\n");
}
};
int main(int argc, char* argv[])
{
Person person;
printf("%d",sizeof(person)); //12
return 0;
}
程序运行后,我们发现仍然打印了12,这也意味着该类大小仍然是12,并没有因为多出来一个虚函数而导致该类变大4字节
我们现将该类成员变量进行赋值,并只保留一个虚函数,为了方便在内存观察
class Person
{
int x;
int y;
public:
Person()
{
x = 1;
y = 2;
}
virtual void method1()
{
printf("method1\n");
}
};
int main(int argc, char* argv[])
{
Person person;
printf("%d",sizeof(person));
return 0;
}
现我们进到该类对象的内存空间,观察这四个大小的字节到底是什么
我们发现,后八个字节正是我们赋值的成员属性,前四个字节便是多出来的四个字节,它是虚函数表的地址,进入该地址便是虚函数表
虚函数表
现我们通过指针去查看虚函数表有什么用
class Person
{
int x;
int y;
public:
Person()
{
x = 1;
y = 2;
}
virtual void method1()
{
printf("method1\n");
}
};
int main(int argc, char* argv[])
{
Person person;
Person* p = &person;
p->method1();
return 0;
}
进入反汇编
发现call要调用的函数的地址是虚函数表上前四个字节数据作为地址,该地址上存储的数据。
现在我们定义两个虚函数,观察虚函数表有什么变化
class Person
{
int x;
int y;
public:
Person()
{
x = 1;
y = 2;
}
virtual void method1()
{
printf("method1\n");
}
virtual void method2()
{
printf("method2\n");
}
};
int main(int argc, char* argv[])
{
Person person;
Person* p = &person;
p->method1();
p->method2();
return 0;
}
我们再次进入反汇编
我们发现第二个虚函数地址正好在虚函数表的第二个四字节。由此我们也可以得知,虚函数表其实就是一个数组,这个数组依次排列着虚函数的地址
总结:当类中有虚函数时,类对象内存大小会多出4字节,这4个字节是指向虚函数表的地址,虚函数表里面依次存储了所有虚函数的地址
现我们可以用函数指针论证以下虚函数表中是不是真的虚函数地址
typedef int (*func)(int *p),这是一个函数指针,我们分析一下
已知func是一个指针,而int *p表示int*类型的函数形参,int表示返回值类型,所以推出*func是一个函数,因此func是一个指向这类函数的指针,即函数指针。该函数指针表示一个int*类型形参,int类型返回值的func类型函数指针。
class Person
{
int x;
int y;
public:
Person()
{
x = 1;
y = 2;
}
virtual void method1()
{
printf("method1\n");
}
virtual void method2()
{
printf("method2\n");
}
};
int main(int argc, char* argv[])
{
Person person;
typedef void (*pMethod)(void); //函数指针声明
pMethod pm1 = (pMethod)(*(int*)(*(int*)&person)); //定义pm1函数指针并将虚函数表中第一个地址值赋给pm1
pMethod pm2 = (pMethod)(*((int*)(*(int*)&person) + 1)); //定义pm2函数指针并将虚函数表中第二个地址值赋给pm2
//接着用函数指针的方式调用
pm1(); //method1
pm2(); //method2
return 0;
}
程序可以正常执行虚函数
如下是虚函数的图解
单继承无重写
图示如下
class Base{
public:
virtual void Function_1(){
printf("Base:Function_1...\n");
}
virtual void Function_2(){
printf("Base:Function_2...\n");
}
virtual void Function_3(){
printf("Base:Function_3...\n");
}
};
class Sub:public Base{
public:
virtual void Function_4(){
printf("Sub:Function_4...\n");
}
virtual void Function_5(){
printf("Sub:Function_5...\n");
}
virtual void Function_6(){
printf("Sub:Function_6...\n");
}
};
单继承有重写
图示如下
如图所示,被重写的父类函数被覆盖了
class Base{
public:
virtual void Function_1(){
printf("Base:Function_1...\n");
}
virtual void Function_2(){
printf("Base:Function_2...\n");
}
virtual void Function_3(){
printf("Base:Function_3...\n");
}
};
class Sub:public Base{
public:
virtual void Function_1(){
printf("Sub:Function_1...\n");
}
virtual void Function_2(){
printf("Sub:Function_2...\n");
}
virtual void Function_6(){
printf("Sub:Function_6...\n");
}
};
多个继承无重写
该情况属于有多个父类时,子类直接继承父类
如图所示,此时有了两个虚函数表。第一张表记录了第一个父类虚函数和子类的虚函数。第二张表记录了第二个父类的虚函数
class Base1{
public:
virtual void Fn_1(){
printf("Base1:Fn_1...\n");
}
virtual void Fn_2(){
printf("Base1:Fn_2...\n");
}
};
class Base2{
public:
virtual void Fn_3(){
printf("Base2:Fn_3...\n");
}
virtual void Fn_4(){
printf("Base2:Fn_4...\n");
}
};
class Sub: public Base1,public Base2{
public:
virtual void Fn_5(){
printf("Sub:Fn_5...\n");
}
virtual void Fn_6(){
printf("Sub:Fn_6...\n");
}
};
多个继承有重写
图示如下
如图所示该情况也有两张表,第一张表记录了第一个父类虚函数和子类的虚函数,但父类被重写的虚函数会被覆盖。第二张表记录了第二个父类的虚函数,同样的父类被重写的虚函数会被覆盖
class Base1{
public:
virtual void Fn_1(){
printf("Base1:Fn_1...\n");
}
virtual void Fn_2(){
printf("Base1:Fn_2...\n");
}
};
class Base2{
public:
virtual void Fn_3(){
printf("Base2:Fn_3...\n");
}
virtual void Fn_4(){
printf("Base2:Fn_4...\n");
}
};
class Sub:public Base1,public Base2{
public:
virtual void Fn_1(){
printf("Sub:Fn_1...\n");
}
virtual void Fn_3(){
printf("Sub:Fn_3...\n");
}
virtual void Fn_5(){
printf("Sub:Fn_5...\n");
}
};
多重继承无重写
图示如下
class Base1{
public:
virtual void Fn_1(){
printf("Base1:Fn_1...\n");
}
virtual void Fn_2(){
printf("Base1:Fn_2...\n");
}
};
class Base2:public Base1{
public:
virtual void Fn_3(){
printf("Base2:Fn_3...\n");
}
virtual void Fn_4(){
printf("Base2:Fn_4...\n");
}
};
class Sub:public Base2{
public:
virtual void Fn_5(){
printf("Sub:Fn_5...\n");
}
virtual void Fn_6(){
printf("Sub:Fn_6...\n");
}
};
多重继承有重写
图示如下
此时,依然一张虚表,依次记录从第一个父类到子类的虚函数地址。但父类被重写的依然会被覆盖
class Base1{
public:
virtual void Fn_1(){
printf("Base1:Fn_1...\n");
}
virtual void Fn_2(){
printf("Base1:Fn_2...\n");
}
};
class Base2:public Base1{
public:
virtual void Fn_1(){
printf("Base2:Fn_1...\n");
}
virtual void Fn_3(){
printf("Base2:Fn_3...\n");
}
};
class Sub:public Base2{
public:
virtual void Fn_3(){
printf("Sub:Fn_3...\n");
}
virtual void Fn_5(){
printf("Sub:Fn_5...\n");
}
};
绑定与多态
绑定:当调用函数时,将函数与函数地址关联到一起的过程
多态:当子类重写父类函数时,编译器可以调用正确的同名函数
有如下代码:
class Base{
public:
int x;
Base()
{
x = 100;
}
void Function_1()//Func1为普通函数
{
printf("Base:Function_1...\n");
}
virtual void Function_2()//Func2为虚函数
{
printf("Base:Function_2...\n");
}
};
class Sub:public Base
{
public:
int x;
Sub()
{
x = 200;
}
void Function_1()
{
printf("Sub:Function_1...\n");
}
virtual void Function_2()
{
printf("Sub:Function_2...\n");
}
};
void Test(Base* pb)
{
int n = pb->x;
printf("%d\n",n); //100
pb->Function_1(); //Base:Function_1...
pb->Function_2(); //Base:Function_2...
}
int main(int argc, char* argv[])
{
Base base; //创建父类对象
Test(&base);
return 0;
}
注意:一个程序的完整执行流程是先编译后运行
当程序编译完成以后,函数的地址就已经确定(如上述代码Test函数),这种情况就叫做编译期绑定或前期绑定。在反汇编中,调用该函数的格式是call + 直接地址
当在程序运行时去调用一个函数时,这个函数地址才会确定,这种情况叫做运行期绑定或动态绑定或晚绑定。在反汇编中,调用该函数的格式是call + 间接地址
我们针对上述代码中Test函数进一步讲解绑定问题。现在进入反汇编观察Test函数
此处多注意父类指针和子类指针能都表示的范围,可以帮助我们理解
由反汇编可知,Test函数中x是已经确定的,是类base的x值为10
我们继续观察反汇编
由E8我们可以发现,Function_1()函数在编译时就已经确定了函数地址。
由FF我们可以发现,Function_2()函数的函数地址并没有被确定,call调用的[edx]的值是一个地址,该地址上的值可能被重写:可能因为虚函数重写导致改变
因此我们可以发现,在一个类中,其普通函数地址和成员变量,在编译期便已经写死不变了。而虚函数表会在执行期再被确定
现在我们分别以Base对象和Sub对象具体化的探究绑定
现以Base对象传入Test函数,观察程序运行,发现调用的Func2函数是父类Base的函数,其他也都是Base的函数
如果以以Sub对象传入Test函数,观察程序运行,发现调用的Func2函数是子类Sub的函数,其他同样也都是Base的函数没有改变
如此验证,可以发现虚函数是动态绑定的,而动态绑定的另一个名字便是多态。一种类型体现出不同的行为,这便是多态
注意:普通函数的重写也属于多态绑定,普通函数的重载属于编译期绑定
现在我们再看一个程序去更深刻理解多态
class Base
{
public:
int x;
int y
Base()
{
x = 1;
y = 2;
}
void Print () // Print没有加virtual
{
printf("Base:%x %x\n",x, y);
}
};
class Sub1:public Base
{
public:
int x;
int y;
Sub1()
{
x = 3;
y = 4;
A =
}
void Print()
{
printf("Sub1:%x %x \n", x, y);
}
};
class Sub2:public Base
{
public:
int x;
int y;
Sub2()
{
x = 6;
y = 7;
B = 8;
}
void Print()
{
printf("Sub2:%x %x \n", x, y);
}
};
void Test(Base* pb) //父类的指针指向子类的对象
{
Base b;
Sub1 s1;
Sub2 s2;
Base* arr[] = {&b, &s1, &s2};
for(int i = 0; i < 3; i++)
{
arr[i]->Print();
}
}
int main(int argc, char* argv[])
{
Sub sub; //创建子类对象
Test(&sub); //父类的指针指向子类的对象
return 0;
}
运行程序后,我们发现,调用的Print()都是父类的Print(),这并没有体现多态的特性
我们对程序进行修改
class Base
{
public:
int x;
int y
Base()
{
x = 1;
y = 2;
}
virtual void Print () // Print没有加virtual
{
printf("Base:%x %x\n",x, y);
}
};
class Sub1:public Base
{
public:
int x;
int y;
Sub1()
{
x = 3;
y = 4;
A =
}
virtual void Print()
{
printf("Sub1:%x %x \n", x, y);
}
};
class Sub2:public Base
{
public:
int x;
int y;
Sub2()
{
x = 6;
y = 7;
B = 8;
}
virtual void Print()
{
printf("Sub2:%x %x \n", x, y);
}
};
void Test(Base* pb) //父类的指针指向子类的对象
{
Base b;
Sub1 s1;
Sub2 s2;
Base* arr[] = {&b, &s1, &s2};
for(int i = 0; i < 3; i++)
{
arr[i]->Print();
}
}
int main(int argc, char* argv[])
{
Sub sub; //创建子类对象
Test(&sub); //父类的指针指向子类的对象
return 0;
}
此时程序分别执行了父类和子类对应的Print()函数
析构函数的虚函数化
现有一个父类一个子类,并用父类指针指向父类对象和子类对象
Base b;
Sub s;
Base* pb = &b; //Base类指针指向自己的对象
Base* ps = &s; //父类指针指向子类的对象
假设父类和子类的析构函数都不是虚函数,那么当子类对象s进行释放的时候,由于是用Base*指针指向的s对象,所以会调用Base类中的析构函数,那么此时,对象s并没有释放
因此我们就需要将父类和子类的析构函数都设置成虚函数,这时候,释放对象s时,调用的就是s的析构函数,对象s正常被释放
作业
重载和重写
重载:一个类中,函数名一样,参数的个数或类型不同
重写:子类中的函数和其父类中的函数名字、参数、返回值一模一样(函数覆盖)
1.单继承无函数覆盖(打印Sub对象的虚函数表):
#include <iostream>
class Base {
public:
virtual void Function_1() {
printf("Base:Function_1...\n");
}
virtual void Function_2() {
printf("Base:Function_2...\n");
}
virtual void Function_3() {
printf("Base:Function_3...\n");
}
};
class Sub :public Base {
public:
virtual void Function_4() {
printf("Sub:Function_4...\n");
}
virtual void Function_5() {
printf("Sub:Function_5...\n");
}
virtual void Function_6() {
printf("Sub:Function_6...\n");
}
};
int main(int argc, char* argv[]) {
Sub sub;
Sub* subp = ⊂
int* p = (int*)*(int*)subp; //指向了虚函数表
for (int i = 0; i < 6; i++)//打印虚函数表
{
std::cout << *(p + i) << std::endl;
}
return 0;
}
我们通过内存可以发现,该类只有一个虚函数表,虚函数表中有六个成员
2.单继承有函数覆盖(打印Sub对象的虚函数表)
#include<stdio.h>
class Base
{
public:
virtual void Function_1() {
printf("Base:Function_1...\n");
}
virtual void Function_2() {
printf("Base:Function_2...\n");
}
virtual void Function_3() {
printf("Base:Function_3...\n");
}
};
class Sub :public Base {
public:
virtual void Function_1() {
printf("Sub:Function_1...\n");
}
virtual void Function_2() {
printf("Sub:Function_2...\n");
}
virtual void Function_6() {
printf("Sub:Function_6...\n");
}
};
int main(int argc, char* argv[]) {
Sub sub;
Sub* subp = ⊂
int* p = (int*)*(int*)subp;
for(int i = 0; i < 3; i++)
{
printf("%08x\n", *p);
}
return 0;
}
我们通过内存发现,该类只有一个虚函数表,虚函数表中只有三个成员
3.体会多态
定义一个父类Base:有两个成员X,Y;有一个函数Print(非virtul)能够打印X,Y的值
定义2个子类:Sub1,有一个成员A;Sub2,有一个成员B。每个子类有一个函数Print(非virtul),打印所有成员----Sub1:打印X Y A;Sub2:打印X Y B
定义一个数组,存储Base Sub1 Sub2对象;再使用一个循环语句调用所有的Print函数
#include<stdio.h>
class Base
{
public:
int x;
int y;
Base()
{
x = 1;
y = 2;
}
void Print()
{
printf("%d %d\n", x, y);
}
};
class Sub1 : public Base
{
private:
int A;
public:
Sub1()
{
x = 3;
y = 4;
A = 5;
}
void Print()
{
printf("%d %d %d\n", x, y, A);
}
};
class Sub2 : public Base
{
private:
int B;
public:
Sub2()
{
x = 6;
y = 7;
B = 8;
}
void Print()
{
printf("%d %d %d\n", x, y, B);
}
};
int main(int argc, char* argv[]) {
Base base;
Base* pbase = &base;
Sub1 sub1;
Sub1* psub1 = &sub1;
Sub2 sub2;
Sub2* psub2 = &sub2;
Base* arr[] = { pbase, psub1, psub2 };
for (int i = 0; i < 3; i++)
{
arr[i]->Print();
}
return 0;
}
运行该程序以后,我们发现,该程序执行了三次父类Base的Print()函数。这是因为我们定义的数组类型是父类指针Base*,因此那么数组成员即使是子类指针,在调用函数时依然是父类指针进行调用,而父类指针只能调用父类的Print()函数
为了解决这个问题,我们将Print()函数定义为虚函数
#include<stdio.h>
class Base
{
public:
int x;
int y;
Base()
{
x = 1;
y = 2;
}
virtual void Print()
{
printf("%d %d\n", x, y);
}
};
class Sub1 : public Base
{
private:
int A;
public:
Sub1()
{
x = 3;
y = 4;
A = 5;
}
virtual void Print()
{
printf("%d %d %d\n", x, y, A);
}
};
class Sub2 : public Base
{
private:
int B;
public:
Sub2()
{
x = 6;
y = 7;
B = 8;
}
virtual void Print()
{
printf("%d %d %d\n", x, y, B);
}
};
int main(int argc, char* argv[]) {
Base base;
Base* pbase = &base;
Sub1 sub1;
Sub1* psub1 = &sub1;
Sub2 sub2;
Sub2* psub2 = &sub2;
Base* arr[] = { pbase, psub1, psub2 };
for (int i = 0; i < 3; i++)
{
arr[i]->Print();
}
return 0;
}
此时运行程序发现正常调用了各个类的Print()函数