[置顶] C++ — 继承和多态的基础虚函数类

                              虚函数类




 上一个博客继承中我们提到虚拟继承,现在我们来探究这种的虚函数,虚函数类的成员函数前面加 virtual 关键字,则这个成员函数称为虚函数,不要


小看这个虚函数,他可以解决继承中许多棘手的问题,而对于多态 那他更重要了,没有它就没有多态,所以这个知识点非常重要,以及后面介绍的虚函


数表都极其重要,一定要 认真的理解~ 现在开始概念 虚函数就又引出一个概念,那就是重写(覆盖),当在子类的定义了一个与父类完全相同的虚函数


,则称子类的这个函数重写(也称覆盖)了父类的这个虚函数。这里先提一下虚函数表,后面会讲到 的,重写 就是将子类里面的虚函数表里的被重写


父类的函数地址全都改成子类函数的地址。


2.纯虚函数



在成员函数的形参后面写上=0,则成员函数为纯虚函数。包含纯虚函数的类叫做抽象类(也叫接口类)


抽象类不能实例化出对象。纯虚函数在派生类 中重新定义以后,派生类才能实例化出对象。

看一个例子:

class Person
{
     virtual void Display () = 0;   // 纯虚函数
protected :
     string _name ;          // 姓名
};

class Student : public Person
{};



先总结一下概念:

1.派生类重写基类的虚函数实现多态,要求函数名、参数列表、返回值完全相同。(协变除外)


2.基类中定义了虚函数,在派生类中该函数始终保持虚函数的特性。


3.只有类的成员函数才能定义为虚函数。


4.静态成员函数不能定义为虚函数。


5.如果在类外定义虚函数,只能在声明函数时加virtual,类外定义函数时不能加virtual。


6.不要在构造函数和析构函数里面调用虚函数,在构造函数和析构函数中,对象是不完整的,可能会发生 未定义的行为。


7.最好把基类的析构函数声明为虚函数。(why?另外析构函数比较特殊,因为派生类的析构函数跟基类的 析构函数名称不一样,但是构成覆盖,这里

是因为编译器做了特殊处理)


8.构造函数不能为虚函数,虽然可以将operator=定义为虚函数,但是最好不要将operator=定义为虚函数, 因为容易使用时容易引起混淆。 






概念终于结束了,现在我们来终于要开始今天真正的主角了,虚函数表!!


何为虚函数表,我们写一个程序,调一个监视窗口就知道了。


下面是一个有虚函数的类:

#include<iostream>
#include<windows.h>
using namespacestd;
 
class Base
{
public:
         virtual void func1()
         {}
 
         virtual void func2()
         {}
 
private:
         inta;
};
 
void Test1()
{
         Base b1;
}
 
int main()
 
{
         Test1();
         system("pause");
         return0;
}


我们现在点开b1的监视窗口


 


这里面有一个_vfptr,而这个_vfptr指向的东西就是我们的主角,虚函数表。一会大家就知道了,无论是单继承还是多继承甚至于我们的菱形继承虚函


数表都会有不同的形态,虚函数表是一个很有趣的东西。


 

 

                         



                       我们来研究一下单继承的内存格局



仔细看下面代码:


#include<iostream>
#include<windows.h>
using namespace std;
 
 
class Base
{
public:
         virtual void func1()
         {
                   cout<< "Base::func1"<< endl;
         }
 
         virtual void func2()
         {
                   cout<< "Base::func2"<< endl;
         }
 
private:
         inta;
};
 
class Derive:public Base
{
public:
         virtual void func1()
         {
                   cout<< "Derive::func1"<< endl;
         }
 
         virtual void func3()
         {
                   cout<< "Derive::func3"<< endl;
         }
 
         virtual void func4()
         {
                   cout<< "Derive::func4"<< endl;
         }
 
private:
         int b;
};


 

对于Derive类来说,我们觉得它的虚表里会有什么?


