继承与多态

本文详细探讨了C++中的继承与多态概念,包括继承的复用本质、继承方式、派生类与基类的关系、构造与析构顺序、同名函数的关系等。在多态部分,讲解了虚函数机制、vftable和vfptr、虚函数表的合并、动多态的发生时机以及虚析构的重要性。此外,还介绍了纯虚函数和四种类型转换,以及多继承和虚继承的概念及其处理机制。
摘要由CSDN通过智能技术生成

继承

继承的本质是复用,也就是沿用父类的变量和作用域,并对其做出的扩展。它会使用一个叫做继承列表的符号(:)来将两个类链接起来。例如:
class student : public people,就是在student类中沿用并扩展了people类。

在习惯上我们将被继承的类叫做基类(父类),继承的类叫做派生类(基类)。
派生类(子类)=》基类(父类),派生类继承了基类
基类=》派生类,基类派生了派生类

继承的方式有三种:
public:共有(任意位置)
protected:保护(子类类和本类类)
private:私有(本类类)

下面用一段代码来更清楚的了解派生类和基类:

class base//基类
{
public:
	base(int a=0)
		:ma(a)
	{}
protected:
	int ma;
};
class derve :public base//派生类
{
public:
	derve(int b=0)
		:mb(b)
	{}
private:
	int mb;
};

可以看到,在基类中定义中了四个字节大小的变量a,在派生类中定义了四个字节大小的变量b。那么在派生类继承基类后,会将基类中的变量a也继承过来。除了通过打印的方法可以知道派生类中的大小,还可以通过命令提示符来查看派生类中空间结构的存放。(具体操作在通过命令提示符查看c++中类的构造可以看到)
在这里插入图片描述
可以看到,在派生类中总共有8个字节大小的数据。总结如下:
派生类的内存布局:
1.派生类会继承基类中的变量
2.基类的布局优先于派生类的布局(派生类要依靠基类的变量来操作)
3.除了成员变量以外作用域也会被继承
此外派生类都可以继承基类除构造析构以外的所有成员,例如:
1.普通成员变量
2.普通成员方法
3.静态成员变量
4.静态成员方法
5.作用域
6.类型
派生类函数的构造析构顺序
调用构造函数:
1.调用基类构造
2.调用派生类构造
调用析构函数:
1.调用派生类
2.调用基类构造
3.释放内存
类和类的关系:
1.组合: a part of has_a
像链表类和节点类的关系,节点是链表的一部分
2.继承: a kinfd of is_a
class people
class student : public people
鸟和喜鹊,猫和动物,人和学生
3.代理
限制底层接口对外提供特性

同名函数的关系:
1.重载:同名不同参,同作用域
2.隐藏:同名不同作用域(继承关系)。注意,派生类中同名的函数会隐藏基类中所有的同名函数
3.覆盖:派生类中同名同参的虚函数覆盖掉基类中同名同参的虚函数。

基类和派生类的指向:只允许基类指向派生类
原因:派生类中继承了基类,里面有基类的数据。但是基类中没有派生类的数据。

多态

概念:同一接口(函数名),不同形态(功能不同)。函数名相同,传入的参数不同(函数重载就是一种多态),
多态分为三种:
1.静多态
编译阶段确定函数的调用
2.动多态(默认动多态)
运行阶段确定函数的调用,通过函数的入口地址来确定
函数的入口地址存放在符号表中,以数据的方式加载进来
在编译阶段将入口地址拷贝一份至数据段
虚函数(virtual)机制提供支持
3.宏多态
预编译阶段确定函数的调用,但是因为宏是不安全的,所以一般不提倡这种做法

虚函数的处理机制

