C++多态

目录

一.多态的概念

二.多态的实现及定义

2.1多态的构成条件

2.2动态绑定与静态绑定

静态绑定

动态绑定

​编辑 2.3虚函数重写的两个例外

1.协变

 2.析构函数的重写

2.4 C++ 11 override和final关键字

 三.纯虚函数

四.多态的原理

1.单继承情况

2.多继承多态的情况

五.虚函数表,虚函数存储位置


一.多态的概念

多态就是多种形态,具体点就是去完成某个行为,当不同的对象去完成时会产生出不同的形态。比如说动物叫,狗叫是一种声音,猫叫是一种声音,老鼠叫是一种声音,但是它们都是一种行为(动物叫),再比如说买票,普通人买票是全价买票,学生买票是半价买票,军人买票是优先买票。具体到类里面(不同对象去完成相同的某种行为),目前为止这句话你可以认为是不同的类对象(不同的对象)中有同样的函数(这就是相同的行为),而他们在调用这个函数时所产生的结果不同。

二.多态的实现及定义

2.1多态的构成条件

多态是在不同继承关系的类对象,去调用同一对象,产生了不同的行为。比如Student继承了person。person对象买票全价,Student对象买票半价

那么在继承中要构成多态还有两个条件:

1.必须通过基类的指针或者引用调用虚函数

2.被调用的函数必须是虚函数(被virtual修饰的函数),且派生类必须对基类的虚函数进行重写

2.2虚函数的重写

虚函数的重写(覆盖):派生类中有一个根基类完全相同的虚函数(即派生类虚函数与基类虚函数的返回值类型,函数名字,参数列表完全相同),称子类的虚函数重写了基类的虚函数

举个例子B继承了A,在A和B里面都有同样的函数print,可是打印出来却是B函数的内容。


#include<iostream>
using namespace std;

class A
{
public:
	virtual void print()
	{
		cout << "A的打印" << endl;
	}
};

class B:public A
{
public:
	virtual void print()
	{
		cout << "B的打印" << endl;
	}
};

int main()
{
	B tamp;
	A* per = &tamp;
	per->print();
}

按继承之前讲的赋值兼容转换相关知识点来讲,要么父类子类都有相同的函数,要么就是父类直接构成隐藏(除了指定类域否则访问不到父类的同名函数)。要么就是不同名的函数,父类指针直接调用父类的内容,父类指针只能访问派生类中继承父类的东西,是无法访问到子类中不是继承父类的东西的,那么虚函数好像看似介于两者之间,首先是同名函数但是没有隐藏起来,同时父类指针却访问到了不是派生类当中不是父类的内容

为了区分开来,我把子类父类当中同名函数调用以及父类指针调用情况也展示出来

同名函数调用

同名函数情况下父类指针直接调用子类当中继承自父类的函数

class A
{
public:
	void print()
	{
		cout << "A的打印" << endl;
	}
};

class B:public A
{
public:
	void print()
	{
		cout << "B的打印" << endl;
	}
};

int main()
{
	B tamp;
	A* per = &tamp;
	per->print();
}

 同名函数情况下子类指针直接调用函数,直接构成隐藏默认调用子类的函数

可以通过指定父类的类域调用父类的同名函数,正常来讲同名函数会报函数重定义错误(除非重载),但是继承体系对他们进行了处理隐藏所以不会报错

 那么究竟为什么会导致父类的指针去调用了子类函数的内容呢,这就涉及到动态绑定和静态绑定

2.2动态绑定与静态绑定

静态绑定

