> 文章列表 > 【c++】多态

【c++】多态

【c++】多态

  1. 多态的概念

多态的概念:通俗来说,就是多种形态,具体点就是去完成某个行为,当不同的对象去完成时会

产生出不同的状态。

  1. 多态的定义及实现

多态是在不同继承关系的类对象,去调用同一函数,产生了不同的行为。比如Student继承了

Person。Person对象买票全价,Student对象买票半价。

那么在继承中要构成多态有两个条件:

1. 必须通过基类的指针或者引用调用虚函数

2. 被调用的函数必须是虚函数,且派生类必须对基类的虚函数进行重写

#include<iostream>
using namespace std;class Person
{
public:virtual void buy_ticket(){cout << "Person--全价" << endl;}};class Student: public Person
{
public:virtual void buy_ticket(){cout << "Student--半价" << endl;}
};class Teacher : public Person
{
public:virtual void buy_ticket(){cout << "Teacher--优先购票" << endl;}
};void Func(Person& p)  
{p.buy_ticket();
}int main()  
{Person p;Student s;Teacher t;Func(p);Func(s);Func(t);return 0;
}
【c++】多态

虚函数:即被virtual修饰的类成员函数称为虚函数。

虚函数的重写(覆盖):派生类中有一个跟基类完全相同的虚函数(即派生类虚函数与基类虚函数的

返回值类型、函数名字、参数列表完全相同),称子类的虚函数重写了基类的虚函数。

Student类和Teacher类继承了Person类并且在派生类中完成了对基类中的虚函数的重写,用这三个类创建出三个对象,调用Func函数,这个函数是用基类的引用接收,用这个引用调用buy_ticket()函数,就打印出三个对象的购票方式,完成了不同的行为,这就是多态的使用。

基类的指针调用虚函数:

void Func(Person* ptr)   //符合多态的条件 
{ptr->buy_ticket();
}
【c++】多态

这个也是可以的,符合多态的定义,但是如果是用以下的方式就不可以,Func函数是Person p对象接收的这属于派生类给基类赋值,用的方法是切割或者切片,调用buy_ticket()时是基类对象p调用的,所以打印出来的都是基类对象中的函数结果。


void Func(Person p)   //普通调用即将三个值都赋值给了p,所以说不构成多态的条件
{p.buy_ticket();
}
【c++】多态

虚函数中的两个特例:

1.协变:派生类重写基类虚函数时,与基类虚函数返回值类型不同。即基类虚函数返回基类对象的指

针或者引用,派生类虚函数返回派生类对象的指针或者引用时,称为协变。

class A{};
class B : public A {};
class Person {
public:virtual A* f() {return new A;}
};
class Student : public Person {
public:virtual B* f() {return new B;}
};

2.构造函数的重写

在之前的使用中类中的析构函数不用重写,但是在使用继承后析构函数不加virtual就有可能出现内存泄漏的问题,举例说明:

#include<iostream>
using namespace std;class Person
{
public:~Person(){delete[] _s;cout << "Person delete:" << _s << endl;}protected:int* _s = new int[20];};class Student :public Person
{
public:~Student(){delete[] _a;cout << "Student delete:" << _a << endl;}protected:int* _a = new int[10];
};int main()
{Person* ptr1 = new Person;Person* ptr2 = new Student;delete ptr1;delete ptr2;return 0;
}
【c++】多态

运行结果得出:没有调用Student的析构函数造成了内存泄露,使用delete会做两件事:1.使用指针调用析构函数,2.operator delete (ptr),调用析构函数时,指针是什么类型就调用什么析构函数属于普通调用,不是多态调用,所以都去调用了Person 的析构函数,造成内存泄漏。

这里期望ptr2能去调用Student的析构函数,是希望根据指针指向的对象决定调用该对象类型的析构函数,而不是根据指针的类型决定调用哪个类的析构函数,这属于多态调用。多态调用就需要对析构函数进行重写。首先重写需要的条件是虚函数和三同:1.函数名2.返回值3.参数,派生类和基类的函数名会被编译器特殊处理成destructor函数,析构函数无参数无返回值所以说在基类的析构函数前加virtual,就完成了对析构函数的重写。在调用派生类的析构函数后系统会自动调用基类的析构函数,修改代码后运行结果如下:

【c++】多态

3. C++11 override 和 final

构造不能被继承的类:

  1. 构造私有:将类的构造函数放到访问限定符private下,派生类继承了该基类就无法完成初始化动作,那继承这个类就没有什么意义了

  1. 在类名后加关键字final

【c++】多态
【c++】多态

