7 多态

7 多态

7.1 多态和虚函数

7.1.1指针实现多态

**多态:**同一种事物的不同形态,即同一名字的事物可以完成不同的功能。

分类

  1. 编译时的多态(静态多态):函数和运算符的重载。
    • 对重载函数的调用,在编译时就能根据实参确定应该调用哪个函数,因此叫编译时的多态;
  2. 运行时的多态(动态多态):和继承和虚函数有关。
  3. 静态关联和动态关联
    • 静态关联(将对象和函数绑定,使用对象名调用虚函数)
    • 动态关联(将对象和函数绑定,使用指针或引用调用虚函数)

背景:

  • 在继承中,通过基类指针只能访问派生类的成员变量,但是不能访问派生类的成员函数.

**解决:**虚函数

  • 函数声明前面增加 virtual 关键字。使得函数变成虚函数。基类指针能够访问派生类的成员函数
#include <iostream>
using namespace std;
//基类People
class People{
public:
    People(char *name, int age);
    virtual void display();  //声明为虚函数
protected:
    char *m_name;
    int m_age;
};
People::People(char *name, int age): m_name(name), m_age(age){}
void People::display(){
    cout<<m_name<<"今年"<<m_age<<"岁了,是个无业游民。"<<endl;
}

//派生类Teacher
class Teacher: public People{
public:
    Teacher(char *name, int age, int salary);
    virtual void display();  //声明为虚函数
private:
    int m_salary;
};
Teacher::Teacher(char *name, int age, int salary): People(name, age), m_salary(salary){}
void Teacher::display(){
    cout<<m_name<<"今年"<<m_age<<"岁了,是一名教师,每月有"<<m_salary<<"元的收入。"<<endl;
}

int main(){
    People *p = new People("王志刚", 23);
    p -> display();
    p = new Teacher("赵宏佳", 45, 8200);
    p -> display();
    return 0;
}
运行结果:
王志刚今年23岁了,是个无业游民。
赵宏佳今年45岁了,是一名教师,每月有8200元的收入

虚函数的意义:

  • 在基类中声明的函数是虚拟的,并不是实际存在的函数,然后在派生类中正式定义该函数。
  • 虚函数的唯一作用就是实现多态

虚函数作用:

​ 允许在派生类中重新定义与基类同名的函数,通过基类指针或引用访问基类和派生类中的同名函数。

多态的实际含义:

  • 基类指针指向基类对象时就使用基类的成员(包括成员函数和成员变量),指向派生类对象时就使用派生类的成员。换句话说,基类指针可以按照基类的方式来做事,也可以按照派生类的方式来做事,它有多种形态,简称多态。

C++提供多态的目的:

​ 可以通过基类指针对所有派生类(包括直接派生和间接派生)的成员变量和成员函数进行“全方位”的访问,尤其是成员函数。如果没有多态,我们只能访问成员变量。

7.1.2 引用实现多态

  • 引用(指针常量),既然可以通过指针实现多态,那么也可以通过引用实现多态
int main(){
    People p("王志刚", 23);
    Teacher t("赵宏佳", 45, 8200);
   
    People &rp = p;
    People &rt = t;
   
    rp.display();
    rt.display();

    return 0;
}
运行结果:
王志刚今年23岁了,是个无业游民。
赵宏佳今年45岁了,是一名教师,每月有8200元的收入。
  • 引用不像指针灵活,指针可以随时改变指向,而引用只能指代固定的对象,在多态性方面缺乏表现力,所以以后我们再谈及多态时一般是说指针。本例的主要目的是让读者知道,除了指针,引用也可以实现多态。

7.2 C++虚函数注意事项以及构成多态的条件

7.2.1 虚函数的注意事项

  1. 虚函数的声明处加上 virtual 关键字,函数定义处可以加也可以不加
  2. 基类中的函数声明为虚函数,这样所有派生类中具有遮蔽关系的同名函数都将自动成为虚函数
  3. 当在基类中定义了虚函数时,如果派生类没有定义新的函数来遮蔽此函数,那么将使用基类的虚函数。
  4. 只有派生类的虚函数覆盖基类的虚函数(函数原型相同,函数名,函数类型,参数个数,参数类型相同 )才能构成多态(通过基类指针访问派生类函数)
  5. 构造函数不能是虚函数
  6. 析构函数可以声明为虚函数,而且有时候必须要声明为虚函数,

7.2.2 多态的条件

  • 存在继承关系
  • 继承中必须有同名的虚函数,并且是覆盖关系。
  • 存在基类的指针(引用)指向子类的对象。
#include <iostream>
using namespace std;