静态绑定又称为前期绑定(早绑定),在程序编译期间就确定了程序的行为,也称为静态多态(比如函数重载)。用函数举例编译阶段会根据函数名,返回类型,参数生成对应的符号表(C++才有这个,所以才能构成函数重载,而C语言只会把函数名生成符号表,参数什么的不考虑),在符号表里记录相关信息,汇编阶段会把中间代码(如汇编语言)转换成机器语言指令,并存储在目标文件中,同时汇编器可能会为函数和变量分配相对偏移量(而不是绝对地址),但这些偏移量是相对于某个段的基地址的(这个设计到操作系统,编译器,硬件就不细讲了)。然而,这并不是真正的内存分配,因为目标文件在被加载到内存中之前,其物理地址是未知的。链接阶段,将多个目标文件(以及库文件)合并成一个可执行文件或库文件,同时使用符号表来解析目标文件中的符号引用(如函数调用、变量访问)。它查找符号的定义,并确保每个引用都指向正确的符号,然后生成可执行文件链接器会确定每个符号(包括函数和变量)在可执行文件中的虚拟地址。这些地址是相对于可执行文件的加载地址的,而不是物理内存地址。然后运行阶段才会将虚拟地址映射到物理内存当中真实的地址。

但是函数声明和定义分离时声明不会生成符号表,主要目的是告诉编译器函数的存在、函数名、返回类型以及参数的类型和数量。然而,函数声明本身并不生成符号表的完整信息。函数声明主要用于检查函数调用时的参数类型和数量是否与声明匹配,以及帮助编译器进行类型检查。在函数定义的地方才会生成符号表。

静态绑定通俗一点讲就是函数的地址在编译链接时确定,如果只有函数声明,在编译阶段生成函数修饰过的函数名符合语义语法的话,其实会暂时通过,在链接的时候会去别的文件的符号表里寻找,如果找到了会补齐填满这个函数的修饰过的内容,如果找不到就会报链接报错,而有定义的话在编译的时候就可以到符号表里找到这个函数的地址(相对地址)

动态绑定

动态绑定通俗一点讲就是在运行时确定函数地址。

讲具体点的话,在编译生成符号表阶段识别到virtual关键字会生成一个指向虚函数表的指针(vtable,vs监视窗口是_vfptr)。虚函数表保存的具体虚函数存的位置的地址(实际上就是指针数组),每个类都有自己的虚函数表。但是同一个类的对象共享一个虚函数表。在运行阶段通过各自类对应的虚函数去找自己的函数的地址。

细讲一下重写函数和动态绑定的编译,重写顾名思义就是按照相对应的规则重新生成一个函数,父类虚函数给子类提供了接口,然后子类在接口的基础加上自己的东西完成重写,实际上有点像之前重载一样直接生成了第二个函数(都是编译器干的活),然后把这个新生成的函数的地址放到派生类虚函数表里。

什么叫做提供接口,然后加上派生类自己的内容呢?比如下面的例子fun函数完成多态重写,按照之前讲的派生类完成重写,实际上会加入自己的内容,所以结果是val=5才对,可是依旧val是A的值。

#define _CRT_SECURE_NO_WARNINGS 1
#include<iostream>
using namespace std;

class A
{
public:
	virtual void fun(int val = 4)
	{
		cout << "A的val值:" << val << endl;
	}
private:
	int _a = 4;
};

class B :public A
{
public:
	virtual void fun(int val = 5)
	{
		cout << "B的val值:" << val << endl;
	}
private:
	int _b = 5;
};

int main()
{
	B b;
	A* per =&b;
	per->fun();//指定父类的类域
}

提供接口就是把函数的开头那一串代码提供过来,重写只能重写实现,不能重写接口,所以B里面函数开头其实是和A一模一样的。

值得注意的是virtual也算提供的接口,也会一起提供过来,所以派生类即使不写virtual也不会报错也依旧能构成多态(因为接口是父类的,父类有就没问题),但是反过来父类不写,子类写virtual是不行的,会报错的

值得注意的是即使把派生类要重写的函数改成private也是不会报错的,原因也是一样父类提供了public访问权限的接口,所以连带着派生类的重写函数也是public访问权限(但是反过来就不行了)

 2.3虚函数重写的两个例外

正常来讲虚函数重写必须满足函数名,返回值,参数列表完全相同,可是有两个例外:协变(返回值类型不同)和析构函数重写(函数名不同)

1.协变

协变是基类与派生类虚函数返回值类型不同,但是这个返回值不同不是所有类型返回值都适合的,只适合基类虚函数返回基类对象的指针或引用,派生类虚函数返回派生类对象的指针或者引用,返回值必须是父子类的指针或引用(返回值之间有父子关系)

