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++(15)——多态与虚函数 -> 正文阅读

[C++知识库]C++(15)——多态与虚函数

多态性

多态性(polymorphism)是考虑不同层次的类中,以及在同一个类中,同名的成员函数之间的关系问题。函数的重载,运算符的重载,属于编译时的多态性(早期绑定:编译期就确定了调用关系)。以虚函数为基础的运行时的多态性(晚期绑定:程序在运行过程中才能确定调用关系 是面向对象程序设计的标志性特征,体现了类推和比喻的思想方法。
在这里插入图片描述

编译时多态

编译器编译时就确定了调用关系,就叫做早期绑定,也叫作动态绑定。
在这里插入图片描述

虚函数

虚函数是一个类的成员函数,定义格式如下:

virtual 返回类型 函数名(参数列表);
  • 关键字virtual指明该成员函数为虚函数,只有类的成员函数才可以定义为虚函数,virtual仅用于类定义中,如果虚函数在在类中声明,类外定义,类外可不加virtual。但在声明时应尽量放在类体内完成。
  • 当某一个类的一个类成员函数被定义为虚函数,则由该类派生出的所有派生类中,该函数始终保持虚函数的特征。

C++中的虚函数的作用主要是实现了多态的机制。关于多态,简而言之就是 用父类型别的指针指向其子类的实例,然后通过父类的指针调用实际子类的成员函数。 这种技术可以让父类的指针有“多种形态”,这是一种泛型技术。所谓泛型技术,说白了就是试图使用不变的代码来实现可变的算法。

运行时的多态

它是通过类继承关系public和虚函数来实现,目的是建立一种通用的程序。

注意: 必须通过引用或者指针调动虚函数时,才能够达到运行时的多态的效果。

总结:运行时多态需要满足以下三个要求:

  1. 公有继承(是一个)
  2. 想要虚化的函数加上virtual关键字
  3. 通过指针或者引用绑定函数

示例:

class Animal
{
private:
    string name;
public:
    Animal(const string & na):name(na){}
public:
    virtual void eat(){}
    virtual void walk(){}
    virtual void tail(){}
    virtual void PrintInfo(){}

    string & Get_name(){return name;}
    const string & Get_name() const {return name;}
};

class Dog:public Animal
{
private:
    string owner;
public:
    Dog(const string & ow,const string &na):Animal(na),owner(ow){}
    virtual void eat(){cout<<"Dog eat:bone"<<endl;}
    virtual void walk(){cout<<"Dog walk:run"<<endl;}
    virtual void tail(){cout<<"Dog tail:Wang wang"<<endl;}
    virtual void PrintInfo()
    {
        cout<<"Dog owner"<<owner<<endl;
        cout<<"Dog name"<<Get_name()<<endl;
    }

};

class Cat:public Animal
{
private:
    string owner;
public:
    Cat(const string & ow,const string &na):Animal(na),owner(ow){}
    virtual void eat(){cout<<"Cat eat:fish"<<endl;}
    virtual void walk(){cout<<"Dog walk:silent"<<endl;}
    virtual void tail(){cout<<"Dog tail:Miao miao"<<endl;}
    virtual void PrintInfo()
    {
        cout<<"Cat owner"<<owner<<endl;
        cout<<"Cat name"<<Get_name()<<endl;
    }

};

void fun(Animal & animal)
{
    animal.eat();
    animal.walk();
    animal.tail();
    animal.PrintInfo();
}

int main()
{
    Dog dog("mk","erha");
    Cat cat("bw","persian");

    fun(dog);
    fun(cat);
    return 0;
}

注意:每一个存在虚函数的类实例化出的对象都存在虚表指针,但是虚表只有一份,
在这里插入图片描述

编联时需要确定的属性

  1. 类型class
  2. 可访问性
  3. 函数的默认值

虚函数表

虚函数的实现是因为存在一张虚函数表,这一过程中发生了同名覆盖,有虚函数的类在编译过程中才会产生虚表。
在这里插入图片描述
下面我们来辨析以下通过指针或引用调动对象调动虚函数的编联方案的异同:
比如下面的代码:
在这里插入图片描述
再来看下面的示例,更加清楚地了解同名覆盖以及虚表指针的转换过程(构造函数发挥作用):
同名覆盖:在子类中重写的虚函数,在虚表中会被替换
在这里插入图片描述
其实,在类中存在虚函数时,构造函数除了构建对象,初始化对象的数据成员以外,还要将虚表地址传递给虚表指针.
在构造函数中也不能完成如下操作:

memset(this,0,sizeof(Base));

这样很危险,因为这样会将对象内的所有成员初始化成0,虚表指针也会被修改。

所以,我们向前面所说的一样,不能将构造函数定义成虚函数,原因是:调动虚函数时,我们需要查表,构造函数是虚表建立的前提,若构造函数为虚函数,那么此时我们调动虚化的构造函数,虚表还没有建立,所以构造函数,拷贝构造函数,移动构造函数都不能被建立成为虚函数。

再来判断下面程序的输出结果:
在这里插入图片描述

  • t1传入自己的this指针,查看自身的虚表,发现自己没有fun函数,于是调动其上一层base的fun函数
  • base传入自己的this指针,查看自身的虚表,发现自己有fun函数,直接调动
  • obj传入自己的this指针,查看自身的虚表,发现自有,于是调动自身的fun函数

继续探究:下面程序的执行结果为什么是:Base::print::10
在这里插入图片描述
前面我们提到了编联时需要确定的属性,也要注意重写虚方法时只需要保证三同(同函数名,同参数类型,同返回类型),并没有要求函数的默认值必须相同。

  • 编译时,我就需要确定可访问性,对于op来说,它是一个Object类型,因此他就可以访问自己的print函数,函数的默认值在编译时也就确定为10,
  • 运行时,发现是通过指针访问虚函数,因此采取动态编联方案,通过查表后又调动Base的print函数

那么为什么可以通过op访问Base的私有虚函数呢?
因为,该程序的编译是可以通过的,在运行时编译器不会考虑访问属性的影响。

虚析构函数

先看下面的代码:

class Object
{
private:
	int value;
public:
	Object(int x = 0):value(x){cout<<"Create Object:"<<this<<endl;}
	virtual void add(){cout<<"Object add"<<endl;}
	virtual ~Object(){cout<<"Destroy Object"<<this<<endl;}
};
class Base:public Object
{
private:
	int num;
public:
	Base(int x = 0):num(x){cout<<"Create Base:"<<this<<endl;}
	virtual void add(){cout<<"Base add"<<endl;}
	~Base(){cout<<"Destroy Base"<<this<<endl;}
};
int main()
{
	Object *op = new Base(10);
	op->add();
	delete op;
	op = NULL;
	return 0;
}

上述代码的执行结果如下:
在这里插入图片描述

我们可以看到,其实它并没有调用Base对象的析构函数,仅仅调用了父类对象的虚构函数,究其原因是因为:二者的析构函数非虚函数,所以编译器在解析时发现,op是一个Object的指针,此时就只会调动Object的析构函数。

为了做到运行时多态的释放,就必须让析构函数变成虚函数

因为,一旦父类的析构函数定义为虚函数,那么所有派生类的析构函数也都会变成虚函数,因此无需在派生类中声明,在调动delete,传入父类指针时,就会将父类对象和子类对象都析构。

修改代码后执行结果如下:
在这里插入图片描述
那么为什么我们要将析构函数定义成虚函数呢?

就是因为在上述的应用场景下,当父对象的指针指向子对象时,我们动态开辟空间,在释放父指针时,我们采用连级释放的机制,从而调动子类的析构。

析构函数的另一个作用

reset重置虚表,以上述代码为例,在析构过程中先析构子对象,然后此时整个Base对象就剩下隐藏基对象的资源和空间,然后重置虚表指针指向隐藏基对象的首地址,再对该对象进行析构。

纯虚函数

纯虚函数是指被标明为不具体实现的虚拟成员函数,它用于这种情况:定义一个基类时,会遇到无法定义基类中虚函数的具体实现,其实现依赖于不同的派生类。
定义纯虚函数的一般格式是:

virtual 返回类型 函数名(参数列表) = 0;

"= 0 "表示程序员将不定义该函数,函数声明是为派生类保留一个位置,其本质是将指向函数体的指针定为NULL。

总结

虚函数和纯虚函数的区别

在这里插入图片描述

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

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