C++编程(四) —— OOP(继承与多态)

本文详细探讨了C++中的继承本质,派生类构造过程,虚函数的静态绑定和动态绑定,特别是虚函数表的工作原理。此外,还介绍了多态的概念,抽象类的作用,以及在实际编程中遇到的问题和解决方案,如虚函数表指针的交换、虚函数形参等。
摘要由CSDN通过智能技术生成


继承的本质和原理

本质:代码的复用
在这里插入图片描述
在这里插入图片描述在这里插入图片描述

派生类的构造过程

1、派生类(不能继承)基类的构造和析构,或者说只能在特定地区调用。
派生类如何初始化从基类继承来的成员?
答:派生类通过调用基类相应的构造函数。

派生类的成员,由派生类的构造和析构负责初始化和清理
基类的成员(派生类继承来的),由基类的构造和析构负责初始化和清理。

class Derive : public Base
{
    Derive(int data) : Base(data), mb(data) {};
    ........
}
int main()
{
	Derive d(20);
}

调用:
在这里插入图片描述

虚函数,静态绑定,动态绑定

静态绑定

class Base
{
public:
	Base(int data = 10) :ma(data) {}
	void show() { cout << "Base::show()" << endl; }
	void show(int) { cout << "Base::show(int)" << endl; }
protected:
	int ma;
};
class Derive : public Base
{
public:
	Derive(int data = 20) :Base(data), mb(data) {}
	void show() { cout << "Derive::show()" << endl; }
private:
	int mb;
};
int main()
{
	Derive d(50);
	Base *pb = &d;

	pb->show(); // 静态(编译时期)的绑定(函数的调用)call  Base::show (01612DAh)
	pb->show(10); //静态绑定 call  Base::show (01612B2h)

	cout << sizeof(Base) << endl; // 4
	cout << sizeof(Derive) << endl; // 8

	cout << typeid(pb).name() << endl; // class Base*
	cout << typeid(*pb).name() << endl; // class Base  class Derive
	
	return 0;
}

其中静态绑定,也就是编译期间的绑定,发生在函数的调用的时期:
pb->show();编译生成的汇编码为call Base::show (01612DAh)
如果发现show是普通函数,就进行静态绑定

虚函数与动态绑定

基类

总结一:
一个类里面定义了虚函数,那么编译阶段,编译器给这个类类型产生一个唯一的vftable虚函数表,虚函数表中主要存储的内容就是RTTI指针和虚函数的地址。当程序运行时,每一张虚函数表都会加载到内存的.rodata区(常量只读区)。

总结二:
一个类里面定义了函数,那么这个类定义的对象,其运行时,内存中开始部分,多存储一个vfptr虚函数指针,指向相应类型的虚函数表vftable。一个类型定义的n个对象,它们的vfptr指向的都是同一张虚函数表。

总结三:
一个类里面虚函数的个数,不影响对象内存大小(存的是指针vfptr),影响的是虚函数表的大小。
在这里插入图片描述
派生类
总结四:
如果派生类中的方法,和基类继承来的某个方法,返回值、函数名、参数列表都相同,而且基类的方法是virtual虚函数,那么派生类的这个方法,自动处理成虚函数,并覆盖虚函数表中原基类虚函数的地址
重写=覆盖
在这里插入图片描述
总结五:
虚函数的依赖
1、虚函数能产生地址,存储在vftable当中
2、对象必须存在(vfptr->vftable->虚函数地址)

要写成虚函数=》把函数地址存到虚函数表中
调用虚函数过程:
获取对象的内存获取虚函数表的指针=》再访问虚函数表=》取得虚函数的地址

