C++多态

目录

一、多态的概念

二、多态的实现

​三、抽象类

四、多态的原理

一、多态的概念

多态的概念,通俗来说,就是多种形态,具体就是去完成某个行为,当不同的对象去完成时会产生不同的状态

我们先来演示一下多态 

class person
{
public:
	virtual void Tastefood()
	{
		cout << "吃火锅:全价" << endl;
	}
};
class student :public person
{
public:
	virtual void Tastefood()
	{
		cout << "吃火锅:半价" << endl;
	}
};
void func(person& p)
{
	p.Tastefood();
}
int main()
{
	person p;
	student s;
	func(p);
	func(s);
	return 0;
}

func里面时父类对象不仅可以传父类也可以传子类,不同对象输出的结果是不同的,这就叫做多态

但是我们把func里面的引用去掉试试呢

居然又是另一种结果了,就不是多态了

二、多态的实现

C++多态有两个条件

1.虚函数重写

2.父类的指针或者引用去调用虚函数

那我们逐步分析

class person
{
public:
	virtual void Tastefood()
	{
		cout << "吃火锅:全价" << endl;
	}
};

在上一篇文章的继承我们写到了virtual,这二者有什么必然关系吗,答案是没有关系,一个是多态的条件,一个是解决菱形继承数据冗余的问题

那虚函数的重写需要什么条件呢

虚函数的重写要满足继承父子关系的两个虚函数:函数名相同,参数相同,返回值相同(三同)

但是三同有例外:协变->返回值可以不同,但是必须是父子类关系的指针或者引用

class person
{
public:
	virtual person* Tastefood()
	{
		cout << "吃火锅:全价" << endl;
	}
};
class student :public person
{
public:
	virtual void Tastefood()
	{
		cout << "吃火锅:半价" << endl;
	}
};

一般的返回值不同都不行

class person
{
public:
	virtual person* Tastefood()
	{
		cout << "吃火锅:全价" << endl;
		return nullptr;
	}
};
class student :public person
{
public:
	virtual student* Tastefood()
	{
		cout << "吃火锅:半价" << endl;
		return nullptr;
	}
};

 

注意:virtual只能修饰成员函数

但是派生类又可以不加virtual,因为派生类重写的是父类的实现,你可以认为把父类这个继承下来了,你父类是虚函数,派生类也是虚函数了

	~person()
	{
		cout << "~person()" << endl;
	}
	~student()
	{
		cout << "~student()" << endl;
	}

我们再把析构加进去

person p;
student s;

满足先子后父,子类析构结束后会去自动调用父类的析构函数

但是我们来看这里

	person* p = new person;
	delete p;
	p = new student;
	delete p;

父类对象去调父类的析构没问题,但是子类对象怎么也去调父类的析构

因为delete根据类型去调,但是我们第四行父类的指针有可能指向父类有可以指向子类

delete由于多态的原因是由p->destructor()+operator delete(p);封装构成的,所以派生类的析构和父类的析构构成隐藏关系,因为它们的函数名被处理成destructor

但是这里又是普通调用,普通调用看类型,两个都是person类型,但是我们期望这里是多态调用,指向父类调用父类,指向子类调用子类,这里我们满足了第一个条件:父类的指针或引用

剩下的我们只要满足虚函数重写就好了

	virtual ~person()
	{
		cout << "~person()" << endl;
	}
	virtual ~student()
	{
		cout << "~student()" << endl;
	}

这里就构成多态了,我们不写虚函数是不是就构成内存泄露了,前面父类加virtual子类不加也是在为这方面考虑,你父类写了你子类就可以不写了

注意:我们虚函数重写是继承父类的接口,重写父类的实现,所以缺省值一般使用的是父类的

重载、覆盖(重写)、隐藏(重定义)的对比

1.重载

两个函数在同一作用域

函数名/参数不同(类型,个数,顺序)

2.重写(覆盖)

