C++多态

目录

虚函数的重写

多态的条件

重载、重写、隐藏的对比​

[override 和 final](#override 和 final)

抽象类

虚函数表指针

多态的原理

单继承中的虚函数表

多继承中的虚函数表

几个问答题


虚函数的重写

虚函数是指被virtual 修饰的成员函数,注意虚函数和虚继承没有关系, 只是都用到了virtual关键字

继承关系中,父子类的两个虚函数如果满足三同(函数名,参数,返回值), 则称构成虚函数重写

cpp 复制代码
class Person {
public:
	//虚函数
	virtual void BuyTicket() { cout << "买票-全价" << endl; }
};

class Student : public Person {
public:
	//虚函数
	virtual void BuyTicket() { cout << "买票-半价" << endl; }
};

虚函数重写的两个例外

1.返回值可以不同,但是必须是父子类关系的指针或引用

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

2.子类的虚函数可以不加virtual关键字, 但父类的虚函数必须加virtual

多态的条件

多态指的是在继承体系中,父子类对象去调用虚函数,产生了不同的行为

多态必须要满足下面两个条件:

1.虚函数重写

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

ps:父类的指针或引用 指向/引用 的是哪个对象,就去调用哪个对象的虚函数

cpp 复制代码
#include <iostream>
using namespace std;

class Person {
public:
	//虚函数
	virtual void BuyTicket() { cout << "买票-全价" << endl; }
};

class Student : public Person {
public:
	//虚函数
	virtual void BuyTicket() { cout << "买票-半价" << endl; }
};

void Func1(Person& p) //父类的引用
{
	p.BuyTicket();
}

void Func2(Person* p) //父类的指针
{
	p->BuyTicket();
}

int main()
{
	Person ps;
	Student st;

	Func1(ps); //买票 - 全价
	Func1(st); //买票 - 半价

	Func2(&ps); //买票 - 全价
	Func2(&st); //买票 - 半价
	return 0;
}

虚函数重写例外2 中 子类的虚函数可以不加virtual的原因:

1.普通函数的继承是一种实现继承,继承的是函数体,目的就是为了使用父类函数的功能;

虚函数的继承是一种接口继承,目的是为了重写,实现多态!

**2.**new出来的对象,我们期望在对象结束时自动去调用析构函数来清理资源,而下面的场景中我们希望指向的空间存放的是什么类型的对象,就去调用哪个对象的析构函数释放资源(这不就是多态么),

cpp 复制代码
int main()
{
	Person* p = new Person;
	delete p;

    p = new Student;
	delete p;

	//打印结果:
	//~Person()
	//~Person()

    //因为没有满足多态的条件,所以调用哪个函数看的是p的类型~
	return 0;
}

而析构函数要构成多态,自然要满足多态的两个条件,第二个条件显然满足了,而第一个条件也就是虚函数要构成重写,函数名首先要相同,而这一点编译器帮我们做了

上一篇博客提到过了,父子类的析构函数名会转化成destructor, 而为啥析构函数名开始不命名成一样的,是因为多态这个语法是在六大默认成员函数之后出来的,~类名()是为了和构造函数对应

其次就是要是虚函数,而每个子类都要写virtual,一旦忘了,就容易造成内存泄露,所以子类的虚函数干脆都可以不加virtual, 这样就可以避免忘记给子类的析构函数加virtual而导致的内存泄露问题

重载、重写**、隐藏****的对比**

overridefinal

final作用

1.修饰类,表明类不能被继承

2.修饰虚函数,表明虚函数不能被重写

cpp 复制代码
class A
{
	virtual void func() final { }
};

class B : public A
{
	virtual void func() { } //err无法重写虚函数
};

override 作用
检查虚函数有没有完成重写,没有重写编译报错

cpp 复制代码
class A
{
	virtual void func(int a) { }
};

class B : public A
{
	virtual void func() override { } //报错,没有完成虚函数重写
};

抽象类

在虚函数的后面写上 =0 ,则这个函数为纯虚函数。 包含纯虚函数的类叫做抽象类(也叫接口
类),抽象类不能实例化出对象; 派生类继承后也不能实例化出对象,只有重写纯虚函数,派生类才能实例化出对象。

cpp 复制代码
class Car
{
public:
	virtual void Drive() = 0;
};

class Benz :public Car
{
};

int main()
{
	Car c; //err, 抽象类无法实例化对象
	Car* p; //可以定义指针
	Benz b; //抽象类的派生类也无法实例化出对象
}

抽象类某种程度上可以强制子类去进行重写,因为如果不重写,父类和子类都无法实例化出对象,也就无法使用成员函数,所以强制若干子类去重写,从而产生多态机制~

cpp 复制代码
{
public:
	virtual void Drive() = 0;
};

class Benz :public Car
{
public:
	virtual void Drive()
	{
		cout << "Benz-舒适" << endl;
	}
};

class BMW :public Car
{
public:
	virtual void Drive()
	{
		cout << "BMW-操控" << endl;
	}
};

void func(Car* ptr)
{
	ptr->Drive();
}

int main()
{
	func(new Benz); //Benz - 舒适
	func(new BMW); //BMW - 操控
}

虚函数表指针

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

	virtual void Func2()
	{
		cout << "Func2()" << endl;
	}

private:
	int _b = 1;
};

