IT数码 购物 网址 头条 软件 日历 阅读 图书馆
TxT小说阅读器
↓语音阅读,小说下载,古典文学↓
图片批量下载器
↓批量下载图片,美女图库↓
图片自动播放器
↓图片自动播放器↓
一键清除垃圾
↓轻轻一点,清除系统垃圾↓
开发: C++知识库 Java知识库 JavaScript Python PHP知识库 人工智能 区块链 大数据 移动开发 嵌入式 开发工具 数据结构与算法 开发测试 游戏开发 网络协议 系统运维
教程: HTML教程 CSS教程 JavaScript教程 Go语言教程 JQuery教程 VUE教程 VUE3教程 Bootstrap教程 SQL数据库教程 C语言教程 C++教程 Java教程 Python教程 Python3教程 C#教程
数码: 电脑 笔记本 显卡 显示器 固态硬盘 硬盘 耳机 手机 iphone vivo oppo 小米 华为 单反 装机 图拉丁
 
   -> C++知识库 -> C++多态 -> 正文阅读

[C++知识库]C++多态

📋 个人简介

  • 💖 作者简介:大家好,我是菀枯😜

  • 🎉 支持我:点赞👍+收藏??+留言📝

  • 💬格言:不要在低谷沉沦自己,不要在高峰上放弃努力!??

    v2-af3cfbda99d44f8eb114cc6c74c92253_720w

前言

之前我们已经将面向对象三大特性中的封装和继承讲了,接下来剩下最后一个环节了,那就是 多态

多态概念

通俗来说,就是去做相同一件事时,不同的人有不同的状态。比如买火车票,普通人是一个价格,而但学生去买就有一定的折扣,这就是一种多态。

多态定义

多态是在不同继承关系的类对象,去调用同一函数,产生不同的行为。而要形成多态,必须满足下面这两个条件。

1. 必须通过基类的指针或者引用调用虚函数

2. 被调用的函数必须是虚函数,且派生类必须对基类的虚函数进行重写

首先解释一下上面的一些名词,基类就是父类。

虚函数,我们在上篇文章讲虚拟继承时使用了一个关键字叫virtual,而虚函数就是用这个关键字修饰的函数,比如下面的 BuyTicket函数就是一个虚函数。

class Person
{
    virtual void BuyTicket()
    {
        cout << "全价票" << endl;
    }
};

那么什么是虚函数的重写(覆盖)呢?

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

还是以买票为例子,我们创建两个类People和Student,二者都有BuyTicket这个行为,但二者两个函数的实现不同,此时我们可以称Student重写了Person类的BuyTicket函数。

class Person
{
public:
    virtual void BuyTicket()
    {
        cout << "全价票" << endl;
    }
};

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

此时,BuyTicket这个行为就构成了多态,当我们使用不同对象的父类引用去调用这些函数时,就会产生不同的行为。

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

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

image-20220803102342446

此时我们可以看到,当我们传子类对象给Buy函数时调用的为子类中的函数,而父类传过去,调用的为父类函数。

虚函数重写两个例外

我们之前提到在C++中构成函数重写的两个条件:

1. 必须通过基类的指针或者引用调用虚函数

2. 被调用的函数必须是虚函数,且派生类必须对基类的虚函数进行重写

大部分情况下,都必须遵循这两个条件,但C++不愧是C艹,没有意外的话,它一定会出意外。

88c57d2eb7ae153b2bd407207dbc5754f8dc78578bba54ee7

  1. 协变(父类与子类返回值类型不同)

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

    在上面这段代码中,我们可以发现两个f()函数的返回值不同,一个为A&,一个为 B&,但此时二者仍然构成函数重写,这种情况我们称之为协变,即父类虚函数返回父类的指针或引用,子类虚函数返回父类的指针或引用。

  2. 析构函数的重写

    class Person
    {
    public:
    	virtual ~Person()
    	{
    		cout << "~Person()" << endl;
    	}
    };
    class Student : public Person
    {
    public:
    	virtual ~Student()
    	{
    		cout << "~Student()" << endl;
    	}
    };
    

    同样是这两个类,其中Student类是从Person类处,继承而来。虽然Person和Student类的两个析构函数名字并不相同,但也构成函数的重写。

    因为~Person() 和 ~Student是我们给析构函数起的名字,而不是它实际的名字,当程序真正去编译时,这两个函数的名字都会被destructor所替换,所以实际上二者名字是一样的。

