👍作者主页:进击的1++
🤩 专栏链接:【1++的C++进阶】
文章目录
- 一,什么是多态?
- 二,剖析多态的调用原理
- 三,抽象类
- 四,多继承中的虚函数表
一,什么是多态?
多态的定义:不同继承关系的类对象,去调用同一个函数,产生不同的行为。再说通俗点就是:一个行为,不同的对象去做会产生不同的结果。
构成多态的条件:
- 被调用的函数必须是虚函数,且派生类必须对基类的虚函数进行重写
重写的条件:
- 是虚函数 (被virtual修饰的成员函数)
- 三同(函数名,参数,返回值)
特例:
- 子类虚函数不加virtual依旧构成重写。
- 重写的协变:返回值可以不同,但必须是父子关系的指针或引用
- 如果基类的析构函数为虚函数,此时派生类析构函数只要定义,无论是否加virtual关键字,
都与基类的析构函数构成重写。虽然函数名不相同,看起来违背了重写的规则,其实不然,这里可以理解为编译器对析构函数的名称做了特殊处理,编译后析构函数的名称统一处理成destructor
而且不符合重写就是隐藏关系,这也是我们判断隐藏关系的条件之一。
2. 必须是基类的指针或引用去调用虚函数。
实例如下:
class A
{
public:virtual void Func1(){cout << "A::Func1()" << endl;}
protected:int _a;
};class B:public A
{
public:virtual void Func1(){cout << "B::Func1()" << endl;}
protected:int _b;
};
void Test1()
{B t1;A& a1 = t1;a1.Func1();A* ptra = &t1;ptra->Func1();
}
上面还遗留一个问题,为什么要把析构函数重写。
来看以下代码:
class A
{
public:virtual ~A(){cout << "~A()" << endl;}A& operator=(const A& a){cout << "A::operator=()" << endl;return *this;}virtual void Func1(){cout << "A::Func1()" << endl;}
protected:int _a;
};class B:public A
{
public:virtual ~B(){cout << "~B()" << endl;}/*B& operator=(const B& a){A::operator=(*this);cout << "B::operator=()" << endl;return *this;}*/virtual void Func1(){cout << "B::Func1()" << endl;}
protected:int _b;
};
void Test1()
{A* ptra1 = new A;A* ptra2 = new B;delete ptra1;delete ptra2;
}
若析构函数没有重写:
我们可以看到new的B类类型的对象没有调用自己的析构而是其指针类的析构函数。这就会存在空间没有释放的问题,造成内存泄漏。
若构成重写后:
我们可以看到new出的B对象调用了自己的析构函数。并且上一篇文章我们讲过,子类对象的析构会先调用自己的构造函数,然后调用父类的构造函数。
这里我们做一个小的总结:通过上述的两段代码,我们会发现,多态调用重写函数,指向哪个对象就去调用哪个对象的重写函数。
二,剖析多态的调用原理
首先我们先来回答这么一个问题:
class A
{
public:virtual void Func1(){cout << "A::Func1()" << endl;}virtual int Func2(){cout << "A::Func2()" << endl;return 0;}
protected:int _a;
};class B:public A
{
public:virtual void Func1(){cout << "B::Func1()" << endl;}protected:int _b;
};void Test2()
{A t2;B t1;cout << sizeof(t1) << endl;}
上述代码中的sizeof的值为多少呢?
答案为12!!可能会有人疑惑,为什么不是8呢?
我们通过监视窗口来观察。
我们发现,除了子类的成员变量和继承的A的成员变量外还多了一个_vfptr。这是虚函数表指针。
那么这个表中放的是什么呢?
我们继续来看下面这段代码:
class A
{
public:virtual void Func1(){cout << "A::Func1()" << endl;}virtual int Func2(){cout << "A::Func2()" << endl;return 0;}virtual int Func4(){cout << "A::Func4()" << endl;return 0;}
protected:int _a;
};class B:public A
{
public:virtual void Func1(){cout << "B::Func1()" << endl;}virtual int Func3(){cout << "B::Func3()" << endl;return 0;}virtual int Func4(){cout << "B::Func4()" << endl;return 0;}
protected:int _b;
};typedef void(*Vfptr)();void PrintVFptr(Vfptr* arr)
{for (int i = 0; arr[i] != nullptr; ++i){printf("vfptr[%d]->%p ", i, arr[i]);Vfptr pf = arr[i];pf();}
}void Test2()
{A t2;B t1;printf("B::vfptr\n");PrintVFptr((Vfptr*)*(int*)(&t1));printf("A::vfptr\n");PrintVFptr((Vfptr*)*(int*)(&t2));}
再解读前,我们先来说明一个东西,只要是该类的虚函数,就会被存入该类的虚函数表中,并且对于单继承来说,每个类只有一份虚函数表,子类继承了父类的虚函数表,并且将重写的虚函数覆盖为自己的。也就是说子类的虚函数表是:继承父类的并进行重写覆盖后+自己的虚函数。
我们回来来解读上结果:Func1,Func4都进行了重写,所以我们发现其A与B打印出的函数地址不同。而Func2是通过继承下来的虚函数,但并没有进行重写,会存在虚表中,因此其在A和B的虚表中的函数指针相同。Func3则是子类中独有的虚函数,因此只在子类的需表中有。
所以多态的原理是:在编译阶段会形成虚函数表,在调用构造函数的初始化列表阶段会对虚函数表进行初始化。当程序运行后,在指向对象的虚函数表中去找对应的虚函数,这也是为什么我们前面说指向谁就调用谁,而对于普通函数来说,其在编译阶段就已经确定了调用谁。
动态绑定与静态绑定
-
静态绑定又称为前期绑定(早绑定),在程序编译期间确定了程序的行为,也称为静态多态,比如:函数重载。
-
动态绑定又称后期绑定(晚绑定),是在程序运行期间,根据具体拿到的类型确定程序的具体行为,调用具体的函数,也称为动态多态。
三,抽象类
什么叫抽象类?
在虚函数的后面写上 =0 ,则这个函数为纯虚函数。包含纯虚函数的类叫做抽象类(也叫接口类),抽象类不能实例化出对象。派生类继承后也不能实例化出对象,只有重写纯虚函数,派生类才能实例化出对象。纯虚函数规范了派生类必须重写,另外纯虚函数更体现出了接口继承。
那么什么是接口继承呢?
普通函数的继承是一种实现继承,派生类继承了基类函数,可以使用函数,继承的是函数的实现。虚函数的继承是一种接口继承,派生类继承的是基类虚函数的接口,目的是为了重写,达成多态。
因此如果不实现多态,不要把函数定义成虚函数。
我们来看一道例题:
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;}
问这道题的输出是什么?
答案是:B->1
这是为什么呢?
首先我们的我们用一个指向B对象的指针p去调用test函数,而test函数是父类中的虚函数,没有重写,会继承到子类B中,这时我们会忽略一个问题—this指针,test中的this指针是A类类型的指针,这符合构成多态的一个条件:父类指针或引用去调用虚函数。 我们将B*指针传过去后会发生切片,在test中调用func(),由于虚函数的继承是几口继承,因此其会继承父类的接口,用子类的实现。感觉像头和身子拼接起来的一样。因此,在这道题中会用到父类中func函数的缺省参数,而实现部分则用的是子类中的func。所以答案为:B->1。
四,多继承中的虚函数表
以以下代码为例:
class Base1
{
public:virtual void func1(){cout << "Base1::func1" << endl;}protected:int _a;};class Base2
{
public:virtual void func1(){cout << "Base2::func1" << endl;}virtual void func2(){cout << "Base2::func2" << endl;}protected:int _b;};class Boss:public Base1,public Base2
{
public:virtual void func1(){cout << "Boos::func1" << endl;}virtual void func2(){cout << "Boos::func2" << endl;}virtual void func3(){cout << "Boos::func3" << endl;}protected:int _c;};typedef void(*Vfptr)();void PrintVFptr(Vfptr* arr)
{for (int i = 0; arr[i] != nullptr; ++i){printf("vfptr[%d]->%p ", i, arr[i]);Vfptr pf = arr[i];pf();}
}int main()
{Base1 b1;Base2 b2;Boss d;cout << "Base1" << endl;PrintVFptr((Vfptr*)*(int*)(&b1));cout << "Base2" << endl;PrintVFptr((Vfptr*)*(int*)(&b2));cout << "Boos--1" << endl;PrintVFptr((Vfptr*)*(int*)(&d));cout << "Boos--2" << endl;PrintVFptr((Vfptr*)(*(int*)((char*)&d + sizeof(Base1))));}
通过上述窗口和打印出的结果看,我们发现多继承其有n个虚函数表,n与继承的父类个数有关。并且,子类自身的虚函数放在第一个虚表中,如上述func3()。
还有一个有趣的现象:
Base1 b1;Base2 b2;Boss d;Base1* ptr1 = &d;Base2* ptr2 = &d;ptr1->func1();ptr2->func1();
若我们用子类的地址赋值给不同的父类指针去调用func1()按理来说,其两个指针都指向同一个子类对象,并且func1()都进行了重写,因此ptr1,ptr2 , Boos虚表中的func1()的地址应该是一样的,但是结果却不一样,这是为什么呢?
下面是d对象的模型:
因此当ptr2去调用func1()时,其传过去的this指针,并不是d的起始地址,因此为了使其this指针变为d的this 指针,这里编译器会进行一个操作,计算出Base1的大小,将ptr2减去Base1的大小,这时this指针就指向了d的起始位置,自然调用的就是d对象中的func1()。
补充两个关键字:
- final:修饰虚函数,表示该虚函数不能再被重写。
- override: 检查派生类虚函数是否重写了基类某个虚函数,如果没有重写编译报错。