虚函数:虚函数的作用是允许在派生类中重新定义与基类同名的函数,并且可以通过基类指针或引用来访问基类和派生类中的同名函数。
操作方法:只需要在一个函数前面加上virtual关键字即可,它是在运行阶段编译的。但是我们知道在运行阶段我们只将.data段和.text段传入到内存中,而虚函数的入口地址是放在符号表中的。所以我们会将虚函数的函数入口地址拷贝一份放入到一个叫.ratable(只读数据段)中,然后将函数入口地址存放到虚函数表中,最后再运行阶段传入到内存中去。

vftable和vfptr

当一个类中有虚函数存在时,就会相应的生成一个虚函数表(vftable),里面存放了对象的类型信息、相对偏移、虚函数的入口地址
虚函数表的结构如下:
RTTI:运行时类型信息,运行阶段确定类型
偏移:虚函数指针相对于整体作用域的偏移(0-vfptr = 偏移)
虚函数入口地址:在运行阶段会传给寄存器(下面有讲)
那既然有一个虚函数表,我们的对象就需要去访问它,这时系统就会给我们生成一个4个字节大小的虚函数指针(vfptr)来指向它,在调动虚函数时,我们会通过虚函数指针去访问到虚函数表中的信息。
大概流程图如下,(参考下面的代码)base* pb = new derve(10);:
在这里插入图片描述

虚函数表的合并

在派生类继承基类的时候,如果基类中有虚函数,那么在派生类中同名同参的函数也会成为虚函数。因为覆盖的关系派生类中的虚函数最终会覆盖掉基类中的虚函数。最终实现虚函数表合并。
同理,那么相应的基类和派生类中都会有虚函数指针。我们知道虚函数指针(vfptr)是占四个字节大小的,所以虚函数指针也会进行合并,它的合并规则是由外向内合并的。

class base
{
public:
	base(int a = 10)
		:ma(a)
	{}
	void show()
	{
		std::cout << "base::ma" << ma << std::endl;
	}
protected:
	int ma;
};
class derve :public base
{
public:
	derve(int b)
		:mb(b)
	{}
	virtual void show()
	{
		std::cout << "derve::mb" << mb << std::endl;
	}
private:
	int mb;
};
int main()
{
	
	std::cout << "base::size:" << sizeof(base) << std::endl;//4
	std::cout << "derve::size:" << sizeof(derve) << std::endl;//8
	base* pb = new derve(10);
	std::cout << "pb type:" << typeid(pb).name() << std::endl;//class base*
	std::cout << "*pb type:" << typeid(*pb).name() << std::endl;//class base
	pb->show();//ma 10  class base.show()基类的show函数
	return 0;
}

创建一个base类,输出a的值,然后创建一个derve类并以共有的方式继承基类base。分别在调用点打印出基类的大小、派生类的大小定义一个基类指针来指向派生类、打印指针的类型、打印给指针解引用的类型、调动show()函数。打印结果如下:
在这里插入图片描述
当我们在基类的show函数中加上一个virtual后,再去运行程序时,结果如下:
这是因为系统会分配一个虚函数指针vfptr指向虚函数表,实现对象针对于虚函数表的共享,而pb在调用show函数时,会通过vfptr指针来找到派生类中的虚函数表,此时就发生了动多态

在这里插入图片描述

动多态的发生时机

第一:我们可以转到反汇编去看,在调动show函数时,传入的是地址还是寄存器。我们知道如果是在编译阶段处理的化,就会直接传入地址,这时候发生的是静多态**。如果是在运行阶段处理的化,就会在编译阶段传入一个寄存器,然后再运行阶段将虚函数入口地址传给寄存器,这时就发生了动多态**
第二:通过对象定义的指针来调动虚函数也会发生动多态,但是要注意:对象必须是完整的(也就是说构造和析构函数是不可以发生动多态的)

虚析构