//基类Base
class Base{
public:
    virtual void func();
    virtual void func(int);
};
void Base::func(){
    cout<<"void Base::func()"<<endl;
}
void Base::func(int n){
    cout<<"void Base::func(int)"<<endl;
}

//派生类Derived
class Derived: public Base{
public:
    void func();
    void func(char *);
};
void Derived::func(){
    cout<<"void Derived::func()"<<endl;
}
void Derived::func(char *str){
    cout<<"void Derived::func(char *)"<<endl;
}

int main(){
    Base *p = new Derived();
    p -> func();  //输出void Derived::func()
    p -> func(10);  //输出void Base::func(int)
    p -> func("http://c.biancheng.net");  //compile error

    return 0;
}

语句p -> func();调用的是派生类的虚函数,构成了多态。

语句p -> func(10);调用的是基类的虚函数,因为派生类中没有函数覆盖它。

语句p -> func("http://c.biancheng.net");出现编译错误,因为通过基类的指针只能访问从基类继承过去的成员,不能访问派生类新增的成员。

7.2.3 虚函数的应用情况

  • 基类的成员函数被继承后希望更改功能。声明为虚函数
  • 成员函数继承后功能不需要更改,或者派生类应用不到该函数。不需要声明为虚函数。

7.3 虚析构函数的必要性

7.3.1 虚析构函数

 #include<iostream>
 using namespace std;
 class Base
 {
 public:
     Base() {}
    virtual ~Base();
};

class Subclass :public Base
{
public:
    Subclass() {}
    ~Subclass();
};
 Base::~Base()
 {
     cout << "Base destructor is called." << endl;
 }

 Subclass::~Subclass()
 {
    cout << "Subclass destructor is called." << endl;
 }
 
 int main()
 {
     Base *b = new Subclass;
     delete b;
    return 0;
 }
输出结果:  

Subclass destructor is called.
Base destructor is called.
这个很简单,非常好理解。
但是,如果把类Base析构函数前的virtual去掉,那输出结果就是下面的样子了:
Base destructor is called.

例子原因分析:

  • 析构函数没有定义为虚函数:使用基类指针删除派生类对象时,仅仅调用基类的析构函数,没有调用派生类的析构函数
  • 析构函数定义为虚函数:使用基类指针删除派生类对象时,派生类的析构函数也被调用了。
    • 原因是当基类析构函数定义为虚函数后,删除对象时会直接调用派生类的析构函数,由于子类析构时会先调用父类的析构函数所以就把子类和继承的父类都析构了。

虚析构函数的本质:

  • 基类的析构函数为虚函数时,尽管派生类的析构函数名字不同,派生类的析构函数也将自动变为虚函数。

  • 当使用父类指针指向子类对象时:

    • 删除父类指针时,系统采用动态关联,自动调用派生类类的清理工作。

7.3.2 构造函数为何不能是虚函数

  1. 从vptr角度解释

    虚函数的调用是通过虚函数表来查找的,而虚函数表由类的实例化对象的vptr指针(vptr可以参考C++的虚函数表指针vptr)指向,该指针存放在对象的内部空间中,需要调用构造函数完成初始化。如果构造函数是虚函数,那么调用构造函数就需要去找vptr,但此时vptr还没有初始化!

  2. 从多态角度解释

    构造函数的作用是提供初始化,在对象生命期仅仅运行一次,不是对象的动态行为,没有必要成为虚函数。

    构造函数是在创建对象时自己主动调用的,不可能通过父类的指针或者引用去调用。那使用虚函数也没有实际意义。,。,。【;

7.4 C++纯虚函数和抽象类

纯虚函数的语法格式

  • virtual 返回值类型 函数名 (函数参数) = 0;

  • 纯虚函数没有函数体,只有函数声明,最后的=0并不表示函数返回值为0,它只起形式上的作用,告诉编译系统“这是纯虚函数”。

抽象类:

  • 包含纯虚函数的类称为抽象类(Abstract Class)。**之所以说它抽象,是因为它无法实例化,也就是无法创建对象。**原因很明显,纯虚函数没有函数体,不是完整的函数,无法调用,也无法为其分配内存空间。

性质

  • 抽象类通常是作为基类,让派生类去实现纯虚函数。**派生类必须实现纯虚函数才能被实例化。**不然纯虚函数会被派生类继承,那么派生类仍然是个抽象类。
  • 一个纯虚函数就可以使类成为抽象基类,但是抽象基类中除了包含纯虚函数外,还可以包含其它的成员函数(虚函数或普通函数)和成员变量。
  • 只有类中的虚函数才能被声明为纯虚函数,普通成员函数和顶层函数均不能声明为纯虚函数