class A{};
class B :public A{};

class cat
{
public:
	virtual A* fun()
	{
		return new A;
	}
private:
	int _a = 4;
};

class dog :public cat
{
public:
	virtual B* fun()//返回值必须是父子类的指针或引用
	{
		return new B;
	}
private:
	int _b = 5;
};

 2.析构函数的重写

如果基类的析构函数为虚函数,此时派生类析构函数只要定义,无论是否加virtual关键字,都与基类的析构函数构成重写,虽然基类与派生类析构函数不同。虽然函数名不同,看起来违背了重写的规则,其实不然因为编译器会对所有的析构函数名特殊处理为destructor

构成多态情况下,父类指针指向派生类对象空间会先调派生类析构函数然后调父类析构函数

如果析构函数不改成虚函数 ,依旧父类指针指向派生类对象,只会调用父类的析构函数。这是因为父类指针指向派生类对象是指向派生类中属于父类的那一部分,因为析构函数被编译器处理成同名destructor,所以这个指针默认就去派生类中父类那一部分中去找析构函数,而不会去派生类去找,所以只会调到父类的析构函数。而多态的情况下,虚函数会完成重写,虽然父类指针指向的是派生类对象中父亲的那一部分,但是依旧会调派生类的析构函数,完成派生类析构然后父类析构(因为派生类析构是这样的先自己然后父亲,内置类型不处理,自定义类型和父类的部分自动调他们自己的析构)

一般涉及到成员动态分配空间了都用虚函数来析构,否则的话可能造成内存泄漏。在没有析构虚函数的基础上,如果派生类有个成员已经分配了空间,但是父类指针指向派生类只会调父类的析构函数,这样派生类当中已经在堆上开辟空间的成员就不会被析构,造成内存泄漏了 

内存泄漏是慢性病不会报错的

 

虚函数情况下,正确析构

请注意在栈上实例化对象,并用父类指针指向它 ,无论你够不构成多态和虚函数,它都会依照先派生类析构,然后父类析构的情况自动调用析构函数。当在栈上创建一个对象(例如,B b;)时,该对象的生命周期是自动管理的。当对象离开其作用域时(例如,函数返回或块结束),其析构函数会自动被调用。在继承的情况下,如果B继承自A,并且B的析构函数被调用,那么B的析构函数中的代码会首先执行,随后自动调用基类A的析构函数。这是因为C++的析构函数调用是隐式的,并且按照从派生类到基类的顺序进行(反正编译器干的活)

2.4 C++ 11 override和final关键字

1.override关键字

 override检查派生类虚函数是否重写了基类某个虚函数,如果没有没有重写编译报错

来个反面例子

 2.final关键字

 final修饰虚函数,表示该虚函数不能再被重写

 三.纯虚函数

在虚函数的后面写上=0,则这个函数为纯虚函数包含纯虚函数的类叫做抽象类(也叫接口类),抽象类不能实例化出对象。派生类继承后也不能实例化出对象,只有重写纯虚函数,派生类才能实例化出对象。

抽象类其实对应的就是现实世界中很难被具体化的东西(其实也可以理解为很大的东西),比如动物类,动物有很多种,猫,狗,所以动物这个概念就很抽象(因为你不能具体化到一种单独的东西)。再比如车,车的概念也很大,边界模糊,只有具体到牌子才能具体化是什么车,比如奔驰,宝马之类

正常纯虚函数派生类实例化,但是此时父类依旧是抽象类是不能被实例化的

 

四.多态的原理

1.单继承情况

多态的实现其实就和四个东西有关,虚函数表指针,虚函数表,虚函数指针,虚函数(其实可以加上jump指令地址,但是这个到多继承虚函数里再细讲)

正常调用函数是在编译链接阶段生成符号表,通过符号表去调用函数,多态是动态绑定(晚绑定)通过虚函数表来进行调用函数,虚函数表记录了各个虚函数的地址,是一个指针数组,在编译链接阶段就生成了。在具体实例化的对象里是不会直接存虚函数表,而是存一个指向虚函数表的指针(_vfptr)