class Base
{
public:
	Base(int data = 10) :ma(data) {}
	// 虚函数
	virtual void show() { cout << "Base::show()" << endl; }
	// 虚函数
	virtual void show(int) { cout << "Base::show(int)" << endl; }
protected:
	int ma;
};
class Derive : public Base
{
public:
	Derive(int data = 20) :Base(data), mb(data) {}
	/*
	总结四:
	如果派生类中的方法,和基类继承来的某个方法,返回值、函数名、参数列表都相同,
	而且基类的方法是virtual虚函数,那么派生类的这个方法,自动处理成虚函数
	重写《=》覆盖
	*/
	void show() { cout << "Derive::show()" << endl; }
private:
	int mb;
};
int main()
{
	Derive d(50);
	Base *pb = &d;

	/*
	pb->Base Base::show 如果发现show是普通函数,就进行静态绑定 call Base::show (01612DAh)
	
	pb->Base Base::show 如果发现show是虚函数,就进行动态绑定了
	mov eax, dword ptr[pb]
	mov ecx, dword ptr[eax]
	call ecx(虚函数的地址) 动态(运行时期)的绑定(函数的调用)
	*/
	pb->show(); //静态(编译时期)的绑定(函数的调用)call  Base::show (01612DAh)
	
	/*
	Base::show(int) 是一个虚函数,此时就得动态绑定了
	mov eax, dword ptr[pb]
	mov ecx, dword ptr[eax]
	call ecx(虚函数的地址) 动态(运行时期)的绑定(函数的调用)
	*/
	pb->show(10); //静态绑定 call  Base::show (01612B2h)

	cout << sizeof(Base) << endl; // 4 8
	cout << sizeof(Derive) << endl; // 8 12

	cout << typeid(pb).name() << endl; // class Base*
	/*
	pb的类型:Base -> 有没有虚函数
	如果Base没有虚函数,*pb识别的就是编译时期的类型  *pb <=> Base类型
	如果Base有虚函数,*pb识别的就是运行时期的类型 RTTI类型
	pb->d(vfptr)->Derive vftable  class Derive 
	*/ 
	cout << typeid(*pb).name() << endl; // class Base  class Derive 
	/*
	Base的两个show函数都是普通的函数
	Base::show()
	Base::show(int)
	4
	8
	class Base *
	class Base
	*/

	/*
	给Base的两个show方法定义成virtual虚函数了
	Derive::show()
	Base::show(int)
	8
	12
	class Base *
	class Derive
	*/

	return 0;
}

虚函数问题

1、哪些函数不能实现为虚函数

  • 构造函数不能成为虚函数。完成了构造函数,对象才会存在,在这之前根本没有虚函数表。
  • 构造函数中调用任何函数都是静态绑定(包括在构造函数中调用的虚函数)
  • 静态成员方法

2、是不是虚函数的调用一定是动态绑定? 不是!

  • 上个问题提到,在类的构造函数中调用虚函数,也是静态绑定
  • 用对象本身调用虚函数,是静态绑定。动态绑定必须由指针/引用调用虚函数
int main()
{
	// 静态绑定
	Base b;
	Derive d;
	b.show();   // 虚函数 call Base::show
	d.show();  // 虚函数 call Derive::show
	
	// 动态绑定
	Base *pb1 = &b;
	pb1->show();
	Base *pb2 = &d;    // 上到下,Derive *pb3 = &b不行(下到上)
	pb2->show();
	// 引用也一样,动态绑定
	Base &rb1 = b;
	rb1.show();
	Base &rb2 = d;
	rb2.show();
	/*
	mov  eax, dword ptr [pb2] // 从对象的头取得虚函数表指针
	mov  ecx, dword ptr [eax] // 从虚函数表中取虚函数的指针
	call ecx                  // call寄存器
	*/
	// 派生类指针引用也一样,动态绑定
	Derive *pd1 = &d;
	pd1->show();
	Derive &rd1 = d;
	rd1.show();
}

虚析构函数

1、为什么析构函数可以是虚?
析构函数调用的时候,对象是存在的

