多态的C++实现
1 多态的原理
什么是多态?多态是面向对象的特性之一,只用父类指针指向子类的对象。
1.1 多态实现的三个条件
- 存在继承
- 虚函数重写
- 父类指针指向子类对象
如下代码,如果并没有增加virtual
关键字,并不会发生多态现象。
#include <iostream>
using namespace std;
class Parent {
public:
Parent(int a, int b)
{
this->p1 = a;
this->p2 = b;
}
//多态的重点:需要加virtual关键字,否则不会发生多态
virtual void add()
{
cout<<"Parent: p1 + p2 "<<endl;
}
private:
int p1;
int p2;
};
class Child :public Parent {
public:
Child(int a, int b, int c, int d):Parent(c,d)
{
this->c1 = a;
this->c2 = b;
}
void add()
{
cout<<"Child: c1 + c2 "<<endl;
}
private:
int c1;
int c2;
};
void play(Parent *base)
{
base->add();
}
int main(int argc, const char * argv[]) {
Child c(1,2,3,4);
Parent *pP = &c;
Child *pC = &c;
play(pP);
play(pC);
return 0;
}
1.2 多态的实现原理
上面的例子中,在基类的函数增加了virtual
关键字后,编译器会自动为子类对应的方法也会增加virtual
关键字
1.2.1 虚函数表
- 当类中声明了虚函数时,编译器会自动为类生成一张虚函数表。
- 虚函数表一个存储类成员函数指针的数据结构
- 虚函数表是有编译器自动生成与维护的
1.2.2 vptr指针
如果存在virtual关键字,编译器在运行时的时候(动态联编)会自动为当前对象增加vptr指针,这个vptr指针指向了当前类的虚函数表。
判断vptr指针是否存在
//如果vptr指针存在,则对象sizeof()之后,大小会发生变化
#include <iostream>
using namespace std;
class Parent {
public:
Parent(int a, int b)
{
this->p1 = a;
this->p2 = b;
}
virtual void add()
{
cout<<"Parent: p1 + p2 "<<endl;
}
private:
int p1;
int p2;
};
int main(int argc, const char * argv[]) {
Parent p(1,2);
//可以分别测试下,添加virtual关键字和不加的内存空间大小
cout<<sizeof(p)<<endl;
}
1.2.2 多态实现原理
- 编译器发现存在
virtual
关键字,则会为类生成一张虚函数表 - 编译器会在动态联编(运行时)时为对象添加一个vptr指针
- 有父类对象指向子类对象存在,且执行了父类方法
- 如果当前对象有vptr指针存在,则会通过vptr指针找到对应的虚函数表,在虚函数表中查找对应的方法地址,执行。
2 vptr指针的分步初始化
2.1 父类构造函数中调用父类的方法,会产生多态吗?
如果再父类的构造函数中,调用父类的虚函数。那么在子类对象初始化的时候,会不会产生多态现象呢,还是仍然调用父类的虚函数呢?
答案是:否。不会产生多态。因为vptr指针是分步初始化的
#include <iostream>
using namespace std;
class Parent {
public:
Parent(int a, int b)
{
this->p1 = a;
this->p2 = b;
this->add(); //调用父类的虚函数,这个地方不会产生多态现象。
}
virtual void add()
{
cout<<"Parent: p1 + p2 "<<endl;
}
private:
int p1;
int p2;
};
class Child :public Parent {
public:
Child(int a, int b, int c, int d):Parent(c,d)
{
this->c1 = a;
this->c2 = b;
}
virtual void add()
{
cout<<"Child: c1 + c2 "<<endl;
}
private:
int c1;
int c2;
};
void play(Parent *base)
{
base->add();
}
int main(int argc, const char * argv[]) {
//虽然会调用父类的构造函数,但是仍然是调用父类的add方法
Child c(1,2,3,4);
return 0;
}
2.2 vptr指针的分步初始化
那么vptr指针是如何初始化的呢?
- 编译器编译时,基类会产生虚函数表,子类也会产生虚函数表
- 当初始化子类对象的时候,先调用父类的构造函数,同时将子类对象的vptr指针指向父类的虚函数表
- 接下来,调用自己的构造函数,同时将vptr指针指向子类的虚函数表
上面的例子中,当调用父类构造函数时,当前vptr指针仍然指向父类的虚函数表,调用的仍然是父类的add方法,不会产生多态。
3 多态带来的问题
可以使用父类指针指向子类的对象,但是指针的类型却改变了,最指针进行++或者--操作时,可能带来意向不到的后果。
#include <iostream>
using namespace std;
class Parent {
public:
Parent(int a, int b)
{
this->p1 = a;
this->p2 = b;
}
virtual void print()
{
cout<<"p1 = "<<p1<<"; p2 = "<<p2<<endl;
}
private:
int p1;
int p2;
};
class Child :public Parent {
public:
Child(int a, int b, int c, int d):Parent(c,d)
{
this->c1 = a;
this->c2 = b;
}
virtual void print()
{
cout<<"c1 = "<<c1<<"; c2 = "<<c2<<endl;
}
private:
int c1;
int c2;
};
int main(int argc, const char * argv[]) {
Child array[] = {Child(1,2,3,4),Child(5,6,7,8),Child(9,10,11,12),Child(13,14,15,17),Child(18,19,20,21)};
Child *c = array;
c->print();
c++;
c->print();
Parent *p = array;
p->print(); //仍然打印的是子类的(没问题)
p++; //执行++操作
p->print(); //执行之后,崩溃(因为p++和c++的步长不一样导致错误)
return 0;
}
4 纯虚函数和虚基类(抽象类)
- 纯虚函数是一个在基类中说明的虚函数,在基类中没有定义,要求任何派生类都定义自己的版本
- 纯虚函数为各派生类提供了一个公共界面(接口的封装和设计、软件的模块功能划分)
- 纯虚函数的声明
virtual void print() = 0;
- 一个具有纯虚函数的基类成为抽象类
注意:
- 抽象类不能建立对象,但可以声明抽象类的指针
- 抽象类不能作为返回来兴,也不能作为参数类型
- 抽象类可以声明抽象类的引用