不同的类之间会生成不同的虚函数表,一般来讲一个类有虚函数就会生成虚函数表,但是同一个类的不同对象用的是同一个虚函数表。

完整代码

#include<iostream>
using namespace std;

class A
{
public:
	virtual void fun(int val = 4)
	{
		cout << "A的val值:" << val << endl;
	}
private:
	int _a = 4;
};

class B :public A
{
private:
	void fun(int val = 5)
	{
		cout << "B的val值:" << val << endl;
	}
private:
	int _b = 5;
};

int main()
{
	B b;
	A a;
}

同一个类的不同对象情况

B实例化的对象,b和a报存的虚函数地址一致

那么又是怎么完成重写,区分开父类的虚函数和子类虚函数的呢,怎么完成派生类的实现和父类的接口连接起来形成新函数的

对基类的处理:在编译过程中,如果编译器发现了一个类中声明了虚函数,它会把基类的虚函数形成一个函数签名(函数原型),并将其地址存储在编译器的符号表中,实际上就是生成一个占位符

对派生类的处理:在解析派生类时,编译器会在派生类的上下文中检查该函数是否存在于基类的符号表中,并且具有相同的签名,同时检查参数类型,返回类型,是否都一致以及检查是否使用了virtual关键字。完成了上述的操作之后编译器确认了是虚函数重写,于是会重写函数,并将重写的虚函数生成机器码,并且更新符号表(编译链接时完成),将重写的函数地址放到派生类对应的虚函数表里(运行时完成,这个过程其实是更新过程,虚函数表的生成和虚函数重写虽然都是在编译器阶段进行,但是虚函数表完成时可没有直接关联到重写后的虚函数地址,此时里面暂存的还是基类的虚函数地址,因为它继承基类,所以必定这个也会直接继承下来。在运行的时候才会把这个地址换成重写后的虚函数地址)。

再概括一下这个虚函数的调用过程,对象通过虚函数表指针找到虚函数表,然后通过虚函数表里存的实际虚函数地址去调用对应的虚函数

举个例子来了解一下,A类里面有虚函数fun,fun2,fun3,派生类B里面有重写的fun,fun2,也有自己独有的虚函数fun4

#define _CRT_SECURE_NO_WARNINGS 1
#include<iostream>
using namespace std;

class A
{
public:
	virtual void fun(int val = 4)
	{
		cout << "A::fun" << val << endl;
	}
	virtual void fun2(int val = 4)
	{
		cout << "A::fun2" << val << endl;
	}
	virtual void fun3(int val = 4)
	{
		cout << "A::fun3" << val << endl;
	}
private:
	int _a = 4;
};

class B :public A
{
private:
	virtual void fun(int val = 5)
	{
		cout << "B::fun1" << val << endl;
	}
	virtual void fun2(int val = 4)
	{
		cout << "B::fun2" << val << endl;
	}


	virtual void fun4(int val = 4)//未完成重写
	{
		cout << "B::fun4" << val << endl;
	}
private:
	int _b = 5;
};

int main()
{
	B b;
	A a;
	A *ptr1=&b;
	A* ptr2 = &a;
}

他们的调用情况如图

,fun函数和fun2函数B完成重写,所以会生成另一个函数,而fun3函数没有完成重写,但是因为继承父类,所以派生类B也有一份fun3地址,但是单纯继承不会完成重写,所以父类与派生类共用一份fun3函数的内容,所以fun3的地址是一样的。而fun4也是虚函数但是不是多态完成重写的函数,是只属于B的函数,所以fun4只存在B的虚函数表里。

但是也许有人疑问,上面B的vs监视窗口里的虚函数表里保存函数地址的内容好像只保存到fun3啊,好像并没有fun4啊。这是因为计算机大部分原则都是只关心使用而不关心底层细节,虚函数表的具体细节所以展示的不明确,可以打印虚函数表的内或者查看虚函数表内存来具体看一下虚函数表的内容

B虚函数表的底层内存的内容

监视窗口的内容