首先子类的fun1()重写了父类的fun1(),虚表里存的是子类的fun1(),接下来父类的fun2(),子类的fun3(),fun4()都是虚函数,所以虚表里会有4个元


素,分别为子类的fun1(),父类fun2(),子类fun3(),子类fun4()。然后我们调出监视窗口看我们想的到底对不对呢?



 


我预计应该是看到fun1(),fun2(),fun3(),fun4()的虚函数表,但是呢这里监视窗口只有两个fun1(),fun2(),难道我们错了????


这里并不是这样的,只有自己靠得住,我觉得这里的编译器有问题,那我们就得自己探索一下了。 但是在探索之前我们必须来实现一个可以打印虚函


数表的函数。

 

typedef void(*FUNC) ();
void PrintVTable(int* VTable)
{
         cout<< " 虚表地址"<<VTable<< endl;
 
         for(inti = 0;VTable[i] != 0; ++i)
         {
                   printf(" 第%d个虚函数地址 :0X%x,->", i,VTable[i]);
                   FUNC f = (FUNC)VTable[i];
                   f();
         }
 
         cout<< endl;
}
 
 
int main()
{
         Derive d1;
         PrintVTable((int*)(*(int*)(&d1)));
         system("pause");
         return0;
}


下图来说一下他的缘由:




我们来使用这个函数:


//单继承
class Base
{
public:
	virtual void func1()
	{
		cout << "Base::func1" << endl;
	}

	virtual void func2()
	{
		cout << "Base::func2" << endl;
	}

private:
	int a;
};

class Derive :public Base
{
public:
	virtual void func1()
	{
		cout << "Derive::func1" << endl;
	}

	virtual void func3()
	{
		cout << "Derive::func3" << endl;
	}

	virtual void func4()
	{
		cout << "Derive::func4" << endl;
	}

private:
	int b;
};
typedef void(*FUNC) ();

void PrintVTable(int* VTable)
{
	cout << " 虚表地址>" << VTable << endl;

	for (int i = 0; VTable[i] != 0; ++i)
	{
		printf(" 第%d个虚函数地址 :0X%x,->", i, VTable[i]);
		FUNC f = (FUNC)VTable[i];
		f();
	}

	cout << endl;
}


int main()
{
	Derive d1;
	PrintVTable((int*)(*(int*)(&d1)));
	system("pause");
	return 0;
}

这里我就要讲讲这个传参了,注意这里的传参不好理解,应当细细的"品味".


PrintVTable((int*)(*(int*)(&d1)));

 

首先我们肯定要拿到d1的首地址,把它强转成int*,让他读取到前4个字节的内容(也就是指向虚表的地址),再然后对那个地址解引用,我们已经拿


到虚表的首地址了,但是此时这个变量的类型解引用后是int,不能够传入函数,所以我们再对他进行一个int*的强制类型转换,这样我们就传入参数


了,开始函数执行了,我们一切都是在可控的情况下使用强转,使用强转你必须要特别清楚的知道内存的分布结构。最后我们来看看输出结果:




到底打印的对不对呢? 我们验证一下:

 




这里我们通过&d1的首地址找到虚表的地址,然后访问地址查看虚表的内容,验证我们自己写的这个函数是正确的。(这里VS还有一个bug,当你第一次


打印虚表时程序可能会崩溃,不要担心你重新生成解决方案,再运行一次就可以了。因为当你第一次打印是你虚表最后一个地方可能没有放0,所以你


就有可能停不下来然后崩溃。)我们可以看到d1的虚表并不是监视器里面打印的那个样子的,所以有时候VS也会有bug,不要太相信别人,还是自己靠


得住。哈哈哈,臭美一下~



                           我们来研究一下多继承的内存格局



探究完了单继承,我们来看看多继承,我们还是通过代码调试的方法来探究对象模型

看如下代码:

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) ();

void PrintVTable(int* VTable)
{
	cout << " 虚表地址>" << VTable << endl;

	for (int i = 0; VTable[i] != 0; ++i)
	{
		printf(" 第%d个虚函数地址 :0X%x,->", i, VTable[i]);
		FUNC f = (FUNC)VTable[i];
		f();
	}

	cout << endl;
}

