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

1、什么是多态

多态性可以简单概括为“一个接口,多种行为”。

也就是说,向不同的对象发送同一个消息, 不同的对象在接收时会产生不同的行为(即方法)。也就是说,每个对象可以用自己的方式去响应共同的消息。所谓消息,就是调用函数,不同的行为就是指不同的实现,即执行不同的函数。这是一种泛型技术,即用相同的代码实现不同的动作。这体现了面向对象编程的优越性。

多态分为两种:

(1)编译时多态:主要通过函数的重载和模板来实现。

(2)运行时多态:主要通过虚函数来实现。

2、几个相关概念

(1)覆盖、重写(override)

override指基类的某个成员函数为虚函数,派生类又定义一成员函数,除函数体的其余部分都与基类的成员函数相同。注意,如果只是函数名相同,形参或返回类型不同的话,就不能称为override,而是hide。

(2)重载(overload)

指同一个作用域出生多个函数名相同,但是形参不同的函数。编译器在编译的时候,通过实参的个数和类型,选择最终调用的函数。

(3)隐藏(hide)

分为两种:

1)局部变量或者函数隐藏了全局变量或者函数
2)派生类拥有和基类同名的成员函数或成员变量。

产生的结果:使全局或基类的变量、函数不可见。

3、几个简单的例子

/******************************************************************************************************
* File:PolymorphismTest
* Introduction:测试多态的一些特性。
* Author:CoderCong
* Date:20141114
* LastModifiedDate:20160113
*******************************************************************************************************/
#include "stdafx.h"
#include <iostream>
using namespace std;
class A
{
public:
  void foo()
  {
    printf("1\n");
  }
  virtual void fun()
  {
    printf("2\n");
  }
};
class B : public A
{
public:
  void foo() //由于基类的foo函数并不是虚函数,所以是隐藏,而不是重写
  {
    printf("3\n");
  }
  void fun() //重写
  {
    printf("4\n");
  }
};
int main(void)
{
  A a;
  B b;
  A *p = &a;
  p->foo(); //输出1。
  p->fun(); //输出2。
  p = &b;
  p->foo(); //输出1。因为p是基类指针,p->foo指向一个具有固定偏移量的函数。也就是基类函数
  p->fun(); //输出4。多态。虽然p是基类指针,但实际上指向的是一个子类对象。p->fun指向的是一个虚函数。按照动态类型,调用子类函数
  return 0;
}

4、运行时多态以及虚函数的内部实现

看了上边几个简单的例子,我恍然大悟,原来这就是多态,这么简单,明白啦!

好,那我们再看一个例子:

class A
{
public:
  virtual void FunA()
  {
    cout << "FunA1" << endl;
  };
  virtual void FunAA()
  {
    cout << "FunA2" << endl;
  }
};
class B
{
public:
  virtual void FunB()
  {
    cout << "FunB" << endl;
  }
};
class C :public A, public B
{
public:
  virtual void FunA()
  {
    cout << "FunA1C" << endl;
  };
};
int _tmain(int argc, _TCHAR* argv[])
{
  C objC;
  A *pA = &objC;
  B *pB = &objC;
  C *pC = &objC; 

  printf("%d %d\n", &objC, objC);
  printf("%d %d\n", pA, *pA);
  printf("%d %d\n", pB, *pB);
  printf("%d %d\n", pC, *pC); 

  return 0;
}

运行结果:

5241376 1563032

5241376 1563032

5241380 1563256

5241376 1563032

细心的同志一定发现了pB出了问题,为什么明明都是指向objC的指针,pB跟别人的值都不一样呢?

是不是编译器出了问题呢?

当然不是!我们先讲结论:

(1)每一个含有虚函数的类,都会生成虚表(virtual table)。这个表,记录了对象的动态类型,决定了执行此对象的虚成员函数的时候,真正执行的那一个成员函数。

(2)对于有多个基类的类对象,会有多个虚表,每一个基类对应一个虚表,同时,虚表的顺序和继承时的顺序相同。

(3)在每一个类对象所占用的内存中,虚指针位于最前边,每个虚指针指向对应的虚表。

先从简单的单个基类说起:

class A
{
public:
  virtual void FunA()
  {
    cout << "FunA1" << endl;
  }
  virtual void FunA2()
  {
    cout << "FunA2" << endl;
  }
}; 