其实你会发现前三个都一模一样,唯独内存里多存了一个地址1e 15 00 f9 f7 7f 00 00,这个地址其实就是fun4的地址。一般来说有虚函数就有虚函数表

2.多继承多态的情况

多继承多态与单继承的多态其实差不了多少,单继承的情况如果有虚函数会自己直接生成一个虚函数表,如果是继承的情况虽然会生成自己的虚函数表,但是虚函数表指针依旧是放到派生类当中父类东西一起(但是如果你在vs编译器监视窗口仔细看的话,会发现派生类虚函数表地址与纯基类对象生成的虚函数表指针不同)。这样处理主要是为了多态和一致性的实现,如果你要实现多态一个重要的条件是基类指针调用,可是在继承体系下基类指针是无法访问到派生类当中除继承自父类函数的剩余部分的,比如有个成员是派生类独有的,那么父类指针就无法调用这个成员。所以如果虚函数表指针单独开一片空间存储,那么父类的指针就无法访问了,那么也就做不到父类指针指向派生类,调用函数实现派生类与父类函数实现不同的重写机制,也就达不成多态了。

而多继承是继承了多少父类就会生成多少个虚函数表,但是如果两个继承当中虚函数重写相同,那么这个重写的虚函数只会生成一份,这样说可能有点抽象,来举个例子来看一下

class A
{
public:
	virtual void fun1()
	{
		cout << "A::fun1" << endl;
	}
private:
	int a = 5;
};

class B
{
public:
	virtual void fun1()
	{
		cout << "B::fun1" << endl;
	}
	virtual void fun2()
	{
		cout << "B::fun2" << endl;
	}
	virtual void fun3()
	{
		cout << "B::fun3" << endl;
	}
private:
	int b = 4;
};

class C :public A, public B
{
public:
	virtual void fun1()
	{
		cout << "C::fun1" << endl;
	}
	virtual void fun2()
	{
		cout << "C::fun2" << endl;
	}
	virtual void fun4()
	{
		cout << "C::fun4" << endl;
	}
private:
	int c = 6;
};

上面的例子里C类继承了A和B,pp是C的实例化,在pp里生成了两个虚函数表, pp当中A的虚函数表里包含重写的fun1函数和自己独有的fun4函数,不过没显示出来,加上单继承情况可以看出来没完成重写的派生类独有的函数都不会显示在监视窗口里,不过可以通过底层的内存情况来查看一下,直接输入虚函数表地址来查看内容就可以了

在内存中可以看到除了已有的13fc这个地址外,还存了119a这个地址,这个地址其实就是派生类当中独有的fun4函数,fun4函数没完成父类A或者B的重写,vs编译器环境下如果是多继承那么默认直接放到第一张虚表里

此时还有一个问题,C继承了A也继承了B,A和B里面都有一分fun1函数,那么最后完成重写的fun1函数是不是同一份呢。直接给出结论这个在A当中重写的fun1函数和B里重写的虚函数确实是同一份函数 。那么为什么同一份虚函数保存的地址不同呢,这个可以通过汇编来查看一下

为了方便查看,定义两个指针来辅助

此时ptr1与ptr2指针指向情况

首先第一步调试状态下运行到ptr1->fun1()语句的位置查看反汇编 

 

 前面的几句基本都不怎么重要,请记住call指令,call指令主要用来调用函数或者子程序调用

 依旧和之前一样按F10跳转到call指令语句

 本质上和监视窗口进入函数差不多,所以直接F11执行,看看call到哪里去了

此时会发现不是直接到函数,而是到了一个jump跳转指令这里,正常来讲函数经过处理后是一句一句指令,而call应该是直接到达第一句语句的位置,为什么会加了一个jump指令呢。这是vs编译器经过特殊处理,基本都要通过jump指令跳转

继承按F11,看看跳转到哪里,此时会发现直接找到了fun1函数,请记住2570h这个地址

再来查看ptr2的情况,相同的步骤我就不细讲了

进入ptr2调函数的反汇编

