C++中的多态

文章目录

前言

多态非常非常重要,面试的时候经常出,这块一定要认真学。

我们知道继承就是复用,那什么是多态呢?

多态就是多种形态。
具体点就是我们要做某个动作,当不同的对象去完成时会产生出不同的结果。

只要是做同样的一件事,不同的人去做,结果不一样,那就可以称为多态。

在程序当中就相当于调用一个函数,不同对象去调,它就会产生不同的结果。

下面我们看一下,多态是怎么玩的。

虚函数

首先学习多态我们第一步要学习虚函数。

关键字virtual, 把virtual加在一个函数的前面,我们就叫做虚函数。

这两个函数什么关系呢?

按照我们以前的理解,成员函数函数名相同,子类隐藏父类,我们叫做隐藏关系。

现在有一个新的关系,它们函数名相同,参数相同,返回值相同,
并且是virtual,它们不叫隐藏,它们叫做重写或者覆盖。

这里没有重载的关系,重载必须在同一个作用域。

我们接下来看一下多态是怎样的。

既可以传父类也可以传子类,因为可以赋值兼容。

你传父类它会调用父类的虚函数,你传子类它会调用子类的虚函数。

也就意味着不同的对象去买票,达到的结果是不一样的。

单看下面这个代码是不知道调用什么的。

多态的条件:

如果不满足条件就实现不了多态。

应用场景

为什么需要多态?

有些地方没有多态解决不了问题。

先来看一个应用场景。

后定义的先析构没有什么问题。

换一个场景。

有一个父类的指针,父类的指针能指向父类的对象,

父类的指针也能指向子类对象。

这个析构函数调出事了。

如果这个子类里有一些资源要释放,没调用析构函数就内存泄漏了。

为什么?

我们先看一下delete由什么东西构成。由两部分构成。

之前讲到过,析构函数不会用上面的名称,它会改成统一的名称,

所以父类和子类的析构函数构成隐藏关系。

为什么要这样改?

因为多态原因。

如果多态的概念,按照我们以前的理解,类型是什么就调用什么的析构函数。

但是这不是我们期望的结果。

我们这里不期望按类型去调,这里可能会造成内存泄漏。

我们这里期望按照指向的对象去调用析构函数

在这里要想正确的调用,必须是多态。

多态的条件

多态的条件上面也提到了,上面的条件缺一不可。

如果不满足会怎么样呢?我们现在来具体看一下。

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 ps;
	Student st;
	Func(ps);
	Func(st);
	return 0;
}

多态的条件:
1.虚函数的重写 - - 三同(函数名、参数、返回值)
2.父类指针或者引用去调用

不满足多态会怎么样?

虽然这两个函数构成隐藏,但是这里并不会体现出隐藏。

子类对象去调用才会有隐藏的关系体现。

子类去调用父类的函数调用不到需要指定,这就是隐藏。

大家看一下上面这个结果是多少?

大家可能认为上面都是全价,因为不满足多态,但是看结果。

这里就要提到虚函数重写的两个例外。

虚函数重写的例外

子类可以不加virtual

上面可以认为子类可以不写virtual, 也可以满足多态。

为什么呢?

它是这样认为的,如果父类不写virtual,那肯定就不是虚函数了。

但是现在这种情况,它认为子类重写了虚函数。

重写体现的叫接口继承,也就是说子类把父类函数的声明给继承下来了,
然后重写继承父类这个函数的实现。

所以就算子类不加virtual也是虚函数,因为父类是虚函数,子类继承下来了。

实际当中我们也不会像上面一样写代码。

它这个语法的设计,唯一有一个优势在这里。

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

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

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

	~Student()
	{
		cout << "~Student()" << endl;
	}
};
void Func(Person* p)
{
	p->BuyTicket();
	delete p;
}

int main()
{
	Func(new Person);
	Func(new Student);

	return 0;
}

我期望上面能够正确的调用析构函数。

但是这里的析构函数调用的不对。

现在我希望delete p调用析构函数也构成多态。