void Test1()
{
	Derive d1;

	int* VTable = (int*)(*(int*)&d1);

	PrintVTable(VTable);

	// Base2虚函数表在对象Base1后面
	VTable = (int *)(*((int*)&d1 + sizeof (Base1) / 4));
	PrintVTable(VTable);
}

int main()
{
	Test1();
	system("pause");
	return 0;
}

现在我们现在知道会有两个虚函数表,分别是Base1和Base2的虚函数表,但是呢!我们的子类里的 fun3()函数怎么办?它是放在Base1里还是Base2里还


是自己开辟一个虚函数表呢?

我们先调一下监视窗口:






监视窗口又不靠谱了。。。。 完全没有找到fun3(). 那我们直接看打印出来的虚函数表。




现在很清楚了,fun3()在Base1的虚函数表中,而Base1是先继承的类,好了现在我们记住 这个结论,当涉及多继承时,子类的虚函数会存在先继承的那

个类的虚函数表里。记住了!


我们现在来看多继承的对象模型:








现在我在写最后一个知识点,为什么尽量最好把基类的析构函数声明为虚函数??




现在我们再来写一个例子,我们都知道平时正常的实例化对象然后再释放是没有一点问题的, 但是 现在我这里举一个特例:


我们都知道父类的指针可以指向子类,现在呢我们我们用一个父类的指针new一个子类的对象。


//多态  析构函数
class Base
{
public:
	virtual void func1()
	{
		cout << "Base::func1" << endl;
	}

	virtual void func2()
	{
		cout << "Base::func2" << endl;
	}

	virtual ~Base()
	{
		cout << "~Base" << endl;
	}

private:
	int a;
};

class Derive :public Base
{
public:
	virtual void func1()
	{
		cout << "Derive::func1" << endl;
	}
	virtual ~Derive()
	{
		cout << "~Derive"<< endl;
	}
private:
	int b;
};

void Test1()
{
	Base* q = new Derive;
	delete q;
}
int main()
{
	Test1();
	system("pause");
	return 0;
}

这里面可能会有下一篇要说的多态,所以可能理解起来会费劲一点。


注意这里我先让父类的析构函数不为虚函数(去掉virtual),我们看看输出结果:




这里它没有调用子类的析构函数,因为他是一个父类类型指针,所以它只能调用父类的析构函数, 无权访问子类的析构函数,这种调用方法会导致内存


泄漏,所以这里就是有缺陷的,但是C++是不会 允许自己有缺陷,他就会想办法解决这个问题,这里就运用到了我们下次要讲的多态 现在我们让加上

为父类析构函数加上virtual,让它变回虚函数,我们再运行一次程序的:





诶! 子类的虚函数又被调用了,这里发生了什么呢??  来我们老方法打开监视窗口。








刚刚这种情况就是多态,多态性可以简单地概括为“一个接口,多种方法”,程序在运行时才决定调用 的函数,它是面向对象编程领域的核心概念。这

个我们下一个博客专门会总结这个。

当然虚函数的知识点远远没有这么一点,这里可能只是冰山一角,比如说菱形继承的虚函数表是什么样? 然后菱形虚拟继承又是什么样子呢? 这些等

我总结一下会专门写一个博客来讨论菱形继承。 虚函数表我们应该已经知道是什么东西了,也知道单继承和多继承中它的应用,这些应该就足够了,这

其实都是为 以后要说到的多态打基础呢,你只要深刻的理解了这些东西,那么多态会特别容易理解。


这些就是虚函数的一点简单见解,有问题还请大家指出,让我改正,有什么不足的也请指出来,让我可以查漏补缺~


下面是我们宿舍自己搞的一个公众号,平时会在里面发发动态,爱学习的你快关注它,我们一起进步呢~



                                           


你可能感兴趣的:(C++,继承,内存,虚函数,多态)