call指令查看

 此时你会发现C::fun1 (07FF7A02025D0h)这一句和上面ptr1的fun1 调用情况C::fun1(07FF7A0202570h)已经不同了,ptr1中2570h直接就是fun1函数第一句的地址,而ptr2不是

让我们继续跳转看一下

可以看出来之前的jump指令是跳转到sub(减)指令,rcx减了一个10h,再来看一下ptr2刚开始进入反汇编的图片,可以看出了rcx是存了ptr2的地址位置的,究竟减10h有什么用呢,我们先走完第二个jump指令再来细谈,先看看现在jump跳转到哪里

可以看出来25D4这个位置上的jump指令跳转到了另一条jump指令而此时j这条新的jump指令报存的地址就是正常跳转到fun1函数的第一条语句的 ,也是殊途同归到了fun1函数。所以这一系列过程证明了,重写的fun1函数确实是同一份

现在再来说说为什么减10h,10h转换成十进制是16,所以ptr2减了一个16。这个减16是减去了整个A的大小,让ptr2直接指向整个派生类的开头,可以打印一下sizeof(A对象)的大小看看

本来ptr2是指向派生类的父类B的开头的 

 

现在减了一个A类大小的偏移量,直接和ptr1一样指向开头 

为什么要让ptr2指针指向开头呢,只要是为了this指针能访问到这个对象里面的所有成员,this是指向对象的指针,虽然ptr2类型是B类,但是它依旧可以是派生类内部的this指针(因为父类指针也可以指向派生类),理论上讲this指针 是可以访问到类里面的所有成员的。但是此时ptr2是指向派生类中部的,是无法访问到派生类当中继承A得来的成员a=5的,所以要把ptr2挪到开头,以便于this指针能够访问到类的所有成员

打印虚函数表

上面的多继承多态情况我说派生类当中没有完成重写的虚函数是会放到默认第一张虚表里的,光看内存可能没什么说服力,我们可以提取虚函数的所有内容,并把虚函数的各个函数的地址调用一样看看具体情况,这样就比较直观了

#include<iostream>
using namespace std;
class A
{
public:
	virtual void fun1()
	{
		cout << "A::fun1" << endl;
	}
	
private:
	int a = 5;
};

class B
{
public:
	virtual void fun1()
	{
		cout << "B::fun1" << endl;
	}
	virtual void fun2()
	{
		cout << "B::fun2" << endl;
	}
	virtual void fun3()
	{
		cout << "B::fun3" << endl;
	}

private:
	int b = 4;
};

class C :public A, public B
{
public:
	virtual void fun1()
	{
		cout << "C::fun1" << endl;
	}
	virtual void fun2()
	{
		cout << "C::fun2" << endl;
	}
	virtual void fun4()
	{
		cout << "C::fun4" << endl;
	}
private:
	int c = 6;
};
typedef void(*vfun)();

void printadd(vfun ptr[])
{
	for (int i = 0; ptr[i] != nullptr; i++)
	{
		vfun f = ptr[i];
		f();
	}

}

int main()
{
	C pp;
	A* ptra = &pp;
	B* ptrb = &pp;
	printadd((vfun*)(*(intptr_t*)ptra));
	cout << endl;
	printadd((vfun*)(*(intptr_t*)ptrb));
}

让我来解释一下上面提取虚函数表内容并且调用函数的代码

打印函数上面的typedef void(*vfun)();

这个其实是将函数指针重命名为vfun,函数指针类型是void (*)();一般typedef可能是先typedef  类型 重命名的新名字,但是函数指针重命名写成typedef void (*)()  vfun就会直接报错,没办法函数指针只能这样重命名

 再来看看调用打印函数的地方

A* ptra = &pp;
    B* ptrb = &pp;
    printadd((vfun*)(*(intptr_t*)ptra));
    cout << endl;
    printadd((vfun*)(*(intptr_t*)ptrb));

