虚函数与多态小览

一、文章来由

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<<result<<endl;

上面的代码错误很明显—数组越界了。但用编译器编译,是不会报错的。可见编译器其实还是挺笨的,然后开始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<iostream>
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<<endl;

    p = &b;
    p->foo();
    p->fun();
    cout<<endl;

    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<iostream>
using namespace std;

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

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

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

    // 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<<endl;

    // 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(谢谢作者让我茅塞顿开)

时间: 2024-09-18 21:35:25

虚函数与多态小览的相关文章

深入解析C++中的虚函数与多态_C 语言

1.C++中的虚函数C++中的虚函数的作用主要是实现了多态的机制.关于多态,简而言之就是用父类型别的指针指向其子类的实例,然后通过父类的指针调用实际子类的成员函数.这种技术可以让父类的指针有"多种形态",这是一种泛型技术.所谓泛型技术,说白了就是试图使用不变的代码来实现可变的算法.比如:模板技术,RTTI技术,虚函数技术,要么是试图做到在编译时决议,要么试图做到运行时决议. 对C++ 了解的人都应该知道虚函数(Virtual Function)是通过一张虚函数表(Virtual Tab

C++虚函数 纯虚函数 与多态的模糊解释

  //虚函数是多态的关键 virtual   我们直接定义对象 来调用函数并不能实现多态性 //多态的特性是 父类的指针可以指向子类的对象 我们可以令父类的指针指向 不同的子类对象MFC类库就是多态的原理 //比如下面我们如果同时让p1 p2分别指向新开辟的 A B类的对象 那么 调用print函数的时候就会出现都调用的基类的函数 //如何解决这个问题 呢? 那么就要用到虚函数了   我们解决的关键是将 基类的成员函数声明为虚函数 那么就可以实现多态 #include <iostream>

C++学习摘要之四:虚函数和多态

多态性与前面提到的数据封装和继承性共同构成了面向对象程序设计的三个重要机制. 1.静态联编与动态联编 由于函数重载的存在,当程序中出现调用同名函数时,编译器会根据函数的参数类型.个数决定调用 执行哪一个同名函数的代码,这种把一个函数的调用与适当的函数实现代码联系在一起的过程,叫做联编 .根据联编的实现阶段的不同,可将其分为静态联编和动态联编两种. 静态联编是在程序编译阶段确定一个函数调用与函数实现代码间的对应关系,这种对应关系确定下来 后,在程序运行过程中就根据这个对应关系去调用执行相应的函数代

C++抽象类小览

一.文章来由 virtual 方法和 virtual 类可以说是c++语言的一大特性,甚至有人说是c++语言的精髓,其实这么说也是有一定道理的,因为运行时多态在c++中体现淋漓尽致,而 virtual 就是为多态服务的.这也是一个一定要搞懂的c++问题,所以有了这篇文章.同时,我觉得这类底层问题不可能一文以蔽之,而且我也相信真正想搞懂这个问题的读者,不会只读我这一篇文章,所以只是小览,同时欢迎讨论和指正. 二.引入原因 其实,引入纯虚函数的原因我在我另一篇文章虚函数与多态小览就有写,不过重要的话

C++中类的多态与虚函数的使用

类的多态特性是支持面向对象的语言最主要的特性,有过非面向对象语言开发经历的人,通常对这一章节的内容会觉得不习惯,因为很多人错误的认为,支持类的封装的语言就是支持面向对象的,其实不然,Visual BASIC 6.0 是典型的非面向对象的开发语言,但是它的确是支持类,支持类并不能说明就是支持面向对象,能够解决多态问题的语言,才是真正支持面向对象的开发的语言,所以务必提醒有过其它非面向对象语言基础的读者注意! 多态的这个概念稍微有点模糊,如果想在一开始就想用清晰用语言描述它,让读者能够明白,似乎不太

C++中的多态与虚函数的内部实现方法_C 语言

1.什么是多态 多态性可以简单概括为"一个接口,多种行为". 也就是说,向不同的对象发送同一个消息, 不同的对象在接收时会产生不同的行为(即方法).也就是说,每个对象可以用自己的方式去响应共同的消息.所谓消息,就是调用函数,不同的行为就是指不同的实现,即执行不同的函数.这是一种泛型技术,即用相同的代码实现不同的动作.这体现了面向对象编程的优越性. 多态分为两种: (1)编译时多态:主要通过函数的重载和模板来实现. (2)运行时多态:主要通过虚函数来实现. 2.几个相关概念 (1)覆盖.

实例讲解C++编程中的虚函数与虚基类_C 语言

虚函数① #include "stdafx.h" #include <iostream> using namespace std; class B0//基类B0声明 { public: void display(){cout<<"B0::display()"<<endl;}//公有成员函数 }; class B1: public B0//公有派生类B1声明 { public: void display(){cout<<

C++中的虚函数

虽然很难找到一本不讨论多态性的C++书籍或杂志,但是,大多数这类讨论使多态性和C++虚函数的使用看起来很难.我打算在这篇文章中通过从几个方面和结合一些例子使读者理解在C++中的虚函数实现技术.说明一点,写这篇文章只是想和大家交流学习经验因为本人学识浅薄,难免有一些错误和不足,希望大家批评和指正,在此深表感谢! 一.基本概念 首先,C++通过虚函数实现多态."无论发送消息的对象属于什么类,它们均发送具有同一形式的消息,对消息的处理方式可能随接手消息的对象而变"的处理方式被称为多态性.&q

.NET中的虚函数

面向对象的程序设计有三大要素,封装.继承和多态.虚函数是多态的重要组成部分,同时又在类的继承关系中有着很多变化.本文讨论.NET中对虚函数的支持. 首先,我们通过一个例子来看看虚函数的普通用法: class CA { public virtual void Foo() { Console.WriteLine("CA.Foo"); } } class CB : CA { public override void Foo() { Console.WriteLine("CB.Foo