final修饰类时类不能被继承,在修饰虚函数时虚函数不能被重写(特殊情况下使用)

【c++】多态
【c++】多态

override:检查派生类虚函数是否重写了基类某个虚函数,如果没有重写就编译报错。

class Car
{
public:virtual void Drive(){}
};
class Benz :public Car {
public:virtual void Drive() override {cout << "Benz-舒适" << endl;}
};

4.重写、重定义、重载的对比

【c++】多态

5.抽象类

在虚函数的后面写上 =0 ,则这个函数为纯虚函数。包含纯虚函数的类叫做抽象类(也叫接口

类),抽象类不能实例化出对象。派生类继承后也不能实例化出对象,只有重写纯虚函数,派生

类才能实例化出对象。纯虚函数规范了派生类必须重写,另外纯虚函数更体现出了接口继承。某种意义上来说抽象类强制需要重写虚函数。

接口继承:如纯虚函数,只提供类似于函数声明的接口,函数的实现需要在派生类中实现

实现继承:普通函数的继承是一种实现继承,派生类继承了基类函数,可以使用函数,继承的是函数的实

现。

6.多态的原理

观察下面这个类构造出对象后对象的大小结果

#include<iostream>
using namespace std;class Base
{
public:virtual void Func1(){cout << "Func1()" << endl;}
private:int _b = 1;
};
int main()
{Base b;cout << sizeof(b) << endl;return 0;
}
【c++】多态

在对象b中存了一个指针和一个成员变量,指针在32位平台下大小为4个字节,所以最终的大小为8.这个指针全称是虚函数表指针(v代表virtual,f代表function)。一个含有虚函数的类中都至少都有一个虚函数表指针,因为虚函数的地址要被放到虚函数表中,虚函数表也简称虚表。多态的实现就依靠虚表来完成。接着看一下派生类中的虚表:

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;Base* ptr = &b;ptr->Func3();ptr = &d;ptr->Func3();ptr = &b;ptr->Func1();ptr = &d;ptr->Func1();return 0;
}
【c++】多态

因为派生类中没有对Func3函数进行重写,所以调用的就是继承下来的基类中的Func3函数,而派生类中对Func1进行了重写(覆盖),所以再用父类的指针进行调用时访问的就是父类指针指向的对象的类型的函数,这就是多态调用。在监视窗口观察基类和派生类的虚函数表:

【c++】多态

虚函数表中存的是虚函数的地址,func3函数不是虚函数所以不进入虚函数表,只有virtual修饰的函数进入虚函数表,在窗口中可以观察到func2函数在基类和派生类的虚函数表中存的地址是相同的,派生类并没有对该函数进行重写所以就继承下来了,对于func1函数派生类完成了重写,所以在派生类中就将重写后的函数地址覆盖掉了原来基类中该函数的地址。Base* ptr1=&b 指的是父类对象。Base* ptr2=&d指的是子类中父类的那一部分,唯一的区别就是虚函数表的不同,继承后子类就会将父类的虚函数表进行重写(覆盖),保证了父类指针指向谁调用谁。

普通调用:编译时决议,属于静态绑定。

多态调用:运行时决议,属于动态绑定。

注意几点:

  1. 基类b对象和派生类d对象虚表是不一样的

  1. 不是虚函数不进虚函数表

3.虚表存的是虚函数指针,不是虚函数,虚函数和普通函数一样的,都是存在代码段的,只是

他的指针又存到了虚表中。另外对象中存的不是虚表,存的是虚表指针。

4.虚函数表本质是一个存虚函数指针的指针数组

5.虚表对于同一个类构造出来的对象是共享的。

7.单继承和多继承关系的虚函数表

  1. 单继承中的虚函数表

class Base { 
public :virtual void func1() { cout<<"Base::func1" <<endl;}virtual void func2() {cout<<"Base::func2" <<endl;}
private :int a;
};
class Derive :public Base { 
public :virtual void func1() {cout<<"Derive::func1" <<endl;}virtual void func3() {cout<<"Derive::func3" <<endl;}private :int b;
};
int main()
{Base b;
Derive d;return 0;}

通过调试查看d对象的虚函数表:

【c++】多态

对象d中存着虚函数表的指针,这个虚函数表的指针指向的虚函数表中存着虚函数的地址,因为派生类对func1函数完成了重写所以,函数指针数组中存的第一个函数地址就是重写后的地址,对func2函数没有进行重写所以函数地址就是基类的func2的函数地址。但是派生类中的func3函数也是虚函数,也应该进d对象的虚函数表,这里却没有打印出来。这是因为vs环境做了处理,现在调用内存窗口查看一下虚函数表指针指向的虚函数表的情况:

