C++ 多态
一般我们问到c++的三大特性是什么?我们基本都知道封装,继承,多态,进一步问什么是多态?我们也能够回答说多态是通过虚函数实现的,是一种通过动态绑定实现对不同的类调用不同的函数接口,等等。。但是再继续问,什么是动态绑定,虚函数怎么实现的,等等,基本上不经常用c++的,就回答不出来了。
继承和多态的作用:
继承对程序编写的作用在于我们可以更容易的定义和其它类相似,但是不相同的新类
而多态的作用在于我们可以通过动态绑定,在使用这些类进行编写程序时,可以一定程度上忽略其区别。
虚函数:
c++中,基类为了将和类型相关的函数和派生类不做任何改变直接继承的类做区分,定义了虚函数。对于某些函数,基类希望派生类定义各自适合自身的版本,此时基类就会将这些函数声明成虚函数。
class Quote{
public:
string isbn() const;
virtual double net_price(size_t n) const;//定义虚函数,派生类各自去定义net_price
}
class Bulk_quote:public Quote{
public:
double net_price(size_t n) const override;
}
动态绑定:
c++中,我们在使用基类的引用(指针)调用虚函数时,就会发生动态绑定。所谓动态绑定,就是在运行时,虚函数会根据绑定对象的实际类型,选择调用函数的版本。
void print(const Quote &item,size_t n){
item.net_price(n)
}
综上,多态发生的条件有三个:
1. 基类中定义了虚函数
2. 派生类中对继承自基类的虚函数进行了覆盖(重写)
3. 存在继承关系
在这种情况下,使用父类的指针或者引用调用虚函数时,这个调用可能在运行时,绑定到不同的子类中,产生不同的行为。
#include <iostream>
using namespace std;
class Person
{
public:
virtual void print(){
std::cout << "I'm a person" << endl;
}
virtual void foo(){}
};
class Chinese : public Person
{
public:
virtual void print(){
std::cout << "I'm as Chinese" << endl;
}
virtual void foo2(){}
};
class American : public Person{
public:
virtual void print(){
std::cout << "I'm as American" << endl;
}
};
//reference
void printPerson(Person& person){
person.print();
}
//pointer
void printPerson(Person* p){
p->print();
}
int main()
{
Person p;
Chinese c;
American a;
printPerson(&p);
printPerson(&c);
printPerson(&a);
return 0;
}
虚函数动态绑定流程
4.3C++动态绑定和虚函数表vtablewww.jianshu.com
前面说了我们的动态绑定就是基类的指针或者引用会在运行时,根据对象的实际类型选择相应的函数版本。那么c++是怎么样去具体做这个事情的呢。这里简单介绍一下:
虚函数表:要讲清楚动态绑定的流程,首先需要了解清楚类里面虚函数表的定义,虚函数表是记录虚函数入口地址的一串数组(实际是一个函数指针数组),它只存在于定义了虚函数的类里面,这里给出一个例子来展示类中虚函数表的存在:
class A{
//空类的大小为1
};
class B{
int m; //大小为4,成员变量会占用类空间
};
class C{
int m;
void f(); //大小为4,非虚函数不会占用类空间
};
class D{
int m;
virtual void f(); //大小为16,定义虚函数会增加类的大小,但是这个增加不是虚函数带来了的,而是指向虚函数表的指针所占的空间
};
class E{
int m;
virtual void f();//大小为16,从这儿可以看到,类里面的多个虚函数都维护在一个虚函数表里面,只有一个指向虚函数表的指针
virtual void g();
};
int main() {
cout<<sizeof(A)<<endl;
cout<<sizeof(B)<<endl;
cout<<sizeof(C)<<endl;
cout<<sizeof(D)<<endl;
cout<<sizeof(E)<<endl;
//cout<<sizeof(B)<<endl;
return 0;
//实际上,虚函数不占用类对象的存储空间,所以含有一个以上的虚函数的类对象大小与仅含一个虚函数大小相同。同时,针对每个类,只维护一个【虚函数表(函数指针数组数组)】用于存放该类中虚函数的地址,每个【含一个及以上虚函数的对象都会含有一个指向该类虚函数表的指针】。
动态绑定虚函数工作原理: 前面提到,每一个类会为所有虚函数数维护一个虚函数表,并且有一个指针指向这个表的首地址,这个虚函数表记录着所有的虚函数的入口地址。那么在动态绑定时,编译器是如何给基类指针返回一个正确的函数入口地址,这里给出一个例子:
class A{
protected:
int a1;
int a2;
public:
virtual void display(){ cout<<"A::display()"<<endl;}
virtual void clone(){ cout<<"A::clone()"<<endl;}
};
class B: public A{
protected:
int b;
public:
virtual void display(){ cout<<"B::display()"<<endl;} override
virtual void init(){ cout<<"B::init()"<<endl;}
};
class C: public B{
protected:
int c;
public:
virtual void display(){ cout<<"C::display()"<<endl;} override
virtual void execute(){ cout<<"C::execute()"<<endl;}
virtual void init(){cout<<"C::init()"<<endl;} override
};
//https://www.jianshu.com/p/fa50296b301c
这里给出A,B,C三个类的内存情况,如下图。可以得出这么几个结论:
- 类的内存占用由成员变量和指向虚函数表的指针组成,同时派生类的成员变量是会把基类的成员变量都继承的
- 同名虚函数在基类和派生类中的虚函数表中,索引是一致的,如下图,A,B,C的display的索引都是0
- 派生类中,一旦对基类中的虚函数进行了覆盖,那么派生类的虚函数表中响应函数的入口地址会被替换成覆盖后的函数的地址。
- 一旦有新的虚函数定义,会加入到当前虚函数表的末端。
上面是虚函数表在继承关系中的更新过程,在实际使用过程中,假定指针p调用虚函数display(), 发生调用时:
- 首先会找到函数的索引,这里display索引是0
- 然后编译器会做一个替换,(*(p->vptr)[0]),找到p指针的函数入口地址
- 程序运行后会执行这条语句,完成函数的调用,这就是动态绑定。
对于不同的虚函数,仅仅就是索引的不同。