虚函数和纯虚函数选择

  • 基类某个成员方法,可由子类个性化实现,也可由基类缺省实现,此时函数定义为虚函数
  • 基类某个成员方法,必须由子类个性化实现,此时定义为纯虚函数
#include <iostream>
using namespace std;

//线
class Line{
public:
    Line(float len);
    virtual float area() = 0;
    virtual float volume() = 0;
protected:
    float m_len;
};
Line::Line(float len): m_len(len){ }

//矩形
class Rec: public Line{
public:
    Rec(float len, float width);
    float area();
protected:
    float m_width;
};
Rec::Rec(float len, float width): Line(len), m_width(width){ }
float Rec::area(){ return m_len * m_width; }

//长方体
class Cuboid: public Rec{
public:
    Cuboid(float len, float width, float height);
    float area();
    float volume();
protected:
    float m_height;
};
Cuboid::Cuboid(float len, float width, float height): Rec(len, width), m_height(height){ }
float Cuboid::area(){ return 2 * ( m_len*m_width + m_len*m_height + m_width*m_height); }
float Cuboid::volume(){ return m_len * m_width * m_height; }

//正方体
class Cube: public Cuboid{
public:
    Cube(float len);
    float area();
    float volume();
};
Cube::Cube(float len): Cuboid(len, len, len){ }
float Cube::area(){ return 6 * m_len * m_len; }
float Cube::volume(){ return m_len * m_len * m_len; }

int main(){
    Line *p = new Cuboid(10, 20, 30);
    cout<<"The area of Cuboid is "<<p->area()<<endl;
    cout<<"The volume of Cuboid is "<<p->volume()<<endl;
  
    p = new Cube(15);
    cout<<"The area of Cube is "<<p->area()<<endl;
    cout<<"The volume of Cube is "<<p->volume()<<endl;

    return 0;
}
运行结果:
The area of Cuboid is 2200
The volume of Cuboid is 6000
The area of Cube is 1350
The volume of Cube is 3375
分析:

它们的继承关系为:Line --> Rec --> Cuboid --> Cube。
Line 抽象类,最顶层的基类,定义了两个纯虚函数 area()volume()。
Rec 类中,实现了 area() 函数,有实现继承来的 volume() 函数,volume() 仍然是纯虚函数,所以 Rec 也仍然是抽象类。

7.5 typeid和typeinfo

7.5.1 typeinfo(类型信息)

  • 基本类型信息(int float),类型信息比较简单,主要指数据的类型
  • 类类型对象信息。类型信息是指所属的类,所包含的成员,和继承关系。

类型信息实质:

  • 类型信息是创建数据的模板,数据占用多大内存、能进行什么样的操作、该如何操作等,这些都由它的类型信息决定。

7.5.2 typeid

typeid,返回typeinfo对象引用,用于获得一个表达式的类型信息,用于描述数据的各种属性

操作对象:

  • 普通变量,常量,对象,类,结构体,普通类型,表达式。

**操作方式:**数据类型,表达式

  • typeid(datatype)
  • typeid(expression)

判断普通数据类型是否相等

char *str;
int a = 2;
int b = 10;
float f;

内置类型比较.png
在这里插入图片描述

判断类的类型信息

  • 对象中含有至少一个虚函数,返回动态类型,需要在运行中计算
  • 不含有虚函数,返回静态类型,编译时可以计算
//普通类继承
class Base{};
class Derived: public Base{};
base * p = new derived;   
  • 则:p是base*类型,*p是Base类型
//带虚函数的类继承
class base
{
public :
    virtual void m(){cout<<"base"<<endl;}
};
class derived : public base
{
public:
    void m(){cout<<"derived"<<endl;}
};
base * p = new derived;
  • 则:p是base*类型,但因为base类具有多态性,则*p表示的是其实际的数据类型 derived类型。

7.6 C++中的RTTI机制解析

背景:

  • 和很多其他语言一样,C++是一种静态类型语言。其数据类型是在编译期就确定的,不能在运行时更改。然而由于面向对象程序设计中多态性的要求,C++中的指针或引用(Reference)本身的类型,可能与它实际代表(指向或引用)的类型并不一致。有时我们需要将一个多态指针转换为其实际指向对象的类型,就需要知道运行时的类型信息,这就产生了运行时类型识别的要求。

RTTI(Run Time Type Identification):

  • 即通过运行时类型识别,程序能够使用基类的指针或引用来检查着这些指针或引用所指的对象的实际派生类型。