int main()
{
	Base b;
	cout << sizeof(b) << endl; //8
}

上面这段代码的打印结果是8, 通过内存窗口观察到,b对象中除了存储_b成员之外,还存储了一个指针,这个指针叫做虚函数表指针,指向的是一张虚函数表,这个虚函数表中存储了对象中所有虚函数的地址

注意:虚函数和虚函数表的都是存储在代码段的!

多态的原理

cpp 复制代码
class Person {
public:
	virtual void BuyTicket() { cout << "买票-全价" << endl; }
	virtual void func() { cout << "virtual void func()" << endl;}
protected:
	int _a = 1;
};

class Student : public Person {
public:
	virtual void BuyTicket() { cout << "买票-半价" << endl; }
protected:
};

void Func(Person& p)
{
	p.BuyTicket();
}

int main()
{
	Person p;
	Student s;
	Func(p);
	Func(s);
	return 0;
}

如上图所示,父类对象和子类对象中的虚函数指针是不同的,指向的也是两张不同的虚表,而BuyTicket()虚函数我们进行了重写,func()虚函数没有进行重写,因此两张表中BuyTicket()地址不同,而func()的地址是一样的!!

当父类对象的指针/引用指向/引用的是父类对象,就会去父类对象中找虚表指针,从而拿到父类BuyTicket()虚函数的地址,调用时call 地址,从而调用父类的虚函数

当父类对象的指针/引用指向/引用的是子类对象,就会去子类对象中找虚表指针,从而拿到子类BuyTicket()虚函数的地址,调用时call 地址,从而调用子类的虚函数

这既是多态的原理~

而多态之所以要求是父类的指针或引用而不能直接是子类拷贝给父类是因为会出现以下情况:

子类对象拷贝给父类对象,会将子类对象中父类对象的那部分拿出来拷贝给子类对象,但不会拷贝虚表指针, 因为如果拷贝了虚表指针,父类对象中保存的虚函数表指针指向的就不是父类的虚函数表了,此时父类的指针/引用指向/引用父类对象,调用的就不是父类的虚函数了,就无法保证多态了~

细节1:子类不重写虚函数,父类和子类的虚函数表也不是同一张

细节2:同一个类创建出的所有对象,共用一张虚表

单继承中的虚函数表

虚函数的地址一定会被放进类的虚函数表!!!

cpp 复制代码
#include <iostream>
using namespace std;

class Base {
public:
	virtual void func1() { cout << "Base::func1" << endl; }
	virtual void func2() { cout << "Base::func2" << endl; }
private:
	int a;
};

class Derive :public Base {
public:
	virtual void func1() { cout << "Derive::func1" << endl; }
	virtual void func3() { cout << "Derive::func3" << endl; }
	virtual void func4() { cout << "Derive::func4" << endl; }
	void func5() { cout << "Derive::func5" << endl; }
private:
	int b;
};

class X :public Derive {
public:
	virtual void func3() { cout << "X::func3" << endl; }
};

// 打印虚表
typedef void (*VFUNC)();
//void PrintVFT(VFUNC a[])
void PrintVFT(VFUNC* a)
{
	for (size_t i = 0; a[i] != 0; i++)
	{
		printf("[%d]:%p->", i, a[i]);
		VFUNC f = a[i];
		f();
	}
	printf("\n");
}


