225-C++继承多态笔试题实战

第1题

#include <iostream>
using namespace std;

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; }
};

int main()
{
	Animal* p1 = new Cat("加菲猫");//vfptr -> Dog vftable		(vfptr在后面被交换了)
	Animal* p2 = new Dog("二哈");//vfptr -> Cat vftable		(vfptr在后面被交换了)

	int* p11 = (int*)p1;
	int* p22 = (int*)p2;
	//交换 int类型 4字节 
	int tmp = p11[0];//p11[0]访问的就是Cat的前4个字节
	p11[0] = p22[0];//p22[0]访问的就是Dog的前4个字节
	p22[0] = tmp;
	//把Cat和Dog的前4字节交换了,也就是两个对象的vfptr交换了 
	p1->bark();//p1 -> Cat vfptr -> Dog vftable  Dog的bark
	p2->bark();//p2 -> Dog vfptr -> Cat vftable  Cat的bark

	delete p1;
	delete p2;

	return 0;
}

在这里插入图片描述

第2题

覆盖:

  • 覆盖和形参的默认值没有关系,和形参的类型和个数有关
  • 返回值、函数名,形参列表相同!
  • 在派生类的虚函数表中,不再存储从基类继承来的覆盖函数,存储派生类的覆盖函数地址

在这里插入图片描述

在这里插入图片描述
动态绑定,调用的是派生类的show方法,但是为什么打印出来的形参默认值i值是基类show的默认值?

p->show();
调用一个函数的时候,先压参数,对于参数带默认值的函数,用户传什么实参,
就压传入的实参,如果用户没有传实参,就压入形参的默认值到栈上。
动态绑定最终调用派生类的show方法是在运行时发生的,在编译阶段
p仅仅是Base类型,编译器只能看到基类Base的show方法,编译的时候
编译器看到这个show方法,调用这个show的指针p是Base类型的,
就去Base类型里面找到这个show,把这个show的默认值 10 push入栈

push 0Ah => 函数调用,参数压栈是在编译时期就确定好的
	mov eax, dword ptr[p]//这就是动态绑定的操作了
	mov ecx, dword ptr[eax]//放的是虚函数表的地址
	call ecx

真真正正调用的时候,不管最终是动态绑定哪个函数,只要是show方法,它的形参的压栈的值始终是固定的10,和派生类的show的参数默认值没有关系了

函数调用,参数压栈是在编译时期就确定好的,因为生成的指令中已经确定好了

在继承结构中,给基类,派生类的同名覆盖方法添加默认值,而且默认值还不同,就没有意义了。基类指针或者引用指向派生类对象进行调用的时候,派生类的虚函数的形参默认值是永远都用不到。

#include <iostream>
using namespace std;

class Base
{
public:
	virtual void show(int i = 10)
	{
		cout << "call Base::show i:" << i << endl;
	}
};
class Derive : public Base
{
public:
	void show(int i = 20)	//覆盖关系和形参的默认值是多少没有关系的 
	{
		cout << "call Derive::show i:" << i << endl;
	}
};
int main()
{
	Base* p = new Derive();//要把基类的析构函数设置成虚析构函数
	/*
	push 0Ah => 函数调用,参数压栈是在编译时期就确定好的
	mov eax, dword ptr[p]
	mov ecx, dword ptr[eax]
	call ecx
	*/
	p->show();
	//看Base作用域的show是虚函数,发生动态绑定,p指向的是派生类对象, p-> Derive vfptr -> Derive vftable
	delete p;

	return 0;
}

第3题

把派生类的show方法改成私有的。请问能不能调用成功?
在这里插入图片描述

是可以正常调用的
在这里插入图片描述
原因:

  • 指针p最终能调用到Derive的show方法是在运行时期才确定的。(指令发生动态绑定)(运行时,成员变量成员方法,内存都是可以访问的)
  • 成员方法能不能调用,就是说方法的访问权限是不是public,这个是在编译阶段就要确定好的。(访问限定符就是控制在编译阶段,看用户写的代码符不符合语法的规范)
  • 所以,编译器在编译时只能看见p指针(基类指针)调用的show方法,它在编译阶段只能看见Base里的show是什么样的,Base里的show是public,所以编译器放行了,可以调用。但至于最终调用的是基类的方法还是派生类的方法,得看汇编形成的是静态绑定还是动态绑定,静态绑定肯定调用的是基类方法,动态绑定靠的是寄存器ecx,只有在运行的时候才知道,放一个虚函数地址在寄存器,然后再从寄存器取出虚函数的地址进行调用。

如果是把基类的show方法设置成private,就不能调用成功了。
在这里插入图片描述
在这里插入图片描述
编译器在编译的时候看到p是Base类型的,看到的是Base::show(),看到是show是私有的,不能调用!!!编译都通不过,哪来的运行呢!!!