父类的析构函数加个virtual就可以了。

父类只需要加virtual就构成多态,子类加不加都无所谓。

协变

这里还有一个例外,返回值可以不同。

返回值不同可以是什么样子的呢?

它不是随便写。

这样会报错。

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

这个例外叫做协变。

最常见的场景就是这样。

协变在实际的应用场景用的很少。

operator的赋值要不要构成虚函数的重写?

不需要。因为子类的成员函数必须调用父类来完成,是复用而不是重写。

是一个其他的父子类也是可以的

这样也可以。都叫做协变。

条件二,必须是父类的指针或者引用去调用

如果是对象去调用会怎样?

判断多态需要把多态的条件严格卡清楚,至于为什么后面会讲原理

下面先来看一道题。

首先子类的指针能不能调用test();?

因为B这个对象虽然没有test();但是它继承了A的test();

条件一:
然后分析一下构不构成多态

缺省参数不同也不影响,它要求的是参数的类型。

条件二:
继承不会把参数类型改了,所以还是A *

所以满足多态

这道题好变态。但是还没有结束,这道题还可以变形。

现在结果是多少?

这里它不满足多态,不是父类的指针或者引用去调用,不满足多态就是正常情况,

哪个对象去调用就调用哪个对象的函数。

如果不构成多态跟重写没有什么关系,就是正常调用。

接口继承和实现继承

普通函数的继承是一种实现继承,

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

所以如果不实现多态,不要把函数定义成虚函数。

override和final

final可以修饰一个类,让这个类不能被继承。

这个叫做最终类。

final还可以用来修饰虚函数,这个虚函数就不能被重写。

如果不想虚函数被继承,就加final,但是实际当中这样的场景极少。

虚函数的意义就是被重写,重写的意义就是满足多态的条件,
所以这里很矛盾,搞了一个虚函数又不想被重写,那搞虚函数有什么意义。

override可以检查是否重写,不是报错

虚函数就是为重写而生的,重写是为多态而生的
不重写的虚函数是没什么意义的,override可以强制我们重写。

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

抽象类

我们可以在虚函数的后面加上赋值0;那这个函数就叫做纯虚函数。

包含纯虚函数的类就叫做抽象类。

抽象类的特点是:不能实例化出对象。

只有重写纯虚函数,子类才能实例化出对象。

纯虚函数不用写实现,直接写声明就可以了。

这个东西的意义是什么?

这个类在现实世界当中没有对应的实体。我不想让这个类实例化出对象。

这个类只是为了让别人能够复用。

比如:Person, Fruit;

抽象类有一个特点,比如一个类去继承了它,这个不能实例化出对象。

为什么?包含纯虚函数的类就叫做抽象类。它继承了也就包含了。

怎么让上面这个子类不是抽象类?

我可以完成重写。

纯虚函数的意义?强制了子类去进行重写。

子类就能够实例化出对象,如果这个类不能实例化出对象,也就没什么意义。

纯虚函数跟override的区别是什么?

一个在父类,一个在子类。一个是检查重写一个是间接的强制重写。

cpp 复制代码
class Car
{
public:
	// 纯虚函数  -- 抽象类 -- 不能实例化出对象
	virtual void Drive() = 0;
};

// 一个类型在现实中没有对应的实体,我们就可以一个类定义为抽象类
class BMW :public Car
{
public:
	virtual void Drive()
	{
		cout << "BMW-操控" << endl;
	}
};
int main()
{
	// Car car;
	BMW bmwCar;

	return 0;
}

语法已经讲的差不多了,我们下一篇文章会讲多态的原理。

总结


1.静态多态的原理?

加了一个函数名的修饰规则,参数不同就会匹配到不同。

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

按照我们之前的理解,inline函数的特点是在调用的地方展开。

inline没有必要有地址,我们之前讲到过inline函数不能声明和定义分离,

因为它不会去链接,链接是调用函数名去找地址。从这个角度,我们认为inline函数不能是虚函数,

因为虚函数的地址要放到虚表去。

