C++多态的含义
1、什么是多态?
(1)通过继承同一个基类,产生了相关的不同的派生类,与基类中同名的成员函数在不同的派生类中会有不同的实现,也就是说:一个接口、多种方法。
(2)多态是面向对象的重要技术之一,它是一种行为的封装,是同一个事物所表现出来的多种形态,简单地说就是:一个接口、多种形态。
(3)那么问题来了,现在,在运行时使用同一个成员名来调用类对象的成员函数,会调用哪个对象的成员函数呢?这就是多态要解决的问题!
2、多态的作用?
(1)多态技术允许将基类指针或基类引用指向派生类对象。
(2)把不同派生类的对象都当作基类对象来看待,可以屏蔽不同派生类之间的差异,从而写出通用的代码以适应需求的不断变化。
3、多态的分类?
(1)多态分为两种:
- 一种是静态多态,是指编译时候的多态,通过函数重载或者运算符重载实现。
- 一种是动态多态,是指运行时候的多态,通过虚函数与基类指针共同作用实现。
4、在C++中如何实现多态?
(1)在C++中,基类指针是用来指向基类对象的,如果用它来指向派生类对象,则进行指针类型转换(上行转换),将派生类指针转换为基类指针,所以该指针将会指向派生类对象中的基类部分,通过该指针是无法调用派生类对象中的成员函数的。
但是,虚函数突破了这一限制。在派生类的基类部分中,派生类的虚函数取代了基类原来的同名虚函数,因此在使基类指针指向派生类对象后,使用该基类指针调用这个同名虚函数成员时就调用了派生类的虚函数。
(2)当把基类的某个成员函数声明为虚函数时,C++允许在其派生类中对该虚函数进行重新定义,赋予它新的功能,并且可以通过基类指针指向同一类族的不同派生类的对象,来调用相应派生类中的该同名虚函数。
由虚函数实现的动态多态性就是:同一类族中不同的派生类对象,对同一函数调用作出不同的响应。
(3)虚函数的使用方法如下:
- 在基类中使用virtual关键字声明成员函数为虚函数(这样就可以在派生类中对该虚函数进行重新定义,赋予它新的功能)。
- 在派生类中重新定义此虚函数,要求函数名、形参列表、返回值类型均要与基类中的虚函数相同,并根据具体需要重新定义它的函数体。
(4)C++规定,当一个成员函数被定义为虚函数后,其派生类中的同名函数都自动成为虚函数(而不一定要有关键字virtual显示声明了),但是为了清晰,习惯上每一层都加上virtual关键字。
举例如下:
- class base
- {
- public:
- base(int a):b(a){}
- void my()
- { cout<<"mybase."<<endl; }
- virtual void fun()
- { cout<<"I am a base."<<endl; }
- private:
- int b;
- };
- class child1:public base
- {
- public:
- child1(int a1,int a2):base(a1),c1(a2){}
- void my()
- { cout<<"mychild."<<endl;}
- virtual void fun()
- { cout<<"I am a child1."<<endl; }
- private:
- int c1;
- };
- class child2:public base
- {
- public:
- child2(int a1,int a2):base(a1),c2(a2){}
- void my() { cout<<"mychild."<<endl;} virtual void fun() { cout<<"I am a child2."<<endl; } private: int c2;};
- base *bptr;
- child1 c1(10,1);
- child2 c2(10,2);
- bptr=&c1;//语句1:基类指针指向派生类对象child1
- bptr->func();//语句2:调用的是child1中的func()
- c1.func();//
- bptr->my();//
- c1.my();//
- bptr=&c2;//语句3:基类指针指向派生类对象child2
- bptr->func();//语句4:调用的是child2中的func()
- c2.func();//
- bptr->my();//
- c2.my();//
5、虚函数与纯虚函数的区别与联系?
(1)应该考虑:对成员函数的调用是通过对象名还是通过基类指针或是基类引用去访问。如果是后两者,则应当声明为虚函数。
(2)虚函数:如果一个类中定义了虚函数virtual,那么这个虚函数是被实现的,其作用就是为了让该虚函数在这个类的的派生类中被覆盖,被实现为不同的功能,从而结合基类指针以实现动态多态性。
(3)纯虚函数:有时,在定义一个虚函数时,并不定义其函数体,即它的函数体是空的,它的作用只是保留一个虚函数名,它关注的是接口的统一性,其具体的功能实现由它的派生类完成。比如:virtual float area(float a,float b ) = 0;
- 最后面的“=0”的作用仅仅只是告诉编译器这是一个纯虚函数。
- 纯虚函数只具有函数的名称,没有函数体,不具备函数的功能,因此不能被调用。只有在其派生类中被重新定义过之后才具备函数的功能,才能被调用。
- 如果在一个类中声明了纯虚函数,但是在其派生类中该纯虚函数并没有被定义,那么该虚函数在这个派生类中仍然为纯虚函数,仍然不具备函数的功能。
(4)抽象类与抽象基类:不用来定义对象而只作为一种基本类型用作被继承的类,称为抽象类;由于它经常用来作基类,故又被称之为抽象基类。凡是包含纯虚函数的类,都是抽象类,这种类不能直接生成对象(实例),它的作用就是作为一个类族的共同基类,或者说是为一个类族提供一个公共接口。
(5)需要说明的是,使用虚函数,系统要有一定的空间开销。当一个类中含有虚函数时,编译系统会为它构造一个虚函数指针vptr(4字节),同时这个虚函数指针指向一个虚函数表vtable,虚函数表是一个指针数组,存放的是该类中的每个虚函数的入口地址。(查表是高效的,因此多态性是高效的。)
6、虚析构函数的作用?
(1)当派生类的对象从内存中撤销时,一般先调用派生类的析构函数释放该对象中的派生类部分,再调用基类的析构函数释放该对象中的基类部分,从而能够完整的释放该对象内存。
(2)但是,当用基类指针指向了一个派生类对象,即 base *bptr = new child;此时用delete bptr;来撤销bptr 指向的动态存储空间时,只会执行基类的析构函数来释放该堆内存中的基类部分,但是并不会执行派生类的析构函数来释放该堆内存中的派生类部分。此时,就会造成内存泄漏现象。
(3)为了避免此类现象发生,我们将基类的析构函数声明为虚析构函数,这样就解决了上述问题(即先调用派生类的析构函数释放该动态空间中的派生类部分,再调用基类的析构函数释放该动态空间中的基类部分,从而能够完整的释放该堆内存)。
(4)如果将基类的析构函数声明为虚析构函数,那么该基类的所有派生类的析构函数都自动成为虚析构函数。
7、类的成员函数的重载、覆盖(重写)、隐藏(重定义)的区别与联系?
答:分别简单讲述一下函数重载、函数覆盖、函数隐藏的概念与特征:
函数重载:重载函数通常用来命名一组功能相似的函数。
(1)两个函数要在相同的类域
(2)两个函数的名称相同
(3)两个函数的的形参列表必须不同
函数覆盖:覆盖是指派生类函数覆盖基类函数。
(1)两个函数要在不同的类域
(2)两个函数的名称相同
(3)基类函数必须是虚函数
(4)两个函数的形参列表和返回值类型要相同
函数隐藏:指派生类的函数屏蔽了与其同名的基类函数。
(1)两个函数在不同的类域
(2)两个函数的名称相同
(3)两个函数的形参列表不同
(4)如果派生类函数与基类函数形参列表相同,但是在基类函数中没有virtual关键字,也会发生函数隐藏