前言:
对于面向对象语言来说,多态是面向对象的三大特性之一,简单一点来说多态就是多种形态,是不同对象接收到同一种消息产生的不同动作或者反应,听起来有点抽象,实际上就是完成一个任务让不同的对象来做产生的效果是不同的。让我们一起来深入了解一下多态吧!
目录
1.多态的概念
2.多态的定义及实现
2.1多态的构成条件
2.2虚函数和虚函数的重写
2.2.1虚函数的重写
2.2.2虚函数重写的特殊情况
2.3c++11关键字final和override
2.4重载,覆盖和重写的辨析
3.抽象类
3.1抽象类的定义
3.2接口继承和实现继承
4.多态是如何实现的
4.1虚函数表
4.2多态的原理
4.3动态绑定和静态绑定
5.多态在单继承和多继承中
5.1单继承中的虚函数表
5.2多继承中的虚函数表
总结
1.多态的概念
多态通俗的说就是多种形态,比如我们在买票的时候不同人群买票的价格和方式是不同的,比如军人可以优先买票,学生票半价等等。
在举个例子,比如我们在抢红包的时候,不同人抢到的红包的金额也是不同的,有的人可能抢的很多,有的人可能抢的很少。之前支付宝做活动的时候,大家都扫码领红包,每个人领的红包都是不一样的,而且随着领取次数的增加,领到的红包的金额也是会越来越少。这些行为都可以认为是多态。
2.多态的定义及实现
2.1多态的构成条件
多态是不同继承关系的类对象,去调用同一函数,产生不同的行为(基类对象和派生类对象去调用同一函数所产生的效果不同)。那么在继承中构成多态的条件有两个:1.被调用的函数必须是虚函数,且必须完成虚函数的重写。2.必须通过基类的指针或者引用去调用虚函数。
例如:
#include<iostream>
using namespace std;
class Person
{
public:
virtual void BuyTicket()
{
cout << "抢到五元钱" << endl;
}
};
class Student:public Person
{
virtual void BuyTicket()
{
cout << "抢到五角钱" << endl;
}
};
void Print(Person& p)
{
p.BuyTicket();
}
int main()
{
Person p;
Student s;
Print(p);//调用父类的BuyTicket
Print(s); //调用Student的BuyTicket
return 0;
}
2.2虚函数和虚函数的重写
2.2.1虚函数的重写
虚函数就是被virtual关键字修饰的成员函数。例如:
虚函数的重写(覆盖):派生类中一定要有和基类中的函数的函数名相同,函数的参数类型相同,函数的返回值相同的函数(三同)。派生类中有和基类相同的虚函数就完成了虚函数的重写,称为派生类的虚函数完成了对基类虚函数的重写。
2.2.2虚函数重写的特殊情况
那么虚函数的重写是不是一定要满足函数名相同,函数的参数相同,函数的返回值也相同呢?
答案是不一定,这里存在特殊情况,如果派生类重写基类的虚函数时,与基类的虚函数的返回值不同。即基类虚函数的返回基类对象的指针或者引用,派生类虚函数返回派生类对象的指针或者引用时,称为协变。例如:
#include<iostream>
using namespace std;
class Base
{
public:
virtual Base* Func()
{
cout << "hello world" << endl;
return this;
}
};
class Derive:public Base
{
public:
virtual Derive* Func()//协变,返回值不同,是派生类Derive的指针
{
cout << "i am so happy" << endl;
return this;
}
};
void Print(Base& b)
{
b.Func();
}
int main()
{
Base b1;
Derive d1;
//多态
Print(b1);//Base类调用实现Base的效果
Print(d1);//Derive类调用实现另外的效果
return 0;
}
注意,派生类重写基类的虚函数时,派生类与基类相同的函数在派生类中不加关键字virtual修饰也是可以实现多态的,例如:
class Base
{
public:
virtual Base* Func()
{
cout << "hello world" << endl;
return this;
}
};
class Derive:public Base
{
public:
//派生类中与基类相同的函数,在基类中不用关键字virtual修饰也开始实现多态
Derive* Func()//协变,返回值不同,是派生类Derive的指针
{
cout << "i am so happy" << endl;
return this;
}
};
这样写不规范,所以一般不推荐这样写。但是如果基类中的虚函数不用virtual修饰是没有办法实现多态的。
注意:析构函数可以是虚函数吗?什么场景下析构函数是虚函数?
析构函数可以是虚函数,如果有以下这样的场景:
#include<iostream> #include<stdio.h> using namespace std; class Base1 { public: virtual void Fun1() { cout << "Base2---Fun1" << endl; } int _b1 = 0; ~Base1() { cout << "~Base()" << endl; } }; class Derive :public Base1 { public: virtual void Fun1() { cout << "Derive---Fun1" << endl; } virtual void Fun2() { cout << "Derive---Fun3" << endl; } ~Derive() { cout << "~Derive()"; } int _d = 0; }; int main() { Base1* p1 = new Derive; delete p1; return 0; }
如果将派生类动态开辟的空间赋值给基类的对象,此时就需要析构函数也是虚函数。
如果没有重写虚函数,此时delete p1的时候就只会调用基类的析构函数。如图:
解决方法:给基类的析构函数加上关键字virtual修饰
2.3c++11关键字final和override
c++11中新增了两个关键字final和override ,从上面可以看出,c++对函数重写的要求比较严格,但是有些情况下由于疏忽,可能导致函数名字母次序写反而无法构成重载,而这种错误在编译期间是不会报出来的,只有在程序运行时没有得到预期结果才会来debug,会得不偿失,因此:c++提供的关键字final和override帮助用户监测是否重写。
override 检查派生类虚函数是否重写了 ,如果虚函数没有进行重写,程序就会报错,例如:
using namespace std; class Base { public: virtual void Func() { cout << "hello world" << endl; } }; class Derive:public Base { public: void Fun()override { cout << "i am so happy" << endl; } };
此时没有完成对虚函数的重写,所以程序编译就会报错,
final:修饰虚函数,表示该虚函数不能被重写,例如:
class Base { public: virtual void Func()final//final修饰虚函数,该虚函数不能被重写 { cout << "hello world" << endl; } }; class Derive:public Base { public: virtual void Func() { cout << "i am so happy" << endl; } };
final修饰类,该类不能被继承,例如:
class Base final { public: virtual void Func() { cout << "hello world" << endl; } }; class Derive:public Base { public: virtual void Func() { cout << "i am so happy" << endl; } };
2.4重载,覆盖和重写的辨析
对比,如图:
3.抽象类
3.1抽象类的定义
在虚函数的后面写上=0 ,则这个函数就是纯虚函数。包含纯虚函数的类叫做抽象类(也叫接口类),抽象类不能实例化出对象。派生类继承也不能实例化出对象,只有重写纯虚函数后,派生类才能实例化出对象。纯虚函数规范了派生类必须重写,另外纯虚函数更加体现了接口继承。
例如:
class Base //有纯虚函数的类叫做抽象类
{
public:
virtual void Func() = 0;//纯虚函数,只需要声明不用实现也可以
};
int main()
{
Base b1;//这里会报错因为抽象类不能实例化出对象
return 0;
}
为什么会有抽象类呢,在现实世界中,我们有具象的事物,画家的派别也有很多种,有山水田园化,写实的画作等,也有抽象的画作,抽象就是在现实中没有对应的实体,很难想象形态,抽象类也是类似的,比如,我们定义的某个类,在现实中没有对于的具体事物,但是有一定的功能,我们就可以将它定义为抽象类。比如植物类,不是某种具体的植物,我们就可以将它定义为抽象类,可以让其他派生类去继承它的接口来实现必须的一些功能和要求。
3.2接口继承和实现继承
普通函数的继承就是一种实现继承,派生类继承了基类的函数 ,可使用基类的函数,继承的是函数的实现。虚函数的继承就是一种接口继承,派生类继承基类虚函数的接口,目的是为了重写,达到多态,继承的是接口。所以如果不实现多态,不要把函数定义为虚函数。
4.多态是如何实现的
4.1虚函数表
有这样一个问题,下面这个程序执行的结果是什么?
#include<iostream>
using namespace std;
class Base
{
public:
virtual void Func1()//虚函数
{
cout << "Func1" <<endl;
}
int _b = 0;
};
int main()
{
Base b1;
cout<<sizeof(b1);//这里的b1的大小是多少个字节呢?(32位平台下)
}
结果如下:
是不是和猜想的有出入呢,为什么会是这个结果呢?让我们一起来分析一下,通过观察我们发现在b对象中除了_b成员外还多了一个_vfptr的指针放在对象的最前面(注意有些平台可能放在后面,这个与平台有关)。对象中的这个指针,我们叫做虚函数表指针(v代表virtual,f代表function)。如图:
一个虚函数的类中至少都有一个虚函数表指针,因为虚函数的地址是要放到虚函数表中的。虚函数表也称为虚表,那么派生类的这个表是怎么样的呢?都放了哪些东西呢?让我们一起来看看吧!
测试代码:
#include<iostream>
using namespace std;
class Base
{
public:
virtual void Func1()
{
cout << "Base" <<endl;
}
int _b = 0;
};
class Derive:public Base
{
virtual void Func1()
{
cout << " Derive" << endl;
}
int _d = 0;
};
int main()
{
Derive d1;
Base b1;
return 0;
}
上面我们新增了一个派生类Derive,并且继承了父类Base,重写了基类的Func1函数。
通过观察我们发现,派生类对象是由两部分构成的,一部分是继承基类的,另一部分是自己的 ,派生类也有虚函数表指针。虚函数表中存着自己和继承基类的虚函数。
基类对象b1和派生类对象d1的虚函数表是不一样的,这里我们可以看见在派生类中,Func1函数完成了重写,所以d1的虚函数表中存的是重写以后的Func1(属于derive类),所以重写也叫作覆盖,如图:
另外对于没有重写的基类的虚函数Func2,也放进了虚函数表中,Func3也继承下来了,但是由于Func3是普通函数所以,没有将它放进续表中。
虚函数表本质上是存虚函数指针的数组,一般情况下,这个数组是以nullptr结束的。
总结一下:派生类的虚函数表的生成:1.先继承基类的虚函数表,将基类的虚函数表的内容拷贝一份到派生类的虚函数表中。2.如果派生类重写了基类的虚函数,就在虚函数表中被重写的基类的虚函数的地址覆盖成派生类的虚函数的地址。3.派生类自己新增的虚函数一次放到虚函数表的最后。
这里有两个问题,虚函数存在哪里,虚函数表存在哪里?
答案是:它们都存在代码段,可能现在就有人不能理解了,为什么虚函数不是存在虚函数表里的而是存放在代码段的呢?注意在虚函数表里面只存放了虚函数的地址,运行的时候编译器是在虚函数表里面找虚函数的地址从而调用虚函数的,虚函数存放的地方和普通函数是一样的,都在代码段,而且虚函数表也是存放在代码段的,为什么呢?其实可以好好思考一下,一个类的对象需要很多虚函数表吗?不需要吧,一个类中的虚函数表只有有一份那么这个类中的所有对象就是可以都是使用的,而且也会足够的,就像是一个班级里面需要多份课表吗,是不是有一份就可以了,多了贴在班级里面也没有很多的用处。
从这里可以看出同一类对象共用一份虚函数表。
我们可以写一份代码来验证一下,验证代码:
#include<iostream> #include<stdio.h> using namespace std; class Base { public: virtual void Func1() { cout << "Base" <<endl; } virtual void Func2() { cout << "Func2" << endl; } void Func3() { cout << "Func3" << endl; } int _b = 0; }; class Derive:public Base { public: virtual void Func1() { cout << " Derive" << endl; } virtual void Func4() { cout << "Func4" << endl; } int _d = 0; }; typedef void(*pFun)(); void PrintFTB(pFun table[]) { } void Print() { //基类对象 Base b1; Base b2; //派生类对象 Derive d1; Derive d2; //去派生类对象虚函数表 cout<<"虚函数表的地址" << *(int*)&d1 << endl;//找到虚函数表的地址 int a = 0;//栈上创建的对象 int *p1 = &a; int *p2 = new int();//堆上创建的对象 const char* p3 = "hello"; printf("栈变量:%p\n", p1); printf("堆变量:%p\n", p2); printf("代码段常量:%p\n", p3); printf("虚函数地址:%p\n", &Derive::Func2 ); printf("普通函数:%p\n", Print); } int main() { Derive d1; Base b1; Print(); return 0; }
结果:
4.2多态的原理
那么我们研究了这么久,多态的原理到底是什么呢?下面的这个代码就是实现多态的代码:
#include<iostream>
using namespace std;
class Base
{
public:
virtual void Func1()
{
cout << "Base" <<endl;
}
virtual void Func2()
{
cout << "Func2" << endl;
}
void Func3()
{
cout << "Func3" << endl;
}
int _b = 0;
};
class Derive:public Base
{
public:
virtual void Func1()
{
cout << "Derive" << endl;
}
virtual void Func4()
{
cout << "Func4" << endl;
}
int _d = 0;
};
void Fun(Base&b)//多态的实现
{
b.Func1();
}
int main()
{
//基类对象
Base b2;
//派生类对象
Derive d1;
Fun(b2);
Fun(d1);
return 0;
}
这里的Fun函数传Base类的对象调用Base::Func1,传Derive类的对象调用Derive::Func1。
如图:
这里观察这幅图中的红色箭头我们发现b指向Base对象时,调用的就是Base类的虚函数,
观察这幅图中的蓝色箭头,我们可以看到b指向Derive对象时,调用的就是Derive类的虚函数。
这样就实现了不同对象去执行同一行为时,展现出不同的形态。
实际上就是在调用时,编译会去b指向的对象的虚函数表中去找要调用的虚函数,因为派生类的虚函数已经被重写了,所以派生类虚函数表中存放的是重写以后的虚函数的地址,当然就实现指向谁就调用谁的虚函数了。
反过来我们思考一下,为什么达到多态要满足两个条件,一个是虚函数的重写,另一个是基类对象的引用或者指针来调用虚函数。反思一下为什么?
再通过下面的汇编代码分析一下
// b中存的是b2对象的引用,将p移动到eax中
001940DE mov eax,dword ptr [b]
// [eax]就是取eax值指向的内容,这里相当于把b2对象头4个字节(虚表指针)移动到了edx
001940E1 mov edx,dword ptr [eax]
// [edx]就是取edx值指向的内容,这里相当于把虚表中的头4字节存的虚函数指针移动到了eax
00B823EE mov eax,dword ptr [edx]
// call eax中存虚函数的指针。这里可以看出满足多态的调用,不是在编译时确定的,是运行起来
以后到对象的中取找的。
001940EA call eax
00头1940EC cmp esi,esp
可以看出满足多态以后的函数调用,不是在编译时确定的而是在运行时去到对象中去找的。不满足多态的函数调用是在运行时确定的。
4.3动态绑定和静态绑定
1.静态绑定:又叫早期绑定,是在程序编译的时候确定程序的行为,也称静态多态。比如函数重载。
2.动态绑定:又称后期绑定,是在程序运行期间,根据具体拿到的类型来确定程序执行的功能,调节具体的函数,也称动态多态。
5.多态在单继承和多继承中
需要注意的是在单继承和多继承关系中,派生类的虚函数表是有所差异的。 下面我们一起来研究一下:
5.1单继承中的虚函数表
我们一起来看看单继承中的虚函数表,测试代码:
#include<iostream> #include<stdio.h> using namespace std; class Base { public: virtual void Fun1() { cout << "Base---Fun1" << endl; } virtual void Fun2() { cout << "Base---Fun2" << endl; } }; class Derive:public Base { public: virtual void Fun1() { cout << "Derive---Fun1" << endl; }//重写了Fun1 virtual void Fun3() { cout << "Derive---Fun3" << endl; } virtual void Fun4() { cout << "Derive---Fun4" << endl; } }; int main() { Derive d1; return 0; }
如图:
这里我们发现d1对象的虚表里面应该有四个虚函数的地址,但是现在为什么只有两个了,其实有时候监视窗口也会骗人的。因为监视窗口也是被优化了的,如果我们想看看不同的对象的虚表里面的内容,可以自己写一个程序来观察一下,代码如下:
typedef void(*pFun)();//对函数指针重新起名字 void PrintVFTable(pFun* pTable) { for (size_t i = 0; pTable[i] != 0; ++i) { printf("pFunarr[i]:%p", i, pTable[i]); pFun f = pTable[i];//调用虚表中的虚函数 f(); } } / 1.先取b的地址,强转成一个int*的指针 // 2.再解引用取值,就取到了b对象头4bytes的值,这个值就是指向虚表的指针 // 3.再强转成pFun*,因为虚表就是一个存pFun类型(虚函数指针类型)的数组。 // 4.虚表指针传递给PrintVTable进行打印虚表 // 5.需要说明的是这个打印虚表的代码经常会崩溃,因为编译器有时对虚表的处理不干净,虚表最 //后面没有放nullptr,导致越界,这是编译器的问题。我们只需要点目录栏的-生成-清理解决方案,再 //编译就好了。 int main() { Derive d1; Base b1; PrintVFTable( (pFun*) (*(int*)&d1) );//取出对象的前四个字节找到虚表 PrintVFTable((pFun*)(*(int*) &b1 )); //打印Base的续表 return 0; }
如图:
如果是单继承我们发现 ,派生类继承了基类的虚函数表,如果派生类的对基类的虚函数进行了重写派生类的虚函数的指针就会覆盖虚表中本来存放基类虚函数的地方。将派生类中其他的虚函数都放到这个续表中。
5.2多继承中的虚函数表
测试代码:
#include<iostream>
using namespace std;
class Base1
{
public:
virtual void Fun1() { cout << "Base1---Fun1" << endl; }
virtual void Fun2() { cout << "Base1---Fun2" << endl; }
int _b1;
};
class Base2
{
public:
virtual void Fun1() { cout << "Base1---Fun1" << endl; }
virtual void Fun2() { cout << "Base1---Fun2" << endl; }
int _b2;
};
class Derive :public Base1,public Base2
{
public:
virtual void Fun1() { cout << "Derive---Fun1" << endl; }
virtual void Fun3() { cout << "Derive---Fun3" << endl; }
virtual void Fun4() { cout << "Derive---Fun4" << endl; }
int _d;
};
typedef void(*pFun)();//对函数指针重新起名字
void PrintVFTable(pFun* pTable)
{
for (size_t i = 0; pTable[i] != 0; ++i)
{
printf("第%d个虚函数的地址:%p", i, pTable[i]);
pFun f = pTable[i];//调用虚表中的虚函数
f();
}
}
int main()
{
Derive d1;
PrintVFTable((pFun*)(*(int*)&d1));//取出对象的前四个字节找到虚表
cout << endl;
PrintVFTable((pFun*)(*(int*)( (char*)&d1+sizeof(Base1)) )); //打印Base2的虚表
return 0;
}
如图:
可以看出多继承派生类的未重写的虚函数放在第一个继承的基类的虚函数表中。派生类对于继承的两个基类的虚函数都进行了重写。
总结
这里有一些练习题有兴趣的同学可以练习一下:
1. 下面哪种面向对象的方法可以让你变得富有( )
A: 继承 B: 封装 C: 多态 D: 抽象
2. ( )是面向对象程序设计语言中的一种机制。这种机制实现了方法的定义与具体的对象无关,而对方法的调用则可以关联于具体的对象。
A: 继承 B: 模板 C: 对象的自身引用 D: 动态绑定
3. 面向对象设计中的继承和组合,下面说法错误的是?()
A:继承允许我们覆盖重写父类的实现细节,父类的实现对于子类是可见的,是一种静态复
用,也称为白盒复用
B:组合的对象不需要关心各自的实现细节,之间的关系是在运行时候才确定的,是一种动
态复用,也称为黑盒复用
C:优先使用继承,而不是组合,是面向对象设计的第二原则
D:继承可以使子类能自动继承父类的接口,但在设计模式中认为这是一种破坏了父类的封
装性的表现
4. 以下关于纯虚函数的说法,正确的是( )
A:声明纯虚函数的类不能实例化对象 B:声明纯虚函数的类是虚基类
C:子类必须实现基类的纯虚函数 D:纯虚函数必须是空函数
5. 关于虚函数的描述正确的是( )
A:派生类的虚函数与基类的虚函数具有不同的参数个数和类型 B:内联函数不能是虚函数
C:派生类必须重新定义基类的虚函数 D:虚函数可以是一个static型的函数
6. 关于虚表说法正确的是( )
A:一个类只能有一张虚表
B:基类中有虚函数,如果子类中没有重写基类的虚函数,此时子类与基类共用同一张虚表
C:虚表是在运行期间动态生成的
D:一个类的不同对象共享该类的虚表
7. 假设A类中有虚函数,B继承自A,B重写A中的虚函数,也没有定义任何虚函数,则( )
A:A类对象的前4个字节存储虚表地址,B类对象前4个字节不是虚表地址
B:A类对象和B类对象前4个字节存储的都是虚基表的地址
C:A类对象和B类对象前4个字节存储的虚表地址相同
D:A类和B类虚表中虚函数个数相同,但A类和B类使用的不是同一张虚表
8. 下面程序输出结果是什么? ()#include<iostream>
using namespace std;
class A {
public:
A(const char* s) { cout << s << endl; }
~A() {}
};
class B :virtual public A
{
public:
B(const char* s1,const char* s2) :A(s1) { cout << s2 << endl; }
};
class C :virtual public A
{
public:
C(const char* s1, const char* s2) :A(s1) { cout << s2 << endl; }
};
class D :public B, public C
{
public:
D(const char* s1, const char* s2,const char* s3,const char* s4) :B(s1, s2), C(s1, s3), A(s1)
{
cout << s4 << endl;
}
};
int main(){
D* p = new D("class A", "class B", "class C", "class D");
delete p;
return 0;
}A:class A class B class C class D B:class D class B class C class A
C:class D class C class B class A D:class A class C class B class D
9. 多继承中指针偏移问题?下面说法正确的是( )
A:p1 == p2 == p3 B:p1 < p2 < p3 C:p1 == p3 != p2 D:p1 != p2 != p3
10. 以下程序输出结果是什么()
class A
{
public:
virtual void func(int val = 1){ std::cout<<"A->"<< val <<std::endl;}virtual void test(){ func();}
};
class B : public A
{
public:
void func(int val=0){ std::cout<<"B->"<< val <<std::endl; }
};
int main(int argc ,char* argv[])
{
B*p = new B;
p->test();
return 0;
}A: A->0 B: B->1 C: A->1 D: B->0 E: 编译出错 F: 以上都不正确
问答题
1. 什么是多态?
2. 什么是重载、重写(覆盖)、重定义(隐藏)?
3. 多态的实现原理?答:参考本节课件内容
4. inline函数可以是虚函数吗?答:可以,不过编译器就忽略inline属性,这个函数就不再是
inline,因为虚函数要放到虚表中去。
5. 静态成员可以是虚函数吗?答:不能,因为静态成员函数没有this指针,使用类型::成员函数
的调用方式无法访问虚函数表,所以静态成员函数无法放进虚函数表。
6. 构造函数可以是虚函数吗?答:不能,因为对象中的虚函数表指针是在构造函数初始化列表
阶段才初始化的。
7. 析构函数可以是虚函数吗?什么场景下析构函数是虚函数?答:可以,并且最好把基类的析
构函数定义成虚函数。
8. 对象访问普通函数快还是虚函数更快?答:首先如果是普通对象,是一样快的。如果是指针
对象或者是引用对象,则调用的普通函数快,因为构成多态,运行时调用虚函数需要到虚函
数表中去查找。
9. 虚函数表是在什么阶段生成的,存在哪的?答:虚函数表是在编译阶段就生成的,一般情况
下存在代码段(常量区)的。选择题答案:
A D C A B D D A C B