两个函数分别在基类和派生类的作用域

函数名/参数/返回值都必须相同(协变例外)

两个函数必须是虚函数

3.重定义(隐藏)

两个函数分别在基类和派生类的作用域

函数名相同

两个基类和派生类的同名函数不构成重写就是重定义

关键字final

在上一篇文章继承里我们写到 final修饰类,不能被继承

final修饰虚函数,不能被重写

关键字override

override修饰派生类的虚函数

它是用来检查派生类是否重写

没有完成重写就会报错

 三、抽象类

在虚函数的后面写上=0,则这个函数为纯虚函数。包含纯虚函数的类叫做抽象类,抽象类不能实例化出对象。派生类继承后也不能实例化出对象,只有重写纯虚函数,派生类才能实例化出对象。纯虚函数规范了派生类必须重写,另外纯虚函数更能体现出了接口继承

class book
{
public:
	virtual void reader() = 0;
};
int main()
{
	book b;
	return 0;
}

虽然无法定义出对象,但是可以定义出指针

class west :public book
{
public:
};

当然它的派生类也继承了纯虚函数,自然也无法实例化出对象

除非你把纯虚函数进行了重写

class west :public book
{
public:
	virtual void reader() {
		cout << "west :西游"<<endl;
	}
};
class country :public book
{
public:
	virtual void reader() {
		cout << "country :三国"<<endl;
	}
};
void func(book* b)
{
	b->reader();
}
int main()
{
	func(new west);
	func(new country);
	return 0;
}

指向哪个调哪个

它和override的区别在于:它间接强制去派生类重写,因为你不重写它就实例化不出对象

那比如说人这个抽象类,根据这个类我们细分出各种职业比如医生,老师,律师等,但是人不是一个具体的职业,人没有具体的职业,自然无法实例化出对象

它这里的多态意味着它想在多个子类中实现

四、多态的原理

虚函数表

class A
{
public:
	virtual void think()
	{
		cout << "class A" << endl;
	}
private:
	int _a;
	char b;
};
int main()
{
	cout << sizeof(A) << endl;
	return 0;
}

看这段代码,熟悉内存对齐规则的人都会说这里为8

但是实际上是12,原因就在这里存了一张虚函数表

只要一个类有虚函数就要多一个指针,把虚函数的地址存到这张表上

class A
{
public:
	virtual void think1()
	{
		cout << "think1" << endl;
	}
	virtual void think2()
	{
		cout << "think2" << endl;
	}
	void think3()
	{
		cout << "think3" << endl;
	}
private:
	int _a;
	char b;
};

这个虚函数表存着两个虚函数的地址 

那我们也可以从这张图里面看到,这个虚函数表的类型其实是一个虚函数指针数组 

多态原理

class A
{
public:
	virtual void think1()
	{
		cout << "think1" << endl;
	}
	virtual void think2()
	{
		cout << "think2" << endl;
	}
	void think3()
	{
		cout << "think3" << endl;
	}
private:
	int _a=1;
};
class B :public A
{
public:
	virtual void think1()
	{
		cout << "B::think1" << endl;
	}
	virtual void think2()
	{
		cout << "think2" << endl;
	}
private:
	int _b = 2;
};
int main()
{
	A aa;
	B b;
	return 0;
}

派生类里面只有一个虚表指针,派生类由两部分组成一个是父类的一个是自己的,但是派生类的虚表指针和父类的不是同一个,因为我们完成的是虚函数的重写,虚函数的重写也叫做覆盖,覆盖成新的虚函数

那么为什么指向父类调父类指向子类调子类呢

因为是父类的指针,我们传父类,这个func里面的指针根据父类的虚函数表去找父类的这个think1,传子类就切割或者切片出属于父类的那一部分,去找虚函数表,但是因为虚函数表子类的已经重写了所以调子类的虚函数

普通调用是在编译链接时,确定地址

多态调用时在运行时,去虚表里面找到函数地址,确定地址,再调用

