frankfan的胡思乱想

学海无涯,回头是岸

多态

多态
这是面向对象编程世界中的三大特性之一,各支持面向对象编程语言都对其有语法上的支持。

分别从多态的产生以及应用场景、多态效果的模拟、以及C++对多态机制的实现原理等进行阐述

多态的产生极其应用场景

在面向对象世界中,类,是基础单位,这本质上是在模拟现实世界中的构建规则。

在面向对象世界中有一个继承的概念,这本质上是对封装的进一步应用,也是代码复用的重要手段,但是从逻辑上讲却存在无法避免的缺陷。

比如正方形继承自矩形,但是矩形可以分别控制临边的不同长度,而正方形只能只能控制所有边的长度一致,因此,当实现『修改』变长方法时,子类与父类产生了不同。

可以认为这种机制既是面向对象的Bug,也是面向对象的feature。

从编程语言的角度上讲,等号(=)两侧的值与类型必须是一致的,当存在基类与派生类是,可以认为派生类(is-a)基类,此时,等号左侧的基类指针便能指向派生类(类型是一致的),那么,当调用父类的接口方法时,到底应该执行的是子类的方法还是父类的方法呢? 从面相对象角度而言应该是父类的方法,而从模拟现实世界的角度而言应该是子类的方法。

而C++中的多态,便是为了处理这种问题而诞生。

当父类指针指向子类,调用父类接口方法时,实质执行的是子类方法,这种特性被称作多态

这种方式的好处在于,可以将父类认为是一个接口,这个接口可以表示各种不同的具体类,通过直接调用父类的方法,就能实现不同类的具体行为。

比如:基类是士兵 从士兵派生出步兵 骑兵 弓箭手 等具体类,士兵有接口方法攻击

可以使用士兵接口指向不同的兵种,直接调用士兵的攻击方法,则实质就会调用具体不同兵种的攻击方法,这样,就带来了极佳的扩展性,能够更加灵活的构建面向对象的世界。

#include <iostream>
using namespace std;

class Soldier{
public:
  
  //通过virtual关键字修饰函数,此函数就是虚函数,C++多态实现的支持
  virtual void attack(){
    cout<<"soldier attack"<<endl;
  }
};

class Infantry:public Soldier{
public:
  void attack(){
    cout<<"Infantry attack"<<endl;
  }
};

class Cavalry:public Soldier{
public:
  void attack(){
    cout<<"Cavalry attack"<<endl;
  }
};

int main(){
  
  Soldier *sol = new Infantry;
  Soldier *sol2 = new Cavalry;
  
  //同一个类名,调用同样的方法,实质是不同对象在执行各种真正的方法
  sol->attack();//Infantry attack
  sol2->attack();//Cavalry attack
  
  return 0;
}

C++通过关键字virtual 来实现多态的特性。而我们也可以通过函数指针的方式来模拟多态效果

多态效果的模拟

函数指针,定义全局函数指针类型非常简单

#include<stdio.h>

void demoFunc(int a){
  int b = a++;
  printf("%d\r\n",b);
}

typedef void(*pfunc)(int);

int main(){

  pfunc demoFuncPtr = demoFunc;
  demoFuncPtr(11);
  return 0;
}

那么在C++中,如何定义以及使用类成员函数的函数指针呢?

#include <iostream>
using namespace std;

class Node{
public:
  int value;
  void showValue(){
    cout<<value<<endl;
  }
};

//定义一个Node类成员函数的函数指针类型
typedef void(Node::*pvalueType)(void);

int main(){
	
  Node node;
  node.value = 12;
  node.showValue();//12
  
  //定义成员函数指针变量pvfunc,并指向成员函数showValue
  pvalueType pvfunc = &Node::showValue;
  
  //使用非常别扭的方式调用成员函数指针
  (node.*pvfunc)();//12
  
   Node *node2 = new Node;
   node2->value = 22;
   (node2->*fptr)();//22
  
  return 0;
}

通过成员函数指针调用成员函数的方式非常奇怪难看,C++中基本很难见到这种实践方式

通过类成员函数指针的方式模拟实现多态

#include <iostream>
using namespace std;
class Node;
class TTNode;

typedef void(Node::*pvalueType)(void);

class Node{
public:
  
  pvalueType showvaluePtr = &Node::showValue;
  int value;
  void showValue(){
    cout<<"node value  "<<value<<endl;
  }
};

class TTNode:public Node{
public:
  
  TTNode(){
    showvaluePtr = (pvalueType)&TTNode::showValue;
  }
  void showValue(){
  	cout<<"ttnode value  "<<value<<endl;
  }
 
};

int main(){
  
	Node *node = new TTNode;
  node->value = 12;
  //通过及其别扭的方式模拟多态
  (node->*(node->showvaluePtr))();//ttnode value 12
  return 0;
}

C++多态的实现机制

C++多态的实现是通过『建表』完成的。通过虚函数表(函数地址数组)间接调用函数实现多态。

#include <iostream>
using namespace std;

class Node{
public:
  Node(int v = 0):value(v){ }
  virtual void show(){
    cout<<"node show"<<endl;
  }
  
  virtual void mixer(){
    cout<<"node mixer"<<endl;
  }
  
private:
  int value;
};

class TTNode(){
public:
  
  void show(){
    cout<<"ttnode show"<<endl;
  }
  
  void mixer(){
    cout<<"ttnode mixer"<<endl;
  }
};

int main(){
  return 0;
}

在编译时,C++便会创建一个虚函数地址的数组,根据虚函数的定义顺序依次将虚函数地址放入这个数组中,这个数组又被称为虚函数表,每个类对应一个虚函数表。

在运行时,C++会将对应的虚函数表的地址安置在具体对象的首地址内存处(第一个成员),实质就是一个数组指针类型,32bit系统下占4字节,64bit系统下占8字节。

所以也就是说,每个类都有一张属于该类的虚函数表,每个对象都有一个虚函数表的指针,执行属于该类的虚函数表地址。

当产生虚函数的多态调用时:

1、通过虚函数表地址找到虚函数表

2、通过虚函数名字对应的顺序,取出数组中对应下标的虚函数地址

3、跳转到该地址

4、调用真正的函数地址

在此过程中,虚函数表的建立处在编译期,而真正将虚函数表的内存地址放入对象中,处在运行期。

在32bit系统中,TTNode类的内存占8字节,其中,从父类Node中继承过来的成员value占4个字节,虚函数表地址占4个字节。

image.png

posted on 2021-12-28 00:23  shadow_fan  阅读(33)  评论(0编辑  收藏  举报

导航