需要声明的,本片博客中的代码及解释都是在vs2013下的x86程序中,涉及的指针都是4bytes。如果要其他
平台下,部分地方需要改动。比如:如果是x64程序,则需要考虑指针是8bytes问题等等
同一事物,在不同场景下的表现出的不同形态
具体的:多态的概念:通俗来说,就是多种形态,具体点就是去完成某个行为,
不同的对象去完成时会产生出不同的状态。
举例子:见人说人话,见鬼说鬼话,
见老婆: 见校长: 见丈母娘:
不同的对象,完成说话这个行为,说话的方式就不一样
举个栗子:比如买票这个行为,当普通人买票时,是全价买
票;学生买票时,是半价买票;军人买票时是优先买票。
再举个栗子: 最近为了争夺支付市场,支付宝年底经常会做诱人的扫红包-支付-给奖励金的活动。那么大家
想想为什么有人扫的红包又大又新鲜8块、10块...,而有人扫的红包都是1毛,5毛....。
实这背后也是一个多态行为。
多态是在不同继承关系的类对象,去调用同一函数,产生了不同的行为。比如Student继承了Person。
Person对象买票全价,Student对象买票半价。
#include
using namespace std;
class person {
public:
virtual void Buyticket(){
cout << "全票" << endl;
}
};
class student :public person{
public:
virtual void Buyticket(){
cout << "半票" << endl;
}
};
void testvirtual(person & d)
{
d.Buyticket();
}
int main(){
student s;
person p;
testvirtual(s);
testvirtual(p);
return 0;
}
结果:
半票
全票
静态的多态:(静态绑定,早绑定)函数重载–add 模板
在程序编译时就确定了函数的具体行为
动态多态(动态绑定,晚绑定):
在程序运行时,确认函数的具体行为。虚函数
1.继承的条件下,基类必须要有虚函数,派生类必须要对基类中的虚函数重写
2.虚函数调用:必须通过基类的指针或者引用调用虚函数
虚函数就是在类成员数前加上virtual关键字
class person {
public:
virtual void Buyticket(){
cout << "全票" << endl;
}
};
重写:在基类中该函数必须为虚函数,派生类如果要重写虚函数,必须与基类的虚函数原型完全相同(返回值,参数列表,函数名字),不包括virtule,但是加上也可以
下面就是在不同类中对虚函数进行的重写
#include
using namespace std;
class person {
public:
virtual void Buyticket(){
cout << "全票" << endl;
}
};
class student :public person{
public:
virtual void Buyticket(){
cout << "半票" << endl;
}
};
void testvirtual(person & d)
{
d.Buyticket();
}
int main(){
student s;
person p;
testvirtual(s);
testvirtual(p);
return 0;
}
另外虚函数的重写也叫作虚函数的覆盖。
例外:
a.返回值类型不同的重写
协变:基类虚函数返回基类对象的指针或引用
派生类的虚函数返回派生类的指针或引用
斜变举例:
如下:对虚函数的定义,返回类型改为引用类型,结果构成了重写
#include
using namespace std;
class person {
public:
virtual person& Buyticket(){
//返回引用类型
cout << "全票" << endl;
return *this;
}
};
class student :public person{
public:
virtual student & Buyticket(){
//返回引用类型
cout << "半票" << endl;
return *this;
}
};
void testvirtual(person & d)
{
d.Buyticket();
}
int main(){
student s;
person p;
testvirtual(s);
testvirtual(p);
return 0;
}
b.函数名字不同的重写
析构函数:子类与基类的析构函数也可以构成重写
如下:
#include
using namespace std;
class Base{
public:
virtual ~Base()
{
cout << "Base virtual" << endl;
}
};
class C_child :public Base
{
public:
virtual ~C_child()
{
cout << "C_child virtual" << endl;
}
};
void test(Base & b){
b.~Base();
}
int main(){
Base b;
C_child d;
test(b);
test(d);
return 0;
}
结果:
Base virtual
C_child virtual
c.不规范的重写行为
在派生类中的重写的虚函数,可以不加virtual关键字,因为派生类继承基类的虚函数保持虚函数的属性
class person {
public:
virtual void Buyticket(){
cout << "全票" << endl;
}
};
class student :public person{
public:
void Buyticket(){
cout << "半票" << endl;
}
};
基类指针指向那个类的对象,程序运行时,就调用哪个虚函数
例如:
下面的test(),传递是那个类的对象就调用,那个类的虚函数
#include
using namespace std;
class Base{
public:
virtual void test1(){
cout << "test1" << endl;
}
};
class C_child :public Base
{
public:
private:
virtual void test1(){
cout << "c_child test1()" << endl;
}
};
void test(Base & b){
b.~Base();
}
1.如果条件不满足:优先调用基类的
2.派生类的虚函数可以不用加virtule,保持基类中的虚函数特性,但是基类中必须加上
3.建议在继承体系中,基类的析构函数上virtule,否则容易发生内存泄漏
举例:
Base* pb=new Derived;
delete pb;
调用的时候默认调用的是静态的(基类的析构),而对象是
子类的,所以要去调用子类的析构函数
如果子类中涉及资源管理,那么就会产生内存泄漏,申请的空间有释放
如下:
析构函数的重写问题:
基类中的析构函数如果是虚函数,那么派生类的析构函数就重写了基类的析构函数。
这里他们的函数名不相同,看起来违背了重写的规则,其实不然,这里可以理解为编
译器对析构函数的名称做了特殊处理,编译后析构函数的名称统一处理成destructor,
这也说明的基类的析构函数最好写成虚函数。
下面是基类和派生类的析构函数,没有加上virtual,那么在释放的时候就会产生冲突,
#include
using namespace std;
class Base{
public:
~Base()
{
cout << "Base virtual" << endl;
}
};
class C_child :public Base
{
public:
~C_child()
{
cout << "C_child virtual" << endl;
}
private:
C_child *d;
};
void test(Base & b){
b.~Base();
}
int main(){
Base *a = new C_child;
test(*a);
return 0;
}
结果:
Base virtual
本来应该调用C_child的析构函数,确调用了基类的析构函数,所以就产生了错误
4.如果多态条件没有满足,使用的对象的静态类型来调用函数
变量有两种类型:
静态类型:声明类型
动态类型:实际类型
如果多态条件没有满足,使用的对象的静态类型来调用函数
例如:
Base* pb=new Derived;
声明的类型为Base
实际类型为Derived;
5.如果多态条满足,使用动态类型来调用函数 (动态类型程序运行时)
做法:
i.在派生类的虚函数列表后加上override;
ii.在基类的虚函数列表后加上final,表示基类不能被重写
final:修饰类,表示类不能被继承
class base final {
};
如下:针对参数类型和返回类型不构成重写,在编译期间就会报错
class Base{
public:
virtual void test1(){
cout << "test1" << endl;
}
};
class C_child :public Base
{
private:
virtual void test1(int) override//报错
{
cout << "c_child test1()" << endl;
}
};
class Base{
public:
virtual void test2()final{
cout << "test2" << endl;
}
};
class C_child :public Base
{
private:
virtual int test2() //报错
{
cout << "test2" << endl;
return 0;
}
};
7.构成重写的两个函数在基类和子类的访问权限可以不一样
但是基类的虚函数必须是可以访问的,要不然继承下来的派生类无法访问
如下:
class Base{
public:
virtual void test1(){
cout << "test1" << endl;
}
};
class C_child :public Base
{
private:
virtual void test1(){
cout << "c_child test1()" << endl;
}
};
普通函数的继承是一种实现继承,派生类继承了基类函数,可以使用函数,继承的是函数的实现。
虚函数的
继承是一种接口继承,派生类继承的是基类虚函数的接口,目的是为了重写,达成多态,继承的是接口。所
以如果不实现多态,不要把函数定义成虚函数
有纯虚函数的类叫抽象类,在虚函数后面加=0,叫纯虚函数
对于函数使用不名确的,可以指定为虚函数,在子类中实现。
1.不能实例化对象,因为类型不明确
class person {
public:
virtual void GO_WC() = 0{
}
};
void test(){
person p;
}
报错:
2.可以创建指针,因为类型有了
但是可以new一个空间,类型不明确
class person {
public:
virtual void GO_WC() = 0{
}
};
void test(){
person *p = nullptr;//没有错
person *p=new person;//编译报错
}
3.抽象类一定要被继承,因为在子类中去实现纯虚函数实现,
如果没有在派生类中重写,则派生类也是抽象类
#include
using namespace std;
class person {
public:
virtual void GO_WC() = 0{
}
};
class wenman :public person
{
public:
virtual void GO_WC(){
cout << "go_wenman_wc" << endl;
}
};
class man :public person
{
public:
virtual void GO_WC(){
cout << "go_man_wc" << endl;
}
};
void test(person&p){
p.GO_WC();
}
int main(){
man d;
wenman w;
test(w);
test(d);
return 0;
}
// :sizeof(Base)是多少?
class Base
{
public:
virtual void Func1()
{
cout << "Func1()" << endl;
}
private:
int _b = 1;
};
结果为:
8
通过观察测试我们发现b对象是8bytes,除了_b成员,还多一个__vfptr放在对象的前面(注意有些平台可能会
放到对象的最后面,这个跟平台有关),对象中的这个指针我们叫做虚函数表指针(v代表virtual,f代表
function)。一个含有虚函数的类中都至少都有一个虚函数表指针,因为虚函数的地址要被放到虚函数表中,
虚函数表也简称虚表。
1.多态中会多四个字节,因为有可能含有多个虚表,存放虚函数表(函数指针数组)的首地址,多出的四个字节为虚表指针
class Base{
public:
virtual void test1(){
cout << "test1()" << endl;
}
virtual void test2(){
cout << "test2()" << endl;
}
private:
int _d;
};
int main(){
Base b;
}
3.派生类的虚表和基类的虚表不同。
去观察b和d对象的窗口,就会发现不同
// 1.我们增加一个派生类Derive去继承Base
// 2.Derive中重写Func1
// 3.Base再增加一个虚函数Func2和一个普通函数Func3
class Base
{
public:
virtual void Func1()
{
cout << "Base::Func1()" << endl;
}
virtual void Func2()
{
cout << "Base::Func2()" << endl;
}
void Func3()
{
cout << "Base::Func3()" << endl;
}
private:
int _b = 1;
};
class Derive : public Base
{
public:
virtual void Func1()
{
cout << "Derive::Func1()" << endl;
}
private:
int _d = 2;
};
int main()
{
Base b;
Derive d;
return 0;
}
先将基类的虚表中的内容拷贝一份,放在虚表中(有几个基类拷贝几分)
再将派生类重写的某个虚函数,用自己的虚函数替换派生类虚表中相同偏移量的虚函数
基类b对象和派生类d对象虚表是不一样的,这里我们发现Func1完成了重写,
所以d的虚表中存的是重
写的Derive::Func1,所以虚函数的重写也叫作覆盖
因为一个派生类可能继承多个虚表,所以一个派生类可能会有多个虚表
对于单继承来说:
1.首先在基类和派生类中都有一个虚表
2.这个表的首地址存放在类的前四个字节中
3.满足多态的条件的情况下,在程序运行起来的时候,根据传入的基类对象找出对应的虚表
4.再根据对应的虚表去拿到相应的虚函数地址
5.最后在通过传入this指针去调用对应的虚函数
如何确定虚表指针在类的前四个字节中
#include
using namespace std;
class Base{
public:
virtual void test1(){
cout << "test1()" << endl;
}
virtual void test2(){
cout << "test2()" << endl;
}
private:
int _d;
};
class Devir :public Base
{
public:
virtual void test1(){
cout << "Devir test1()" << endl;
}
virtual void test2(){
cout << "Devir test2()" << endl;
}
};
typedef void(*test) (void) ;//确定出虚表中元素类型函数指针类型的
int main(){
Base b;
Devir d;
cout<<"虚函数表地址:"<<(int*)(&b)<<endl;
cout<<"虚函数表第一个函数地址:"<<(int*)*(int*)(&b)<<endl;
//最后将函数地址转换成相应的类型
(*((test)*((int *)*((int*)&b))))();
(*(((test)*((int *)*((int*)&b) + 1))))();
return 0;
}
结果:
test1();
test2();
// p中存的是mike对象的指针,将p移动到eax中
001940DE mov eax,dword ptr [p]
// [eax]就是取eax值指向的内容,这里相当于把mike对象头4个字节(虚表指针)移动到了edx
001940E1 mov edx,dword ptr [eax]
// [edx]就是取edx值指向的内容,这里相当于把虚表中的头4字节存的虚函数指针移动到了eax
00B823EE mov eax,dword ptr [edx]
00B823EE mov eax,dword ptr [edx]
// call eax中存虚函数的指针。这里可以看出满足多态的调用,不是在编译时确定的,是运行起来以后到对象的
中取找的。
001940EA call eax
00头1940EC cmp esi,esp
注意:不满足多态条件的函数在编译的时候就确定函数地址,和满足多态条件的函数确认时间不同,因为在编译期间函数没有运行起来,对象还没有确定,而多态的调用必须根据基类对象的引用去调用,对象都没确定,当浩调用函数当然也无法确定。
动态绑定与静态绑定:
一个派生类有多个基类
class D:public B1,public B2 {};
#include
using namespace std;
class Base1{
public :
virtual void Func1(){
cout << "Func1" << endl;
}
virtual void Func2(){
cout << "Func2" << endl;
}
};
class Base2{
public:
virtual void Func3(){
cout << "Func3" << endl;
}
virtual void Func4(){
cout << "Func4" << endl;
}
};
class Dervir :public Base1, public Base2
{
public:
virtual void Func1(){
cout << "Dervir Func1" << endl;
}
virtual void Func3(){
cout << "Dervir Func3" << endl;
}
virtual void Func5(){
cout << "Dervir Func5" << endl;
}
};
int main(){
Dervir d;
Base1 b1;
Base2 b2;
return 0;
}
多继承的虚拟表:
先将基类的虚表中的内容拷贝一份,放在虚表中(有几个基类拷贝几分)
再将派生类重写的某个虚函数,用自己的虚函数替换派生类虚表中相同偏移量的虚函数
对于派生类自己增加的虚函数将其放在派生类的第一个虚表后
对于自己的虚函数在第一个表中是不可见的,但是可以通过内存窗口看到:
通过对比可以发现第一张表中在调试窗口只有两条数据,而在内存窗口中有三条数据,
而第二张表在监视窗口有两条数据,在内存窗口中也是两条数据。
这也验证了派生类将自己的虚函数放在第一张表中。