当前位置: 首页 > news >正文

C++多态_virtual

前言:在此文章开始之前,我们要知道面向对象编程思想是什么?说白了就是节省代码,也就是所谓的"代码复用"

代码复用的两种体现方式:1、继承;2、共用相同的函数

本篇文章我们围绕着"共用相同的函数"展开多态的详解,文章使用到的工具是vs2010,文章中展现的汇编代码,我会给出解释,不用太在意。

目录

为什么父类的指针可以指向子类?

        那么派生类的指针能否指向基类呢?

什么是多态? 

        virtual关键字 

        virtual实现多态的底层逻辑

        纯虚函数---抽象类

        纯虚函数的用途

总结


在多态的讲解开始之前,我们还应该知道一个概念:父类指针或引用可以指向子类

为什么父类的指针可以指向子类?

        不懂继承的可以先看一下我的这篇文章《继承浅谈》,大概了解一下继承的基本概念即可。在继承中我们知道,假设父类A定义变量x,y;子类B定义变量z,那他们的关系如下:

        从上面的图中可以看出,派生类成员的范围是要比基类的更加广泛的,因为派生类不仅仅继承了基类的成员,同时也有自己定义的成员

        那么假设我们定义一个派生类对象B b;他的成员如下:

        然后我们定义一个A类的指针指向B类的对象A *a = &b;如下:

        因为A类成员只有x,y宽度总共是八字节,所以A类一级指针指向B类也只能访问八个字节的内容,因为A* a=&b;时,a是指向b类首地址的,所以这里的指针a只能访问到前八字节的内容,也就是变量x和变量y(派生类继承过来的成员),如下:

        当然这一切只是理论,实践如下:

#include <stdio.h>
#include <Windows.h>

class Person{                    // 基类Person:成员有a、b
public:
	int a;
	int b;
	Person(){};
	Person(int a,int b){
		this->a = a; this->b = b;
	}
};

class Teacher:public Person{     // 派生类Teacher:成员有基类的a、b和自己的c
public:
	int c;
	Teacher(){};
	Teacher(int a,int b,int c):Person(a,b)
	{
		this->c = c;
	}
};

int main()
{
	Teacher t(10,20,30);
	Person *p = &t;
	
	system("pause");
	return 0;
}

         当我们打出p->的时候,可以看出编译器已经给出优化了,p是只能访问到a和b的

         那么真的不能访问到成员c吗?其实是可以的,使用指针偏移:

        运行:

         可以看到,基类其实是可以访问到派生类定义的成员的,不过假如派生类中有c和d,那么基类访问派生类中的d成员就比较难了;所以总结就是:父类指针访可以访问子类自己定义的成员,但没必要。

        那么派生类的指针能否指向基类呢?

        可以,但没必要

        首先,还是先来一波推理,假设我们定义对象A a;那么他的成员如下:

        我们再使用派生类指针指向基类对象B* b = &a; 如下:

        从理论上来看,子类指针是不能指向父类对象的,但是为什么我说“可以,但没必要呢?”

        首先我们在编译器中定义父类的对象,用子类指针指向父类试试:

        编译是通过不了的,不过我们可以使用强制转换,如下:

         编译成功。

        我们使用t->看看会弹出来什么,如下:

        他竟然可以访问到c,但是我们并没有初始化c成员,所以打印出来的肯定是一个垃圾值,这也就应了我们那句:可以,但没必要。 

什么是多态? 

        多态按字面的意思就是多种形态。当类之间存在层次结构,并且类之间是通过继承关联时,就会用到多态。

        C++ 多态意味着调用成员函数时,会根据调用函数的对象的类型来执行不同的函数。

        我们知道,面向对象编程思想就是为了节省代码的重复编写,那么我会围绕节省代码编写的例子去讲多态,可能例子会有些莫名其妙,但是知道这个道理就行了。

        我们先准备一下代码:

#include <stdio.h>
#include <Windows.h>

class Person{                    // 基类Person:成员有a、b
public:
	int a;
	int b;
	Person(){};
	Person(int a,int b){
		this->a = a; this->b = b;
	}
    void print()                // 基类添加一个打印成员的函数,会继承到派生类
    {
        printf("%d %d\n",a,b);
    }
};

class Teacher:public Person{     // 派生类Teacher:成员有基类的a、b和自己的c
public:
	int c;
	Teacher(){};
	Teacher(int a,int b,int c):Person(a,b)
	{
		this->c = c;
	}
};