首先可以知道的是虚函数表指针一定是在整个派生类的当中继承父类的那一部分东西的最上层,指针在32位情况下是4个字节,在64位情况下是8个字节,也就是说取各自父类的虚函数表指针最上面4个或者8个字节就可以了。首先我做了处理,通过父类的指针指向派生类的,这样其实就是指向派生类当中父类的部分,比如ptra其实就是指向了派生类当中属于A的那一部分,然后怎么在这个指针的基础上取出8个或者4个字节(虚函数表指针在最上面,指针占4个地址或者8个)呢,如果是32位的话直接把ptra转换成int类型的地址就可以了,直接强转就可以了(int*)ptrb,因为int类型本来就占4个字节,解引用这样就将最上面的4个字节的内容取出来了。但是此时解引用得到的是整型,所以通过函数指针类型强制转换成函数指针类型(vfun*)(*(int*)ptra);这样就将虚函数表指针取出来了。至于为什么用intptr_t,这个其实也是整型类型,区别在于intptr_t强制类型转换更安全,而且intptr_t在32位情况下是4个字节,在64位下是8个字节,可以不用根据电脑位数设置对应的类型去取,比如32位下可以用int去取,因为int本身就占4个字节,而64位下又得换个8字节类型了

然后再来具体说说打印虚函数表的函数

void printadd(vfun ptr[])
{
    for (int i = 0; ptr[i] != nullptr; i++)
    {
        vfun f = ptr[i];
        f();
    }

}
 

 之前通过一系列操作    printadd((vfun*)(*(intptr_t*)ptra));把虚函数表指针传过来了,虚函数表里传的是各个函数的地址,所以它可以看出是一个函数指针数组,通过循环就可以取到各个函数地址了

 那么这个循环什么时候停止呢,在内存里可以看到虚函数表里的内容与其他数据之间有一堆0隔开,其实这个是空指针nullptr的意思,所以ptr[i]==nullptr就可以停止了,可以确认已经打印完了虚函数表的内容了

 vfun f = ptr[i];
        f();

这又是什么意思呢?这其实就是将虚函数表的内容取出来,这个内容是函数的地址,知道地址当当然可以调用了,f()其实就是通过函数的地址调用函数

最后打印出来就可以很直白地看出来未重写的fun4函数默认放到第一张虚函数表了

五.虚函数表,虚函数存储位置

可以写一个代码,分别打印出静态区和常量区,栈区以及堆区的地址,看看虚函数表与他们哪个更接近,哪个就是虚函数表的位置

#define _CRT_SECURE_NO_WARNINGS 1
#include<iostream>
using namespace std;
class A
{
public:
	virtual void fun1()
	{
		cout << "A::fun1()";
	}
};

class B:public A
{
public:
	virtual void fun1()
	{
		cout << "B::fun1()";
	}
};

int main()
{
	B b;
	A* ptra = &b;
	printf("虚函数表:%p\n", *((intptr_t*)ptra));

	static int st = 0;//静态区
	printf("静态区:%p\n", &st);

	const char* ao = "jjjj";//常量区
	printf("常量区:%p\n",ao);

	int* ptr = new int(5);//堆区
	printf("堆区:%p\n", ptr);

	int k = 0;//栈区
	printf("栈区:%p\n", &k);
}

 

从打印结果来看,虚函数表地址9C0BE40和静态区9C0F4CC以及常量区9C0BC90都接近的,但是相比之下还是常量区更接近一点,所以虚函数表存在常量区里面。而虚函数与普通成员函数都一样,也是存在公共代码段里面 。虚函数表编译时就直接存在了,虚函数表指针构造时才初始化给对象的

来解答几个问题

静态成员函数可以是虚函数吗

不行,静态成员函数没有this指针,他可以指定类域调用,但是无法构成多态,没有意义

构造函数可以是虚函数吗

不可以,会编译报错,对象中的虚表指针是构造函数阶段才初始化的,虚函数多态调用,要到虚表里面去找,但是虚表指针还没有初始化

内联函数可以是虚函数吗

可以,普通调用,内联函数还是内联函数,inline起作用;多态调用,内联就不起作用了

对象访问普通函数和虚函数哪个快

普通调用是一样快,多态调用,虚函数要慢一点,因为多了一步去虚函数表里查找

 

  • 20
    点赞
  • 13
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值