✨前言:本文会对C++中多态这个概念进行详细介绍,通过用法和原理两个方面进行阐述,主要会对多态的概念,多态的实现和原理,以及对单继承和多继承中的虚函数表模型进行分析,还介绍了C++11中的两个关键字
override
和final
的使用.
多态:通俗来讲,就是多种形态,同一件事情,由不同类型的对象去完成时,会表现出多种状态.
比如,在日常生活中,当我们去旅游景点游玩时,需要买票,买票时的票价会根据买票人的身份不同,所对应的价格也就不一样,当我们是普通人时,可能会是正常票价,是学生时,可能会打五折,是儿童时,会免门票.
那么,我们将这个动作,用一个BuyTicket()
的函数来表示,当我们用不同的身份(对象)去调用这个函数时,它会表现出三种不同的价格(状态).这就是多态.
C++多态意味着调用成员函数时,会根据调用成员函数的对象的类型来执行不同的函数,并产生不同的行为,比如Student
类继承了Person
类,Student
类买票半价,Person
类买票全价.
看如下代码:
class Person
{
public:
//加virtual代表虚函数,下文中会讲解
virtual void BuyTicket()
{
cout << "Person::全价" << endl;
}
};
class Student : public Person
{
public:
virtual void BuyTicket()
{
cout << "Student::半价" << endl;
}
};
class Children : public Person
{
public:
virtual void BuyTicket()
{
cout << "Children::免门票" << endl;
}
};
int main()
{
Person* p;
Student s;
Children c;
//指向Student
p = &s;
p->BuyTicket();
//指向Children
p = &c;
p->BuyTicket();
return 0;
}
执行结果如下:
可以看到,我们使用同一个父类指针去分别指向不同的子类对象时,并且调用BuyTicket()
函数时,分别调用了Student
和Children
子类的BuyTicket()
函数.这便是多态的实现方式.
所以,我们也可以得到构成多态的两个条件:
1.必须通过基类的指针或引用调用虚函数
2.被调用的函数必须是虚函数,且派生类必须对基类的虚函数进行重写.
在C++中,基类将类型相关的函数和派生类不做改变直接继承的函数区分对待.对于某些函数,基类希望它的派生类各自定义合适自己的版本,此时基类就将这些函数声明成虚函数.
虚函数:被Virtual
修饰的函数即为虚函数
虚函数的重写(覆盖):派生类中有一个跟基类完全相同的虚函数(派生类的虚函数与基类虚函数的返回值,函数名,形参列表完全相同),称子类的虚函数重写了基类的虚函数.
在C++中,我们可以看到,对于虚函数重写的要求是比较严格的,要求三同,但在实际当中,往往会因为不小心可能会将派生类中的要重写的虚函数的函数名或者参数列表没有写成相同的,从而定义了一个新的函数,这种情况编译器也不会报错,只有当我们自己去运行发现结果不对时才能反应到,因此:C++11
就提供了两个关键字:override
来帮助用户检测是否被重写.
1.override
:检查派生类是否重写了基类中的某个虚函数,如果没有则编译报错
class Person
{
public:
virtual void BuyTicket()
{
cout << "Person::全价" << endl;
}
};
class Student : public Person
{
public:
virtual void BuyTicket() override
{
cout << "Student::半价" << endl;
}
};
如果我们使用override
来标记某个函数,但这个函数并没有重写基类中的某个虚函数,则编译器就会报错.
2.final
:修饰虚函数,表明该虚函数不能被重写.
class Person
{
public:
virtual void BuyTicket() final
{
cout << "Person::全价" << endl;
}
};
class Student : public Person
{
public:
//会编译报错,Person中的BuyTicket()函数已经被声明成final
virtual void BuyTicket() override
{
cout << "Student::半价" << endl;
}
};
如果我们把函数定义成了final
,之后任何想要覆盖该函数的操作都将引发错误.
对于这三种概念的对比,用一张图来表示:
通过这张图,对于这三个概念的理解可以进一步的加深.
我们来看这样一个场景,比如我们现在要实现一个书店卖书折扣的问题,假如我们需要书店程序支持不同的折扣策略,例如我们可能提供购买量不超过某个限额时,可以享受折扣,一旦超过,均按原价支付,或者只有购买量超过一定数量时,所有书籍均享受折扣,否则全都不打折.
于是我们可以定义一个类Dis_quote
来支持不同的折扣策略,Dis_quote
负责保存购买量的值和折扣值,其他的表示某种特定策略的类将继承自Dis_quote
.
另外,每个派生类通过定义自己的strategy()
函数来实现不同的折扣策略.
class Dis_quote
{
public:
Dis_quote(std::size_t amount = 0, double discount = 0.0)
: _amount(amount)
, _discount(discount)
{}
virtual void strategy()
{
cout << "负责提供给子类继承的购买量和折扣值、不提供折扣策略" << endl;
}
protected:
std::size_t _amount; //购买量
double _discount; //折扣值
};
class Bulk_quote : public Dis_quote
{
public:
Bulk_quote(std::size_t amount = 100, double discount = 8.8)
:Dis_quote(amount, discount)
{}
virtual void strategy()
{
cout << "Bulk_quote策略:当购买量超过amount(如果未指定就是100)本时,打8.8折" << endl;
}
};
class Less_quote : public Dis_quote
{
public:
Less_quote(std::size_t amount = 50, double discount = 8.8)
:Dis_quote(amount, discount)
{}
virtual void strategy()
{
cout << "Less_quote策略:当购买量小于amount(如果未指定就是50)时,打8.8折,一旦大于amount,按原价" << endl;
}
};
显然,Dis_quote
类与任何特定的折扣策略都无关,因此Dis_quote
中的strategy()
函数是没有实际含义的.
所以对于Dis_quote
类,我们并不希望去直接使用它,因为这毫无意义,我们也不希望Dis_quote
类创建对象,Dis_quote
表示的是一种通用概念,而不是某种具体折扣策略.
此时,我们就可以将Dis_quote
类中的strategy()
函数设置成纯虚函数,从而实现我们的设计意图.
在函数体的位置(在声明语句的分号之前),写上= 0
,这个函数就为纯虚函数. 包含纯虚函数的类叫抽象类,抽象类不能实例化出对象.
注意: 由于虚函数的继承为接口继承,所以继承了具有纯虚函数的基类的派生类也不能实例化对象,只有重写虚函数,派生类才能实例化出对象,纯虚函数规范了派生类必须重写,纯虚函数也体现了虚函数的接口继承.
class Dis_quote
{
public:
Dis_quote(std::size_t amount = 0, double discount = 0.0)
: _amount(amount)
, _discount(discount)
{}
//纯虚函数
virtual void strategy() = 0;
protected:
std::size_t _amount; //购买量
double _discount; //折扣值
};
class Base
{
public:
virtual void func1()
{
cout << "func1()" << endl;
}
private:
int _a;
};
int main()
{
Base b;
cout<<sizeof(b)<<endl;
return 0;
}
运行结果:
我们先来看一段代码,我们定义了一个Base
基类,然后通过sizeof
计算基类大小,通过运行结果我们发现基类大小为8bytes
,也就是说,除了一个_a
成员,还多了一个_vfptr
放在了对象的前面(某些平台可能会放在对象的最后面,这个与平台有关),对象中的这个指针叫做虚函数表指针(v代表virtual,f代表function).
一个含有虚函数的类中都至少有一个虚函数表指针,因为虚函数的地址要被放到虚函数表中,虚函数表也简称虚表.
这是在VS
监视窗口下观察到的b
对象内部结构.
那么如果是一个继承了这个基类的派生类,派生类的虚函数表又是怎样的结构呢?我们将上边的代码进行改造然后接着分析:
通过对上面的代码进行改造:
Derive
类继承Base
类Derive
类中重写func1
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 _a = 1;
};
class Derive : public Base
{
public:
virtual void func1()
{
cout << "Derive::func1()" << endl;
}
private:
int _b = 1;
};
int main()
{
Base b;
Derive d;
return 0;
}
紧接着我们再使用VS
的监视窗口去观察b
对象和d
对象的内部结构:
通过观察,我们发现了以下几点:
d
中也有一个虚表指针,d
对象由两部分构成,一部分是从Base
中继承而来的成员,另一部分是Derive
类自己的成员.b
对象和派生类d对象虚表是不一样的,这里我们发现func1
完成了重写,所以d的虚表中存的是重写的Derive::func1
,所以虚函数的重写也叫作覆盖,覆盖就是指虚表中虚函数的覆盖。重写是语法的叫法,覆盖是原理层的叫法.func2
继承下来后是虚函数,所以放进了虚表,func3
也继承下来了,但是不是虚函数,所以不会放进虚表.nullptr
.注意:这里有一个容易混淆的地方,虚函数存在哪里?虚表存在哪里?
可能会有类似:虚函数存在虚表中,虚表存在对象中. 而这样的回答是错误的.
注意虚表存的是虚函数指针,不是虚函数,虚函数和普通函数一样,都是存在代码段中的,只是它的指针又存在了虚表中.另外对象中存的不是虚表,存的是虚表指针.那虚表是存在哪里的呢?
我们可以写一段代码来验证:
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 _a = 1;
};
class Derive : public Base
{
public:
virtual void func1()
{
cout << "Derive::func1()" << endl;
}
private:
int _b = 1;
};
int a = 0;
int main()
{
Base b1;
int b = 0; //栈区
static int c = 0; //静态区
int* d = new int[10]; //堆区
const char* str = "hello world"; // 常量区/代码段
printf("栈区:%p\n", &b);
printf("静态区/数据段:%p\n", &a);
printf("静态区/数据段:%p\n", &c);
printf("堆区:%p\n", d);
printf("常量区/代码段:%p\n", str);
printf("虚表:%p\n", (*((int*)&b1)));
return 0;
}
我们通过查看虚表的地址和哪一个存储区域的数据地址非常接近来判断,在对虚表找地址时,由于虚表指针一般存在对象的前4
个字节中,所以我们可以先将b1
对象的地址转换为int*
的地址以便我们在解引用时能够取出前4
个字节的内容也就是虚表指针.
运行结果如下:
从运行结果我们可以明显可以看出:虚表的地址和常量区/代码段的地址很相近,所以我们也可以推断出虚表是存放在代码段/常量区.
从我们上面的分析,那么多态的原理到底是什么呢?
我们先来看一段代码:
class Person
{
public:
virtual void BuyTicket()
{
cout << "成人-全价" << endl;
}
};
class Student : public Person
{
public:
virtual void BuyTicket()
{
cout << "学生-半价" << endl;
}
};
void Func(Person* people)
{
people->BuyTicket();
}
int main()
{
Person p;
//传入p->调用Person::BuyTicket
Func(&p);
//传入s->调用Student::BuyTicket
Student s;
Func(&s);
return 0;
}
运行结果:
也就是说,对于Func
函数内部利用Person类型的指针去调用BuyTicket()
函数,当people
指针指向p
对象时,就调用Person
中的BuyTicket
,当people
指针指向s
对象时,就调用Student
中的BuyTicket
.
people
是指向p
对象时,people->BuyTicket
在p
的虚表中找到虚函数是Person::BuyTicket
.people
是指向s
对象时,people->BuyTicket
在s
的虚表中找到虚函数是Student::BuyTicket
.
下面我们来看汇编代码分析:
传入s
对象时也是类似.
下面我们再来看普通函数的调用:
我们用p对象调用BuyTicket
函数,首先BuyTicket
虽然是虚函数,但是p
是对象,不满足多态的条件,所以这里是普通函数的调用转换成地址时,是在编译时已经从符号表确认了函数的地址,直接call
地址.
int main()
{
Person p;
p.BuyTicket();
//传入p->调用Person::BuyTicket
Func(&p);
//传入s->调用Student::BuyTicket
Student s;
Func(&s);
return 0;
}
这里我们也看见了,直接call
地址.
上面的汇编代码已经很好解释了什么是静态绑定和动态绑定.
在这里,我们需要关注的是派生类对象的虚表模型,因为基类的虚表结构我们已经看过.
我们先来看一段代码:
class Base
{
public:
virtual void func1()
{
cout << "Base::func1()" << endl;
}
virtual void func2()
{
cout << "Base::func2()" << endl;
}
private:
int _a = 1;
};
class Derive : public Base
{
public:
virtual void func1()
{
cout << "Derive::func1()" << endl;
}
virtual void func3()
{
cout << "Derive::func3()" << endl;
}
virtual void func4()
{
cout << "Derive::func4()" << endl;
}
private:
int _b = 1;
};
int main()
{
Base b;
Derive d;
return 0;
}
当我们使用visual studio2019的监视窗口去观察b对象和d对象的内部结构时:
派生类中func3
和func4
也是虚函数,但是监视窗口中我们发现看不见func3
和func4
,这里是编译器的监视窗口故意隐藏了这两个函数,也可以认为是他的一个小bug
。那么我们如何查看d
的虚表呢?下面我们使用代码打印出虚表中的函数:
class Base
{
public:
virtual void func1()
{
cout << "Base::func1()" << endl;
}
virtual void func2()
{
cout << "Base::func2()" << endl;
}
private:
int _a = 1;
};
class Derive : public Base
{
public:
virtual void func1()
{
cout << "Derive::func1()" << endl;
}
virtual void func3()
{
cout << "Derive::func3()" << endl;
}
virtual void func4()
{
cout << "Derive::func4()" << endl;
}
private:
int _b = 1;
};
typedef void (*VFPTR) ();
void PrintVFTable(VFPTR vfTable[])
{
printf("vfptr虚表地址:%p\n", vfTable);
for (size_t i = 0; vfTable[i] != nullptr; i++)
{
printf("第[%d]个虚函数地址->%p\n", i + 1, vfTable[i]);
VFPTR f = vfTable[i];
f();
}
}
int main()
{
Base b;
Derive d;
PrintVFTable((VFPTR*)(*((int*)&b)));
cout << "-------------------------" << endl;
PrintVFTable((VFPTR*)(*((int*)&d)));
return 0;
}
运行结果:
结果说明func3
和func4
作为虚函数,它们的地址是被放在虚表中的.
class Base1 {
public:
virtual void func1()
{
cout << "Base1::func1" << endl;
}
virtual void func2()
{
cout << "Base1::func2" << endl;
}
private:
int b1;
};
class Base2 {
public:
virtual void func1()
{
cout << "Base2::func1" << endl;
}
virtual void func2()
{
cout << "Base2::func2" << endl;
}
private:
int b2;
};
class Derive : public Base1, public Base2
{
public:
virtual void func1()
{
cout << "Derive::func1" << endl;
}
virtual void func3()
{
cout << "Derive::func3" << endl;
}
private:
int d1;
};
typedef void (*VFPTR) ();
void PrintVFTable(VFPTR vfTable[])
{
printf("vfptr虚表地址:%p\n", vfTable);
for (size_t i = 0; vfTable[i] != nullptr; i++)
{
printf("第[%d]个虚函数地址->%p\n", i + 1, vfTable[i]);
VFPTR f = vfTable[i];
f();
}
}
int main()
{
Derive d;
PrintVFTable((VFPTR*)(*((int*)&d)));
cout << "---------------------" << endl;
PrintVFTable((VFPTR*)(*(int*)((char*)&d + sizeof(Base1))));
return 0;
}
在多继承中,Derive类中会存在两个虚表指针,其模型如下:
也就是说,在d
对象中,包含两部分,一部分是从Base1
中继承而来的,另一部分从Base2
继承来的,并且多继承派生类的未重写的虚函数放在第一个继承基类部分的虚函数表中.
运行上面的代码:
其中PrintVFTable((VFPTR*)(*(int*)((char*)&d + sizeof(Base1))));
这句代码,其实是从d
对象的起始地址处,跳过Base1
部分,也就是跳转到Base2
部分的起始地址处,从而打印Base2
部分的虚表.
由Base2
虚表的打印结果我们也可以看到,Base2
部分的虚函数func1
也被Derive::func1
重写.
由上述代码的运行结果也验证了我们的结论.