C++面向对象(十)从继承到多态

上一讲,我们说到基类指针指向派生类对象,由于指针类型的缘故,使得基类指针无法访问派生类的方法和成员变量,这一讲我将带大家看虚函数是如何解决该问题的。

先看一下上一讲的代码:

class base
{
public:
	base(int a) :ma(a){}
	~base(){}
	void show(){ cout << "base::show()" << endl; }
	void show(int i){ cout << "base::show(int)" << endl; }
protected:
	int ma;
};
class dev:public base   
{
public:
	dev(int data) :base(data), mb(data){}
	~dev(){}
	void show(){ cout << "dev::show()" << endl; }
private:
	int mb;
};
int main()
{	
	dev d(10);
	base *p = &d;  //基类指针指向派生类对象
	p->show();      //反汇编:call        base::show
	return 0;
}
执行结果:
base::show()
请按任意键继续. . .

由于指针类型的关系,只能调用到基类的show方法

一、虚函数与覆盖关系
成员方法前加上virtual关键字,就构成了一个虚函数;
假设父类中有一个虚函数 fun() ,子类中也有与父类一个同名、同参、同返回值的函数,会被编译器自动改为虚函数。
实例:

//我在基类中添加一个 虚函数
class base
{
public:
	base(int a) :ma(a){}
	~base(){}
	virtual void show(){ cout << "base::show()" << endl; }
	void show(int i){ cout << "base::show(int)" << endl; }
protected:
	int ma;
};

//派生类中 同名 同返回值  同参的普通函数自动被设置为 虚函数
class dev:public base   
{
public:
	dev(int data) :base(data), mb(data){}
	~dev(){}
	void show(){ cout << "dev::show()" << endl; }
	//等同于 virtual void show(){ cout << "dev::show()" << endl; }
private:
	int mb;
};

覆盖:
函数的名字、返回值、参数列表相同,而且基类中的函数是个虚函数,构成覆盖;
dev中的show()和base中的show()构成覆盖。
覆盖指的是虚函数表上的覆盖。

在上一讲的基础上,仅仅增加了一个virtual关键字,再来执行下结果:

int main()
{	
	dev d(10);
	base *p = &d;  //基类指针指向派生类对象
	p->show();
	return 0;
}

执行结果:
dev::show()
请按任意键继续. . .

我们发现调用的不再是基类的show方法,而是派生类的show方法。
原因下面解释。

二、虚函数指针和虚函数表

class base
{
public:
	base(int a) :ma(a){}
	~base(){}
	virtual void show(){ cout << "base::show()" << endl; }
	virtual void show(int i){ cout << "base::show(int)" << endl; }
protected:
	int ma;
};

实际上,我们分别用含有虚函数和不带虚函数的类实例化一个对象,发现sizeof()得到的数值相差4B(32位系统),实际上多了一个虚函数指针。
在这里插入图片描述
1.虚函数指针指向虚函数表,虚函数表保存在虚函数的入口地址。
2.虚函数表在编译阶段产生,该表存放在.rodata段,只有一份,一个类型对应一张vftable,相同类型的对象共享同一张vftable,生命周期与程序一样。

三、存在继承关系中虚函数指针和虚函数表何去何从:

class base
{
public:
	base(int a) :ma(a){}
	~base(){}
	virtual void show(){ cout << "base::show()" << endl; }
	virtual void show(int i){ cout << "base::show(int)" << endl; }
protected:
	int ma;
};
class dev:public base   
{
public:
	dev(int data) :base(data), mb(data){}
	~dev(){}
	virtual void show(){ cout << "dev::show()" << endl; }
private:
	int mb;
};

当子类中也有虚函数时,按理说也会有一个虚函数指针,那么是不是我派生类对象中就有两个vfptr?
不是,当父类和基类中都有虚函数时,vftable会进行合并。
在这里插入图片描述
什么样的函数可以设置为虚函数:
1.首先得有对象,因为虚函数在虚函数表中,vfptr又保存在对象的前四个字节,对象都没又怎么去找到然后调用虚函数呢?
所以:
构造函数的不能为虚函数,因为先产生对象再去查虚函数表;
静态函数也不可以,因为静态函数不是对象的,是整个类共有的,没有对象也可以调用;
析构函数可以设置虚函数,析构基类对象要用到,后面会说到。
2.函数要能取地址。虚函数表中存放的都是虚函数的地址,要是不能取地址怎么找到调用这个函数呢?
所以:
内联函数不能设置为虚函数,因为不产生符号所以不能取地址。

四、多态
1.多态的分类:

静态的多态(早绑定):编译时的绑定,编译时就知道调的是哪个函数
(比如模板、函数重载)
动态的多态(晚绑定):运行时的绑定,运行的时候才知道最终调的哪个函数
(比如虚函数)
在没有虚函数的情况下的汇编代码:

call        base::show

因为指针p就是base类型,编译器就去确定了调用的是哪个函数

有虚函数,且基类指针指向派生类对象调用方法的汇编:

move eax,dword ptr[p]
move ecx,dword ptr[eax]
call ecx
实际上,编译的时候ecx中保存的虚函数表的虚函数的某个函数的地址
编译的时候并不知ecx会存放谁的地址,运行的时候才会知道

2.运行时的类型:RTTI
实际上,我们上面的内存结构中虚函数表的内容还没有画完全:
在这里插入图片描述
我们来验证一下:

