多态
1. 多态的概念和意义
回忆一下上一节继承(二)函数重写示例代码中的how_to_print(),当时我们所期望的结果是
- 根据实际的对象类型判断如何调用重写函数
- 父类指针(引用)指向
- 父类对象则调用父类中定义的函数
- 子类对象则调用子类中定义的重写函数
要实现上述期望结果,需要用到多态的知识。
多态是面向对象理论中的概念,其含义为
- 根据实际的对象类型决定函数调用的具体目标
- 同样的调用语句在实际运行时有多种不同的表现形态
C++语言直接支持多态的概念
- 通过使用virtual关键字对多态进行支持
- 被virtual声明的函数叫做虚函数
- 虚函数被重写后可表现出多态的特性
静态联编 VS 动态联编
- 静态联编:在程序编译期间就能确定具体的函数调用,如函数重载
- 动态联编:在程序运行期间才能确定具体的函数调用,如重写虚函数
#include <iostream>
#include <string>
using namespace std;
class Parent
{
public:
virtual void print()
{
cout << "I'm Parent." << endl;
}
};
class Child : public Parent
{
public:
/* 重写父类虚函数,改变打印语句行为 */
void print()
{
cout << "I'm Child." << endl;
}
};
void how_to_print(Parent *p)
{
p->print(); // 动态联编,展现多态的行为
}
int main()
{
Parent p;
Child c;
p.print(); // 静态联编
c.print(); // 静态联编
how_to_print(&p); // 动态联编,Expected: I'm Parent
how_to_print(&c); // 动态联编,Expected: I'm Child
return 0;
}
多态的意义
- 在程序运行过程中展现出动态的特性
- 函数重写只可能发生在父类与子类之间,且必须多态实现,否则没有意义
- 多态是面向对象组件化程序设计的基础特性
示例代码:江湖恩怨
#include <iostream>
#include <string>
using namespace std;
class Boss
{
public:
int fight()
{
int ret = 10;
cout << "Boss::fight() : " << ret << endl;
return ret;
}
};
class Master
{
public:
virtual int eightSwordKill()
{
int ret = 8;
cout << "Master::eightSwordKill() : " << ret << endl;
return ret;
}
};
class NewMaster : public Master
{
public:
int eightSwordKill()
{
int ret = Master::eightSwordKill() * 2;
cout << "NewMaster::eightSwordKill() : " << ret << endl;
return ret;
}
};
void field_pk(Master *master, Boss *boss)
{
int k = master->eightSwordKill();
int b = boss->fight();
if( k < b )
{
cout << "Master is killed..." << endl;
}
else
{
cout << "Boss is killed..." << endl;
}
cout << endl;
}
int main()
{
Boss boss;
Master master;
NewMaster newMaster;
cout << "Master vs Boss" << endl;
field_pk(&master, &boss);
cout << "NewMaster vs Boss" << endl;
field_pk(&newMaster, &boss);
return 0;
}
2. C++多态的实现原理
- 当类中声明虚函数时,编译器会在类中生成一个虚函数表
- 虚函数表是一个存储成员函数地址的数据结构,该数据结构由编译器自动生成与维护
- 虚函数会被编译器放入虚函数表中
- 存在虚函数时,每个类对象中都有一个指向虚函数表的指针
- 虚函数表指针位于类对象的起始位置,再往后才是成员变量
3. 构造析构与虚函数
- 构造函数不可能成为虚函数,构造函数中不可能发生多态行为
- 在构造函数执行结束后,虚函数表指针才会被正确的初始化
- 析构函数可以成为虚函数,析构函数中不可能发生多态行为
- 在析构函数执行时,虚函数表指针已经被销毁
- 建议在设计类时将析构函数声明为虚函数
/*
* 1、构造函数不可能成为虚函数,析构函数可以并且建议设计为虚函数
* 2、构造函数和析构函数中调用虚函数不能发生多态行为,只会调用当前类中定义的函数版本
*/
#include <iostream>
#include <string>
using namespace std;
class Base
{
public:
Base()
{
cout << "Base()" << endl;
func();
}
virtual void func()
{
cout << "Base::func()" << endl;
}
virtual ~Base()
{
func();
cout << "~Base()" << endl;
}
};
class Derived : public Base
{
public:
Derived()
{
cout << "Derived()" << endl;
func();
}
virtual void func()
{
cout << "Derived::func()" << endl;
}
~Derived()
{
func();
cout << "~Derived()" << endl;
}
};
int main()
{
Base *p = new Derived();
cout << endl;
delete p;
return 0;
}
4. C++对象模型分析
继承对象模型
在C++编译器的内部,类可以理解为结构体
- 在内存中class依旧可以看作变量的集合
- class与struct遵循相同的内存对齐规则
- class中的成员函数与成员变量的分开存放的
程序运行时,类对象退化为结构体的形式
- 所有成员变量在内存中依次排布
- 成员变量间可能存在内存空隙(结构体字节对齐)
- 可以通过内存地址直接访问成员变量
- 访问权限关键字在运行时失效
子类是由父类成员(包括父类虚函数表指针)叠加子类新成员得到的
#include <iostream>
#include <string>
using namespace std;
class Demo
{
protected:
int mi;
int mj;
public:
virtual void print()
{
cout << "mi = " << mi << ", "
<< "mj = " << mj << endl;
}
};
class Derived : public Demo
{
private:
int mk;
public:
Derived(int i, int j, int k)
{
mi = i;
mj = j;
mk = k;
}
void print()
{
cout << "mi = " << mi << ", "
<< "mj = " << mj << ", "
<< "mk = " << mk << endl;
}
};
struct Test
{
void *pVTable;
int mi;
int mj;
int mk;
};
int main()
{
cout << "sizeof(Demo) = " << sizeof(Demo) << endl; //sizeof(Demo) = 12,1个虚函数表指针 + int mi + int mj
cout << "sizeof(Derived) = " << sizeof(Derived) << endl; //sizeof(Derived) = 16,1个虚函数表指针 + int mi + int mj + int mk
Derived d(1, 2, 3);
Test *p = reinterpret_cast<Test *>(&d);
cout << endl;
cout << "Before changing ..." << endl;
d.print();
/*
* 通过p指针改变了d对象中成员变量的值,说明Derived的内存模型与struct Test相同:
* 1.起始位置为虚函数表指针
* 2.再往后为父类成员叠加子类新成员
*/
p->mi = 4;
p->mj = 5;
p->mk = 6;
cout << endl;
cout << "After changing ..." << endl;
d.print();
return 0;
}
多态对象模型
父类子类虚函数表
class Demo
{
protected:
int mi;
int mj;
public:
virtual int add(int value)
{
return mi + mj + value;
}
};
class Derived : public Demo
{
private:
int mk;
public:
virtual int add(int value)
{
return mk + value;
}
};
程序运行时展现多态行为
void run(Demo *p, int v)
{
p->add(v);
}
- 编译器确认add()是否为虚函数
- Yes:编译器在具体对象VPTR所指向的虚函数表中查找add()的地址
- No:编译器直接可以确定add()成员函数的地址
5. 用C实现面向对象,展现继承与多态本质
代码实现
c_oop.h
#ifndef _C_OOP_H_
#define _C_OOP_H_
typedef void Base;
typedef void Derived;
Base *Base_Create(int i, int j);
int Base_GetI(Base *pThis);
int Base_GetJ(Base *pThis);
int Base_Add(Base *pThis, int value);
void Base_Free(Base *pThis);
Derived *Derived_Create(int i, int j, int k);
int Derived_GetK(Derived *pThis);
int Derived_Add(Derived *pThis, int value);
void Derived_Free(Derived *pThis);
#endif
c_oop.c
#include "c_oop.h"
#include <stdlib.h>
struct VTabel
{
int (*pAdd)(void *, int);
};
struct ClassBase
{
struct VTabel *vptr;
int mi;
int mj;
};
struct ClassDerived
{
struct ClassBase base;
int mk;
};
static int Base_Virtual_Add(Base *pThis, int value);
static Derived_Virtual_Add(Derived *pThis, int value);
static struct VTabel Base_vtab =
{
Base_Virtual_Add
};
static struct VTabel Derived_vtab =
{
Derived_Virtual_Add
};
Base *Base_Create(int i, int j)
{
struct ClassBase *ret = (struct ClassBase *)malloc(sizeof(struct ClassBase));
if (ret != NULL)
{
ret->vptr = &Base_vtab;
ret->mi = i;
ret->mj = j;
}
return ret;
}
int Base_GetI(Base *pThis)
{
struct ClassBase *pBase = (struct ClassBase *)pThis;
return pBase->mi;
}
int Base_GetJ(Base *pThis)
{
struct ClassBase *pBase = (struct ClassBase *)pThis;
return pBase->mj;
}
static int Base_Virtual_Add(Base *pThis, int value)
{
struct ClassBase *pBase = (struct ClassBase *)pThis;
return pBase->mi + pBase->mj + value;
}
int Base_Add(Base *pThis, int value)
{
struct ClassBase *pBase = (struct ClassBase *)pThis;
return pBase->vptr->pAdd(pThis, value);
}
void Base_Free(Base *pThis)
{
if (pThis != NULL)
{
free(pThis);
}
}
Derived *Derived_Create(int i, int j, int k)
{
struct ClassDerived *ret = (struct ClassDerived *)malloc(sizeof(struct ClassDerived));
if (ret != NULL)
{
ret->base.vptr = &Derived_vtab; //关联子类对象和子类虚函数表
ret->base.mi = i;
ret->base.mj = j;
ret->mk = k;
}
return ret;
}
int Derived_GetK(Derived *pThis)
{
struct ClassDerived *pDerived = (struct ClassDerived *)pThis;
return pDerived->mk;
}
static Derived_Virtual_Add(Derived *pThis, int value)
{
struct ClassDerived *pDerived = (struct ClassDerived *)pThis;
return pDerived->mk + value;
}
int Derived_Add(Derived *pThis, int value)
{
struct ClassDerived *pDerived = (struct ClassDerived *)pThis;
return pDerived->base.vptr->pAdd(pThis, value);
}
main.c
#include "c_oop.h"
#include <stdio.h>
void run(Base *p, int v)
{
int r = Base_Add(p, v);
printf("r = %d\n", r);
}
int main()
{
Base *pb = Base_Create(1, 2);
Derived *pd = Derived_Create(3, 4, 5);
printf("pb->Add(3) = %d\n", Base_Add(pb, 3));
printf("pd->Add(3) = %d\n", Derived_Add(pd, 3));
run(pb, 3);
run(pd, 3);
Base_Free(pb);
Base_Free(pd);
return 0;
}
总体设计思路
面向对象程序最关键的地方在于必须能够表现三大特性:封装,继承,多态!
- 封装指的是类中的敏感数据在外界是不能访问的
- 继承指的是可以对已经存在的类进行代码复用,并使得类之间存在父子关系
- 多态指的是相同的调用语句可以产生不同的调用结果
因此,如果希望用C语言完成面向对象的程序,那么必须实现这三个特性;
否则,最多只算得上基于对象的程序(程序中能够看到对象的影子,但是不完全具备面向对象的三大特性)。
封装设计分析
通过void*
指针保证具体的结构体成员不能在外界被访问,以此模拟C++中private和protected。因此,在头文件中定义了如下的语句:
typedef void Base;
typedef void Derived;
用Base*
指针和Derived*
指针指向具体的对象时,无法访问对象中的成员变量,这样就达到了“外界无法访问类中私有成员” 的封装效果!
继承设计分析
继承的本质是父类成员与子类成员的叠加,所以在用C语言写面向对象程序的时候,可以直接考虑结构体成员的叠加。
代码中的实现直接将struct ClassBase base作为struct ClassDerived的第一个成员,以此表现两个自定义数据类型间的继承关系,所以有了下面的代码:
struct ClassBase
{
struct VTabel *vptr;
int mi;
int mj;
};
struct ClassDerived
{
struct ClassBase base;
int mk;
};
多态设计分析
多态在C++中是通过虚函数表完成的,而虚函数表是C++编译器自主产生和维护的数据结构。因此,接下来要解决的问题就是如何在C语言中自定义虚函数表。
通过结构体变量模拟C++中的虚函数表是比较理想的一种选择,所以有了下面的代码:
struct VTabel
{
int (*pAdd)(void *, int);
};
有了类型后就可以定义实际的虚函数表了,在C语言中用具有文件作用域的全局变量表示实际的虚函数表是最合适的,因此有了下面的代码:
static int Base_Virtual_Add(Base *pThis, int value);
static Derived_Virtual_Add(Derived *pThis, int value);
static struct VTabel Base_vtab =
{
Base_Virtual_Add
};
static struct VTabel Derived_vtab =
{
Derived_Virtual_Add
};
每个对象中都拥有一个指向虚函数表的指针,而所有父类对象都指向Base_vtab,所有子类对象都指向Derived_vtab。
当一切就绪后,实际调用虚函数的过程就是通过虚函数表中的对应指针来完成的。