跳至主要內容

继承多态与虚函数案例分析

张威大约 6 分钟c/c++多态

继承多态与虚函数案例分析

题目一:猫狗叫声问题

//动物基类  泛指  类-》抽象一个实体的类型
class Animal
{
public:
	Animal(string name):_name(name){}
	//纯虚函数
	virtual void bark() = 0;
protected:
	string _name;
};

//动物实体类
class Cat : public Animal
{
public:
	Cat(string name):Animal(name){}
	void bark()
	{
		cout << _name << "bark:miao miao!" << endl;
	}
};

class Dog : public Animal
{
public:
	Dog(string name):Animal(name){}
	void bark()
	{
		cout << _name << "bark:wang wang!" << endl;
	}
};

class Pig : public Animal
{
	public:
	Pig(string name):Animal(name){}
	void bark()
	{
		cout << _name << "bark:heng heng!" << endl;
	}
};

int main()
{
	Animal *p1 = new Cat("加菲猫");
	Animal *p2 = new Dog("二哈");

	int *p11 = (int*)p1;
	int *p22 = (int*)p2;
	int tmp = p11[0];
	p11[0] = p22[0];
	p22[0] = tmp;

	p1->bark();
	p2->bark();

	delete p1;
	delete p2;

	return 0;
}

分析一下:

int *p11 = (int*)p1;
int *p22 = (int*)p2;
int tmp = p11[0];//p11[0]访问的Cat的前4个字节
p11[0] = p22[0];//p22[0]访问的Dog的前4个字节
p22[0] = tmp;

主要原因是Cat的vfptr存放Dog的vftable地址,Dog的vfptr存放Cat的vftable地址。因此发生动态绑定时,输出的结果被交换

题目二:继承结构中基类派生类同名覆盖方法不同默认值问题

class Base
{
public:
	virtual void show(int i = 10)
	{
		cout << "call Base::show i:" << i << endl;
	}
};

class Derive : public Base
{
public:
	virtual void show(int i = 20)
	{
		cout << "call Derive::show i:" << i << endl;
	}
};

int main()
{
	Base *p = new Derive();
	p->show();
	delete p;

	return 0;
}

分析一下:

我们简单看一下,有virtual,是动态绑定,指向派生类对象。会调用派生类的show,但是为什么调用的派生类的方法,函数的却使用了基类的参数的值?

push 0Ah =》函数调用,参数压栈在编译时期确定好的
mov eax, dword ptr[p]
mov ecx, dword ptr[eax]
call ecx

我们来看一下函数调用过程,调用一个函数时要先压参数,若没有传入实参,会将形参默认值压栈。运行时候才发生动态绑定调用派生类的show方法。

p是基类指针调用的show(),编译器只能看见基类的show,将基类的10压栈,参数压栈在编译时期确定好的。真正调用时不管是静态还是动态绑定,只要是是show()方法,形参压栈的值都是固定的10。

题目三:派生类的方法写成私有的可以正常调用吗

class Base
{
public:
	virtual void show()
	{
		cout << "call Base::show" << endl;
	}
};

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

int main()
{
	Base *p = new Derive();
	p->show();
	delete p;

	return 0;
}

输出结果:成功调用

分析一下:p->show()最终能够调用Derive的show,是在运行时期才确定的。但是是不是public,是在就需要确定好的。编译阶段编译器只能看见Base中的show为public,编译器可以调用;但最终调用的是基类的方法还是派生类的方法取决于形成的汇编指令是静态绑定还是动态绑定,因为为动态绑定最后成功调用派生类的show。

但是如果我们交换一下基类与派生类的访问限定符:

class Base
{
private:
	virtual void show()
	{
		cout << "call Base::show" << endl;
	}
};

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

输出结果:编译出错。

只有使用派生类指针此时才可以调用:道理与前面相同。

Derive *p = new Derive();
p->show();
delete p;

题目四:下面两段代码是否正确

class Base
{
public:
	Base()
	{
		cout << "call Base()" << endl;
		clear();
	}
	void clear()
	{
		memset(this, 0, sizeof(*this));//Base大小赋为0
	}
	virtual void show()
	{
		cout << "call Base::show() " << endl;
	}
};

class Derive : public Base
{
public:
	Derive()
	{
		cout << "call Derive() " << endl;
	}
	void show()
	{
		cout << "call Derive::show() " << endl;
	}
};

int main()
{
	//1
	Base *pb1 = new Base();
	pb1->show();//动态绑定
	delete pb1;

	//2
	Base *pb2 = new Derive();;
	pb2->show();//动态绑定
	delete pb2;

	return 0;
}

1输出结果: 执行失败

分析一下1: 基类的对象内存如图,vfptr指向Base vftable,当调用clear()时,将基类的对象内存清为0,虚函数指针也变为0地址,进行动态绑定时,访问不到,调用时出错,程序崩溃。

2输出结果:执行成功

分析一下2: 派生类中也有一个vfptr,首先调用基类的构造,构造基类部分,然后调用派生类构造,构造派生类部分。我们vfptr里面存储的是vftable的地址,底层来看指令一定有一个地方将vfptr写入vftable中。

push ebp
mov ebp,esp
sub esp,4Ch
#rep stos esp<->ebp #0xCCCCCCCC #windows会将esp和ebp初始化为0xCCCCCCCC;而gcc/g++只做前3步,没有这个

先了解一下这个:每一个函数进来首先push ebp,将调用方函数站点地址压进来,调用方函数站点地址放入当前函数栈底。再将esp赋给ebp,ebp指向当前函数栈底,sub esp 4Ch为当前函数开辟栈帧,rep sots esp<->ebp当前函数栈帧初始化为0;准备工作做完之后,指向当前函数第一行指令。vfptr <—>&Base::vftable,每次层构造函数都会执行刚才的步骤,派生类中会将vfptr<—>&Derive::vftable。当我们new一个Derive对象时,首先调用基类构造,基类构造首先会将基类的vftable写入vfptr,再调用clear会将虚函数的值清为0;再调用派生类的构造函数,==派生类构造函数压完栈初始化后会将&Derive::vftable的地址写入到虚函数指针中。==当我们用指针调用show()vfptr是有效的,能够从虚函数表中取出来派生类重写的show(),因此执行成功。