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进行重写。

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

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

下面这样才是多态。

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

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

相关推荐
我们的五年1 分钟前
【Linux课程学习】:进程描述---PCB(Process Control Block)
linux·运维·c++
以后不吃煲仔饭11 分钟前
Java基础夯实——2.7 线程上下文切换
java·开发语言
进阶的架构师12 分钟前
2024年Java面试题及答案整理(1000+面试题附答案解析)
java·开发语言
前端拾光者16 分钟前
利用D3.js实现数据可视化的简单示例
开发语言·javascript·信息可视化
The_Ticker17 分钟前
CFD平台如何接入实时行情源
java·大数据·数据库·人工智能·算法·区块链·软件工程
程序猿阿伟17 分钟前
《C++ 实现区块链:区块时间戳的存储与验证机制解析》
开发语言·c++·区块链
傻啦嘿哟35 分钟前
如何使用 Python 开发一个简单的文本数据转换为 Excel 工具
开发语言·python·excel
大数据编程之光39 分钟前
Flink Standalone集群模式安装部署全攻略
java·大数据·开发语言·面试·flink
初九之潜龙勿用40 分钟前
C#校验画布签名图片是否为空白
开发语言·ui·c#·.net
爪哇学长1 小时前
双指针算法详解:原理、应用场景及代码示例
java·数据结构·算法