#include<iostream>
#include<typeinfo>
using namespace std;
int main()
{	
	dev d(10);
	base *p = &d;  //基类指针指向派生类对象

	cout << sizeof(base) << endl;
	cout << sizeof(dev) << endl;
	cout << typeid(p).name() << endl;    //p很明显时base*类型
	cout << typeid(*p).name() << endl;   //*引用的对象是d,类型是dev类型,所以*p是dev类型

	base &q = dev(100);
	cout << typeid(q).name() << endl;
	//引用在使用的时候自带解引用
	return 0;
}

执行结果:
8
12
class base *
class dev
class dev
请按任意键继续. . .

3.理解重点:
尽管指针p的类型是一个基类指针,但是实际上指针p指向的是派生类的对象,这个类型实际上在RTTI中标明,RTTI通过虚函数指针vfptr找到,所以通过指针找方法的时候肯定会找到派生类的内存中的虚函数表,最终调用的派生类的方法。

五、什么情况发生多态:
1.存在于继承关系;
2.继承关系中必须有同名的虚函数,并且他们的是覆盖函数。
3.基类指针/引用指向派生类对象,指针或者引用去调用方法,这是最常用的。
(实际上,基类指针指向基类对象调用虚函数会发生多态)
(派生类指针指向派生类对象调用虚函数会发生多态)
也就是说,只要用指针或者引用调用虚函数都会发生多态。

必须存在继承关系;
继承关系中必须有同名的虚函数,并且它们是覆盖关系(函数原型相同)。
存在基类的指针,通过该指针调用虚函数。

六、纯虚函数和抽象类

class animal
{
public:
	animal(string name) :_name(name){}
	virtual void bark() = 0;  
	//纯虚函数,无法实现,因为不知道该怎么实现,无法代表某个具体的实体
protected:
	string _name;
};

纯虚函数:首先是个虚函数,无法进行具体的实现,所以 =0 ;
含有纯虚函数的类,就叫抽象类。
所以派生类从基类继承后一定要把基类中的纯虚函数重写,否则派生类也称抽象类了。

由于抽象类是对某一类的抽象说明,所以不能用来实例化对象;
但是可以定义指针和引用。

七、最后总结一个问题:
1.什么时候根据虚函数表中的RTTI看类型:
存在虚函数就看虚函数表的类型RTTI,因为我要去确定指针指向的究竟是什么东西,然后再根据类型查对应的虚函数表,调用相应的虚函数。

所以

base d(10);
base *p = &d;
p->show();

不难理解,为什么调用的是派生类中的show
p的类型是base *不假,但是*p实际上指的是d这个对象,也就是派生类对象
我在调用虚函数的时候,我会查看RTTI的类型,那不用说肯定是派生类类型
typeid(*p).name() == class dev
自然最终调用的是dev虚函数表中对应的方法了,
dev的show在编译的时候已经将base的show重写覆盖了,所以调用的是dev的show。 

2.判断多态的大概流程是什么样的?
1.在编译期看是不是虚函数,如果没有则不会发生多态,调用函数按照指针或者引用的类型来即可;
2.如果是一个虚函数,是否通过指针或者引用调用该虚函数,如果仅仅通过对象调用函数,不会发生多态;
3.既是虚函数,又通过指针/引用调用虚函数,先看指针真正指向对象是什么类型,即对象内存的虚函数表的RTTI,得到虚函数表的RTTI,就去查看这个类型对应的虚函数表,找个这个虚函数,调用即可,这样就发生了多态。

这里举一个经典的例子:

class base
{
public:
	base(int a) :ma(a){}
	~base(){}
	void show(){ cout << "base::show()" << endl; }
protected:
	int ma;
};
class dev :public base
{
public:
	dev(int data) :base(data), mb(data){}
	~dev(){}
	virtual void show(){ cout << "dev::show()" << endl; }
private:
	int mb;
};

int main()
{
	base *p = new dev(100);
	p->show();   //base -> show()
	return 0;
}

执行结果:
base::show()
请按任意键继续. . .

没有产生动态多态,调用的是基类的方法:
1.base中没有虚函数,也就没有虚函数表,所以就不会发生多态;
2.所以就没必要去看 指针指向的对象 的RTTI究竟是什么了;
3.直接根据指针的类型调用方法即可。

指针的类型是base,编译的时候只能看到base,发现base中压根没有虚函数
所以根本不会发生多态。

注意理解下面的语句:

  1. 父类中没有虚函数,子类中有无法构成多态,因为编译的时候只能看到指针对应的类型中的方法,发现我们调用的函数不是虚函数,那么子类就无法重写父类的方法,就没法构成多态;
  2. 父类中有虚函数,子类中没有,如果子类中有函数与父类虚函数函数原型一样,编译器会自动将子类的那个普通函数设置为虚函数,达到重写的目的,然后都在虚函数表中存放,用的时候查虚函数表,达到了多态的目的。

虚函数的作用就是为了让子类去重写,重写了调用虚函数时候根据指针或者引用实际的执行的类型查这个类型的虚函数表,即动态绑定。

编译期能看到的是一个指针对应的类型中的存不存在虚函数,也就直接决定能不能进行动态绑定,不存在就无法构成多态。
如果基类存在虚函数,再看我们指针调用的这个函数在基类中是不是虚函数,如果不是,就不能构成重写(覆盖),就不能构成多态。如果是虚函数,则会根据指针实际执行的对象类型查找虚函数表进行函数调用,达成多态

猜你喜欢

转载自blog.csdn.net/KingOfMyHeart/article/details/90138336
今日推荐