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++初阶》(跑路人笔记)

前言

多态是基于继承而实现的一种方式, 我们可以通过实现不同对象调用相同函数进行不同事情的发生.

就像下图一样

image-20220801132335185

这里和函数模板实现重载也有些相似都是使用同样的函数实现不同的功能但是,我们的多态可以一次实现多个函数的实现。在很多方面上都是个不错的选择。

下面让我们来讲解一下多态的实现及原理等。

多态的简单介绍

概念

通俗来说,就是多种形态,具体点就是去完成某个行为,当不同的对象去完成时会产生出不同的状态

多态的定义和实现

我们的多态是基于继承所实现的,我们用子类来继承父类的虚函数并将其进行重写操作。然后我们使用父类的指针或者引用来实现多态的实现。

上面一句话我们有两个疑点虚函数 重写操作这些都是什么及如何操作,

虚函数

我们只需要将函数类中的函数前加上virtual就可以实现我们的虚函数了。

如下代码:

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

虚函数重写

虚函数的作用就是构成重写,当基类中的虚函数和子类中的函数的返回值,参数,函数名都相同的时候我们的就可以实现重写。

class Person
{
public:
	virtual void Buy()//重写
	{
		cout << "原本买票--全价" << endl;
	}
};
class Student:public Person
{
public:
    /*注意:在重写基类虚函数时,派生类的虚函数在不加virtual关键字时,虽然也可以构成重写(因
为继承后基类的虚函数被继承下来了在派生类依旧保持虚函数属性),但是该种写法不是很规范,不建议
这样使用*/
	virtual void Buy()//重写
	{
		cout << "学生买票--半价"<<endl;
    }
};

上图中我们的Buy函数就实现了重写。

我们重写的函数就将不再继承父类函数的函数体,而只继承函数的声明。

证明如下图:


class T1
{
public:
	virtual void test(int pos = 1)
	{
		cout <<"T1:" << pos << endl;
	}
};
class T2: public T1
{
public:
	virtual void test(int pos = 2)
	{
		 
		cout <<"T2:" << pos << endl;
	}
};
void Test(T1& t)
{
	t.test();
}
int main()
{
	T1 t1;
	T2 t2;
	Test(t1);
	Test(t2);
	return 0;
}

执行代码如下图:

image-20220801144354516

可以证明我们的虚拟函数确实是只继承了我们的函数声明。

函数重写的两个例外

协变(基类和派生类的虚函数返回值类型不同)

派生类重写基类虚函数时,与基类虚函数返回值类型不同。即基类虚函数返回基类对象的指针或引用,派生类虚函数返回派生类对象的指针或者引用时,成为协变。(了解即可)

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

析构函数的重写(基类于派生类函数名称不同)

如果基类的析构函数为虚函数,此时派生类析构函数只需要定义无论是否加上virtual关键字,派生类的析构函数都与基类的析构函数构成重写,即使基类于派生类的析构名字不同。但是我们这里其实可以理解成我们的编译器对析构函数的名称做了特殊处理,编译后析构函数的名称统一处理成destructor


这样我们的多态的实现其实就可以实现了。

我们来看一下实例

class Person
{
public:
	virtual void Buy()
	{
		cout << "原本买票--全价" << endl;
	}
};
class Student:public Person
{
public:
	virtual void Buy()
	{
		cout << "学生买票--半价"<<endl;
	}
};
class Teacher:public Person
{
public:
	virtual void Buy()
	{
		cout << "老师买票--优先" << endl;
	}
};
//上面类中我们对Buy函数进行了重写
void Pay(Person& p)// 必须使用父类的指针或引用来调用函数
{
	p.Buy();
}
int main()
{
	Student s;
	Teacher t;
	Person p;
	Pay(t);
	Pay(p);
	Pay(s);
	return 0;
}

代码结果如下图:

image-20220801151759068

C++11 override 和final

C++对虚函数的重写的要求是非常严格的。但是我们人有时候会因为疏忽的原因比如将函数名打错。这是我们的编译器并不会因此而报错。所以可能会因为这样的愿意而产生很难找到的bug。所以我们的C++11就增加了

override 这个关键字会帮你检查有没有实现重写,如果没有实现重写就报错。

使用案例如下:

class Car
{
public:
 virtual void Drive(){}
};
class Benz :public Car 
{
public:
 virtual void Drive() override {cout << "Benz-舒适" << endl;}//用于检查是否完成了重写
};

