面向对象的多态性(C++)
以C++为例三大特效:封装、继承、多态,面向对象的编程语言都具有这些特性。
那么本节来谈谈多态性,尽量说的简单些容易理解!
多态什么意思?即运行时多态,以相同的方式处理不同类型的对象,产生不同的结果!
请说人话,官方定义看不懂。。。。。。
多态即多种形态
1.普通函数的处理
首先需要知道在C++中对于类的处理,也是相当于把类转化为C的结构体的处理。让我们先来了解C++对于成员函数的处理的了解后才能了解虚函数最终到多态。
先来看看这段代码,
#include <string>
#include <iostream>
using namespace std;
class Person
{
public:
int sex;
int age;
char name[32];
public:
void test()
{
cout << "Person::test()" << endl;
}
};
int main()
{
Person p;
p.test();
system("pause");
return 0;
}
运行中可以看到变量p的结构,但看不到类中的test()函数,其实编译后最终会把类里面的函数转化为普通的函数,因此类的实例对象的结构里并不包含成员函数。
编译程序后test()函数地址是确定的。
当我们调用p.test()函数时,我们来看看如何找到类的成员函数的。首先转到汇编代码查看:
1)首先将变量p地址保存到ecx 也就是我们经常看到书中所提到的this指针(this:指向对象自己的地址)
2)在test()函数内部中如果需要访问类实例对象的成员,那么只需要取得这个this指针就可以找到实例对象的成员变量了
对于一个实例对象可以包含成员函数和成员变量,例如javascript中是可以这样编写的,为了节省空间和代码重用性,c++处理函数最终会变为C中普通函数的处理,对于类中成员函数使用类成员变量的最终会转换为this去访问,编译后那么这个函数地址可以确定,调用时直接call 地址就可以了
2.虚函数的处理
//#include <cstdlib>
//#include <cstdio>
//#include <cstring>
#include <string>
#include <iostream>
using namespace std;
class Base {
public:
int age;
int sex;
virtual void fun(){
cout << "Person::fun()" << endl;
}
virtual void XXX() {
cout << "Person::XXX()" << endl;
}
public:
virtual void getName() {
cout << "Person::getName()" << endl;
}
};
class SubA : public Base
{
public:
int X;
virtual void fun() {
cout << "SubA::fun()" << endl;
XXX();
}
virtual void getName() {
cout << "SubA::getName()" << endl;
}
};
class SubB : public Base
{
public:
int Y;
virtual void fun() {
cout << "SubB::fun()" << endl;
}
virtual void getName() {
cout << "SubB::getName()" << endl;
}
};
int main()
{
Base base;
SubA subA;
SubB subB;
Base* pbase = &subA;
pbase->fun();
pbase->XXX();
pbase = &subB;
pbase->fun();
pbase->XXX();
system("pause");
return 0;
}
1)C++程序编译后每个类中的函数地址都是确定的,如上面代码中运行到SubA subA;位置时,那么系统就会为它开辟空间【其实本质就是调用构造函数、赋值存储等操作,栈空间本身就已经开辟好了的,如果是堆申请就是动态分配内存了,只是我们把某一块内存区域当做变量,对于栈空间上分配就是栈指针上下移动就可以得到获得地址位置】,这里没有写构造函数,但编译器会为我们自动添加一个无参的构造函数,所以不分析,如下图
2)当我们定义并分配内存的一个类对象变量时,对于类中有虚函数时(因为代码完成编译后就可以确定哪些类中有哪些虚函数,即每个类的虚函数表函数指针数组是确定的),实例化对象时调用构造函数时将在this首位置加入这个确定的虚函数的函数指针的数组的指针变量
3.多态的实现
如上面代码中
pbase指向不同的实例对象时,相同的代码pbase->fun()执行不同的代码就是产生了多态,也是说执行时多态!
总结:
1)程序编译后虚函数表指针的函数指针数组是已经确定的;
2)在定义变量申请内存时调用构造函数,构造函数中进行虚函数表指针的赋值;
3)在实例对象调用虚函数时,是进行数组的偏移得到实际函数地址进行调用的,因为数组的偏移位置是可以确定的;
4)每个实例对象中保存了虚函数指针地址,地址指向虚函数的函数指针的数组;
5)当基类指针指向不同派生类对象时,就能根据虚函数表数组位置索引得到实际地址(因此程序编译后就可以确定偏移位置),该地址指向派生类的重写函数;
6)虚函数表位置是根据写代码的顺序来安排的,即可确定数组所有索引位置该存放什么虚函数的地址;
不同派生类对象内的虚函数表数组的指针指向的虚函数表数组位置都相同,简单来说重写的就覆盖基类的,其实每个类只要有virtual关键字,就有虚函数表数组(内部存储函数指针)
程序在运行时,当我们在代码中改变给父类指针指向任意派生类对象时(pParent = pSubObj),那么随着派生对象的改变调用虚函数(pParent->fun()),就会调用实际对象中的虚函数,这就是运行时多态的体现!
本质:
1) pbase指向的是派生对象的首地址,那么由于当建立新对象时,这个对象在内存中只存储(__vpfn+类中成员变量),首地址即是虚函数指针的地址,根据这个地址进行偏移[+0,+4,+8,+12 ......]来找到派生类的虚函数
简单C模仿虚函数
#define _CRT_SECURE_NO_WARNINGS
#include <stdlib.h>
#include <stdio.h>
#include <string.h>
#define CLASS struct
CLASS Parent
{
void** __vpfn;
int age;
int sex;
};
// 虚函数
void fun1Parent(int x, CLASS Parent* This)
{
printf("fun1Parent %d--%p\r\n", x, This);
}
// 普通函数
void fun2Parent(CLASS Parent* This)
{
}
// 虚函数
void fun3Parent(CLASS Parent* This)
{
printf("fun3Parent---%p\r\n", This);
}
void* Parent_vpfn[] = {
fun1Parent,
fun3Parent
};
int main()
{
//Parent p;
CLASS Parent p = {Parent_vpfn};
//p.fun1Parent(100);
((void (*)(int x, CLASS Parent* This))(*(p.__vpfn)))(100,&p);
//p.fun3Parent();
//((int*)(*(p.__vpfn))+1)
system("pause");
return 0;
}