int main()
{
	Teacher t(10,20,30);
	Person *p = &t;
	
	system("pause");
	return 0;
}

        我们在main函数上方定义两个函数,一个用来打印父类成员,一个用来打印子类的,如下:

        大家先不要管父类可以直接调用类成员print()来输出成员的这个问题,因为这里只是举个例子 

        从上面的图中可以看出,因为子类继承了父类的print,所以子类可以直接t->print();其实这也就是代码复用的第一种体现方式:继承;文章开头有说过。

        但是我们会发现,我们写了两个函数,一个用来打印父类一个用来打印子类,函数的大致内容都差不多,那么这是不是也算是代码的重复编写了?是不是也违背了面向对象的编程思想呢?

        是的。

        接下来我们就要解决这个问题。怎么解决呢?       

        首先我们上面讲过,父类的指针或引用是可以直接指向子类对象的,那么如果我们函数的形参设置成一个父类的指针,是不是就意味着实参不仅可以传递父类对象还可以传递子类对象?

        通过这一想法,我们可以把两个函数改成一个,并且可以传递父类或者子类的对象

        如下:

        验证一下,我们先传入父类对象看是否报错:

 

        没有问题,那么我们传入子类:

        也没有问题。

        那么这是不是就达到我们的目的了?减少了重复代码的编写;确实减少了代码的编写,但是还没有达到我们的目的,我们传入子类对象输出一下: 

        我们子类明明有三个成员,但是为什么只输出了两个,这很明显他调用的并不是子类继承过去的print,而是调用的父类的print; 

        解决方法如下:

        首先我们需要在子类中重写一个print函数,记住是重写!重写函数的要求就是函数的返回值类型和函数名、参数列表等都必须与父类一致,那么我们重写的函数如下:

        为什么一定要用同样的函数名呢?因为我们Myprint函数中调用的就是print(),为了实现统一的接口,所以我们必须使用同样的函数名。

        那么这样做是不是就可以传入父类对象就调用父类的print,传入子类的对象就调用子类的print呢?我们通过输出结果来看一下:

        子类依旧无法输出我们想要的结果,这是为什么呢?

        下面就要介绍一个关键字了。

        virtual关键字 

        Virtual是C++ OO机制中很重要的一个关键字。只要是学过C++的人都知道在类Base中加了Virtual关键字的函数就是虚拟函数。

        基类的函数调用如果有virtual则根据多态性调用派生类的,如果没有virtual则是正常的静态函数调用,还是调用基类的。

        那么我们在基类的print前面加上virtual关键字,如下:

        传入父类对象:

        没问题,传入子类对象:

        也没问题。

        通过上面的内容就真真正正的达到了我们的目的,不仅实现了代码的复用,同时也实现了所谓的多态。 

        那么我们如何知道加virtual关键字就实现了多态呢?virtual底层实现多态的思想又是什么呢?

        virtual实现多态的底层逻辑

        我们先把virtual关键字去掉,然后再如下地方下断点:

        CTRL+ALT+F7重新编译、F5调试、ALT+8转到反汇编,如下:

        可以看出,没有virtual关键字的时候,底层是直接指定了调用Person::print()函数,在编译的时候就指定了调用父类的print,那么运行结果肯定不会是子类print输出的内容啊。

        那么virtual关键字下又是什么样的呢?加上关键字,依旧这里下断点,汇编如下:

        这就是virtual函数实现多态的思想:传入一个没有指定的地址

        没有virtual时:指定调用父类的print

        有virtual时   : 调用一个地址,这个地址可以是父类的print也可以是子类的print,需要看你传入的是哪个类的对象。

        那么virtual关键字底层又是怎么确定调用父类还是子类的函数呢?

        这个问题与virtual底层实现的虚表有关,关于虚表的概念以后会细讲,这里我们暂且知道virtual底层是如何实现多态的就行了(传入一个没有指定的地址);

        纯虚函数---抽象类

        我们知道virtual关键字修饰的函数是虚函数,那么纯虚函数是什么样的呢?

        这就是纯虚函数。

        纯虚函数有几个需要注意的特点:

        1、含有纯虚函数的类被称为抽象类,不能创建对象;

        2、虚函数可以直接使用,也可以被子类重写之后以多态的形式调用,而纯虚函数必须在子类中实现该函数才能使用;

        我们先来看第一点,我们使用纯虚类Person创建一个对象试试:

        创建失败,不过我们可以创建父类指针:

        编译成功!

        因为我们知道父类的指针是可以指向子类的,所以纯虚类(抽象类)不能创建对象其实对我们的影响不大;但是我们也必须记住这一点 ;

        再来看第二点:虚函数可以直接使用,也可以被子类重写之后以多态的形式调用,而纯虚函数必须在子类中实现该函数才能使用;

        也就是说,如果父类定义了纯虚函数,那么子类是必须要去实现这个函数的

        先整理一下代码,如下:

#include <stdio.h>
#include <Windows.h>

class Person{
public:
	int a;
	int b;
	Person(){};
	Person(int a,int b){
		this->a = a; this->b = b;
	}
	virtual void print() = 0;
};

class Teacher:public Person{
public:
	int c;
	Teacher(){};
	Teacher(int a,int b,int c):Person(a,b)
	{
		this->c = c;
	}
};


