初识C++ · 多态(1)

目录

前言:

[1 多态的定义和实现](#1 多态的定义和实现)

[1.1 虚函数和重写](#1.1 虚函数和重写)

[1.2 override 和 final](#1.2 override 和 final)

[2 多态的原理](#2 多态的原理)


前言:

封装,继承,多态是面向对象的三大特点,今天就来介绍多态的一部分内容。

多态,顾名思义,一种行为的多种形态,就买票这个行为而言,成年人买票一般是全价,学生买票一般都是半价,这就是一种多态,一种行为可以产生不同的结果。

那么现在就进入到多态的基本学习。


1 多态的定义和实现

先看一段多态的代码使用:

cpp 复制代码
class Person
{
public:
	virtual void BuyTicket()
	{
		cout << "买票 -> 全价" << endl;
	}
};
class Student : public Person
{
public:
	virtual void BuyTicket()
	{
		cout << "买票 -> 半价" << endl;
	}
};
void Func(Person& p)
{
	p.BuyTicket();
}
int main()
{
	Person p1;
	Student s1;
	Func(p1);
	Func(s1);
	return 0;
}

这就是多态的基本使用,打印的结果分别是全价和半价,现在从里面的元素进行入手。

第一,参数明明是Person的引用,为什么传Student也可以,这里就用到了继承的切片的概念,基类不可以赋值给派生类,但是派生类可以赋值给基类,因为派生类赋值给基类的时候可以切片,即切除了派生类的元素。

第二,这里也用到了virtual关键字,相同的关键字在不同的地方有不同的作用,这里的virtual是用来重写函数的。被virtual实现的函数叫做虚函数。

所以现在可以得出多态实现的两个条件,第一个是必须通过基类的指针或者引用来调用虚函数,

第二个是调用的函数必须是虚函数,而且是在派生类中进行重写了的。

那么,什么是虚函数和重写?

1.1 虚函数和重写

虚函数就是被关键字virtual修饰的函数,虚函数的重写就是派生类中,同样被virtual修饰的,并且函数名,参数,返回值都和基类是一样的,改变派生类的虚函数的实现方式,就是重写。

所以重写有三同,分别是函数名,参数,返回值

但是呢,重写也是有例外的,如下:

i) 协变

协变就是基类和派生类的函数都满足虚函数和重写的定义,但是返回值不同,基类的返回值返回的是基类的指针,派生类的返回值返回的是派生类的指针,返回的指针的基类或者是派生类可以不是该基类或者是该派生类,可以是这样:

cpp 复制代码
class A {};
class B :public A {};
class Person
{
public:
	virtual A* BuyTicket()
	{
		cout << "买票 -> 全价" << endl;
		return nullptr;
	}
};
class Student : public Person
{
public:
	virtual B* BuyTicket()
	{
		cout << "买票 -> 半价" << endl;
		return nullptr;
	}
};

这种就是协变,注意是返回值的类型就好了。当然返回值也可以是Person* 和Student*,只要满足返回值是基类和派生类就可以了。

ii) 析构函数的重写

这里就开始有人好奇了,为什么析构函数可以重写,三同只满足了一个参数相同,其他都不满足,但是可以构成重写,这是因为什么呢?

先来看一段代码:

cpp 复制代码
class Person
{
public:
	virtual A* BuyTicket()
	{
		cout << "买票 -> 全价" << endl;
		return nullptr;
	}

	~Person()
	{
		cout << "~Person" << endl;
	}

};
class Student : public Person
{
public:
	virtual B* BuyTicket()
	{
		cout << "买票 -> 半价" << endl;
		return nullptr;
	}
	
 	~Student()
	{
		cout << "~Student" << endl;
	}
};
void Func(Person& p)
{
	p.BuyTicket();
}
int main()
{
	Person* p1 = new Person;
	Person* s1 = new Student;
	delete p1;
	delete s1;
	return 0;
}

这里的程序结果是打印了两次 ~Person,但是我们的预期结果应该是调用一次Student的析构,一次Person的析构,一次Person的析构。

在此之前我们应该了解一个事就是在析构的时候编译器会将析构函数名字作特殊处理,统一叫做destructor,在继承那里构成隐藏关系,在多态这里,两个析构函数满足虚函数重写的规则,所以这里我们应该把两个函数修饰为虚函数,这样才可以满足多态函数调用:

cpp 复制代码
class Person
{
public:
	virtual A* BuyTicket()
	{
		cout << "买票 -> 全价" << endl;
		return nullptr;
	}

	virtual ~Person()
	{
		cout << "~Person" << endl;
	}

};
class Student : public Person
{
public:
	virtual B* BuyTicket()
	{
		cout << "买票 -> 半价" << endl;
		return nullptr;
	}
	
 	virtual ~Student()
	{
		cout << "~Student" << endl;
	}
};

此时打印的结果才是正确的。

这里呢,再提及一个点就是,对于虚函数来说,基类函数加了virtual,派生类函数可以不用加了,但是这个习惯其实不太好,感觉代码可读性下降了一点?

所以这里不做过多强调。

1.2 override 和 final

override是用来判断虚函数是否进行了重写的,没有重写就直接报错,如下:

cpp 复制代码
class Person
{
public:
	A* BuyTicket()
	{
		cout << "买票 -> 全价" << endl;
		return nullptr;
	}
	virtual ~Person()
	{
		cout << "~Person" << endl;
	}
};
class Student : public Person
{
public:
	virtual B* BuyTicket() override
	{
		cout << "买票 -> 半价" << endl;
		return nullptr;
	}
 	virtual ~Student()
	{
		cout << "~Student" << endl;
	}
};

final的使用就是这个虚函数不能被重写,final的意思就是最终的意思,也好理解,就是已经到最后了,也就不能重写了:

cpp 复制代码
	virtual void Fun() final
	{
		cout << "aaa" << endl;
	}

但是你以为final的用法只有这个吗?错辣!

现在引入一个话题:如何让类不可以被继承?

两种方法,第一种是final,第二种是对构造函数下手:

cpp 复制代码
class A final 
{

};

class B :public A 
{

};

对吧,final好理解,最后的类。

第二种,我们将构造函数变为私有的,这样就可以防止类被继承了,因为派生类构造的时候也会调用基类的构造,这里变为私有的就构造不了了,这种方法的思路挺厉害的:

cpp 复制代码
class A
{
private:
	A()
		:_a(1)
	{}
	int _a = 0;
};

class B :public A 
{

};

2 多态的原理

现在进入的是多态的原理部分,牵扯到原理的部分,这里就不得不看内存了。

cpp 复制代码
class Base
{
public:
	virtual void Func()
	{
		cout << "Func()" << endl;
	}

private:
	int _b = 1;
	char ch = 'a';
};

int main()
{
    Base b1;
	cout << sizeof(b1) << endl;
	return 0;
}

在X64的环境下,类Base的大小是多少呢?

有坑~大小为16。

因为VS的最小对齐数为8,int + char等于5,但是为什么不是8呢?因为这里面还是一个虚函数表指针,即大小为8,相加13,但是要满足是8的倍数,所以大小就是16。

什么?哪里来的虚函数表指针?

_vfptr就是虚函数表指针,这是在监视窗口看得到的。

虚函数表指针,也就是函数表指针咯,说直白点就是函数指针数组指针,这个指针,指向的函数指针数组,数组里面存放的是函数指针,那么这些函数指针指向的是哪些函数?

指向的就是被virtual修饰的虚函数。

cpp 复制代码
class Base
{
public:
	virtual void Func1()
	{
		cout << "Func1()" << endl;
	}
	virtual void Func2()
	{
		cout << "Func2()" << endl;
	}
	virtual void Func3()
	{
		cout << "Func3()" << endl;
	}
	virtual void Func4()
	{
		cout << "Func4()" << endl;
	}

private:
	int _b = 1;
	char ch = 'a';
};

那我们用这个类来观察一下,这里为了方便将环境改为X86

这是指针指向的内容,现在我们用内存2来观察一下这些地址。

该指针指向的内容,确实是一个表,存放的是函数指针。这是单个类的观察,现在引入多态的概念,再来观察一下虚函数表:

cpp 复制代码
class Person 
{
public:
	virtual void BuyTicket() { cout << "买票-全价" << endl; }
private:
	int _a;
};
class Student : public Person
{
public:
	virtual void BuyTicket() { cout << "买票-半价" << endl; }
private:
	int _b;
};
void Func(Person* p)
{
	p->BuyTicket();
}
int main()
{
	Person Mike;
	Person Mike1;
	Func(&Mike);
	Student John;
	Func(&John);
	return 0;
}

现在满足多态条件,那么我们首先调试一下Mike和Mike1:

可以看到虚表指针都是一样的,并且我们对Mike取地址后:

第一个也是虚表指针,这个指针指向的就是虚表,当然了,虚表指针同一个程序里面都是一样的,毕竟都是同一个类实例化出来的,函数指针肯定要一样的。

在内存窗口看p的地址的时候,就会发现虚表指针也在这里,说明满足多态的时候,虚表指针指向谁,就调用谁的虚函数了。

Student是一样的,就不调试了,我们再来看一眼汇编:

满足多态就会生成这么一大堆指令,在运行的时候确定应该调用谁的函数,如果不满足多态,就是这样,在编译的时候就确定了调用谁的函数:

直接就选择调用了。

我们现在就知道了,虚函数表实际是一个函数指针数组,那么这种情况:

cpp 复制代码
class Base
{
public:
	virtual void Func1()
	{
		cout << "Base::Func1()" << endl;
	}
	virtual void Func2()
	{
		cout << "Base::Func2()" << endl;
	}
	
private:
	int _b = 1;
};
class Drive :public Base
{
public:
	virtual void Func1()
	{
		cout << "Drive::Func1()" << endl;
	}	
	virtual void Func3()
	{
		cout << "Drive::Func3()" << endl;
	}	
	virtual void Func4()
	{
		cout << "Drive::Func4()" << endl;
	}
private:
	int _a = 2;
};
int main()
{
	Base b1;
	Drive d1;
	return 0;
}

Func1是被重写了的,Func2没有被重写,3 和 4 也只是被virtual修饰了一下,调试的时候就会发现:

b1是正常的,两个函数都有,但是d1不是正常的,因为只有1 和 2,有2正常,因为是被继承下来的,1也正常,本来就有,但是为什么3 4没有呢?

这时候我们去看一下虚表:

虚表这里除了有Func1 Func2,还有两个疑似是指针的地址,但是为什么监视窗口没有呢?这个我们也无从而知,我们就大胆赌一下,那两个地址就是Func3 Func4的地址,我们现在就把d1里面的四个函数的地址取出来进行对比即可。

这里用到了函数指针,我们知道了d1的地址,但是我们只需要前4个字节,因为前4个字节是虚表指针,但是呢我们应该如何取到前4个字节呢?

int* 解引用之后是int没错吧?那么我们取到了d1的地址,强转为int*,再解引用,是不是就获得了前4个字节?再强转为函数指针,就完成了,这个方法是很妙的,将C语言的指针用到了极致,这时候去查看地址是不是一样的,就会发现是一样的,所以监视窗口有时候也是不太准的。

cpp 复制代码
typedef void(*VFPTR)();

void PrintVFT(VFPTR* vft)
{
	for (size_t i = 0; i < 4; i++)
	{
		printf("%p->", vft[i]);
		VFPTR pf = vft[i];
		(*pf)();
	}
}

int main()
{
	Base b1;
	Drive d1;
	VFPTR* ptr = (VFPTR*)(*((int*)&d1));
	PrintVFT(ptr);
	return 0;
}

感谢阅读!

相关推荐
汤米粥几秒前
小皮PHP连接数据库提示could not find driver
开发语言·php
冰淇淋烤布蕾4 分钟前
EasyExcel使用
java·开发语言·excel
我爱工作&工作love我7 分钟前
1435:【例题3】曲线 一本通 代替三分
c++·算法
拾荒的小海螺10 分钟前
JAVA:探索 EasyExcel 的技术指南
java·开发语言
马剑威(威哥爱编程)35 分钟前
哇喔!20种单例模式的实现与变异总结
java·开发语言·单例模式
娃娃丢没有坏心思36 分钟前
C++20 概念与约束(2)—— 初识概念与约束
c语言·c++·现代c++
lexusv8ls600h37 分钟前
探索 C++20:C++ 的新纪元
c++·c++20
lexusv8ls600h42 分钟前
C++20 中最优雅的那个小特性 - Ranges
c++·c++20
白-胖-子1 小时前
【蓝桥等考C++真题】蓝桥杯等级考试C++组第13级L13真题原题(含答案)-统计数字
开发语言·c++·算法·蓝桥杯·等考·13级
好睡凯1 小时前
c++写一个死锁并且自己解锁
开发语言·c++·算法