三个概念的辨析

目前为止,我们已经学了重载,重写,重定义(隐藏),那么三者有什么区别呢?

image-20220803105745204

抽象类

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

再继续以买票为例子,首先我们将Person类定义为一个抽象类。

class Person
{
protected:
    std::string _name;
    int _age;

public:
    void virtual BuyTicket() = 0;
};

我们将Person类的BuyTicket函数设置为纯虚函数,此时Person类就是一个抽象类,我们无法使用Person类去实例化一个对象。只有当我们的类去重写这个虚函数之后,才能去实例化对象。

class Student : public Person
{
private:
    int _code;

public:
    virtual void BuyTicket()
    {
        cout << "半价票" << endl;
    }
};

此时我们就可以用Student类去实例化一个对象。

普通函数的继承是一种实现继承,派生类继承了基类函数,可以使用函数,继承的是函数的实现。虚函数的继承是一种接口继承,派生类继承的是基类虚函数的接口,目的是为了重写,达成多态,继承的是接口。所以如果不实现多态,不要把函数定义成虚函数。

多态实现原理

接下来,我们就来看看底层,去揭开多态的神秘面纱,看看多态究竟是如何去实现的。

88b017fe88a921bbbc8bd14e614722563ee2a5e7f6c64741d

虚函数表

概念

首先我们先来看看下面这道题目,看看大家能否答对?

//32位平台下
class a
{
public:
	virtual void f()
	{
		int c = 0;
		++c;
	}
private:
	int _b = 0;
};

int main()
{
    a tmp;
	cout << sizeof(tmp);
	return 0;
}

大家觉得这个程序应该输出什么呢?

我相信肯定会有人马上喊:答案是4,因为函数不存在类中,类中只存成员变量,这个类中只有b成员占4个字节,所以答案是4。好巧不巧,我一开始也以为是这个样子,并且觉得这个题这么简单有什么好考的。可当程序运行起来,我傻眼了。

image-20220911144555252

答案是8,这是为什么? 接下来我们使用调试来看看,这个类中到底有什么东西,使它的大小变成了8字节。

image-20220911144836159

我们可以发现,在tmp对象中还偷偷藏了一个指针,那么这个指针又是用来干什么的呢?

image-20220911145102055

我们发现了一个熟悉的东西,这个不正是我们所写的虚函数的名字吗

这个隐藏在暗处的指针,我们称作虚函数指针,它指向了一片空间,这片空间称之为虚函数表,这个表存放的就是类中虚函数的地址。

存在原因

现在我们知道了什么是虚函数指针,以及虚函数表的概念,那么为什么需要这个表和这个指针呢?我们接下来再往后看

class A
{
public:
	virtual void Func1()
	{
		cout << "A::f1()" << endl;
	}

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

	void Func3()
	{
		cout << "A::f3()" << endl;
	}
private:
	int _a;
};

class B : public A
{
private:
    int _b;
};

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

我们分别写了两个类,一个A类作为父类,其中有三个不同的函数,一个B类作为子类,从A类继承而来。首先我们并不对B类进行任何操作,来看看A类和B类中成员的情况。

image-20220911152054478

我们可以看到二者的虚函数指针指向同一片空间,存的虚函数表相同,我们还可以发现,虚函数表中并没有Func3,说明在虚函数表中存放的只有虚函数的地址。

接下来我们在B类中对Func1进行一个重写,来看看是否会发生什么变化。

class B : public A
{
public:
	virtual void Func1()
	{
		cout << "B::f1()" << endl;
	}
private:
	int _b;
};

image-20220911153007963

此时我们发现b对象中的虚函数指针发生了变化,它不再与a对象的虚函数指针指向相同空间,同时b对象的虚函数表中的第一个函数也发生了变化,变为了它自己的成员函数。

根据目前的现象,我们可以得出一个结论:所谓重写实际上是对虚函数指针所指向的位置进行一个重写

