C++(15)——多态与虚函数

多态性

多态性(polymorphism)是考虑不同层次的类中,以及在同一个类中,同名的成员函数之间的关系问题。函数的重载,运算符的重载,属于编译时的多态性(早期绑定:编译期就确定了调用关系)。以虚函数为基础的运行时的多态性(晚期绑定:程序在运行过程中才能确定调用关系 是面向对象程序设计的标志性特征,体现了类推和比喻的思想方法。
在这里插入图片描述

编译时多态(静态多态)

静态多态:函数重载,模板(函数模板和类模板)
编译器编译时就确定了调用关系,就叫做早期绑定,也叫作动态绑定。
在这里插入图片描述

虚函数

  1. 如果一个类中定义了虚函数,那么在编译阶段,编译器需给这个类的类型产生一个唯一vftable虚函数表,虚函数表中主要存储的内容就是RTTI指针(类型字符串)和虚函数的地址。
  2. 当程序运行时,每一张虚函数表都会加载到内存的.rodata区(只读)
  3. 实例化出的对象中除了成员变量外,还存在一个虚表指针(内存的开始部分),指向虚函数表,一个类型定义的n个对象,他们的vfptr指向的都是同一张虚函数表。
  4. 一个类里面虚函数的个数,不影响对象内存的大小(vfptr),影响的是虚函数表的大小

虚函数是一个类的成员函数,定义格式如下:

virtual 返回类型 函数名(参数列表);
  • 关键字virtual指明该成员函数为虚函数,只有类的成员函数才可以定义为虚函数,virtual仅用于类定义中,如果虚函数在在类中声明,类外定义,类外可不加virtual。但在声明时应尽量放在类体内完成。
  • 当某一个类的一个类成员函数被定义为虚函数,则由该类派生出的所有派生类中,该函数始终保持虚函数的特征。写不写virtual都一样

C++中的虚函数的作用主要是实现了多态的机制。关于多态,简而言之就是 用父类型别的指针指向其子类的实例,然后通过父类的指针调用实际子类的成员函数。 这种技术可以让父类的指针有“多种形态”,这是一种泛型技术。所谓泛型技术,说白了就是试图使用不变的代码来实现可变的算法。

运行时的多态

它是通过类继承关系public和虚函数来实现,目的是建立一种通用的程序。
继承结构中,基类指针(引用)指向派生类对象,通过该指针(引用)调用同名覆盖方法(虚函数)。基类指针指向哪个派生类,就会调用哪个派生类对象的覆盖方法,称为多态。
多态的底层是通过动态绑定实现的,基类指针指向谁就会访问谁的vfptr,进而访问其vftable,由此访问的当然是对应的派生类对象的方法了。
注意: 必须通过引用或者指针调动虚函数时,才能够达到运行时的多态的效果。

总结:运行时多态需要满足以下三个要求:

  1. 公有继承(是一个)
  2. 想要虚化的函数加上virtual关键字
  3. 通过指针或者引用绑定函数

示例:

class Animal
{
private:
    string name;
public:
    Animal(const string & na):name(na){}
public:
    virtual void eat(){}
    virtual void walk(){}
    virtual void tail(){}
    virtual void PrintInfo(){}

    string & Get_name(){return name;}
    const string & Get_name() const {return name;}
};

class Dog:public Animal
{
private:
    string owner;
public:
    Dog(const string & ow,const string &na):Animal(na),owner(ow){}
    virtual void eat(){cout<<"Dog eat:bone"<<endl;}
    virtual void walk(){cout<<"Dog walk:run"<<endl;}
    virtual void tail(){cout<<"Dog tail:Wang wang"<<endl;}
    virtual void PrintInfo()
    {
        cout<<"Dog owner"<<owner<<endl;
        cout<<"Dog name"<<Get_name()<<endl;
    }

};

class Cat:public Animal
{
private:
    string owner;
public:
    Cat(const string & ow,const string &na):Animal(na),owner(ow){}
    virtual void eat(){cout<<"Cat eat:fish"<<endl;}
    virtual void walk(){cout<<"Dog walk:silent"<<endl;}
    virtual void tail(){cout<<"Dog tail:Miao miao"<<endl;}
    virtual void PrintInfo()
    {
        cout<<"Cat owner"<<owner<<endl;
        cout<<"Cat name"<<Get_name()<<endl;
    }

};

void fun(Animal & animal)
{
    animal.eat();//animal->dog->dog::vftable::&dog::bark
    animal.walk();
    animal.tail();
    animal.PrintInfo();
}

int main()
{
    Dog dog("mk","erha");
    Cat cat("bw","persian");

    fun(dog);
    fun(cat);
    return 0;
}

注意:每一个存在虚函数的类实例化出的对象都存在虚表指针,但是虚表只有一份,
在这里插入图片描述

编联时需要确定的属性
  1. 类型class
  2. 可访问性
  3. 函数的默认值

虚函数表

虚函数的实现是因为存在一张虚函数表,这一过程中发生了同名覆盖,有虚函数的类在编译过程中才会产生虚表。
覆盖具体指的就是虚函数表中虚函数地址的覆盖。
在这里插入图片描述
下面我们来辨析以下通过指针或引用调动对象调动虚函数的编联方案的异同:
比如下面的代码:注意,关注点在于:成员方法是普通函数还是虚函数
在这里插入图片描述

再来看下面的示例,更加清楚地了解同名覆盖以及虚表指针的转换过程(构造函数发挥作用):

在这里插入图片描述
其实,在类中存在虚函数时,构造函数除了构建对象,初始化对象的数据成员以外,还要将虚表地址传递给虚表指针.
在构造函数中也不能完成如下操作:

memset(this,0,sizeof(Base));

这样很危险,因为这样会将对象内的所有成员初始化成0,虚表指针也会被修改。

所以,我们向前面所说的一样,不能将构造函数定义成虚函数
原因是: 虚函数的存在必须依赖以下两个前提:

  1. 虚函数能产生地址,存储在vftable中,
  2. 对象必须存在(存在对象才有vfptr——>才能找到vftable——>才能找到虚函数的地址)

调动虚函数时,我们需要查表,构造函数是虚表建立的前提,若构造函数为虚函数,那么此时我们调动虚化的构造函数,虚表还没有建立,所以构造函数,拷贝构造函数,移动构造函数都不能被建立成为虚函数。

注意问题:是不是虚函数的调用就一定是动态绑定? 肯定不是的,因为在构造函数中,调用的任何函数都是静态绑定的,调用虚函数,也不会发生静态绑定。

因此,虚函数通过指针或者引用调用,才会发生动态绑定。

再来判断下面程序的输出结果:
在这里插入图片描述

  • t1传入自己的this指针,查看自身的虚表,发现自己没有fun函数,于是调动其上一层base的fun函数
  • base传入自己的this指针,查看自身的虚表,发现自己有fun函数,直接调动
  • obj传入自己的this指针,查看自身的虚表,发现自有,于是调动自身的fun函数

继续探究:下面程序的执行结果为什么是:Base::print::10
在这里插入图片描述
前面我们提到了编联时需要确定的属性,也要注意重写虚方法时只需要保证三同(同函数名,同参数类型,同返回类型),并没有要求函数的默认值必须相同。

  • 编译时,我就需要确定可访问性,对于op来说,它是一个Object类型,因此他就可以访问自己的print函数,函数的默认值在编译时也就确定为10,
  • 运行时,发现是通过指针访问虚函数,因此采取动态编联方案,通过查表后又调动Base的print函数

那么为什么可以通过op访问Base的私有虚函数呢?
因为,该程序的编译是可以通过的,在运行时编译器不会考虑访问属性的影响。

static静态成员方法不能加上virtual关键字,因为static修饰的方法并不依赖于对象。

虚析构函数

先看下面的代码:

class Object
{
private:
	int value;
public:
	Object(int x = 0):value(x){cout<<"Create Object:"<<this<<endl;}
	virtual void add(){cout<<"Object add"<<endl;}
	virtual ~Object(){cout<<"Destroy Object"<<this<<endl;}
};
class Base:public Object
{
private:
	int num;
public:
	Base(int x = 0):num(x){cout<<"Create Base:"<<this<<endl;}
	virtual void add(){cout<<"Base add"<<endl;}
	~Base(){cout<<"Destroy Base"<<this<<endl;}
};
int main()
{
	Object *op = new Base(10);
	op->add();
	delete op;
	op = NULL;
	return 0;
}

上述代码的执行结果如下:
在这里插入图片描述

我们可以看到,其实它并没有调用Base对象的析构函数,仅仅调用了父类对象的虚构函数,究其原因是因为:二者的析构函数非虚函数(普通函数),所以编译器在解析时发现,op是一个Object的指针,此时就只会调动Object的析构函数。

为了做到运行时多态的释放,就必须让析构函数变成虚函数

因为,一旦父类的析构函数定义为虚函数,那么所有派生类的析构函数也都会变成虚函数,发生同名覆盖,因此无需在派生类中声明,在调动delete,传入父类指针时,就会将父类对象和子类对象都析构。

修改代码后执行结果如下:
在这里插入图片描述
那么为什么我们要将析构函数定义成虚函数呢?

就是因为在上述的应用场景下,当父对象的指针(引用)指向堆区new出来的派生类对象时,在释放父指针时,我们采用连级释放的机制,从而调动子类的析构,否则会导致派生类的析构函数无法调用。

注意:在没有虚函数的类中,我们没必要将析构函数定义为虚函数,因为这个类并不会参与到继承关系中,也就不需要连级释放。

析构函数的另一个作用

reset重置虚表,以上述代码为例,在析构过程中先析构子对象,然后此时整个Base对象就剩下隐藏基对象的资源和空间,然后重置虚表指针指向隐藏基对象的首地址,再对该对象进行析构。

纯虚函数

纯虚函数是指被标明为不具体实现的虚拟成员函数,它用于这种情况:定义一个基类时,会遇到无法定义基类中虚函数的具体实现,其实现依赖于不同的派生类。
定义纯虚函数的一般格式是:

virtual 返回类型 函数名(参数列表) = 0;

"= 0 "表示程序员将不定义该函数,函数声明是为派生类保留一个位置,其本质是将指向函数体的指针定为NULL。

总结

虚函数和纯虚函数的区别

在这里插入图片描述

  • 0
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值