两种操作符

  • typeid操作符,返回指针和引用所指的实际类型;
  • dynamic_cast**(类型强制转换操作符),将指向派生类的基类指针或引用安全地转换为其派生类类型的指针或引用**。

7.6.1 dynamic_cast 类型强制转换符

背景:

  • 为什么需要类型的强制转换,

    根本原因:基类的指针无法访问派生类中除(基类成员的函数和变量),如果需要访问需要使用。需要将指针的类型强制转换为派生类的类型。dynamic_cast<D*>(pb)->g(),该表达式同时不会影响pb的原类型

性质:

  • 该转换符用于将一个指向派生类的基类指针或引用转换为派生类的指针或引用
  • 注意dynamic_cast转换符只能用于含有虚函数的类

7.7 深入理解C++函数的动态绑定和静态绑定

总结:

  • 只有虚函数才使用的是动态绑定,其他的全部是静态绑定
    • 普通函数,静态绑定,根据静态类型选择调用编译时确定的函数
    • 覆盖虚函数,符合多态条件,动态绑定,根据动态类型选择运行时的函数

对象的静态类型和动态类型:

  • 对象的静态类型:对象在声明时采用的类型。是在编译期确定的
  • 对象的动态类型:目前所指对象的类型。是在运行期决定的。对象的动态类型可以更改,但是静态类型无法更改。

7.7.1 无法通过对象实现多态,只能通过指针或者引用

  • 指针和引用的动态类型和静态类型可能会不一致,但是对象的动态类型和静态类型是一致的。因此无法直接通过对象实现多态。
class B
{
}
class C : public B
{
}
class D : public B
{
}
D* pD = new D();//pD的静态类型是它声明的类型D*,动态类型也是D*
B* pB = pD;//pB的静态类型是它声明的类型B*,动态类型是pB所指向的对象pD的类型D*
C* pC = new C();
pB = pC;//pB的动态类型是可以更改的,现在它的动态类型是C*
D d=D()//d的静态类型D,动态类型D
B b=B(d)//b的静态类型B,动态类型是D

函数静态绑定和动态绑定

  • 静态绑定:绑定的是对象的静态类型,某特性(比如函数)依赖于对象的静态类型,发生在编译期。
  • 动态绑定:绑定的是对象的动态类型,某特性(比如函数)依赖于对象的动态类型,发生在运行期。
lass B
{
    void DoSomething();
    virtual void vfun();
}
class C : public B
{
    void DoSomething();//首先说明一下,这个子类重新定义了父类的no-virtual函数,这是一个不好的设计,会导致名称遮掩;这里只是为了说明动态绑定和静态绑定才这样使用。
    virtual void vfun();
}
class D : public B
{
    void DoSomething();
    virtual void vfun();
}
D* pD = new D();
B* pB = pD;

pD和pB都指向同一个对象。因为函数DoSomething是一个no-virtual函数,它是静态绑定的,也就是编译器会在编译期根据对象的静态类型来选择函数。pD的静态类型是D*,那么编译器在处理pD->DoSomething()的时候会将它指向D::DoSomething()。同理,pB的静态类型是B*,那pB->DoSomething()调用的就是B::DoSomething()。

    pD->vfun()和pB->vfun()调用的是同一个函数吗?
是的。因为vfun是一个虚函数,它动态绑定的,也就是说它绑定的是对象的动态类型,pB和pD虽然静态类型不同,但是他们同时指向一个对象,他们的动态类型是相同的,都是D*,所以,他们的调用的是同一个函数:D::vfun()。
上面都是针对对象指针的情况,对于引用(reference)的情况同样适用。 

7.7.2 默认参数时静态绑定

  • 虚函数是动态绑定的,

  • 缺省参数是静态绑定的。但是为了执行效率,

class B
{
 virtual void vfun(int i = 10);
}
class D : public B
{
 virtual void vfun(int i = 20);
}
D* pD = new D();
B* pB = pD;
pD->vfun();
pB->vfun();
缺省参数是静态绑定的,pD->vfun()时,pD的静态类型是D*,所以它的缺省参数应该是20;同理,pB->vfun()的缺省参数应该是10

注意:

  • 绝不重新定义继承而来的缺省参数(Never redefine function’s inherited default parameters value.)

7.8 虚函数指针 和虚函数表(多态的实现机制)

虚函数表.png
在这里插入图片描述

虚函数表2.png
在这里插入图片描述

基本知识:

  • 空类占用一个字节(用于标识类对象)
  • 虚函数占用4个字节(类中包含一个vfptr)虚函数指针(32位系统)。
  • 普通函数,和类静态绑定,没有函数表,执行速度较快。