2、什么是时候必须将基类的析构实现为虚函数?
基类的指针(引用)指向堆上new出来的派生类对象,deleta pb(基类的指针)调用析构函数的时候,必须发生动态绑定,否则会导致派生类的析构函数无法调用。
换而言之,在父类中如果有任何一个虚函数,那么它的析构函数也必须是虚函数。否则,在删除子类对象时,只会调用父类的析构函数,而不会调用子类的析构函数,会造成内存泄漏。
如果基类的析构函数是虚函数,那么派生类的析构函数自动成为虚函数(虽然名字不一样)

多态

如何解释多态

静态(编译时期)的多态:函数重载、模板(函数模板和类模板)

bool compare(int, int) {}; 
bool compare(double, double) {}
compare(10,10)
call compare_int_int 在编译阶段就确定好调用的函数版本。

template<typename T>
bool compare(T a, T b) {}; 
campare<int>(10, 20);  => 实例化一个 campare<int>

动态(运行时期)的多态:
在继承结构中,基类指针(引用)指向派生类对象,通过该指针(引用)调用同名覆盖方法(虚函数),基类指针指向哪个派生类对象,就会调用哪个派生类对象的同名覆盖方法,称为多态。
多态底层是通过动态绑定来实现的
pbase => 访问谁的虚函数表指针 => 访问谁的虚函数表 => 调用对应的派生类对象的方法

应用:
使用基类指针进行

//不满足开-闭原则,不这么写
void bark(Cat &cat)   
{
	cat.bark():   // Cat::bark()是虚函数,动态绑定
}
-----------------------------------------------------------------
void bark(Animal *p)  // 满足开-闭原则,不会因为派生类的增加再增加API接口  
{
	p->bark():   // Animal::bark()是虚函数,动态绑定
	/*
	p->cat Cat vftable &Cat::bark
	.......
	*/
}
int main()
{
	Cat cat("cat");
	Dog dog("dog");
	Pig pig("pig");
	
	bark(&cat);  // 取cat的虚函数表=》找到cat的虚函数指针
	bark(&dog);
	bark(&pig);
}

抽象类

本质

定义Animal类的初衷,并不是让Animal取实例化
初衷是:
1、让所有的动物实体类通过继承Animal直接复用该属性。如string _name
2、给所有的派生类保留统一的覆盖/重写接口

拥有纯虚函数的类叫抽象类。抽象类不能实例化对象,但是可以定义指针和引用变量。

问题

1 虚函数表指针的交换

在这里插入图片描述

2 虚函数形参

函数调用,参数压栈是在编译时期就确定好的。如果基类和派生类虚函数的默认输入形参不同,则编译器看不到派生类虚函数的参数。所以会出现调用派生类的虚函数,但是用的是基类的默认输入形参。
在这里插入图片描述
在这里插入图片描述

show结果:
call Derive::show i : 10

3 虚函数的权限

如果基类虚函数权限为public,派生类虚函数的权限为private,以下代码可以正常运行:
在这里插入图片描述
访问权限的确定都是发生在程序编译阶段。在程序运行期间才会确认调用到Dervie::show。

4 虚函数指针写入虚函数表的时机

虚函数指针在编译类的构造函数时写入虚函数表。
在这里插入图片描述
调用

int main()
{
	Base *pb1 = new Base();
	/*
	mov  eax, dword ptr [pb1] // 从对象的头取得虚函数表指针
	mov  ecx, dword ptr [eax] // 从虚函数表中取虚函数的指针 eax:0x00000000,已经不再是Base::vftable了
	call ecx                  // call寄存器
	*/
	pb1->show();   // 出错
	delete pb1;
	
	Base *pb2 = new Derive();
	/*
	mov  ecx, dword ptr [pb2] // 从对象的头取得虚函数表指针
	mov  eax, dword ptr [edx] // 从虚函数表中取虚函数的指针
	call eax                  // call寄存器
	*/
	pb2->show();   // 正确,因为在派生类的构造函数,汇编码重新将show的地址写到了Derive::vftable中
	delete pb2; 
	
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

秋雨qy

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

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

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

打赏作者

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

抵扣说明:

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

余额充值