class C :public A
{
  virtual void FunA()
  {
    cout << "FunA1C" << endl;
  }
};
int _tmain(int argc, _TCHAR* argv[])
{
  A *pA = new A;
  C *pC = new C;
  typedef void (*Fun)(void); 

  Fun fun= (Fun)*((int*)(*(int*)pA));
  fun();//pA指向的第一个函数
  fun = (Fun)*((int*)(*(int*)pA) +1);
  fun();//pA指向的第二个函数
  
  fun = (Fun)*((int*)(*(int*)pC));
  fun();//pC指向的第一个函数
  fun = (Fun)*((int*)(*(int*)pC) + 1);
  fun();//pC指向的第二个函数
  return 0;
}

运行结果:

FunA1
FunA2
FunA1C
FunA2

是不是有点晕?没关系。我一点一点解释:pA对应一个A的对象,我们可以画出这样的一个表:

      

这就是对象*pA的虚表,两个虚函数以声明顺序排列。pA指向对象*pA,则*(int*)pA指向此虚拟表,则(Fun)*((int*)(*(int*)pA))指向FunA,同理,(Fun)*((int*)(*(int*)pA) + 1)指向FunA2。所以,出现了前两个结果。

根据后两个结果, 我们可以推测*pC的虚表如下图所示:

      

也就是说,由于C中的FunA重写(override)了A中的FunA,虚拟表中虚拟函数的地址也被重写了。

就是这样,这就是多态实现的内部机制。

我们再回到最初的问题:为什么*pB出了问题。

根据上边的结论,我们大胆地进行猜测:由于C是由A、B派生而来,所以objC有两个虚拟表,而由于表的顺序,pA、pC都指向了对应于A的虚拟表,而pB则指向了对应于B的虚拟表。做个实验来验证我们的猜想是否正确:

我们不改变A、B、C类,将问题中的main改一下:

int _tmain(int argc, _TCHAR* argv[])
{
  C objC;
  A *pA = &objA;
  B *pB = &objC;
  C *pC = &objC;
  
  typedef void (*Fun)(void); 

  Fun fun = (Fun)*((int*)(*(int*)pC));
  fun();//第一个表第一个函数
  fun = (Fun)*((int*)(*(int*)pC)+1);
  fun();//第一个表第二个函数
  fun = (Fun)*((int*)(*((int*)pC+1)));
  fun();<span style="white-space:pre"> </span>//第二个表第一个函数
  fun = (Fun)*((int*)(*(int*)pB));
  fun();//pB指向的表的第一个函数
  return 0;
}

哈哈,和我们的猜测完全一致:

FunA1C
FunA2
FunB
FunB

我们可以画出这样的虚函数图:

        

暂且这样理解,编译器执行B *pB = &objC时不是仅仅是赋值,而是做了相应的优化,将pB指向了第二张虚表。

说了这么多,我是只是简单地解释了虚函数的实现原理,可究竟对象的内部的内存布局是怎样的?类数据成员与多个虚表的具体内存布局又是怎样的?编译器是如何在赋值的时候作了优化的呢?我在以后的时间里会讲一下。

以上就是小编为大家带来的C++中的多态与虚函数的内部实现方法全部内容了,希望大家多多支持我们~

时间: 2016-12-18

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

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

解析C++编程中virtual声明的虚函数以及单个继承

虚函数 虚函数是应在派生类中重新定义的成员函数. 当使用指针或对基类的引用来引用派生的类对象时,可以为该对象调用虚函数并执行该函数的派生类版本. 虚函数确保为该对象调用正确的函数,这与用于进行函数调用的表达式无关. 假定基类包含声明为 virtual 的函数,并且派生类定义了相同的函数. 为派生类的对象调用派生类中的函数,即使它是使用指针或对基类的引用来调用的. 以下示例显示了一个基类,它提供了 PrintBalance 函数和两个派生类的实现 // deriv_VirtualFunctions

C++中继承与多态的基础虚函数类详解