但是我们真正的答案我们要验证一下:

这个函数能编译通过。为什么?

这个时候有两种思路,第一种,多验证几个环境。

第二种,我们知道如果一个函数成为inline,那它就没有地址,

那有没有可能,这个函数压根没有成为inline。

因为我们之前说过inline对编译器只是一个建议。

所以有些编译器可能看符合多态就不转成inline,不符合多态就看长度。

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

我们知道虚函数要放进虚表,把静态成员函数放到虚表不合适。

因为虚表一定是通过对象去找的,静态成员函数可以不通过对象调用,他没有this指针。

那它也不适合实现多态。

虚函数的意义就是要实现多态,把静态成员函数搞成虚函数,可以通过类型调用,不去虚表里

去找,所以它就不是为了实现多态。

静态成员函数不喜欢使用对象去调用,我只想用类型去调用不想用对象调用的时候才会

用静态成员函数。

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

构造函数不可以是多态,因为派生类的构造函数必须显示去掉用父类构造函数。

而重写是指向父类调用父类,指向子类调用子类。

构造函数假设可以是虚函数有意义吗?没有意义。
这里还涉及一个先有鸡还是先有蛋的问题。

假设能搞成多态,在调用的时候去虚函数里找,但是这个时候没有虚表,

因为虚表那个指针是在构造函数那个阶段初始化。

注意,初始化列表初始的是虚表指针,不是虚表。虚表编译的时候就确定好了,

它是放在常量区。

5.拷贝构造和赋值能不能是虚函数?

拷贝构造原因跟赋值类似。

primer这本书有个说法,派生类的构造,拷贝构造和赋值是合成版本,
合成版本指的是父类调用父类的一部分,子类调用子类的一部分,它是合成的。
虚函数是为了完成重写,指向父类调用父类,指向子类调用子类。重写是非父即子。
合成版本不能做到。

派生类的赋值写成虚函数语法上允许,但是这样最好也不要这样搞,否则是派生类没办法完。

6.析构函数建议构成虚函数。

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

回答普通函数快这个答案是不够准确的。

上面这种情况认为func2();一定快,因为一个是直接确定地址,然后call
一个是去对象的虚表找到这个地址,最后再call.

虚函数的调用并不一定是要去多态里去找

虚函数的重写只是实现多态的其中一个条件之一。还得看构不构成多态。

这个地方是去虚表里找还是普通调用?

这里没有对A进行重写。

这不是多态,但是多态调用(去虚表中找虚函数的地址)。编译器要区分就很麻烦。

我们之前讲过只要是虚函数都会进入虚表,哪怕没有重写。
其实它只要是虚函数,它就构成多态调用了

下面这样才是多态。

要达到多态的效果。指向父类调用父类的,指向子类调用子类的。

不要把虚函数表和虚基表搞混了

相关推荐
ITMan彪叔7 分钟前
Java MQTT 主流开发方案对比
java·后端
召摇14 分钟前
Java 21到25的核心API演进总结
java·后端
赵谨言18 分钟前
基于python人物头像的卡通化算法设计与实现
开发语言·经验分享·python
应用市场19 分钟前
Qt C++ 图形绘制完全指南:从基础到进阶实战
开发语言·c++·qt
知其然亦知其所以然24 分钟前
SpringAI 玩转 OCI GenAI:这次我们聊聊 Cohere 聊天模型
java·后端·spring
楼田莉子26 分钟前
python小项目——学生管理系统
开发语言·python·学习
金銀銅鐵29 分钟前
[Java] 观察 CompactStrings 选项的影响
java·后端
是2的10次方啊29 分钟前
🎯 HashMap源码深度解析:从"图书馆"到"智能仓库"的进化史
java
paopaokaka_luck33 分钟前
绿色环保活动平台(AI问答、WebSocket即时通讯、协同过滤算法、Echarts图形化分析)
java·网络·vue.js·spring boot·websocket·网络协议·架构
yuanpan34 分钟前
使用Python创建本地Http服务实现与外部系统数据对接
开发语言·python·http