谈虚函数与多态

c++中较为复杂的就是虚函数表这一块,很多人只是貌似懂了,会用,知道其大概内存模型,却没有对其有一个非常清晰的理解,这篇文章一为我的笔记,二旨在给很多c++的同学一个比较有格调的解释。

话不多说,我们直接看代码:

class A
{

public:
	int a;
	A(int a) { this->a = a; }
	void Run()
	{
		std::cout << "A::Run" << "----" << a << std::endl;
	}

};

class B :public A
{
private:int b = 10;
public:
	B(int a) : A(a)
	{
		this->b = b;
	}
	void Run()
	{
		std::cout << "B::Run" << "----" << a <<"----" << b << std::endl;
	}
        void run()
	{
		std::cout << "B::run" << "----" << a <<"----" << b << std::endl;
	}

};

int _tmain(int argc, _TCHAR* argv[])
{
	A a(1), *pa;
	B b(2), *pb;

	pa = &a; pa->Run();
	pb = &b; pb->Run();
	pa = &b; pa->Run();      // pa = &b; pa->run()(错误); pa-> 无法指向run()函数!
	pb = (B*)&a; pb->Run();
	return 0;
}

我们直接来看函数运行结果:

为什么是这样的结果呢?

我们来细细分析:

1,2 的结果我们都能理解,

3,4 如何解释呢?

(3):

pa = &b; pa->Run();

B类是A类的派生类,我们用基类指针指向派生类对象,为何结果是:

扫描二维码关注公众号,回复: 2881864 查看本文章

调用了基类A的行为函数,打印B类的参数(2)呢?

我们知道,我们实例化的pa,pb是在栈区的,而函数是属于代码区,这里要提到当类进行实例化如:A  a(1),类中的静态成员及普通函数是不会占取a的内存的,因为类成员函数实际上与普通的全局函数一样。 只不过编译器在编译的时候,会在成员函数上加一个参数,传入这个对象的指针。成员函数地址是全局已知的,对象的内存空间里根本无须保存成员函数地址。 对成员函数(非虚函数)的调用在编译时就确定了。 像 a.Run() 这样的调用会被编译成形如 _A_Run( &a ) 的样子。

故我们运行的结果就能理解了,pa->Run会编译成 _A_Run(pa ),执行的是A类的Run()函数。pa->run 无法执行,因为函数编译会形成如 _B_run( pb ),pb限定为B类型,而pa明显不是。具体可以看下图,我输出了pa的类型。

   B(int a) : A(a)
    {
        this->b = b;
    }

B类实例 B b(2),这句代码使B:A(a)a初始化赋值为2。

 (4):

派生类对象包含基类对象,把基类转为派生类是比较危险的操作,如此例,输出了一个不得了的数字,原因也很简单,

void Run()
	{
		std::cout << "B::Run" << "----" << a <<"----" << b << std::endl;
	}

A类内存就一个4字节,B类内存为8字节(前面4字节为a,后面4字节为b),4字节强转为8字节,前面4字节为A的a值,后面四字节内存泄露,不知道什么值,

故输出结果为上图。

到此我们对继承函数的调用应该有了一定的理解。

我们应该知道类的内存存取模型,空类占一字节,类中的静态成员及普通函数是不会占取类的内存的,所有虚函数占4字节

(存放第一个虚函数名地址),并位于类内存的前4字节,知道这个特性我们就可以做点有趣的事情。

class C 
{
private:int c = 20;
public:
	
	void virtual Run_1()
	{
		std::cout << "C::Run_1"  << std::endl;
	}
	void virtual Run_2()
	{
		std::cout << "C::Run_2" << std::endl;
	}
};

看这个类,我们很清楚的知道它有8字节,前面四字节为虚函数表首地址,后面四字节为c的值,我记得我老师曾讲过一句话,如果你知道一个类的内存存取模型,你可以有无数种办法取出他的数据,是的,这里我们便开始庖丁解牛这个C类了。(这个c类很简单,庖丁解牛似乎太够份了,但是这种思路是很重要的,因为很多文件加密、解密就是基于这个思路的)

C c;
	struct temp
	{
		int *a;
		int b;
	};
	temp t;
	t =*(temp*)&c;
        //temp *t;
	//t =(temp*)&c;

 我们非常清楚c类内存存取为4(指针)+4(数据),故我设计了一个4+4的结构体去匹配&c的地址,并对&c做了强转,

 我们j简简单单就发现c的值安安静静躺在b的怀中,但是a是个什么值,我们刚刚讲了,a是虚函数表的首地址,那怎么用a调用出虚函数呢,嗯恩,开始秀操作了哟:

 

其实非常简单,解引用就是了,是不是surprise,那么第二个虚函数怎么调用了,也是非常简单的操作:我们先看一张图:

 

我们已经知道虚函数1的地址,怎么调用虚函数二呢,这就是一个数组吗,我们就用数组的方式调用它就好了,我们虚函数1就是数组首地址呀,

(t->a)+1不就是第二个虚函数地址了吗。

下面代码与上面代码类似,因其更有通用性,故在此贴出。

    C c;
    pFun pf = NULL;
    for (int i = 0; i < 2; ++i)
    {
    pf = (pFun)(*((int *)*(int *)&c + i));
    pf();
    }

 好了,相信到这里,你对虚函数的存取方式应该有一个深刻的理解了,至于虚函数实际运用能达到什么效果

看下面的图就一目了然:

class A
{

public:
	int a;
	A(int a) { this->a = a; }
	void virtual Run()
	{
		std::cout << "A::Run" << "----" << a << std::endl;
	}

};

 仅仅只在Run()前加一个virtual,会有什么变化呢。

 结果显然:这才是我们想要的效果,函数调用是通过内存数据去匹配,而不是其它,这样我们就给子类开放了自由空间,子类可以重写我们的虚函数,而我们基类也可以多态的调用子类重写的函数。

关于虚函数,这里再提几点:

1:子类继承虚函数表的表项,不继承虚函数表(重写的虚函数会改变地址)。

2:虚函数是占子类内存空间的,这也是为什么能多态的原因。

3:当基类有纯虚函数时,基类的析构函数必须为虚析构。

猜你喜欢

转载自blog.csdn.net/yuyuchiyue/article/details/81119459