int main()
{
	void (*f1)();
	VFUNC f2;

	Base b;
	PrintVFT((VFUNC*)*((int*)&b));

	Derive d;
	PrintVFT((VFUNC*)*((int*)&d));

	X x;
	PrintVFT((VFUNC*)*((int*)&x));
	return 0;
}

多继承中的虚函数表

cpp 复制代码
class Base1 {
public:
	virtual void func1() { cout << "Base1::func1" << endl; }
	virtual void func2() { cout << "Base1::func2" << endl; }
private:
	int b1;
};
class Base2 {
public:
	virtual void func1() { cout << "Base2::func1" << endl; }
	virtual void func2() { cout << "Base2::func2" << endl; }
private:
		int b2;
};
class Derive : public Base1, public Base2 {
public:
	virtual void func1() { cout << "Derive::func1" << endl; }
	virtual void func3() { cout << "Derive::func3" << endl; }
private:
	int d1;
};

// 打印虚表
typedef void (*VFUNC)();
//void PrintVFT(VFUNC a[])
void PrintVFT(VFUNC* a)
{
	for (size_t i = 0; a[i] != 0; i++)
	{
		printf("[%d]:%p->", i, a[i]);
		VFUNC f = a[i];
		f();
	}
	printf("\n");
}

int main()
{
	Derive d;
	PrintVFT((VFUNC*)(*(int*)&d));

	Base1* ptr1 = &d;
	PrintVFT((VFUNC*)(*(int*)ptr1));

	//PrintVFT((VFUNC*)(*(int*)((char*)&d+sizeof(Base1))));
	Base2* ptr2 = &d;
	PrintVFT((VFUNC*)(*(int*)ptr2));
	return 0;
}

可以看到,多继承情况下,子类中的虚函数func3是放在继承的第一个父类的虚函数表中的!

几个问答题

1.inline函数可以是虚函数吗?

答:inline函数可以是虚函数,不过如果是多态调用,inline就没有做用,函数照样会有地址, 放进虚函数表,最后call 地址 调用;如果是普通调用,inline才起作用,函数不会生成地址,直接在调用的地方展开

2.静态成员可以是虚函数吗?

答:不可以, 编译直接报错,因为static成员函数没有this指针,指定类域就可以调用,而虚函数存在的目的就是为了实现多态,而多态调用必然要去对象中找到虚函数表指针,然后找到虚函数表去调用函数,而静态成员函数不需要对象就可以调用,无法构成多态,没有意义!

3.构造函数可以是虚函数吗?

答:不可以, 因为多态调用是要去对象中拿到虚函数表指针最终才能调用虚函数,而虚函数表指针是在构造函数的初始化列表中才初始化赋值的,因此无法调用虚函数, 也就无法实现多态!

4.析构函数可以是虚函数吗,什么场景下析构函数是虚函数?

答:析构函数最好是虚函数,当父类指针 = new 子类对象, delete 父类指针, 这种场景下只有析构函数重写,delete时才能构成多态,才能正确的调用子类的析构函数释放申请的资源

5.对象访问普通函数快还是访问虚函数更快?

如果是普通调用,是一样快的;如果是多态调用,访问普通函数更快,因为访问虚函数要到虚函数表里去找函数的地址

相关推荐
2401_857439692 小时前
SSM 架构下 Vue 电脑测评系统:为电脑性能评估赋能
开发语言·php
SoraLuna2 小时前
「Mac畅玩鸿蒙与硬件47」UI互动应用篇24 - 虚拟音乐控制台
开发语言·macos·ui·华为·harmonyos
xlsw_2 小时前
java全栈day20--Web后端实战(Mybatis基础2)
java·开发语言·mybatis
Dream_Snowar3 小时前
速通Python 第三节
开发语言·python
唐诺3 小时前
几种广泛使用的 C++ 编译器
c++·编译器
高山我梦口香糖4 小时前
[react]searchParams转普通对象
开发语言·前端·javascript
冷眼看人间恩怨4 小时前
【Qt笔记】QDockWidget控件详解
c++·笔记·qt·qdockwidget
信号处理学渣5 小时前
matlab画图,选择性显示legend标签
开发语言·matlab
红龙创客5 小时前
某狐畅游24校招-C++开发岗笔试(单选题)
开发语言·c++
Lenyiin5 小时前
第146场双周赛:统计符合条件长度为3的子数组数目、统计异或值为给定值的路径数目、判断网格图能否被切割成块、唯一中间众数子序列 Ⅰ
c++·算法·leetcode·周赛·lenyiin