换成派生类的指针就可以调用了,编译器编译的时候看到的就是Derive::show了,是公有的,可以调用,编译通过,然后看到show是虚函数,就动态绑定,最终调用的也是Derive::show()
在这里插入图片描述

在这里插入图片描述

第4题

在这里插入图片描述

在这里插入图片描述

这两段代码,运行起来,是正确的还是错误的?

上面代码

先看上面的代码:
在这里插入图片描述
在这里插入图片描述
代码运行起来有问题。

原因:
基类指针指向基类对象,基类对象的内存是这样的:

  • 基类对象的虚函数指针vfptr指向其vftable虚函数表
  • 在构造的时候调用了clear函数,把内存清0了,vfptr变成0地址了。
    在这里插入图片描述
  • 然后pb1->show,进行动态绑定的时候,从虚函数指针取虚函数表,就访问不到了,而且0地址也是不能读不能写的,调用的时候出错了。

下面代码

在这里插入图片描述

在这里插入图片描述
代码运行OK。

  • 基类指针指向派生类对象。派生类对象的内存结构如下:
  • 首先是调用基类的构造,基类的内存空间清0了。然后调用派生类的构造。
  • vfptr里面存储的是vftable的地址

也就是说,在指令上,一定有一个地方类型的vftable虚函数表的地址要写入vfptr虚函数指针里面。这个指令是在什么时候去写的?

  • 每一个函数从左括号到第一行代码之间,有指令生成!!
	Base()
	{
		/*
		push ebp//把调用方函数的栈地址压进来
		mov ebp, esp//ebp指向当前函数的栈底
		sub esp, 4Ch//给当前函数开辟栈帧
		rep stos esp<->ebp//当前函数栈帧初始化为0xCCCCCCCC(windows VS GCC/G++)//初始化工作做完之后,就执行当前函数的第一行指令
		//但是注意:如果这个类里面有虚函数,生成的对象前4个字节有vfptr
		//就是现在,函数栈帧开辟完,把虚函数表的地址给虚函数指针vfptr
		//然后才是C++代码执行
		vfptr <- &Base::vftable
		*/
		cout << "call Base()" << endl;
		clear();
	}

每一层构造函数,如果是虚函数都会这样做的!(从基类继承来的虚函数也是一样的,都会这样做)
在这里插入图片描述
在这里插入图片描述
当我们new一个Derive对象的时候:

  • 首先调用基类的构造函数;
  • 基类构造的时候把基类的虚函数表写入到基类的虚函数指针vfptr;
  • 然后调用clear,把vfptr指针的值变成0x00000,相当于这个虚函数指针没有指向什么东西了。

在这里插入图片描述

  • 基类的构造函数调用完了,该调用派生类的构造函数了。
  • 派生类的构造函数压完栈,做完栈的初始化,继续把派生类的虚函数表的地址写入到虚函数指针里面。
  • 这样,指针调用show的时候,因为指针指向的是派生类对象,vfptr就是有效的,指向的是派生类的虚函数表,能够从派生类的虚函数表中取出派生类的show方法。
  • clear只是在基类的构造函数中去调用而已。

在这里插入图片描述

class Base
{
public:
	Base()
	{
		/*
		push ebp
		mov ebp, esp
		sub esp, 4Ch
		rep stos esp<->ebp 0xCCCCCCCC(windows VS GCC/G++)
		vfptr <- &Base::vftable
		*/
		cout << "call Base()" << endl;
		clear();
	}
	void clear() { memset(this, 0, sizeof(*this)); }
	virtual void show()
	{
		cout << "call Base::show()" << endl;
	}
};
class Derive : public Base
{
public:
	Derive()
	{
		/*
		push ebp
		mov ebp, esp
		sub esp, 4Ch
		rep stos esp<->ebp 0xCCCCCCCC(windows VS GCC/G++)
		vfptr <- &Derive::vftable
		*/
		cout << "call Derive()" << endl;
	}
	void show()
	{
		cout << "call Derive::show()" << endl;
	}
};
int main()
{
	//Base *pb1 = new Base();
	/*
	mov eax, dword ptr[pb1]
	mov ecx, dword ptr[eax] eax:0x00000000 不是Base::vftable
	call ecx
	*/
	//pb1->show();//动态绑定
	//delete pb1;

	Base *pb2 = new Derive();
	//动态绑定 call Derive::show()
	/*
	vfptr里面存储的是vftable的地址
	vfptr <- vftable
	*/
	pb2->show(); 
	delete pb2;

	return 0;
}

  • 2
    点赞
  • 7
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

liufeng2023

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值