首先我们要知道,当基类中的指针指向派生类对象时,它指向的是基类中的那部分。在调用析构函数时,会发现派生类的析构函数并没有调动,这样是会发生内存泄漏的。我们知道在调用析构函数时,是先调用派生类的析构函数再调用基类的虚构函数的。但是当我们直接释放掉基类的内存时,那么派生类中的内存就会无法得到释放。这时就产生了内存泄漏。
解决方法:将基类中的析构函数设为虚函数,这样再派生类中就会覆盖掉基类中的虚函数,从而在调动析构函数时,可以将基类和派生类的空间一块释放掉。

虚函数的写入时机

虚函数是在构造函数之前写入的。。如果有基类的指针指向派生类对象,将构造函数全部置为0,会不会出错呢?是不会的,因为基类指针指向的是派生类中基类的那部分空间,所以不会将派生类的构造函数置为0。

纯虚函数

概念:基类中保留接口,通过派生类来实现功能
virtual+函数名 = 0;
抽象类:拥有纯虚函数的类,不能实例化对象,但可以通过指针来引用对象

class anmial//基类anmial
{
public:
	anmial( std::string name)
		:mname(name)
	{}
	virtual void bark() = 0;//纯虚函数,只保留接口
protected:
	std::string mname;
};
class dog:public anmial//派生类dog
{
public:
	dog(std::string name)
		:anmial(name)
	{}
	virtual void bark()//对基类中纯虚函数的实现
	{
		std::cout << mname << "wang wang wang" << std::endl;
	}
};
class cat :public anmial//派生类cat
{
public:
	cat(std::string name)
		:anmial( name)
	{}
	virtual void bark()//对基类中纯虚函数的实现
	{
		std::cout << mname << "miao miao miao" << std::endl;
	}
};
void showanmioal(anmial* pb)//基类的指针可以指向任意一个派生类对象
{
	if (typeid(*pb) == typeid(cat))//类型比较,如果pb指向的类型和派生类cat相同
	{
		std::cout << "cat can 喵喵喵" << std::endl;
	}
	else//如果不同调用bark函数
	{
		pb->bark();
	}
}
int main()
{
	dog DOG("dog");
	cat CAT("cat");
	showanmioal(&DOG);//dog类和cat类不同,执行else()
	showanmioal(&CAT);//类型相同,打印cat can 喵喵喵
	return 0;
}

四种转换类型

1.const_cast:去除常性,将一个常量去除常性
2.static_cast:安全性更高,如果有安全隐患就会报错
3.reinterpret_cast:指针类型的转换
4.dynamic_cast:RTTI信息转换

多继承和虚继承

概念:派生类继承了两个或两个以上的基类
典型的堆积成就是‘菱形继承’,假设基类为A,基类派生出两个派生类B和C,同时派生类D又继承了B和C。
在这里插入图片描述
虚继承
如果B和C都继承A的情况下,再让D同时去继承B,C。呢么就会再D的内存布局下同时线两份A的数据,后果就是在给ma赋值时系统将无法选择从而报错。解决这个问题的方法就是让B,C在继承A的时候是以虚继承的方式来继承。

class A
{
public:
	A(int a)
		:ma(a)
	{}
protected:
	int ma;
};
class B :virtual public A//以虚继承的方式继承基类A
{
public:
	B(int b)
		:A(b),mb(b)
	{}
protected:
	int mb;
};
class C :virtual public A//以虚继承的方式继承基类A
{
public:
	C(int c)
		:A(c), mc(c)
	{}
protected:
	int mc;
};
class D :public B, public C
{
public:
	D(int d)
		:A(d), B(d),C(d),md(d)
	{}
protected:
	int md;
};

布局:非虚基类的布局优先于虚基类
在D类中会把重复的数据移到后面去,同时在原来的地方添加一个虚继承指针(vbptr),vbptr指针指向的是一张虚继承表里面存放着两个偏移:vbptr相对于作用域的偏移和vbptr相对于虚基类数据的偏移
在这里插入图片描述

虚基类的处理和继承顺序相关:
构造顺序:虚基类的构造优先于非虚基类
析构顺序:非虚基类的析构优先于虚基类

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值