为了实现重写,我们需要这个指针和这个表的存在

动态链接和静态链接

虚函数表和虚函数指针的事我们先告一段落,等会我们再来看它,接下来呢,我们来看看普通调用和多态调用,二者在汇编代码上有何区别。

我们把刚刚的A类和B类拿过来,再次使用。

class A
{
public:
	virtual void Func1()
	{
		cout << "A::f1()" << endl;
	}

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

	void Func3()
	{
		cout << "A::f3()" << endl;
	}

private:
	int _a = 0;
};

class B : public A
{
public:
	virtual void Func1()
	{
		cout << "B::f1()" << endl;
	}

	void Func2()
	{
		cout << "B::f2()" << endl;
	}

private:
	int _b = 0;
};

int main()
{
	B b;
	A* a = &b;
	a->Func1();
	a->Func3();
	return 0;
}

我们来看看Func1()和Func3()二者汇编代码的区别。

image-20220911155919337

我们可以很明显的发现多态调用一个函数,比普通调用一个函数多了很多的汇编代码,多出来的汇编代码其实就是一个查表的过程。多态调用其实就是查虚函数表,然后从虚函数中取出要调用的函数的地址,再去call。而普通函数的调用就是直接去call这个函数的地址。

这种在程序运行期间,根据具体拿到的类型确定程序的具体行为,调用具体的函数,称为动态链接。

既然有动态链接,当然也有静态链接**,静态链接是在程序编译期间就确定了程序的行为,调用具体的函数**。比如:函数重载。

总结

有了上面的虚函数表和动态链接的知识后,我们再来补上多态原理的最后一环,来解释一下为什么多态需要使用父类的指针或引用才能实现。

大家记不记得我们在聊继承的时候,讲过一个切片。不记得的朋友可以移步去看看(56条消息) C++继承_。菀枯。的博客-CSDN博客

在进行切片时,我们也会将子类的虚函数指针给父类。因此函数的重写原理如下:

  1. 子类首先会从父类处继承原本的虚函数指针。

  2. 子类改变继承下来的虚函数指针,指向新的虚函数表的地址。

  3. 当我们使用父类的指针或引用时,会发生切片,虚函数表为原来对象的虚函数表

  4. 虚函数的调用实际上是一个查虚函数表的过程,根据不同对象传递的不同的虚函数表,我们就能实现不同对象调用同一函数时产生不同的效果。

结语

1647941444633

欢迎各位参考与指导!!!

  C++知识库 最新文章
【C++】友元、嵌套类、异常、RTTI、类型转换
通讯录的思路与实现(C语言)
C++PrimerPlus 第七章 函数-C++的编程模块(
Problem C: 算法9-9~9-12:平衡二叉树的基本
MSVC C++ UTF-8编程
C++进阶 多态原理
简单string类c++实现
我的年度总结
【C语言】以深厚地基筑伟岸高楼-基础篇(六
c语言常见错误合集
上一篇文章      下一篇文章      查看所有文章
加:2022-09-13 10:54:41  更:2022-09-13 10:57:33 
 
开发: C++知识库 Java知识库 JavaScript Python PHP知识库 人工智能 区块链 大数据 移动开发 嵌入式 开发工具 数据结构与算法 开发测试 游戏开发 网络协议 系统运维
教程: HTML教程 CSS教程 JavaScript教程 Go语言教程 JQuery教程 VUE教程 VUE3教程 Bootstrap教程 SQL数据库教程 C语言教程 C++教程 Java教程 Python教程 Python3教程 C#教程
数码: 电脑 笔记本 显卡 显示器 固态硬盘 硬盘 耳机 手机 iphone vivo oppo 小米 华为 单反 装机 图拉丁

360图书馆 购物 三丰科技 阅读网 日历 万年历 2024年5日历 -2024/5/19 12:20:30-

图片自动播放器
↓图片自动播放器↓
TxT小说阅读器
↓语音阅读,小说下载,古典文学↓
一键清除垃圾
↓轻轻一点,清除系统垃圾↓
图片批量下载器
↓批量下载图片,美女图库↓
  网站联系: qq:121756557 email:121756557@qq.com  IT数码