【c++】多态

调试发现d对象的func3函数的确被放进了虚函数表,并且放到了表的最下方,在vs 的环境下虚函数表是以空指针结尾的,但是Linux就不一定,虚函数表的结尾要以具体的平台决定。

2.打印虚函数表

#include<iostream>
using namespace std;typedef void(*VFPtr)();  //声明一个函数指针即 typedef void (*)() VFPTR;class Base {
public:virtual void func1() { cout << "Base::func1" << endl; }virtual void func2() { cout << "Base::func2" << endl; }
private:int a;
};
class Derive :public Base {
public:virtual void func1() { cout << "Derive::func1" << endl; }virtual void func3() { cout << "Derive::func3" << endl; }private:int b;
};void printvfptr(VFPtr vfp[])   //打印虚函数表
{for (int i = 0; vfp[i] != nullptr; ++i)  //在vs的平台下虚函数表是以nullptr结尾的,所以循环的判定方式可以用nullptr{                                       //但是linux平台不一定是这样,那判定循环的条件就不是这个printf("[%d]:%p\\t", i, vfp[i]);vfp[i]();}
}
int main()
{Base b;Derive d;printvfptr((VFPtr*)*(int**)&b);//虚函数表指针是在对象的头四个字节或头8个字节,具体要根据平台是多少位的,//所以将对象强制转换为(int**)再解引用就自适应了平台的环境,取出了虚表指针//打印函数是用VFPtr接收的所以再强制转换为VFPtr*cout << endl;printvfptr((VFPtr*)*(int**)&d);return 0;
}
【c++】多态

3.多继承中的虚函数表

#include<iostream>
using namespace std;
typedef void(*VFPtr)();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 _d;};void printfvfptr(VFPtr vfp[])
{for (int i = 0; vfp[i] != nullptr; ++i){printf("[%d]:%p", i, vfp[i]);vfp[i]();}printf("\\n");
}
int main()
{Base1 b1;Base2 b2;Derive d;//打印的是基类对象自己的虚函数表printfvfptr((VFPtr*)*(int*)&b1);   printfvfptr((VFPtr*)*(int*)&b2);//打印的时派生类对象中的两个基类中的虚函数表Base1* ptr1 = &d;printfvfptr((VFPtr*)*(int*)ptr1);Base2* ptr2 = &b2;printfvfptr((VFPtr*)*(int*)ptr2);//结果可以看到子类对象将自己独有的虚函数放到了第一个继承的基类的虚函数表中return 0;
}

结果:

【c++】多态

可以看到:多继承派生类的未重写的虚函数放在第一个继承基类部分的虚函数表中。

8.总结

  1. 内联函数可以是虚函数吗?

可以,但是失去了inline的属性(在调用时展开)因为虚函数的地址要放入虚函数表里,如果是多态调用就没有inline的属性了,如果是普通调用,可以保持inline属性。

2. 静态成员可以是虚函数吗?

不能,因为静态成员函数没有this指针,使用类型::成员函数 的调用方式无法访问虚函数表,所以静态成员函数无法放进虚函数表。

3. 构造函数可以是虚函数吗?

不能,因为对象中的 虚函数表指针 是在 构造函数初始化列表阶段才初始化的 。虚函数的意义是多态,多态调用时到虚函数表中去找,构造函数之前还没初始化,如何去找?

4. 析构函数可以是虚函数吗?什么场景下析构函数是虚函数?

可以,并且最好把基类的析构函数定义成虚函数。析构函数名统一会被处理成destructor()

5. 对象访问普通函数快还是虚函数更快?

首先如果是普通对象,是一样快的。如果是指针对象或者是引用对象,则调用的普通函数快,因为构成多态,运行时调用虚函数需要到虚函数表中去查找。

6. 虚函数表是在什么阶段生成的,存在哪的?

虚函数表是在编译阶段就生成的 ,一般情况下存在代码段(常量区)的。( 虚函数表指针初始化是指把虚函数表的指针放到对象中去,但生成仍是在编译阶段 )

7.虚基表与虚函数表的区别

虚基表中存的是偏移量,偏移量是为了找到虚基类,虚函数表中存的是虚函数的地址,为了在多态调用时根据对象的类型找到对应的函数。

8.C++语言的多态性分为编译时的多态性和运行时的多态性

运行时多态是动态绑定,也叫晚期绑定;运行时的多态性可通过虚函数实现。编译时多态是静态绑定,也叫早期绑定,主要通过重载实现;编译时的多态性可通过函数重载和模板实现