【转】C++继承、多态、虚函数
https://juejin.im/post/5a30f789f265da431c704cf8
所谓继承,是指在一个已存在类的基础上建立一个新类。已存在的类称为基类(base class)或父类,新建立的类称为派生类(derived class)或子类。这样,子类与父类就形成了层次关系(用箭头表示继承方向) :
而派生的概念则是从父类角度看待继承所形成的,它与继承的差异只是看待问题的角度不相同:一个从下往上看,另一个从上往下看, 本质上指的是同一事物。
继承性是面向对象程序设计最重要的特征之一
继承抽取了类之间的共同点,减少了代码冗余,是实现软件重用的重要手段,继承减少了代码冗余;
继承还是实现软件功能扩充的重要手段;
继承反映了类的层次结构,并支持对事物从一般到特殊的描述,这符合人的认知规律和行动准则。
说明:
1、继承方式有三种,即:public(公有继承)、private(私有继承)、protected(保护继承),如果省略,则默认为private继承方式;
2、多重继承方式下,各基类之间要用逗号分开,每一个基类都有自己的继承方式,它们互不影响;
3、派生类只要写出其新增成员的声明或定义即可,基类的内容不必重复。
继承的访问修饰:
基类中 继承方式 子类中 public & public继承 => public public & protected继承 => protected public & private继承 => private protected & public继承 => protected protected & protected继承 => protected protected & private继承 => private private & public继承 => 子类无权访问 private & protected继承 => 子类无权访问 private & private继承 => 子类无权访问
class Box{ private: double length; protected: double height; public: double width; double getLength(); double getHeight(); double getWidth(); Box(double l,double w,double h){ this->length = l; this->width = w; this->height = h; } }; double Box::getLength(){ return length; } double Box::getHeight(){ return height; } double Box::getWidth(){ return width; } class ColorBox : public Box{ private: int color; public: int getColor(); ColorBox(double l, double w, double h, int c) : Box(l, w, h){ this->color = c; }; }; int ColorBox::getColor(){ return color; } void main(){ ColorBox b(0.2,0.6,0.5,20); cout << "ColorBox--->width:"<< b.getWidth() << endl; b.Box::width = 100.5; cout << "Box----->width:" <<b.Box::getWidth() << endl; system("pause"); } /*输出如下 colorBox--->width:0.6 Box--->width:100.5 colorBox--->width:100.5 */
单一继承下的析构函数
说明
1、派生类与基类的析构函数没有什么联系,彼此独立,派生类或基类的析构函数只做各自类对象消亡前的清理工作;
2、在派生过程中,基类的析构函数不能继承,如果需要析构函数的话,就要在派生类中重新定义;
3、派生类析构函数的定义方法与没有继承关系的类中析构函数的定义方法完全相同,只要在函数体中负责把派生类新增的非对象成员的清理工作做好就够了,系统会自己调用基类及子对象的析构函数来对基类及子对象进行清理。
析构函数的执行顺序:
1 与构造函数的执行顺序正好相反 2 先析构派生类自己; 3 再析构各个子对象:如果有多个子对象,析构顺序与这些子对象在类中的说明次序相反; 4 最后才是析构基类。
多继承
//人 class Person{ }; //公民 class Citizen{ }; //学生,既是人,又是公民 class Student : public Person, public Citizen{ };
多重继承的定义格式
class 派生类名 :[继承方式] 基类名1,[继承方式] 基类名2,…… { // 派生类成员声明; };
多重继承下的构造函数:
1.当基类中声明有默认形式的构造函数,派生类构造函数可以不向基类构造函数传递参数; 2.若基类中未声明构造函数,派生类中也可以不声明构造函数,全采用默认形式的构造函数; 3.当基类声明有带形参的构造函数时,派生类也应声明带形参的构造函数,并将参数传递给基类构造函数。
派生类构造函数的定义格式:
派生类构造函数(总参数表) :基类1构造函数(参数表), ..., 基类n构造函数(参数表), 子对象1的构造函数(参数表), ..., 子对象k的构造函数(参数表)
{
//派生类新增成员的初始化语句;
}
多重继承下派生类构造函数的执行顺序:
1)先构造基类:按照派生类定义时各基类在冒号后的声明顺序执行 ;
2)再构造子对象:按照各子对象在派生类的说明顺序执行 ;
3)最后才是构造派生类本身 。
析构函数的执行顺序:
1)与它的构造函数执行顺序相反 : 2)先析构派生类自己; 3)再析构各子对象; 4)最后才是析构各基类。
继承的二义性
二义性报错:error: request for member ‘hello’ is ambiguous
当一个派生类是由多个基类派生而来时,如果这些基类中的成员有一些的名称相同,那么使用一个表达式引用了这些同名的成员,就会出现无法确定是引用哪个基类成员的情况,这就是对基类成员访问的二义性。
要避免此种情况,可以在成员名前用对象名及基类名来限定。
格式: 对象名.基类名::成员名 对象名.基类名::成员函数名(参数表)
示例:
class A{ public: char* name; }; class A1 : virtual public A{ // 注意这里是virtual声明,否则在B中调用name时将报“二义性”错误 }; class A2 : virtual public A{ }; class B : public A1, public A2{ }; void main(){ B b; b.name = "jason"; //指定父类显示调用 //b.A1::name = "xiaoming"; //b.A2::name = "xiaoming"; system("pause"); }
支配规则:一个派生类中的名字将优先于它的基类中相同的名字,这时二者之间不存在二义性,当选择该名字时,使用支配者(派生类中)的名字,这称为支配规则。
可见性原则:在外层(基类)声明的标识符,如果在内层(派生类)没有同名的标识符,则它在内层(派生类)仍可见;如果内层(派生类)声明了同名的标识符,则外层(基类)的标识符不可见,即它被内层(派生类)同名标识符所覆盖。
虚基类
如果一个派生类从多个基类中派生,而这些基类又有一个共同的基类,则在这个派生类将保留共同基类的多份副本【会导致二义性错误】。
如下图所示:
要让派生类只保留共同基类的一份副本,可以让这个共同基类说明为虚基类。
虚基类的定义格式
格式: class 派生类 :virtual[继承方式] 基类名
虚基类及其派生类的构造函数和析构函数
构造函数。要求虚基类的构造函数只能调用一次。直接或间接继承虚基类的所有派生类,都必须在构造函数的成员初始化表中列出对虚基类的初始化。因此,要注意以下几点;
1)虚基类的构造函数在所有非虚基类之前调用;
2)若同一层次中包含多个虚基类,这些虚基类的构造函数按它们说明的次序调用;
3)若虚基类由非虚基派生而来,则先调用基类构造函数,再调用派生类的构造函数。
析构函数调用顺序正好与它的构造函数调用顺序相反。
多态性
顾名思义,多态就是“多种形态”的意思。它是面向对象程序设计的一个重要特征。
在面向对象方法中一般是这样表述多态性的:同样的消息被不同类型的对象接收时导致的不同行为。所谓消息是指对类的成员函数的调用,不同行为是指不同的实现,也就是调用了不同的函数。
多态的类型:
重载多态:普通函数或类的成员函数重载就属于这种类型
强制多态:强制数据类型的变化,以适用函数或操作的要求
包含多态:类族中定义于不同类的同名成员函数的多态行为,主要通过虚函数来实现
参数多态:类模板属于这种类型,使用实际的类型才能实例化
多态的实现
C++的多态性有两类:
1、静态多态性:也就是静态联编下实现的多态性,即是在程序编译时就能确定调用哪一个函数,函数重载和运算符重载实现的就是静态多态性;
2、动态多态性:也就是动态联编(虚函数)下实现的多态性,它只有在程序运行时才解决函数的调用问题,虚函数是实现动态多态性的基础
什么是联编 ? 又称为关联或绑定,是描述编译器决定在程序运行时,一个函数调用应执行哪段代码的一个术语,它把一个标识符与一个存储地址联系起来。
发生动态的条件:
1.继承 2.父类的引用或者指针指向子类的对象 3.函数的重写
虚函数声明格式:
virtual 函数返回类型 函数名(形参表) { //函数体 }
虚函数使用说明:
1、只有类的成员函数才能声明为虚函数,普通函数不存在继承关系,不能声明为虚函数;
2、virtual关键字出现在虚函数的声明处,在虚函数的类外定义时不加virtual;
3、静态成员函数不能声明为虚函数;
4、内联函数不能声明为虚函数;
5、构造函数也不能声明为虚函数,因为它是在对象产生之前运行的函数;
6、析构函数可以是虚函数而且通常声明为虚函数。
#include <iostream> using namespace std; // 普通飞机 class Plane{ public: virtual void fly(); virtual void land(); }; void Plane::fly(){ cout << "起飞" << endl; } void Plane::land(){ cout << "降落" << endl; } // 直升飞机 class Jet : public Plane{ virtual void fly(); virtual void land(); }; void Jet::fly(){ cout << "直升飞机起飞" << endl; } void Jet::land(){ cout << "直升飞机降落" << endl; } // 喷气式飞机 class Copter : public Plane{ virtual void fly(); virtual void land(); }; void Copter::fly(){ cout << "喷气式飞机起飞" << endl; } void Copter::land(){ cout << "喷气式飞机降落" << endl; } void bizPlay(Plane& p){ p.fly(); p.land(); } int main(){ Plane p1; bizPlay(p1); Jet p2; bizPlay(p2); Copter p3; bizPlay(p3); } /* 输出如下 起飞 降落 直升飞机起飞 直升飞机降落 喷气式飞机起飞 喷气式飞机降落 */
为什么要用指针->虚函数()方式而不是对象.虚函数()方式来调用虚函数 ?
如果采用对象.虚函数()方式调用,只能得到一个个具体类的结果,不具备“跨类”功能。相反,指针则有“跨类”的能力,除此之外,引用也具备这种能力,以后我们将把指针、引用同等看待。
虚析构函数
为什么要引入虚析构函数 ?
用new命令建立派生类对象时返回的是派生类指针,根据赋值兼容规则,可以把派生类指针赋给基类指针。当用delete 基类指针 来删除派生类对象时,只调用基类的析构函数, 不能释放派生类对象自身占有的内存空间。
这一问题在引进虚析构函数后能够得到解决。
虚析构函数的声明格式 : virtual ~类名() { 函数体 }
虚析构函数与一般虚函数的不同之处 :
当基类的析构函数被声明为虚函数时,它的派生类的析构函数也自动成为虚函数,这些析构函数不要求同名;
一个虚析构函数的版本被调用执行后,接着就要调用执行基类版本,依此类推,直到执行到派生序列的最开始的那个析构函数的版本为止,也即说派生类析构函数、基类析构函数能够依次被执行。
何时需要虚析构函数?
- 通过基类指针删除派生类对象时;
- 通过基类指针调用对象的析构函数。
纯虚函数和抽象类
纯虚函数
1)在某些情况下, 在基类中不能为虚函数提供具体定义, 这时可以把它说明为纯虚函数。它的定义留给派生类来完成。
2)纯虚函数的声明格式:
1 class 类名 2 { 3 ... 4 virtual 返回类型 函数名(形参表) = 0; 5 ... 6 }
注意:空虚函数与纯虚函数的区别。
使用说明
1、纯虚函数没有也不允许有函数体,如果强行给它加上将会出现错误;
2、最后的“ = 0”并不表示函数的返回值为0,它只是形式上的作用,告诉编译系统“这是纯虚函数”;
3、是一个纯虚函数的声明语句,它的末尾应有分号;
4、纯虚函数的作用:在基类中为派生类保留一个虚函数的名字,以便派生类根据需要进行定义。如果在基类没有保留虚函数的名字,则无法实现多态性。
5、如果在一个类中声明了纯虚函数,而在其派生类中没有对该函数进行定义,则该虚函数在派生类中仍然为纯虚函数。
1 class Shape{ 2 public: 3 //纯虚函数 4 virtual void sayArea() = 0; 5 void print(){ 6 cout << "hi" << endl; 7 } 8 }; 9 10 //圆 11 class Circle : public Shape{ 12 public: 13 Circle(int r){ 14 this->r = r; 15 } 16 void sayArea(){ 17 cout << "圆的面积:" << (3.14 * r * r) << endl; 18 } 19 private: 20 int r; 21 }; 22 23 void main(){ 24 //Shape s; 25 Circle c(10); 26 c.sayArea(); 27 system("pause"); 28 }
抽象类的定义和存在意义:
1.当一个类具有一个纯虚函数,这个类就是抽象类
2.抽象类不能实例化对象
3.子类继承抽象类,必须要实现纯虚函数,如果没有,子类也是抽象类
抽象类的作用:为了继承约束,根本不知道未来的实现
抽象类不能用作参数类型、函数返回类型或强制类型转换,但可以声明抽象类的指针或引用。
使用虚函数表。C++对象使用虚表, 如果是基类的实例,对应位置存放的是基类的函数指针;
如果是继承类,对应位置存放的是继承类的函数指针(如果在继承类有实现)。
所以 ,当使用基类指针调用对象方法时,也会根据具体的实例,调用到继承类的方法。
虚函数作用是实现多态,更重要的,虚函数其实是实现封装,使得使用者不需要关心实现的细节。
在很多设计模式中都是这样用法,例如Factory、Bridge、Strategy模式。
有虚函数的类内部有一个称为“虚表”的指针(有多少个虚函数就有多少个指针),这个就是用来指向这个类虚函数。也就是用它来确定调用该那个函数。
实际上在编译的时候,编译器会自动加入“虚表”。虚表的使用方法是这样的:如果派生类在自己的定义中没有修改基类的虚函数,就指向基类的虚函数;
如果派生类改写了基类的虚函数(就是自己重新定义),这时虚表则将原来指向基类的虚函数的地址替换为指向自身虚函数的指针。
那些被virtual关键字修饰的成员函数,就是虚函数。
虚函数的作用,用专业术语来解释就是实现多态性(Polymorphism),多态性是将接口与实现进行分离;用形象的语言来解释就是实现以共同的方法,但因个体差异而采用不同的策略。
每个类都有自己的vtbl,vtbl的作用就是保存自己类中虚函数的地址,我们可以把vtbl形象地看成一个数组,这个数组的每个元素存放的就是虚函数的地址。
虚函数的效率低,其原因就是,在调用虚函数之前,还调用了获得虚函数地址的代码。