虚函数表(vtbl)(属于类):

  • 当一个类中有虚函数时, 编译期间, 就会为这个类分配一片连续的内存 (这就是虚表vftable), 来存放虚函数的地址。 (VS中虚表内存分配在代码段),虚表被同个类的对象共享。
  • 在有虚函数的类(有虚表的类)被继承后, 虚表也会被拷贝给派生类. 注意, 编译器会给派生类新分配一片空间来拷贝基类的虚表, 将这个虚表的指针给派生类, 而并不是沿用基类的虚表, 在发生虚函数的重写时, 重写的是派生类为了拷贝基类虚表新创建的这虚表中的虚函数地址
  • 虚表本质**:上是一个在编译时就已经确定好了的**void* 类型的指针数组 .
  • 虚表为所有这个类的对象所共享. 注意, 是通过给每个对象一个虚表指针_vfptr共享到的虚表**.

虚函数指针(vptr)(属于类实例)

  • 对象内存中只保存着指向虚表的指针 (也就是虚函数表指针vfptr) , (虚函数其实和普通函数一样, 存放在代码段) , 当这个类实例出对象时, 每个对象都会有一个虚函数表指针vfptr

7.8.1 单继承中的虚表

  • 单继承未重写虚函数

    父类虚表拷贝一份,将新的虚表地址赋予派生类作为虚函数指针

    如果派生类中新增了虚函数, 则会加继承的虚表后面

  • 单继承重写虚函数: 继承的虚表中被重写的虚函数地址会在继承虚表时被修改为派生类函数的地址(如下面例子中把A::func()修改成了B::func()的地址)
    (注意: 此时基类的虚表并没有被修改, 修改的是派生类自己的虚表)

函数重写本质虚函数表内函数地址的重写

  • 实际上就是在继承基类虚表时, 把基类的虚函数地址修改为派生类虚函数的地址
#include<iostream>
using namespace std;
class A {
	int m_a;
public:
	virtual void func(){}
	virtual int func1() {}
};
class B :public A {
public:
	virtual void func() {}
	virtual void func2() {}
};
int main() {
        A a1;
        A a2;
	B b;
	system("pause");
	return 0;
}

虚表1.png
在这里插入图片描述

7.8.2 多继承中的虚表

  • 多继承不重写虚函数: 继承的多个基类中有多张虚表, 派生类会全部拷贝下来, 成为派生类的多张虚表, 如果派生类有新的虚函数, 会加在派生类拷贝的第一张虚表的后面(拷贝的第一张虚表是继承的第一个有虚函数(或虚表)的基类的)

    • 所有的虚函数指针放到对象最前面
  • 多继承重写虚函数 : 规则与不重写虚函数相同,

    • 但需要注意的是, 如果多个基类中含有相同的虚函数, 所有含有这个函数的基类虚表都会被重写 (改的是派生类自己拷贝的基类虚表, 并不是基类自己的虚表)
//ClassA1是第一个基类,拥有普通函数func1(),虚函数vfunc1() vfunc2()。
//ClassA2是第二个基类,拥有普通函数func1(),虚函数vfunc1() vfunc2(),vfunc4()。
//ClassC依次继承ClassA1、ClassA2。普通函数func1(),虚函数vfunc1() vfunc2() vfunc3()。

多继承_虚函数.png
在这里插入图片描述

多态的原理
我们回忆一下多态的两个构成条件

  1. 通过指向派生类对象的基类的指针或引用调用虚函数

  2. 被调用的函数必须是被派生类重写过的虚函数

多态实现本质:

  • 基类指针或基类引用指向子类对象,调用虚函数时,通过vfptr和vftable去找到子类对象的虚函数,通过函数重写,不同子类虚函数的实现不同。实现多态。

小结

  • 同一类对象共享虚表:一个有虚函数的类,它实例出的所有对象通过虚表指针vfptr共享类的虚表

  • 对象中存放的是虚函数(表)指针vfptr, 不是虚表. vfptr是虚表的首地址, 指向虚表

  • 虚表中存放的时虚函数地址, 不是虚函数, 虚函数和普通函数一样, 存放在代码段

  • 虚表是在编译阶段生成的, 一般分配在在代码段(常量区), 例如VS中

派生类的虚表生成:

  • 先将基类中的虚表内容拷贝一份到派生类虚表中
  • 如果派生类重写了基类中某个虚函数,用派生类自己的虚函数覆盖虚表中基类的虚函数
  • 派生类自己新增加的虚函数按其在派生类中的声明次序增加到派生类虚表的最后。
  • 如是多继承, 则派生类新增加的虚函数地址最后一个虚函数地址后面
  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值