继承与多态
继承与多态的分析
- 继承
- 继承与访问限定比较
- 派生类和基类关系
- 派生类的构造顺序
- 基类对象(指针)派生类对象(指针)的转换
- 重载和隐藏
- 虚函数
- 静态绑定与动态绑定
- 指针调用
- 其他调用的绑定方式
- 虚函数实现的依赖
- 多态
继承
继承的本质:
- 实现代码的复用
- 在基类中提供统一的虚函数接口,可以让派生类进行重写,就可以使用多态了。(具体看本文
多态
章节)。
class A
{
public:A(int a1 = 1, int a2 = 2, int a3 = 3) :a1_(a1), a2_(a2), a3_(a3) {}~A(){}int a1_;
protected:int a2_;
private:int a3_;};class B:public A
{
public:B(int b1 = 10, int b2 = 20, int b3 = 30) :b1_(b1), b2_(b2), b3_(b3) {}~B() {}int b1_;
protected:int b2_;
private:int b3_;};
B类继承了A中的成员变量和方法,如下图所示。并且由于在派生的时候会在变量前面加入基的限定符,如A:a1_,所以即使A类和B类的成员变量和方法同名,也不会冲突。
B b;b.show();//当调用show方法时,优先调用B类自己定义的成员变量和方法
继承与访问限定比较
public:可以在类的内部和外部访问。
protected:只能在类的内部以及派生类(子类)中访问。
private:只能在类的内部访问。
Tips:
在c++中,默认情况下,类的成员(属性和方法)的访问权限是private。
class(默认private)、struct(默认public)。
访问权限顺序:public>protected>private
继承方式:public、protected、private
重点:
基类到派生类的访问权限是不能大于基类的访问权限的,比如说protected继承方式,那么基类中的public成员变量只能变成protected方式
派生类继承访问权限分析:
这里以公有继承为例:
#include <iostream>
using namespace std;
class A
{
public:A(int a1 = 1, int a2 = 2, int a3 = 3) :a1_(a1), a2_(a2), a3_(a3) {}~A(){}int a1_;
protected:int a2_;
private:int a3_;};class B:public A
{
public:B(int b1 = 10, int b2 = 20, int b3 = 30) :b1_(b1), b2_(b2), b3_(b3) {}~B() {}//void show(){cout << "基类公有变量a1_:"<<a1_;//可以访问cout << "基类保护变量a2_:" << a2_;//可以访问cout << "基类私有变量a3_:" << a3_; //不能访问}int b1_;
protected:int b2_;
private:int b3_;};
int main()
{//外部访问基类变量权限限定B b;cout << "基类公有变量a1_:" << b.a1_; //可以访问cout << "基类保护变量a2_:" << b.a2_; //不能访问cout << "基类保护变量a3_:" << b.a3_; //不能访问return 0;
}
依次改变B的继承方式,总结可以得到下表
注意:派生类从基类继承private的成员,但是派生类无法直接访问。
class定义派生类,默认继承方式是private继承;
struct定义派生类,默认继承方式是public继承。
派生类和基类关系
派生类的构造顺序
- 派生类的构造和析构函数,负责初始化和清理派生类部分
- 派生类从基类继承来的成员,由基类的构造和析构函数负责初始化和清理工作
下面是代码验证
class Base
{
public:Base(int data) :dataa_(data) { cout << "Base()" << endl; }~Base() { cout << "~Base()" << endl; }
protected:int dataa_;
};class Derive :public Base
{
public:Derive(int data):datab_(data), Base(data){cout << "Derive()" << endl;}~Derive() { cout << "~Derive()" << endl; }private:int datab_;
};
int main()
{Derive b(200);return 0;
}
执行结果为
派生类的完整生命流程如下
1、派生类调用基类的构造函数,初始化从基类继承的成员
2、调用派生类自身的构造函数,初始化派生类成员
3、调用派生类的析构函数,析构派生类成员
4、调用基类析构函数,释放派生类中从基类继承来的成员
基类对象(指针)派生类对象(指针)的转换
一般来说,派生类相对基类来说是占用更大的内存空间的,基于这一点理解以下结论。
- 将派生类对象赋值给基类对象,可以,赋值后的基类对象只能返回基类的成员
- 将基类对象赋值给派生类对象,错误,(因为基类可能没有包含派生类独有的那部分数据)
- 将派生类对象指针(引用)给基类对象指针(引用),可以,(基类指针是指向派生类对象的,但是由于基类指针的限定,只能访问派生类中的基类成员。)
- 将基类对象指针(引用)给派生类对象指针(引用),错误
总结
:在继承结构的转换,一般只支持从派生类到基类的转换
重载和隐藏
重载关系:一组函数要重载,必须要处在同一个作用域当中;并且函数名字相同,参数列表不同
隐藏关系:在继承结构当中,派生类的同名成员,把基类的同名成员给隐藏调用了。
class Base
{
public:void show() { cout << "Base::show()" << endl; }void show(int) { cout << "Base::show(int)" << endl; }};class Derive :public Base
{};
int main()
{Derive b;b.show();b.show(10);return 0;
}
代码的执行结果为,调用的是基类的方法
class Base
{
public:void show() { cout << "Base::show()" << endl; }void show(int) { cout << "Base::show(int)" << endl; }};class Derive :public Base
{
public:void show() { cout << "Derive ::show()" << endl; }
};
int main()
{Derive b;b.show();//因为派生类定义了show方法,所有基类的同名方法show()和show(int)被隐藏//只能通过b.Base::show()、b.Base::show(10)调用基类的show方法//b.show(10); return 0;
}
因为重载是要处于同一作用域内才起作用,而基类和派生类处于不同作用域,当派生类定义了与基类同名的函数,基类中的与其同名及其重载函数就被隐藏了。
虚函数
①如果一个类里面定义了虚函数,那么在编译阶段,编译器会给这个类产生一个唯一的vftable,即虚函数表,其中主要存储的就是RTTI(Run-Time Type Information,运行时类型识别)指针和虚函数的地址。
当程序运行时,每一张虚函数表都会加载到内存的.rodata区(只读数据区),不可更改
class Base
{
public:Base(int data=10):dataa_(data){}//虚函数virtual void show() { cout << "Base::show()" << endl; }virtual void show(int) { cout << "Base::show(int)" << endl; }
protected:int dataa_;
};
如Base类中定义了两个虚函数,其生成的虚函数表如下
②如果使用带有虚函数的类定义一个对象,其大小为成员变量的大小+虚函数指针的大小(一般为8字节),该虚函数指针指向虚函数表。(该Base类因为定义了虚函数,所以有一个用户不可见的vfptr指针)。
一个类型定义的多个对象,他们的vfptr都是指向同一个虚函数表。
Base b1;
Base b2;
Base b3;
//这三个对象的vfptr都是指向Base类的虚函数表
③一个类里面的虚函数的个数,不会影响对象的内存大小(不是说多个虚函数,就需要多个vfptr,vfptr始终指向的是虚函数表);虚函数的个数影响的是虚函数表的大小
基类是虚函数对派生类的影响
如果派生类中的方法,和从基类中继承的某个方法,满足:
- 返回值、函数名、参数列表都相同
- 基类的方法是虚函数
则派生类的这个方法,会被处理成虚函数
class Derive :public Base
{
public:Derive(int data=20):Base(data),datab_(data){}void show() { cout << "Derive ::show()" << endl; }
protected:int datab_;
};
这里派生类的show()方法满足上述条件,发生覆盖(在虚函数中,派生类的Derive::show()覆盖了基类中的Base::show())。
静态绑定与动态绑定
指针调用
class Base
{
public://普通的show函数,是静态绑定//void show() { cout << "Base::show()" << endl; }//virtual show函数,是动态绑定virtual void show() { cout << "Base::show()" << endl; }
private:int dataa_;
};class Derive :public Base
{
public:void show() { cout << "Derive::show()" << endl; }
private :int datab_;
};
int main()
{Derive d;Base* pb = &d;pb->show();cout << "基类Base大小:" << sizeof(Base) << endl;cout << "派生类Derive大小:" << sizeof(Derive) << endl;cout << typeid(pb).name() << endl;cout << typeid(*pb).name() << endl;return 0;
}
pb是基类指针,但其指向的是派生类对象,由于基类指针的限定,当调用pb->show()方法时,就自动去派生类中的基类部分去找show方法的实现。
1.如果Base::show()是普通方法,则进行静态绑定(call Base::show())
2.如果Base::show()是虚函数,则进行动态绑定,反汇编代码如下
具体步骤为:
- 根据pb指向的对象的前四个字节获取虚函数指针vfptr的值(其指向的对象是一个派生类Derive对象)
- 根据vfptr获取其指向的虚函数表(这里的虚函数表为,Derive的虚函数表)
- 根据虚函数表得到其对应的虚函数(这里的&Derive::show()虚函数重写了&Base::show(),所以最后调用的是Derive::show()方法)
Base::show()方法是否是virtual的输出比较
- 对于普通方法,Base类中有一个int类型成员变量,占4字节;Derive继承基类的成员变量+自身定义的int类型变量,共8字节。
- 对于Base::show()方法是虚函数,则有一个虚函数指针vfptr,8字节,则基类大小为4+8=12(我这里是64位系统,8字节对齐,变成了16个字节大小);派生类在基类的16字节基础上加上本身的int成员变量4字节,再次内存对齐,共24字节。
对于pb指向类型的理解
Derive d;Base* pb = &d;pb->show();cout << typeid(*pb).name() << endl;
/*对于这里pb指向的类型:取决于Base有没有虚函数
*如果Base没有虚函数,*pb识别的就是编译时期的类型;
*如果Base有虚函数,*pb识别的就是运行时期的类型 RTTI类型。
*/
前提:pb为Base类型的指针,指向的是Derive的派生类对象:
- Base没有虚函数,派生类中没有虚函数,则识别的是编译时期的类型,即Base类。
- Base存在虚函数,存在Derive类的虚函数表,则识别的就是运行时候的RTTI类型,即为Derive类。
其他调用的绑定方式
class Base
{
public:virtual void show() { cout << "Base::show()" << endl; }
private:int dataa_;
};class Derive :public Base
{
public:void show() { cout << "Derive::show()" << endl; }
private :int datab_;
};int main()
{/*************对象本身调用*****************/Base b;Derive d;//这里因为是对象本身访问自己的成员方法,发生的是静态绑定//无论show是不是虚函数,都是发生静态绑定//很好理解,通过对象本身调用,在编译时期就可以确定形式,不用动态绑定b.show(); //静态绑定d.show(); //静态绑定/*************指针方式调用*****************/Base *pb1=&b;pb1->show(); //动态绑定Base *pb2=&d;pb2->show(); //动态绑定/*************引用方式调用*****************/Base &ref1=b;ref1.show(); //动态绑定Base &ref2=d;ref2.show(); //动态绑定return 0;
}
总结
:动态绑定必须当通过指针(引用)调用虚函数,才会发生。
虚函数实现的依赖
- 虚函数能产生地址,存储与vftable中
- 对象必须存在(通过vfptr—>>>vftable—>>>虚函数地址)
构造函数不存在虚函数,因为这个时候还没有对象产生(不满足虚函数依赖的条件二)。即使在类的构造函数中,调用了虚函数,也是静态绑定。
static静态成员方法也不能被实现成虚函数方法,(其不依赖于对象,不满足条件二)
虚析构函数的实现
情景
:基类的指针(引用)ptr指向堆上new出来的派生类对象时,delete ptr
;
class Base
{
public:Base(int data) :dataa_(data) { cout << "Base()" << endl; }~Base() { cout << "~Base()" << endl; }
protected:int dataa_;
};class Derive :public Base
{
public:Derive(int data):datab_(data), Base(data){cout << "Derive()" << endl;}~Derive() { cout << "~Derive()" << endl; }private:int datab_;
};
int main()
{Base* pb = new Derive(10);delete pb;return 0;
}
在本篇 派生类的构造顺序
一小节中提到,派生类的构造顺序是基类构造–派生类构造–派生类析构–基类析构。
但是这里却没有调用派生类的析构函数,如果派生类有指向外部资源,就会造成内存泄露。
这里为什么没有调用派生类的析构函数?
这里pb是Base类型的指针,当调用delete pb的时候,在Base类中找到其对于的析构函数,Base::~Base(),这里发生的是静态绑定
解决方法
1.使用Derive 类型的指针指向开辟的内存(不是本节重点)
Derive* pb = new Derive(10);delete pb;
2.将基类的析构函数定义为virtual(发生动态绑定)
重点:
基类的析构函数是virtual函数,那么派生类的析构函数自动成为virtual函数。
class Base
{
public:Base(int data) :dataa_(data) { cout << "Base()" << endl; }virtual ~Base() { cout << "~Base()" << endl; }
protected:int dataa_;
};
这里发生的是动态绑定,pb是Base类型指针,指向派生类对象(存在虚函数),所以这里发生动态绑定;由于派生类的虚析构函数重写了基类的虚析构函数,所以这里调用的是派生类的析构函数。
多态
静态(编译时期)多态:
- 函数重载,
bool compare(int a,int b),bool compare(double a,double b);
- 模板(函数模板、类模板)
动态(运行时期)多态:
虚函数机制,调用哪个函数在运行时决定。
基类指针(引用)调用哪个派生类对象,就会调用该派生类对象方法,称为多态。
如代码所示
class Animal
{
public:Animal(string name):name_(name){}virtual void bark() {}
protected:string name_;
};
class Cat:public Animal
{
public:Cat(string name) :Animal(name) {}void bark() { cout << name_ << "bark:miao miao~~~" << endl; }};class Dog :public Animal
{
public:Dog(string name) :Animal(name) {}void bark() { cout << name_ << "bark:wang wang~~~" << endl; }};
class Pig :public Animal
{
public:Pig(string name) :Animal(name) {}void bark() { cout << name_ << "bark:heng heng~~~"<<endl; }};
//animal作为基类指针,当传入派生类的地址后,发生动态绑定,调用对应类的bark方法
void bark(Animal* animal)
{animal->bark();
}
int main()
{Cat cat("加菲猫");Dog dog("汪汪队");Pig pig("佩奇");bark(&cat);bark(&dog);bark(&pig);return 0;
}
这里bark函数根据传入的派生类类型,通过基类指针指向,从而实现调用不同派生类的函数。
抽象类和普通类的区别
普通类是用于抽象一个实体的类型,比如这里的Cat类、Dog类、Pig类等;
而这里的Animal作为抽象类:
//1.string name_;让所有的动物实体通过继承Animal直接复用该属性
//2.给所有的派生类保留统一的覆盖/重写接口class Animal
{
public:Animal(string name):name_(name){}//因为bark在animal中并没有实际的作用,只是为派生类提供一个统一的接口,定义为纯虚函数//纯虚函数virtual void bark()=0;
protected:string name_;
};
拥有纯虚函数的类,叫做抽象类。
抽象类不能实例化对象(抽象类并不是为了抽象某个类型而存在的。),但是可以定义指针和引用变量。