final 这个关键字会让你修饰的虚函数成为无法被重写


class Car
{
public:
	virtual void Drive() final {}
};
class Benz :public Car
{
public:
	virtual void Drive() { cout << "Benz-舒适" << endl; }//这个因为final的原因将无法重写。
};

重写、重载、重定义(隐藏)的区别

image-20220801155735600

抽象类

抽象类其实就有纯虚函数函数的类。

纯虚函数:

在虚函数函数后面加上=0即可

class T1
{
public:
	virtual void test(int pos = 1) = 0;
};

这里我们T1中的test函数就是纯虚函数。拥有纯虚函数的类就是抽象类。

抽象类是无法直接定义对象的,只有纯虚函数重写才可以定义对象,但是我们的抽象类可以使用类型的指针和引用。所以我们可以用纯虚函数来编写需要重写的函数。

抽象类更能体现接口继承

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

多态的原理

image-20220801170125388

这里我们来看看我们的A类对象虽然只有一个int _a的对象但是它的大小却是8。我们通过调试窗口观察一下。

image-20220801170924312

可以看见我们多了一个vfptr这个其实就是我们的虚函数表指针。通过这个调试窗口可以看见_vfptr指向的位置其实是一个指针,我们多创建几个虚函数试试。

image-20220801171909433

可以看见我们的_vfptr其实就是一个表,我们的test1的虚函数和test2虚函数的地址都会在这个表内存一份。

如果我们用子类来继承这样的让我们看看子类的虚函数表指针及虚函数表

image-20220801184043406

可以当我们没有对虚函数进行重写的时候我们和父类公用了一个虚函数表。

此时我们重写一个test1来观察:

image-20220801184823768

可以看出我们的test1在B中重写后B中就虚函数表中的test2位置就发生了改变。

所以这也可以说明我们的多态其实底层也是多创建了一个函数用于使用罢了。

不过为什么我们使用的父类的指针(引用)来接收的我们子类的类的时候却会调用我们子类的函数呢?

其实原因也很简单,我们的类在将子类传给父类的时候会先发生切片,但是这个切片会将我们的虚函数表指针同样给切下来,且 值不变。

image-20220802223657517

所以我们的父类指针(引用)就可以由此来得到虚函数表。而如果我们没有使用指针(引用)的话我们直接使用父类的类来接收值的时候我们的虚表就不再是子类的值了。也就无法完成多态的实现。

而这种调用函数的方式又与我们之前的普通调用有所不同。

下图中func1()函数是虚函数而func2()不是虚函数。

image-20220803205341437

可以看出我们的func1函数和func2函数调用有着明显的不同。

我们将func1这种通过变量值来调用函数的叫做运行时决议

而将func2这种直接调用的函数叫做编译时决议

我们的运行时决议的步骤要比我们的编译时决议多得多。

总结一下我们的派生类虚表的生成:1.先将基类中的虚表内容拷贝一份到派生类虚表中2.如果派生类重写了基类中某个虚函数,用派生类自己的虚函数覆盖虚表中基类的虚函数3.派生类自己新增加的虚函数按其在派生类中的声明次序增加到派生类虚表的最后。

这里我们要添加一个知识点: 我们的 虚函数和虚表存在哪里?答:虚函数和普通函数一样都在代码段中,而我们的虚表在VS中存在代码段中。

静态绑定和动态绑定

  1. 静态绑定又称为前期绑定(早绑定),在程序编译期间就确定了程序的行为,也成为静态多态,如:函数重载
  2. 动态绑定又称为后期绑定(晚绑定),是在程序运行期间,根据集体拿到的类型确定程序的集体行为,调用具体的函数,也成为动态绑定。

结尾

再摆烂我就是狗🐕!!!!!!!!!!!!!!!

  C++知识库 最新文章
【C++】友元、嵌套类、异常、RTTI、类型转换
通讯录的思路与实现(C语言)
C++PrimerPlus 第七章 函数-C++的编程模块(
Problem C: 算法9-9~9-12:平衡二叉树的基本
MSVC C++ UTF-8编程
C++进阶 多态原理
简单string类c++实现
我的年度总结
【C语言】以深厚地基筑伟岸高楼-基础篇(六
c语言常见错误合集
上一篇文章      下一篇文章      查看所有文章
加:2022-08-06 10:25:04  更:2022-08-06 10:27:21 
 
开发: 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年11日历 -2024/11/23 13:23:45-

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