前言 本文主要给大家介绍了关于C++中继承与多态的基础虚函数类的相关内容,分享出来供大家参考学习,下面话不多说了,来一起看看详细的介绍吧. 虚函数类 继承中我们经常提到虚拟继承,现在我们来探究这种的虚函数,虚函数类的成员函数前面加virtual关键字,则这个成员函数称为虚函数,不要小看这个虚函数,他可以解决继承中许多棘手的问题,而对于多态那他更重要了,没有它就没有多态,所以这个知识点非常重要,以及后面介绍的虚函数表都极其重要,一定要认真的理解~ 现在开始概念虚函数就又引出一个概念,那就是重写(覆

C++ 中const修饰虚函数实例详解

C++ 中const修饰虚函数实例详解 [1]程序1 #include <iostream> using namespace std; class Base { public: virtual void print() const = 0; }; class Test : public Base { public: void print(); }; void Test::print() { cout << "Test::print()" << end

C++ 虚函数的详解及简单实例

C++ 虚函数的详解 虚函数的使用和纯虚函数的使用. 虚函数是在基类定义,然后子类重写这个函数后,基类的指针指向子类的对象,可以调用这个函数,这个函数同时保留这子类重写的功能. 纯虚函数是可以不用在基类定义,只需要声明就可以了,然后因为是纯虚函数,是不能产生基类的对象,但是可以产生基类的指针. 纯虚函数和虚函数最主要的区别在于,纯虚函数所在的基类是不能产生对象的,而虚函数的基类是可以产生对象的. // pointers to base class #include <iostream> usi

对python中的six.moves模块的下载函数urlretrieve详解

实验环境:windows 7,anaconda 3(python 3.5),tensorflow(gpu/cpu) 函数介绍:所用函数为six.moves下的urllib中的函数,调用如下urllib.request.urlretrieve(url,[filepath,[recall_func,[data]]]).简单介绍一下,url是必填的指的是下载地址,filepath指的是保存的本地地址,recall_func指的是回调函数,下载过程中会调用可以用来显示下载进度. 实验代码:以下载cifa

Python类的继承、多态及获取对象信息操作详解

本文实例讲述了Python类的继承.多态及获取对象信息操作.分享给大家供大家参考,具体如下: 继承 类的继承机制使得子类可以继承父类中定义的方法,拥有父类的财产,比如有一个Animal的类作为父类,它有一个eat方法: class Animal(object): def __init__(self): print("Animal 构造函数调用!") def eat(self): print("Animal is eatting!") 写两个子类,Cat和Dog类,继

一些php项目中比较通用的php自建函数的详解

以下一些php函数是我们it动力最常用的项目开发函数,这些函数还算是在比较多的项目中使用到的,也是比较通用的.1.请求接口的处理函数 复制代码 代码如下: /**  * curl访问程序接口  * @param string  * @return array  */  function getCurlDate($url, $datas, $key) {      $datas['time'] = $_SERVER['REQUEST_TIME'] + 300;      $post_data['p

WordPress中创建用户角色的相关PHP函数使用详解

WordPress 默认有 "订阅者"."投稿者"."作者"."编辑" 和 "管理员" 五个用户角色,权限由低到高,但默认的五个角色可能不够我们用,这时可以使用 add_role() 函数创建一个角色. 用法 add_role( $role, $display_name, $capabilities ); 参数 $role (字符串)(必须)用户角色 ID 默认值:None $display_name (字

C++中函数指针详解及代码分享

函数指针 函数存放在内存的代码区域内,它们同样有地址.如果我们有一个int test(int a)的函数,那么,它的地址就是函数的名字,如同数组的名字就是数组的起始地址. 1.函数指针的定义方式:data_types (*func_pointer)( data_types arg1, data_types arg2, ...,data_types argn); c语言函数指针的定义形式:返回类型 (*函数指针名称)(参数类型,参数类型,参数类型,-); c++函数指针的定义形式:返回类型 (类名

JS中call/apply、arguments、undefined/null方法详解

a.call和apply方法详解 -------------------------------------------------------------------------------- call方法: 语法:call([thisObj[,arg1[, arg2[, [,.argN]]]]]) 定义:调用一个对象的一个方法,以另一个对象替换当前对象. 说明: call 方法可以用来代替另一个对象调用一个方法.call 方法可将一个函数的对象上下文从初始的上下文改变为由 thisObj 指

java中synchronized(同步代码块和同步方法)详解及区别

 java中synchronized(同步代码块和同步方法)详解及区别 问题的由来: 看到这样一个面试题: //下列两个方法有什么区别 public synchronized void method1(){} public void method2(){ synchronized (obj){} } synchronized用于解决同步问题,当有多条线程同时访问共享数据时,如果进行同步,就会发生错误,Java提供的解决方案是:只要将操作共享数据的语句在某一时段让一个线程执行完,在执行过程中,其他