热门标签 | HotTags
当前位置:  开发笔记 > 编程语言 > 正文

虚函数与多态小览

一、文章来由Bill又写文章来由了哇~~早就想好好搞清这个问题了,这是c++领域里面比较难搞定的一块知识点,而且最近在看设计模式,里面有涉及这块,之前学过的不用容易玩忘记,于是就干脆研

一、文章来由

Bill又写文章来由了哇~~早就想好好搞清这个问题了,这是c++领域里面比较难搞定的一块知识点,而且最近在看设计模式,里面有涉及这块,之前学过的不用容易玩忘记,于是就干脆研究透一点,也好碰到、用到的时候不心慌~于是有了这篇文章。

二、从编译时和运行时说起

2.1 编译时:

顾名思义就是正在编译的时候。就是编译器帮你把源代码翻译成机器能识别的代码。(当然只是一般意义上这么说,实际上可能只是翻译成某个中间状态的语言。比如Java只有JVM识别的字节码,C#中只有CLR能识别的MSIL)

编译时就是简单的作一些翻译工作。如果发现错误编译器就告诉你。比如点击微软的VS点下build,如果下面有errors或者warning信息,那都是编译器检查出来的。所谓这时的错误就叫编译时错误,这个过程中做类型检查叫编译时类型检查,或静态类型检查(所谓静态嘛就是没把真把代码放内存中运行起来,而只是把代码当作文本来扫描下)。所以说编译时还分配内存肯定是错误的。

2.2 运行时:

所谓运行时就是代码跑起来了,被装载到内存中去了。(你的代码保存在磁盘上没装入内存之前是死的,静态的;只有跑到内存中才变成活的)。而运行时类型检查就与前面讲的编译时类型检查(或者静态类型检查)不一样。不是简单的扫描代码。而是在内存中做些操作,做些判断。

例如,在C++中:

int arr[] = {1,2,3};
int result = arr[4];
cout<

上面的代码错误很明显—数组越界了。但用编译器编译,是不会报错的。可见编译器其实还是挺笨的,然后开始Debug,可能会报错(也可能不报错,本人用的vs2012 debug模式就没有)。但实际上运行时做数组的越界检查不是C++里面支持的特性,这里你dubug是VS中的一些工具给你做的检查。你如果点运行时选的是release而不是debug的话会发现一切正常运行,但得到的结果不确定的(因为不知道arr[4]所指的内存里存放的是什么)。

2.3 c++多态在编译时和运行时【底层机制,很重要】:

那C++为什么不在运行时做数组越界检查呢?
这应该主要是考虑到性能问题吧,C++设计之初为了达到与C差不多的效率,就尽量不会在运行时多做些额外的检查,因为这样无疑会降低性能的,但有些地方却是必须得做运行时类型检查,比如多态,不在运行时做类型检查就无法确定类型。

举个简单例子,假如有父类Father,继承自Father的子类Son,这两个类中都有虚函数Fun

Father fa;
Son so;
fa
= so;
fa.Fun(); //在编译时,实际上是把Fun当作Father类中的Fun看待

但在运行时实际上这里的Fun是调用的Son中的函数Fun,所以不做运行时类型检查是无法确定的

2.4 编译时多态和运行时多态

关于编译时和运行时,多态还有一个问题很重要—编译时多态和运行时多态

多态性是面向对象程序设计的重要特征之一。所谓多态性是指当不同的对象收到相同的消息时,产生不同的动作。C++的多态性具体体现在运行和编译两个方面,在程序运行时的多态性通过继承虚函数来体现,而在程序编译时多态性体现在函数和运算符的重载上。

C++支持两种多态性:

编译时多态:程序运行前发生的事件 —— 函数重载、运算符重载 —— 静态绑定
运行时多态:程序运行时发生的事件 —— 虚函数机制 —— 动态绑定

三、多态性小览

上面说了这么多,才开始重头戏。

3.1 多态定义

定义:多态性可以简单地概括为“一个接口,多种方法”,程序在运行时才决定调用的函数,它是面向对象编程领域的核心概念。多态(polymorphism),字面意思多种形状。

C++多态性是通过虚函数来实现的,虚函数允许子类重新定义成员函数,而子类重新定义父类的做法称为覆盖(override),或者称为重写。

这里我觉得要补充,重写的话可以有两种,直接重写成员函数重写虚函数,只有重写了虚函数的才能算作是体现了C++多态性。

而重载则是允许有多个同名的函数,而这些函数的参数列表不同,允许参数个数不同,参数类型不同,或者两者都不同。编译器会根据这些函数的不同列表,将同名的函数的名称做修饰,从而生成一些不同名称的预处理函数,来实现同名函数调用时的重载问题。

3.2 重载和重写

上面的定义,有了一个定性的认识,C++多态性是通过重写了虚函数实现的,有必要看看重载和重写。
其实这两个概念对于我们来说肯定并不陌生,但是有很多细节的地方容易被忽略。

方法重载:

  1. 对于面向对象而言,必须在同一个类里面
  2. 方法名相同
  3. 参数类型不同 如:public void test ( int i , int j ){} 和 public void test ( int i , float j ){}
  4. 参数数目不同 如:public void test ( int i ){} 和 public void test ( int i , int j ){}
  5. 和方法的返回值无关 如:public int test( int i ){} 和 public void test( int i , int j) {} 也属于方法重载

方法重写是指重写父类的方法

1.类必须继承了父类才可以重写父类的方法;
2.必须和父类的方法的返回值,参数列表和方法名一样才算重写!(这点很重要)

3.3 早绑定与晚绑定

  多态与非多态的实质区别就是函数地址是早绑定还是晚绑定。如果函数的调用,在编译器编译期间就可以确定函数的调用地址,并生产代码,是静态的,地址就是早绑定的。而如果函数调用的地址不能在编译器期间确定,需要在运行时才确定,这就是晚绑定。

  那么多态的作用是什么呢,封装可以使得代码模块化,继承可以扩展已存在的代码,他们的目的都是为了代码重用。而多态的目的则是为了接口重用。也就是说,不论传递过来的究竟是那个类的对象,函数都能够通过同一个接口调用到适应各自对象的实现方法。

  最常见的用法就是声明基类的指针,利用该指针指向任意一个子类对象,调用相应的虚函数,可以根据指向的子类的不同而实现不同的方法。如果没有使用虚函数的话,即没有利用C++多态性,则利用基类指针调用相应的函数的时候,将总被限制在基类函数本身,而无法调用到子类中被重写过的函数。因为没有多态性,函数调用的地址将是一定的,而固定的地址将始终调用到同一个函数,这就无法实现一个接口,多种方法的目的了。

3.4 代码分析

3.4.1 例1

#include
using namespace std;

class A
{
public:
void foo()
{
printf("A类中:1\n");
}
virtual void fun()
{
printf("A类中:2\n");
}
};

class B : public A
{
public:
void foo()
{
printf("B类中:3\n");
}
void fun()
{
printf("B类中:4\n");
}
};

int main(void)
{
A a;
B b;

A *p = &a; //基类指针
p->foo();
p->fun();
cout<
p = &b;
p->foo();
p->fun();
cout<
B *ptr = (B *)&a;
ptr->foo();
ptr->fun();

return 0;
}

运行出来的结果如图:
这里写图片描述

分析:

  第一个p->foo()和p->fun()都容易理解。
 

  第二个输出结果是1、4。p->foo()和p->fun()是基类指针指向子类对象,完全体现了多态的用法,p->foo()由于指针是个基类指针,指向是一个固定偏移量的函数,因此此时指向的就只能是基类的foo()函数的代码了,因此输出的结果还是1。

  而p->fun()指针是基类指针,指向的fun是一个虚函数,由于每个虚函数都有一个虚函数列表,此时p调用fun()并不是直接调用函数,而是通过虚函数列表找到相应的函数的地址,因此根据指向的对象不同,函数地址也将不同,这里将找到对应的子类的fun()函数的地址,因此输出的结果也会是子类的结果4。

还有一个很变态的东东

B *ptr = (B *)&a;
ptr->foo();
ptr->fun();

  一个用子类的指针去指向一个强制转换为子类地址的基类对象。结果,这两句调用的输出结果是3,2。
  

并不是很理解这种用法,从原理上来解释,由于B是子类指针,虽然被赋予了基类对象地址,但是ptr->foo()在调用的时候,由于地址偏移量固定,偏移量是子类对象的偏移量,于是即使在指向了一个基类对象的情况下,还是调用到了子类的函数,虽然可能从始到终都没有子类对象的实例化出现。

所以这种情况只看指针类型

  而ptr->fun()的调用,可能还是因为C++多态性的原因,由于指向的是一个基类对象,通过虚函数列表的引用,找到了基类中fun()函数的地址,因此调用了基类的函数。

所以这种情况只看绑定对象类型

由此可见多态性的强大,可以适应各种变化,不论指针是基类的还是子类的,都能找到正确的实现方法。

3.4.2 例2

//小结:1、有virtual才可能发生多态现象
// 2、不发生多态(无virtual)调用就按原类型调用
#include
using namespace std;

class Base
{
public:
virtual void f1(float x)
{
cout<<"Base::f1(float)"< }
virtual void f2(float x)
{
cout<<"Base::f2(float)"< }
void f3(float x)
{
cout<<"Base::f3(float)"< }
};

class Derived : public Base
{
public:
virtual void f1(float x)
{
cout<<"Derived::f1(float)"<//多态、重写
}
void f2(int x)
{
cout<<"Derived::f2(int)"<//隐藏
}
void f3(float x)
{
cout<<"Derived::f3(float)"<//隐藏
}
};

int main(void)
{
Derived d;
Base *pbase = &d;
Derived *pderi = &d;

// Good : behavior depends solely on type of the object
pbase->f1(3.14f); // Derived::f1(float) 3.14
pderi->f1(3.14f); // Derived::f1(float) 3.14
cout<
// Bad : behavior depends on type of the pointer
pbase->f2(3.14f); // Base::f2(float) 3.14
pderi->f2(3.14f); // Derived::f2(int) 3
cout<
// Bad : behavior depends on type of the pointer
pbase->f3(3.14f); // Base::f3(float) 3.14
pderi->f3(3.14f); // Derived::f3(float) 3.14

return 0;
}

运行结果如图所示:
这里写图片描述

分析:
首先纠正一个错误,很多人认为重写和覆盖不同,其实就是一个东西。

先看第一组输出,这既是重写父类方法,又是实现多态,虽然子类中任然是一个virtual方法,它可以继续被它的子类所重写。

然后关于后面的两组,又一个令人迷惑的概念重磅登场了~~

3.5 令人迷惑的隐藏规则

本来仅仅区别重载与覆盖并不算困难,但是C++的隐藏规则使问题复杂性陡然增加。

这里“隐藏”是指派生类的函数屏蔽了与其同名的基类函数

规则如下:

(1)如果派生类的函数与基类的函数同名,但是参数不同。此时,不论有无 virtual关键字,基类的函数将被隐藏(注意别与重载混淆)。第二组就是这样的例子,如果把子类Derived中的void f2( int x ) 改成 void f2( float x ),输出结果就是两个 Derived,此时就是重写父类方法而实现多态了

(2)如果派生类的函数与基类的函数同名,并且参数也相同,但是基类函数没有 virtual关键字。此时,基类的函数被隐藏(注意别与覆盖混淆)。

上面的程序中:

(1)函数Derived::f1(float)覆盖了Base::f1(float)。
(2)函数Derived::f2(int)隐藏了Base::f2(float),而不是重载。
(3)函数Derived::f3(float)隐藏了Base::f3(float),而不是覆盖。

四、C++纯虚函数

4.1 定义

纯虚函数是在基类中声明的虚函数,它在基类中没有定义,但要求任何派生类都要定义自己的实现方法。在基类中实现纯虚函数的方法是在函数原型后加“=0”
如: virtual void funtion()=0

4.2 引入原因

1、为了方便使用多态特性,我们常常需要在基类中定义虚拟函数。

2、在很多情况下,基类本身生成对象是不合情理的。

例如,动物作为一个基类可以派生出老虎、孔雀等子类,但动物本身生成对象明显不合常理。为了解决上述问题,引入了纯虚函数的概念,将函数定义为纯虚函数(方法:virtual ReturnType Function()= 0;),则编译器要求在派生类中必须予以重写以实现多态性。同时含有纯虚拟函数的类称为抽象类,它不能生成对象。这样就很好地解决了上述两个问题。

4.3 相似概念

1、多态性
指相同对象收到不同消息或不同对象收到相同消息时产生不同的实现动作。C++支持两种多态性:编译时多态性,运行时多态性。
a、编译时多态性:通过重载函数实现
b、运行时多态性:通过虚函数实现。
这个在上面也有说明

2、虚函数
虚函数是在基类中被声明为virtual,并在派生类中重新定义的成员函数,可实现成员函数的动态覆盖(Override)

3、抽象类
包含纯虚函数的类称为抽象类。由于抽象类包含了没有定义的纯虚函数,所以不能定义抽象类的对象。

五、C++多态的应用

来个首尾呼应,应用是在工程里面,涉及设计模式的复杂的架构会用到多态。
看到设计模式中有:
这里写图片描述

这里写图片描述

不必指明用哪一个对象,直接用父类根据指向的子类对象抽象调用不同的方法,这正是c++多态的应用场景。

—END—


参考文献

[1] http://blog.csdn.net/hackbuteer1/article/details/7475622(谢谢作者让我茅塞顿开)


推荐阅读
  • 本文介绍了C++中省略号类型和参数个数不确定函数参数的使用方法,并提供了一个范例。通过宏定义的方式,可以方便地处理不定参数的情况。文章中给出了具体的代码实现,并对代码进行了解释和说明。这对于需要处理不定参数的情况的程序员来说,是一个很有用的参考资料。 ... [详细]
  • CF:3D City Model(小思维)问题解析和代码实现
    本文通过解析CF:3D City Model问题,介绍了问题的背景和要求,并给出了相应的代码实现。该问题涉及到在一个矩形的网格上建造城市的情景,每个网格单元可以作为建筑的基础,建筑由多个立方体叠加而成。文章详细讲解了问题的解决思路,并给出了相应的代码实现供读者参考。 ... [详细]
  • [大整数乘法] java代码实现
    本文介绍了使用java代码实现大整数乘法的过程,同时也涉及到大整数加法和大整数减法的计算方法。通过分治算法来提高计算效率,并对算法的时间复杂度进行了研究。详细代码实现请参考文章链接。 ... [详细]
  • 本文介绍了C#中数据集DataSet对象的使用及相关方法详解,包括DataSet对象的概述、与数据关系对象的互联、Rows集合和Columns集合的组成,以及DataSet对象常用的方法之一——Merge方法的使用。通过本文的阅读,读者可以了解到DataSet对象在C#中的重要性和使用方法。 ... [详细]
  • 本文介绍了RPC框架Thrift的安装环境变量配置与第一个实例,讲解了RPC的概念以及如何解决跨语言、c++客户端、web服务端、远程调用等需求。Thrift开发方便上手快,性能和稳定性也不错,适合初学者学习和使用。 ... [详细]
  • 本文探讨了C语言中指针的应用与价值,指针在C语言中具有灵活性和可变性,通过指针可以操作系统内存和控制外部I/O端口。文章介绍了指针变量和指针的指向变量的含义和用法,以及判断变量数据类型和指向变量或成员变量的类型的方法。还讨论了指针访问数组元素和下标法数组元素的等价关系,以及指针作为函数参数可以改变主调函数变量的值的特点。此外,文章还提到了指针在动态存储分配、链表创建和相关操作中的应用,以及类成员指针与外部变量的区分方法。通过本文的阐述,读者可以更好地理解和应用C语言中的指针。 ... [详细]
  • 成功安装Sabayon Linux在thinkpad X60上的经验分享
    本文分享了作者在国庆期间在thinkpad X60上成功安装Sabayon Linux的经验。通过修改CHOST和执行emerge命令,作者顺利完成了安装过程。Sabayon Linux是一个基于Gentoo Linux的发行版,可以将电脑快速转变为一个功能强大的系统。除了作为一个live DVD使用外,Sabayon Linux还可以被安装在硬盘上,方便用户使用。 ... [详细]
  • 闭包一直是Java社区中争论不断的话题,很多语言都支持闭包这个语言特性,闭包定义了一个依赖于外部环境的自由变量的函数,这个函数能够访问外部环境的变量。本文以JavaScript的一个闭包为例,介绍了闭包的定义和特性。 ... [详细]
  • 本文介绍了在mac环境下使用nginx配置nodejs代理服务器的步骤,包括安装nginx、创建目录和文件、配置代理的域名和日志记录等。 ... [详细]
  • Android源码深入理解JNI技术的概述和应用
    本文介绍了Android源码中的JNI技术,包括概述和应用。JNI是Java Native Interface的缩写,是一种技术,可以实现Java程序调用Native语言写的函数,以及Native程序调用Java层的函数。在Android平台上,JNI充当了连接Java世界和Native世界的桥梁。本文通过分析Android源码中的相关文件和位置,深入探讨了JNI技术在Android开发中的重要性和应用场景。 ... [详细]
  • 在CentOS/RHEL 7/6,Fedora 27/26/25上安装JAVA 9的步骤和方法
    本文介绍了在CentOS/RHEL 7/6,Fedora 27/26/25上安装JAVA 9的详细步骤和方法。首先需要下载最新的Java SE Development Kit 9发行版,然后按照给出的Shell命令行方式进行安装。详细的步骤和方法请参考正文内容。 ... [详细]
  • 本文介绍了如何使用C#制作Java+Mysql+Tomcat环境安装程序,实现一键式安装。通过将JDK、Mysql、Tomcat三者制作成一个安装包,解决了客户在安装软件时的复杂配置和繁琐问题,便于管理软件版本和系统集成。具体步骤包括配置JDK环境变量和安装Mysql服务,其中使用了MySQL Server 5.5社区版和my.ini文件。安装方法为通过命令行将目录转到mysql的bin目录下,执行mysqld --install MySQL5命令。 ... [详细]
  • MongoDB用户验证auth的权限设置及角色说明
    本文介绍了MongoDB用户验证auth的权限设置,包括readAnyDatabase、readWriteAnyDatabase、userAdminAnyDatabase、dbAdminAnyDatabase、cluster相关的权限以及root权限等角色的说明和使用方法。 ... [详细]
  • 本文讨论了如何在codeigniter中识别来自angularjs的请求,并提供了两种方法的代码示例。作者尝试了$this->input->is_ajax_request()和自定义函数is_ajax(),但都没有成功。最后,作者展示了一个ajax请求的示例代码。 ... [详细]
  • 本文介绍了使用哈夫曼树实现文件压缩和解压的方法。首先对数据结构课程设计中的代码进行了分析,包括使用时间调用、常量定义和统计文件中各个字符时相关的结构体。然后讨论了哈夫曼树的实现原理和算法。最后介绍了文件压缩和解压的具体步骤,包括字符统计、构建哈夫曼树、生成编码表、编码和解码过程。通过实例演示了文件压缩和解压的效果。本文的内容对于理解哈夫曼树的实现原理和应用具有一定的参考价值。 ... [详细]
author-avatar
无休止的等待Happy_212
这个家伙很懒,什么也没留下!
PHP1.CN | 中国最专业的PHP中文社区 | DevBox开发工具箱 | json解析格式化 |PHP资讯 | PHP教程 | 数据库技术 | 服务器技术 | 前端开发技术 | PHP框架 | 开发工具 | 在线工具
Copyright © 1998 - 2020 PHP1.CN. All Rights Reserved | 京公网安备 11010802041100号 | 京ICP备19059560号-4 | PHP1.CN 第一PHP社区 版权所有