【C++进阶(九)】C++多态深度剖析

💓博主CSDN主页:杭电码农-NEO💓

⏩专栏分类:C++从入门到精通

🚚代码仓库:NEO的学习日记🚚

🌹关注我🫵带你学习C++

🔝🔝



多态

  • [1. 前言](#1. 前言)
  • [2. 多态的概念以及定义](#2. 多态的概念以及定义)
  • [3. 多态的实例调用情况](#3. 多态的实例调用情况)
  • [4. 构成多态的两个特例](#4. 构成多态的两个特例)
  • [5. 多态的底层原理分析(一)](#5. 多态的底层原理分析(一))
  • [6. 多态底层原理分析(二)](#6. 多态底层原理分析(二))
  • [7. 多态中的两个关键字](#7. 多态中的两个关键字)
  • [8. 抽象类以及虚函数的几个结论](#8. 抽象类以及虚函数的几个结论)
  • [9. 总结以及拓展](#9. 总结以及拓展)

1. 前言

继承和多态这两兄弟常常一起出现

继承是实现多态的前提!

本章重点:

本篇文章着重讲解多态的概念以及
定义,多态的底层原理和析构函数重写
以及函数重写的两个例外条件
多继承中的虚函数表关系.其中,简单介绍
的部分有抽象类的概念以及定义和
继承与多态中的两个新增关键字

注:如果你不知道什么是继承,或继承
的知识掌握不牢固,请先阅读下面文章:

C++继承深度剖析


2. 多态的概念以及定义

概念: 通俗来说,多态就是多种状态
父子对象完成相同任务会产生不同的结果

比如:
学生和普通人都去买门票
学生是半价,而普通人是全价

在继承中构成多态要有两个条件:

  1. 必须通过基类的指针或引用调用虚函数
  2. 被调用的函数必须是虚函数
    并且子类的虚函数要被重写

现在的你可能有一万个问号

什么是虚函数?什么是重写?

没关系,我们一步一步讲!

关键字virtual加在成员函数前
这个成员函数就是虚函数!


虚函数的重写(也叫覆盖):

派生类中有一个跟基类完全相同的虚函数(即派生类虚函数与基类虚函数的,返回值类型、函数名字、参数列表完全相同),称子类的虚函数重写了基类的虚函数

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

上面的代码中,BuyTicket函数就被重写了!

概念讲完,下一步进行实战!


3. 多态的实例调用情况

构成多态的条件就两个,一定要熟记!

一定要熟记!一定要熟记!重要的事情说三遍

下面是多态的实例:

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

int main()
{
	Person* p1 = new Person;
	Person* p2 = new Student;
	p1->BuyTicket();
	p2->BuyTicket();
	return 0;
}

我们知道一个事实:

基类的指针或引用可以指向/引用

子类的对象,我们称为切片

p1和p2是基类指针,它们调用的
函数恰好还被重写了,所以这里符合
多态,p1指针指向的内容是Person
所以它调用Person中的函数,然而p2
指针指向的内容是Student,所以它
调用的是Student中的函数!

依次打印:"买票-全家","买票-半价"


4. 构成多态的两个特例

  1. 特例一: 子类的虚函数不写virtual
    依旧构成多态
cpp 复制代码
class Person {
public:
 	virtual void BuyTicket() { cout << "买票-全价" << endl; }
};
class Student : public Person {
public:
 	void BuyTicket() { cout << "买票-半价" << endl; }
};
Person* p1 = new Person;
Person* p2 = new Student;
p1->BuyTicket();
p2->BuyTicket();

这样写也是构成多态的!

  1. 特例二: 基类与派生类虚函数返回值类型不同
    也可以构成多态(返回值必须满足某种条件)
cpp 复制代码
class A{};
class B : public A {};

class Person {
public:
 	virtual A* f() {return new A;}
};
class Student : public Person {
public:
 	virtual B* f() {return new B;}
};

父类的返回值要返回父类
子类的返回值要返回子类

  1. 注意事项1: 父类不写virtual,而子类的同名
    函数写了virtual,这是不构成多态的!
cpp 复制代码
class Person {
public:
 	void BuyTicket() { cout << "买票-全价" << endl; }
};
class Student : public Person {
public:
 	virtual void BuyTicket() { cout << "买票-半价" << endl; }
};

不构成多态!

  1. 注意事项2: 在继承体系中,父子类的同名
    函数不构成重写就构成隐藏,不可能构成重载!

5. 多态的底层原理分析(一)

如果你单纯的认为Base类只有一个

整型变量占用空间的话,那你就上当啦!

事实上在32位机器下,这里的结果是8

在64位机器下,这里的结果是16!

这是因为它除了有一个变量外,还有
一个指针,此指针指向一个虚函数表

我们通过以下的代码来观察内存:

cpp 复制代码
class A
{
public:
	virtual void func1()
	{
		cout << "父类func1";
	}
private:
	int _a;
};
class B : public A
{
public:
	virtual void func1()
	{
		cout << "子类func1";
	}
private:
	int _b;
};

int main()
{
	A a;
	B b;
	return 0;
}

此指针叫虚表指针:vfptr,也就是
virtual function ptr

这个指针并不是直接指向虚函数的地址
而是指向一个虚函数表,可以理解位一个
数组,此数组中存放着此对象中所有的虚
函数的地址,它们的关系可以用下图表示:

注:不管有没有继承体系或多态
只要有虚函数就有虚表!


6. 多态底层原理分析(二)

现在得出一个结论:有虚函数的

类对象中还存放了一个虚表指针!

那么父类和子类的虚表指针和指向
的内容有什么不同或相同处吗?
形成多态现象的原理又是什么?
我来一一解答这些问题:

  1. 通过下面的代码来观察内存情况
    得出父子类虚表的关联:
cpp 复制代码
class A
{
public:
	virtual void func1()
		cout << "父类func1";
	virtual void func2()
		cout << "父类func2";
private:
	int _a;
};
class B : public A
{
public:
	virtual void func1()
		cout << "子类func1";
private:
	int _b;
};
int main()
{
	A a;
	B b;
	return 0;
}

请看下图观察情况:

结论:

父类和子类的虚表指针是不同的
证明父子类各有一张虚函数表!
函数func1在子类中被重写了,所以
父子类虚表中的func1函数地址是不同的
函数func2没有被子类重写,所以
父子类虚表中的func2函数地址是相同的

拓展结论:同一个类的不同对象共用一个虚表

  1. 多态的原理深度剖析:

当一个函数A被重写时,它的父类虚表存放
父类函数A的地址,子类虚表存放的是子类
函数A的地址!

当父类的指针或引用指向子类空间时
调用虚函数时,会到指向对象的虚表中
中找到对应的虚函数地址,进行调用!

拓展结论: 父子类都只有A函数或无函数时

  1. 若父类写了虚函数A,而子类
    甚至没有写函数A,此时子类对象中
    存储的虚函数地址与父类相同

  2. 若父类甚至没有写函数A,而子类
    直接写了虚函数A,则父类对象中没有
    虚表,而子类对象中有虚表(存放A)


7. 多态中的两个关键字

  1. final:修饰虚函数,表示该虚函数不能被重写
  1. override:检查子类类虚函数是否重写了
    基类虚函数如果没有重写编译报错

8. 抽象类以及虚函数的几个结论

抽象类概念:

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

抽象类的只需了解概念,实际中

使用到的场景很少

关于虚函数的几个小结论:

  1. 析构函数最好定义为虚函数
  2. 构造函数不能定义为虚函数
  3. 静态成员函数不能是虚函数
  4. 内联函数(inline)不能是虚函数

为什么说析构函数最好定义为虚函数?

请看下面的例子:

cpp 复制代码
class Person {
public:
 	virtual ~Person() {cout << "~Person()" << endl;}
};
class Student : public Person {
public:
 	virtual ~Student() { cout << "~Student()" << endl; }
};
// 只有派生类Student的析构函数重写了Person的析构函数
//下面的delete对象调用析构函数,才能构成多态
//才能保证p1和p2指向的对象正确的调用析构函数。
int main()
{
 	Person* p1 = new Person;
 	Person* p2 = new Student;
 	delete p1;
 	delete p2;
 	return 0;
}

若析构函数不是虚函数,delete ptr2时
不符合多态,ptr2是Person类型指针
就只会调用Person类的析构,会有问题

若析构函数是虚函数,delete ptr2时
构成多态的条件,指针指向父类的对象
就调用父类的析构,指向子类的对象
就调用子类的析构,这样才是正确的!


9. 总结以及拓展

多态在校招的笔试面试中考察的

非常之多,很多面试官都喜欢在这

上面考察学生的掌握C++语法的程度

所以同学们请耐心学习!

拓展阅读:

多继承场景下的多态


🔎 下期预告:二叉搜索树 🔍

相关推荐
嘿BRE3 分钟前
【C++】几个基本容器的模拟实现(string,vector,list,stack,queue,priority_queue)
c++
drebander6 分钟前
使用 Java Stream 优雅实现List 转化为Map<key,Map<key,value>>
java·python·list
乌啼霜满天2499 分钟前
Spring 与 Spring MVC 与 Spring Boot三者之间的区别与联系
java·spring boot·spring·mvc
tangliang_cn14 分钟前
java入门 自定义springboot starter
java·开发语言·spring boot
程序猿阿伟15 分钟前
《智能指针频繁创建销毁:程序性能的“隐形杀手”》
java·开发语言·前端
Grey_fantasy25 分钟前
高级编程之结构化代码
java·spring boot·spring cloud
新知图书26 分钟前
Rust编程与项目实战-模块std::thread(之一)
开发语言·后端·rust
威威猫的栗子28 分钟前
Python Turtle召唤童年:喜羊羊与灰太狼之懒羊羊绘画
开发语言·python
力透键背28 分钟前
display: none和visibility: hidden的区别
开发语言·前端·javascript
bluefox197929 分钟前
使用 Oracle.DataAccess.Client 驱动 和 OleDB 调用Oracle 函数的区别
开发语言·c#