int main()
{
	
	system("pause");
	return 0;
}

        定义一个父类对象和子类对象:

        都不可以,都是说类是抽象的

        我们先写上父类的实现,在外部:

        没用,还是抽象类;

        我们在子类中实现一下,把父类的实现删掉:

 

        子类成功创建对象。

        因为父类创建纯虚函数后,不管父类写没写纯虚函数的实现,都不能创建对象,所以我们也就不管父类了;因此我们纯虚函数的特点二大致可以总结为:如果父类写了virtual ... =0;纯虚函数,那么父类无论如何都不能创建对象,并且子类必须实现这个纯虚函数,也就是重写;

        另外,如果有第二个派生类继承了Person,那么它也必须实现这个纯虚函数,否则不能创建对象,这里我就不验证了,小伙伴们可以自己测试一下;

        讲了这么多,纯虚函数的意义是什么呢?有什么用呢?

        纯虚函数的用途

        如果父类没有什么用,可以说是工具类,那么就可以在父类中定义纯虚函数,子类继承这个父类时必须去实现这个函数,这也就达到了目的,这也就是我们常说的接口类;父类中含有纯虚函数一般目的都是为了实现接口类;

        直接看一个案例代码:

#include <stdio.h>
#include <Windows.h>
#define PI 3.14

class Shape                        // 形状类
{
public:
	virtual double area() = 0;
};

class Circular:public Shape        // 圆类继承形状类
{
private:
	double r;
public:
	Circular(double r)
	{
		this->r = r;
	}
	double area()
	{
		return PI*r*r;
	}
};

void printArea(Shape* s)            // 输出
{
	printf("%lf\n",s->area());
}

int main()
{
	Circular c(1.2);
	printArea(&c);

	system("pause");
	return 0;
}

        在上面的代码中,我们只写了一个子类(圆),这很难体现出接口(抽象类)的重要性,如果有多个子类同时继承这一个抽象类的话,那么接口的重要性以及多态就会变得更加明显

        首先我们定义了一个形状类Shape,然后定义了一个圆类继承了这个形状类;

        形状类里有一个函数,是返回面积的,但是我们形状类,你知道他是个什么形状吗?Shape没有任何被指定的形状,所以Shape中的double area没有任何意义啊,那没有意义你还实现它干嘛?你不知道他的形状那么你的函数实现(返回面积)怎么写?

        所以我们把Shape类中的area声明为一个纯虚类;提供接口;

        我们看printArea函数;我们知道抽象类父类不能创建对象,但是能创建指针指向子类,所以父类给printArea函数提供了指针;我们printArea中调用了area函数,又因为父类的area是纯虚函数,那么所有的子类都必须实现area这个函数,这也就给printArea中调用area的输出实现了多种可能,也就是多态;

        我们可以简单总结一下:如果基类中的函数没有任何实现的意义,那么可以定义为纯虚函数

总结

        1、父类指针或引用可以直接指向子类对象;子类指针也可以指向父类对象,但没必要;

        2、C++ 多态意味着调用成员函数时,会根据调用函数的对象的类型来执行不同的函数。

        3、virtual关键字修饰的函数称为虚函数,virtual是实现多态的关键。

        4、虚函数的目的是提供一个统一的接口,被继承的子类重写,以多态的形式调用;

        5、如果基类函数没有任何实现的意义,那么可以定义为纯虚函数virtual ... = 0;

        6、含有纯虚函数的类被称为抽象类,不能创建对象;

        7、虚函数可以直接被使用,也可以被子类重写以后以多态的形式调用,而纯虚函数必须在子类中实现该函数才可以使用;

以上便是文章的所有内容,有过有讲得不好的地方,还请大家指出,谢谢观看!

相关文章:

  • 【算法】LeetCode:栈与队列篇
  • 【Linux】进程通信 | 管道
  • python 办公自动化(Excel)
  • 前端面试题记录(大环境不太友好的2022篇)
  • 为什么python量化书籍都不讲金融只讲编程?
  • Cadence Allegro DXF结构图的导入详细教程
  • 【Leetcode】拿捏链表(一)——206.反转链表、203.移除链表元素
  • C语言实现三子棋小游戏(源码+教程)
  • 爱心html制作并部署github
  • 【蓝桥杯真题练习】STEMA科技素养练习题库 答案版013 持续更新中~
  • Mysql 当前月每天累计统计,Mysql 本月第一天
  • 第一个发布成功的UI组件库
  • 【python】点燃我,温暖你 ,快来Get同款~
  • Flutter:webview_flutter插件使用
  • 学习python第6天
  • [附源码]计算机毕业设计JAVAjsp求职招聘平台开发
  • C++ opencv 图像像素的逻辑操作
  • 【Revit二次开发】模型中存储数据——参数和外部存储(Parameter, Schema and Entity)
  • 基于粒子群优化和模拟退火算法增强传统聚类研究(Matlab代码实现)
  • 前端经典面试题 | 性能优化之图片优化
  • 媒体:酒店、民宿临时毁约涨价,怎么管?
  • 中青旅:第一季度营业收入约20.54亿元,乌镇景区接待游客数量同比减少6.7%
  • 华夏银行一季度营收降逾17%、净利降逾14%,公允价值变动损失逾24亿
  • 贵州茅台一季度净利268亿元增长11.56%,系列酒营收增近两成
  • 君亭酒店:2024年营业收入约6.76亿元, “酒店行业传统增长模式面临巨大挑战”
  • “五一”假期倒计时,节前错峰出游机票降价四成