多态的概念
我们已经在上一章说明C++的继承概念,不妨考虑一个情况:如果子类当中定义了和父类相同的函数会发生什么情况了?
函数重写
- 在子类当前定义与父类原型相同的函数
- 函数的重写发生在子类和父类之间
举一个例子
#include <iostream>
using namespace std;
class Parent
{
public:
Parent(int value)
{
this->a = value;
cout << "Parent" << endl;
}
void print(void)
{
cout << "Parent value is " << this->a << endl;
}
protected:
private:
int a;
};
class Child:public Parent
{
public:
Child(int a, int value) :Parent(10)
{
this->b = value;
cout << "Parent value is " << this->b << endl;
}
void print(void)
{
cout << "Child" << endl;
}
protected:
private:
int b;
};
int main()
{
Parent* p1 = nullptr;
Parent b1(20);
Child c1(10, 10);
p1 = &b1;
p1->print();
p1 = &c1;
p1->print();// 执行的是子类的函数还是父类的函数
return 0;
}
对上面的代码进行调试分析,当使用父类指针p指向子类中和父类的同名的成员函数,代码会运行到父类同名的函数中,同理我们也可以测试以下使用引用来进行相同的操作。
- 父类中被重写的函数依然会继承给子类
- 默认情况下子类中重写的函数将会隐藏父类中的函数
- 通过作用域分辨符::可以访问到父类中被隐藏的函数
面向对象的新需求
编译器的做法显然不是我们所期望的,我们期望实现的如下这样的需求
- 根据实际的对象类型来判断重写函数的调用
- 如果父类指针指向的是父类对象则调用父类中定义的函数
- 如果父类指针指向的是子类对象则调用子类中定义的函数
就如上面的代码
多态的实现
- C++中通过virtual关键字对多态进行了控制
- 使用vitual关键字修辞后的函数被重写后可以展现出多态的特性
不多说废话,咱们直接使用一个例子来说明多态的实例
#include <iostream>
using namespace std;
class HeroFighter
{
public:
virtual int power(void)
{
return 10;
}
protected:
private:
};
class AdvHeroFighter:public HeroFighter
{
public:
virtual int power(void)
{
return 20;
}
protected:
private:
};
class EnemyFighter
{
public:
int attack(void)
{
return 15;
}
protected:
private:
};
void testFunction(HeroFighter *hf, EnemyFighter *ehf)
{
if (hf->power() > ehf->attack())
{
cout << "主角赢了" << endl;
}
else
{
cout << "主角GG" << endl;
}
}
int main()
{
HeroFighter hf;
AdvHeroFighter advHf;
EnemyFighter ehf;
testFunction(&advHf, &ehf);
cout << "C++程序" << endl;
return 0;
}
这个代码中有三个类,普通战机类HeroFighter
,升级后的战机AdvHeroFighter
,地方战机EnemyFighter
其中AdvHeroFighter
继承与HeroFighter
并且重写了attack
方法,使用多态的方法对其进行实现,通过测试函数testFunction
来对其进行测试后,发现输出的类的不同,编译器会自动的找到对应的类下的重写函数。
好的,那么有人就会好奇,多态的有什么优势,或者使用多态可以给我我们带来什么?在讨论这个话题之前,我们先说下面对对象的三个特征,封装,继承和多态
- 封装: 突破了C语言函数的概念,使用类做函数的参数,可 以使用对象的成员和成员函数
- 继承:实现了类与类之间关系和代码的复用
- 多态:可以使用未来,可以在编写一个框架,其他的实现可以根据接口来实现代码。
实现多态的三个条件:
- 要存在继承
- 要有函数重写
- 用父类指针<父类引用>指向子类对象
多态是设计模式的基础,多态是框架的基础
多态的理论基础
动态联编和静态联编
- 联编是指一个程序模块,代码之间相互关联的过程
- 静态联编
,是程序的匹配,连接在编译阶段实现,也称之为早期的匹配,重载函数使用静态联编 - 动态联编是指程序联编推迟到运行时进行,所以又称之为晚期联编<迟绑定>
理论分析
- C++和C相同是静态编译型语言
- 在编译时,编译器自动根据指针的类型判断指向的是一个怎么样的对象;所以编译器认为父类指针指向的是父类对象。
- 由于程序没有运行,所以不可能知道父类指针指向的具体是父类对象还是子类对象,但是从程序的安全来说,编译器假设父类指针是指向父类对象,所以调用父类指针指向的成员函数也是父类的成员函数。
多态的C++实现
virtual
关键字,告诉编译器这个函数要支持多态;不要根据指针类型判断如何调用;而是要根据指针所指向的实际对象类型来判断如何如何调用,而加上virtual
关键字的函数叫虚函数,虚函数分为两种:一般的虚函数和纯虚函数。
虚析构函数
上面说到虚函数的概念,那么在什么情况下声明虚函数
- 构造函数不能是虚函数。建立一个派生类对象时,必须从类的层次的根开始。沿着继承路径逐个调用构造函数。
- 析构函数可以是虚函数,虚析构函数用于做指引 delete 运算符正确析构对象
这样说明可以不是很清楚,所以我写一个demo来说明一下虚析构函数的作用
#define _CRT_SECURE_NO_WARNINGS
#include <iostream>
#include "string.h"
using namespace std;
class A
{
public:
A()
{
p = new char[20];
strcpy(p, "objA");
cout << "A构造函数" << endl;
}
~A()
{
delete [] p;
cout << "A析构函数" << endl;
}
protected:
private:
char* p;
};
class B: public A
{
public:
B()
{
p = new char[20];
strcpy(p, "objB");
cout << "B构造函数" << endl;
}
~B()
{
delete[] p;
cout << "B析构函数" << endl;
}
protected:
private:
char* p;
};
class C :public B
{
public:
C()
{
p = new char[20];
strcpy(p, "objC");
cout << "C构造函数" << endl;
}
~C()
{
delete[] p;
cout << "C析构函数" << endl;
}
protected:
private:
char* p;
};
void run(A *base)
{
delete base;
}
int main()
{
C *myC = new C();
run(myC);
//run(&myC);
return 0;
}
在不使用虚析构函数时,得到的结果为
在个析构函数增加了virtual
关键字后,得到的结果
这样就可以很清楚的明白虚函数的威力了,如果你想要通过父类指针释放所有的子类资源就可以使用虚析构函数
多态原理的探究
基本理论
- 当类中声明一个虚函数时,编译器会在类中生成一个虚函数表
- 虚函数表是一个存储类的成员函数指针的数据结构
- 当然这个虚函数是由编译器自动生成并进行维护的
- virtual成员函数会被编译器放入虚函数表
- 当类如果存在虚函数,每一个对象中都有一个指向虚函数表的指针(C++编译器给父类对象,子类对象对象提前布局了vptr指针;当进行多态发生,C++编译器不需要区分是子类对象或者父类对象,只需要在base指针中找到vptr指针即可)
- 一般vptr作为类对象的第一个成员
就拿下面的代码作为例子:
#include <iostream>
using namespace std;
class Parent
{
public:
Parent(int val = 0)
{
this->a = a;
}
virtual void function(void)
{
cout << "父类的function(void)" << endl;
}
virtual void function(int a, int b)
{
cout << "父类的function(int a, int b)" << endl;
}
void print()
{
cout << "我是父类" << endl;
}
protected:
private:
int a;
};
class Child : public Parent
{
public:
Child(int val = 0, int val01 = 0):Parent(val)
{
this->b = b;
}
virtual void function(void)
{
cout << "子类的function(void)" << endl;
}
virtual void function(int a, int b)
{
cout << "子类的function(int a, int b)" << endl;
}
void print()
{
cout << "我是子类" << endl;
}
protected:
private:
int b;
};
void run(Parent *p_base)
{
// 在这个地方会有多态现象的发生
p_base->print();
}
int main()
{
Parent p_parent;
Child p_child;
run(&p_parent);
run(&p_child);
cout << "C++程序" << endl;
return 0;
}
类中声明一个虚函数时,编译器会在类中生成一个虚函数表
在实际声明一个对象,每一个对象中都有一个指向虚函数表的指针(C++编译器给父类对象,子类对象对象提前布局了vptr指针;当进行多态发生,C++编译器不需要区分是子类对象或者父类对象,只需要在base指针中找到vptr指针即可),如下图
所以编译器就是有两个过程来处理函数
- 成员函数不是虚函数,编译器可直接确定被调用的成员函数(静态链编,根据类的类型来确定执行的成员函数)
- 成员函数是虚函数,编译器根据对象中Vptr指针,所指的虚函数表查找对应的虚函数来执行
- 查找和调用时在运行时完成的(实现了所谓的动态链编)
需要说明的是:
- 通过虚函数表指针Vptr调用重写函数是在程序运行的阶段,因此需要通过寻址操作才能确定真正的调用的函数,而普通成员函数在编译时就已经确定了调用的函数,所以在效率上使用虚函数的效率是低很多的。
- 所以在效率的考虑上,并没有必要将所有的成员函数定义为虚函数。
证明vptr指针的存在
说了多态的实现原理,始终离不开一个vptr指针,那么怎么证明vptr指针的存在了
#include <iostream>
using namespace std;
class Parent1
{
public:
Parent1(int val = 0)
{
this->a = a;
}
virtual void function(void)
{
cout << "父类1的function(void)" << endl;
}
virtual void function(int a, int b)
{
cout << "父类1的function(int a, int b)" << endl;
}
void print()
{
cout << "我是父类1" << endl;
}
protected:
private:
int a;
};
class Parent2
{
public:
Parent2(int val = 0)
{
this->a = a;
}
void function(void)
{
cout << "父类2的function(void)" << endl;
}
void function(int a, int b)
{
cout << "父类2的function(int a, int b)" << endl;
}
void print()
{
cout << "我是父类2" << endl;
}
protected:
private:
int a;
};
int main()
{
cout << "Parent1的字节长度" << sizeof(Parent1) <<"\n"<< "Parent2的字节长度" << sizeof(Parent2) << endl;
return 0;
}
在Parent1中定义了虚函数,在Parent2中没有定义虚函数,根据上面我们说的多态的实现原理,Parent1应该会比Parent2多一个vptr指针,通过sizeof()进行判断,结果如下:
那么我们就证明了vptr指针的存在,那么更深一步的探究,在构造函数能否使用虚函数,实现多态,其实这个问题就是对象中vptr指针在什么时候被初始化,同样为了说明这个初始化的时机,我先举一个demo
#include <iostream>
using namespace std;
class Parent
{
public:
Parent(int val = 0)
{
this->a = a;
print();
}
virtual void function(void)
{
cout << "父类的function(void)" << endl;
}
virtual void function(int a, int b)
{
cout << "父类的function(int a, int b)" << endl;
}
void print()
{
cout << "我是父类" << endl;
}
protected:
private:
int a;
};
class Child : public Parent
{
public:
Child(int val = 0)
{
this->a = a;
print();
}
void function(void)
{
cout << "子类的function(void)" << endl;
}
void function(int a, int b)
{
cout << "子类的function(int a, int b)" << endl;
}
void print()
{
cout << "我是子类" << endl;
}
protected:
private:
int a;
};
int main()
{
Child c1;
return 0;
}
输出的结果为
那为什么会是这样的结果?
其实这就是vptr指针的分步初始化的过程
- 首先是初始化c1.vptr指针,初始化时分步的
- 当执行父类的构造函数时,c1.vptr是指向父类的虚函数表,当父类的构造函数执行完毕后,会把c1.vptr指向子类的虚函数表
结论:子类c1.vptr指针分步完成指向
一些多态有关的问题思考:
- 是否可以将类的所有成员函数都声明为虚函数,为什么?
- 构造函数中调用虚函数可以实现多态吗?为什么
- 虚函数表中被编译器初始化的过程是怎么样的?
- 父类的构造函数调用虚函数,可以发生多态吗?
- 为什么要定义虚析构函数?(虚析构函数的意义是什么)
- 谈谈你对多态的理解,下面几个问题需要搞清楚:
- 多态的实现方式和效果
- 多态的理论基础
- 多态的重要意义
- C++编译器是如何实现多态的