那对象为什么不行呢

void func(A  p)
{
	p.think1();
}

就像我们A a=b,就是切割出子类对象中父类那一部分,成员拷贝给父类,但是不会拷贝虚函数表指针

那我们假设父类对象=子类对象会拷贝虚函数表指针

那么多态调用,指向父类,调用还是父类虚函数吗?不一定

因为你把子类的虚表拷过去,你调用父类的时候还是父类的虚函数吗,假设我子类的虚函数中间实现的时候有赋值改变什么的,这就完不成多态调用了

如果子类的虚表被拷过去,这里传父类的对象指向子类的虚表,父类的对象怎么能去调子类的虚函数,所以不会拷贝虚函数表指针,因为会造成逻辑紊乱

那如果我们子类不重写虚函数,父类和子类的虚表一不一样?

不一样,虚表本来也没多大的空间,没必要一样搞出其他麻烦 

但是同一个类是共用一张虚表的

那么虚函数存在哪里的?虚函数表又是存在哪里的

虚函数和普通函数一样都是存在代码段的,同时把虚函数地址存了一份在虚函数表

虚函数表因为它不允许修改,又跟静态变量一般可以供一个类多个对象使用,所以应该存在代码段(常量区)

那么虚函数一定是放在虚表里面的吗

class A
{
public:
	virtual void think1()
	{
		cout << "think1" << endl;
	}
	virtual void think2()
	{
		cout << "think2" << endl;
	}
	void think3()
	{
		cout << "think3" << endl;
	}
private:
	int _a=1;
};
class B :public A
{
public:	
	virtual void think1()
	{
		cout << "B::think1" << endl;
	}
	virtual void think4()
	{
		cout << "think4" << endl;
	}
private:
	int _b = 2;
};
void func(A  p)
{
	p.think1();
}
int main()
{
	A a;
	B b;
	return 0;
}

看监视窗口我们可以发现b的虚函数表里面继承了think2,重写了think1,但是自身的虚函数think4不见了,难道虚函数真的不一定存在虚函数表里面吗,其实监视窗口是会骗人的,它给你看到的都是包装过的,但是内存不会骗人

 

但是b的虚函数表我们只能确定前两个,其他的我们只能做到怀疑但是做不到确定

我们在前面说了,虚函数表本质上是一个函数指针数组,那我们是不是可以通过函数指针数组的方式来打印函数指针,来看到呢

typedef void (*VFNC)();
void printf(VFNC* a)
{
	for (size_t i = 0; a[i] != 0; i++)
	{
		printf("[%d]:%p\n", i, a[i]);
		VFNC f=a[i];
		f();
	}cout << endl;
}
int main()
{

	A a;
	printf((VFNC*)(*((int*)&a)));
	B b;
	printf((VFNC*)(*((int*)&b)));
	return 0;
}

接下来我们来逐步解析一下,首先是定义一个函数指针数组,(VFNC*)(*((int*)&a))这个我来给大家逐步拆分一下,我们知道虚函数表里面存的是一个指针,我们只要访问一个对象的头四个字节就能访问到虚函数表指针,这时候我们想到了int,但是int和指针是不相近的类型,但是指针和指针之间可以来回转换,但是由于我们解引用完是int,我们传过去的是要函数指针数组,所以我们强转成VFNC*

printf那里面我们就可以通过打印地址来发现了,但是我们还可以添加函数指针来验证是否构成多态来进一步验证,f就是虚函数的地址,我们知道函数名就是可以当作地址来访问,而且我们的参数为了方便访问弄成了无参的

不仅访问到了,还构成了多态

多继承的派生类有两张虚表,派生类的虚函数存在继承的第一个父类里面。

多态在这里写完了,多态涉及到很多涉及内存的东西,有时候监视窗口不起作用的时候,我们就需要往内存里面看,写的不好的地方欢迎大家指出,接下来要进入搜索二叉树了

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值