C++ 面向对象程序三大特性之 多态

多态的概念

不同类的对象对同一消息作出不同的响应就叫做多态,通俗来讲,就是去完成某个行为,当不同的对象去完成时会产生出不同的结果

例如去网吧上机,如果你是vip,那么你上网的价格就会比普通用户上网的价格低;如果你不是vip,那么你上网的价格就是原价。这就是生活中的一种多态现象

多态的定义及使用

例如以下程序,就是一种多态的体现

class Vip
{
public:
	virtual void online()
	{
		cout << "7折优惠" << endl;
	}
};

class Common : public Vip
{
public:
	virtual void online()
	{
		cout << "全价无优惠" << endl;
	}
};

void fun(Vip& v)
{
	v.online();
}

void test()
{
	Vip v;
	Common m;
	fun(v);
	fun(m);
}

运行结果:
在这里插入图片描述
vip对象以引用的方式赋值给父类对象v,v调用online函数,此时调用的是基类的online函数,输出7折优惠;common对象以引用的方式赋值给父类对象v,v调用online函数,此时调用的是子类的online函数,输出全价无优惠;这就体现了不同对象去完成一件事会有不同的结果,这就是多态。那想要实现多态,需要什么必要条件呢?

实现多态的前提(缺一不可)

  1. 多态的体现是体现在基类和派生类中的,所以多态必须有继承
  2. 该函数必须是虚函数
  3. 虚函数需要被子类重写
  4. 通过父类的指针或者引用调用虚函数,切片操作

前3点都好理解,第四点用代码解释

//多态
void fun(Vip& v)
{
	v.online();
}
void fun1(Vip* v)
{
	v->online();
}

//非多态
void fun2(Vip v)
{
	v.online();
}

在这里插入图片描述

虚函数

在函数名面前加上关键字virtual,则该函数就为虚函数。
格式:virtual 返回值 函数名(参数)

class Vip
{
public:
	virtual void online()
	{
		cout << "7折优惠" << endl;
	}
};

虚函数的重写

虚函数的重写(覆盖):派生类中有一个跟基类完全相同的虚函数称子类的虚函数重写了基类的虚函数

重写(覆盖)要求:派生类中的虚函数要与基类中的虚函数的函数名参数列表返回值都要完全相同

虚函数在多态中的注意事项:在重写基类虚函数时,派生类的虚函数在不加virtual关键字时,虽然也可以构成重写,但是该种写法不是很规范,不建议这样使用(因为继承后基类的虚函数被继承下来了在派生类依旧保持虚函数属性)

虚函数重写的两个例外

1、协变(返回值不同)
派生类重写基类虚函数时,与基类虚函数返回值类型可以不同,但是返回值类型必须是有继承关系的指针或者引用

class A //基类A
{};

class B : public A //派生类B
{};
class Vip
{
public:
	virtual A* online() //返回值是基类的指针,且必须是基类的指针
	{
		cout << "7折优惠" << endl;
		return new A;
	}
};

class Common : public Vip
{
public:
	virtual  B* online()//返回值是派生类的指针,且必须是派生类的指针
	{
		cout << "全价无优惠" << endl;
		return new B;
	}
};

运行结果:
在这里插入图片描述
2、析构函数的重写(名字不同)
如果将基类的析构函数置为虚函数,那么派生类中的显示定义的析构函数无论是否是虚函数,都与基类中的析构函数构成了重写。因为在底层,编译器对析构函数名做了特殊处理,在底层的名字都为destructor,所以构成了重写

我们来看以下代码

class Vip
{
public:
	virtual void online()
	{
		cout << "7折优惠" << endl;
	}
	~Vip()
	{
		cout << "~Vip" << endl;
	}
};

class Common : public Vip
{
public:
	virtual void online()
	{
		cout << "全价无优惠" << endl;
	}
	~Common()
	{
		if (_name)
		{
			delete[] _name;
			cout << "delete[] _name" << endl;
		}
		cout << "~Common" << endl;
	}
private:
	char* _name = new char[100];
};

运行结果:此时并不会释放子类中的资源,产生了内存泄漏的问题
在这里插入图片描述
要想防止内存泄漏,就必须使析构函数有多态的行为,在基类中的析构函数置为虚函数,使此析构函数与派生类中的析构函数构成重写

	virtual ~Vip()
	{
		cout << "~Vip" << endl;
	}

运行结果:这样子就不会发生内存泄漏了
在这里插入图片描述
为什么析构函数要被定义成虚函数?
:实现多态时,我们通过基类指针指向子类对象,在delete基类指针时,我们希望先调用子类的析构函数,再调用父类的析构函数,要实现这个目的,析构函数就必须定义成虚函数,否则只会调用父类的析构函数,子类的析构函数不会被调用

C++11中 override 和 final

如果我们在写多态的代码时,由于我们的疏忽,将函数的名字、返回值或者参数写错了,此时就无法构成重写。而这种错误在编译期间是不会报错的,只有在程序运行时才能发现。这时候为了解决这种问题,C++11引入了两个关键字overridefinal

override
格式:重写的函数 override ----用在派生类中的虚函数
作用:检查派生类虚函数是否重写了基类某个虚函数,如果没有重写编译报错

class Vip
{
public:
	virtual void online() 
	{
		cout << "7折优惠" << endl;
	}
};

class Common : public Vip
{
public:
	//接口继承,不继承基类中的实现,需要自己重写实现
	virtual void online() override
	{
		cout << "全价无优惠" << endl;
	}
};

在这里插入图片描述
final
格式1:class 类名 final ----用在基类中
格式2:函数名 final ----用在基类中的虚函数
作用:1、被final修饰的类不能被继承;2、被final修饰的虚函数不能被重写

class A final
{};
class B : A
{};

在这里插入图片描述

class Vip
{
public:
	//定义继承,将该函数的实现也继承过去且无法修改
	virtual void online() final
	{
		cout << "7折优惠" << endl;
	}
};

class Common : public Vip
{
public:
	
	virtual void online()
	{
		cout << "全价无优惠" << endl;
	}
};

在这里插入图片描述

重载、重写、隐藏的区别与联系

函数重载:同一作用域内被声明的几个具有不同参数的同名函数,根据参数列表确定调用哪个函数,且不关心函数的返回值
函数隐藏:是指派生类的函数屏蔽了与其同名的基类函数,只要同名函数,不管参数列表是否相同,基类函数都会被隐藏
重写覆盖:是指派生类中存在重新定义的函数。其函数名,参数列表,返回值类型,所有都必须同基类中被重写的函数一致。只有函数体不同,派生类调用时会调用派生类的重写函数,不会调用被重写函数。重写的基类中被重写的函数必须有virtual修饰

类别作用域函数名参数列表返回值类型是否有virtual修饰
函数重载同一作用域相同不同无要求无要求
函数隐藏不同作用域(父类和子类)相同无要求无要求父类函数不能有virtua
重写覆盖不同作用域(父类和子类)相同相同相同(协变除外)父类函数必须有

抽象类

纯虚函数的定义:在虚函数的后面写上=0 ,则这个函数为纯虚函数

//纯虚函数
virtual void fun() = 0
{}

抽象类的定义:包含纯虚函数的类叫做抽象类(也叫接口类)

//抽象类
class A
{
public:
	//纯虚函数
	virtual void fun() = 0
	{}
};

class B : public A
{
public:
	virtual void fun()
	{
		cout << "B::fun()" << endl;
	}
};

class C : public A
{
public:
	virtual void fun()
	{
		cout << "C::fun()" << endl;
	}
};

注意:抽象类不能实例化出对象。派生类继承后也不能实例化出对象,只有重写纯虚函数,派生类才能实例化出对象

class D: public A
{};

在这里插入图片描述
抽象类具有规划性,如假如你要去网吧上网,那么你必须提供身份证,也就是派生类必须提供重写提供身份证的这个虚函数,如果不提供,就不能上网,不重写,也就是不能使用该类
引入纯虚函数的目的
1、为了方便使用多态特性,我们常常需要在基类中定义虚拟函数
2、在很多情况下,基类本身生成对象是不合情理的。例如,动物作为一个基类可以派生出老虎、孔雀等子类,但动物本身生成对象明显不合常理
  为了解决上述问题,引入了纯虚函数的概念,将函数定义为纯虚函数,则编译器要求在派生类中必须予以重写以实现多态性。同时含有纯虚拟函数的类称为抽象类,它不能生成对象。这样就很好地解决了上述两个问题

多态的原理

虚函数指针、虚函数、虚函数表指针

我们先来看看以下类中的大小

class Base
{
public:
	virtual void fun()
	{
		cout << "fun()" << endl;
	}
protected:
	int _a = 1;
};

运行结果:
在这里插入图片描述
我们发现是8个字节,为什么会是8个字节呢?难道函数也占用空间了吗?我们创建一个Base对象看看这个对象中都包含了哪些成员
在这里插入图片描述
我们可以发现,在对象b中不仅包含了自己定义的一个成员变量_a,好包含了一个void**类型的指针,所以大小才会8个字节,那为什么会有这个指针呢?那肯定跟这个虚函数脱不了关系。其实这个指针就是虚函数表指针----__vfptr(v代表virtual,f代表 function,ptr代表指针),我们再来看看虚函数表指针里的内容
在这里插入图片描述
虚函数表指针指向的是一个虚表----vftable,也就是说虚表指针是虚表的首地址 。而虚表中存放的是虚函数指针,虚函数指针也就是就是虚函数的地址。所以虚表也就是一个指针数组
我们再来分析一下虚函数指针和虚函数表的关系

class Base
{
public:
	virtual void fun1()
	{
		cout << "fun1()" << endl;
	}
	virtual void fun2()
	{
		cout << "Base::fun2()" << endl;
	}
	void fun3()
	{
		cout << "Base::fun3()" << endl;
	}
private:
	int _b = 1;
};

class Derive : public Base
{
public:
	virtual void fun1()
	{
		{
			cout << "Derive::fun1()" << endl;
		}
	}
private:
	int _d = 2;
};

创建Base对象和Derive对象,查看他们的虚表指针。我们在基类中创建了两个虚函数,而虚函数指针会存放在虚表中,虚表中的两个元素,第一个元素是就是fun1的虚函数指针,第二个元素就是fun2的虚函数指针
在这里插入图片描述
我们画一幅图来形象理解虚表指针、虚表、虚函数指针、虚函数的关系
在这里插入图片描述

搞懂了虚表指针和虚表的关系后我们再来看看派生类中的虚表指针,派生类中也有一个虚表指针,派生类继承了基类的虚表,也就是将基类中的虚表拷贝一份,再用一个新的虚表指针指向该虚表。派生类当存在同名、返回值、参数列表都相同的虚函数时,会将虚表中的相同虚函数指针给覆盖掉,所以重写也可以叫做覆盖,原因就是这样子得来的。例如派生类中的fun1重写基类中的fun1,此时派生类中的虚表存放的就是派生类fun1的虚函数指针。所以之所以可以产生多态行为,其实就是派生类中的虚函数指针覆盖了基类中的指定的虚函数指针,从而调用时就会调用派生类的虚函数了
在这里插入图片描述
但是我们要知道,其实虚表不是存放在对象当中的,只有虚表指针才存放在对象中。我们先证明对象中只存放虚表指针而不存放虚表,我们知道虚表的大小是和虚函数的个数有关,那么我们创建不同个数的虚函数的类,他的大小如果一样,那么就表示虚表是不存放在对象中的,反之存在。

class A
{
public:
	virtual void fun1()
	{
		cout << "fun1()" << endl;
	}
	virtual void fun2()
	{
		cout << "fun2()" << endl;
	}
private:
	int _a = 1;
};

class B
{
public:
	virtual void fun3()
	{
		cout << "fun3()" << endl;
	}
private:
	int _a = 2;
};

运行结果:我们发现即使虚函数的个数不同,类的大小都是相同的,也就表明虚表是不存放在类中的
在这里插入图片描述
我们再总结理一理虚表指针、虚函数指针、虚函数它们各自存放的位置。虚表指针是存放在一个对象中的起始位置或者末尾位置(平台不同位置不同,一般是起始位置);虚函数指针是存放在虚表中的;虚函数和普通函数一样,都是存放在代码段中的那虚表到底存在哪里呢?

在我们程序员能用的到的内存有栈、堆、数据段、代码段。也就是所虚表肯定存在在这四个内存中的其中一个,那么我们如何判断呢?我们可以粗糙得查看地址的远近,查看虚表的地址和那块内存的地址相近,最相近的那地址,也就可以认为是在那一块内存中

void test()
{
	int a = 10; //栈
	int* ptr = new int;//堆
	static int s = 1; //数据段
	const char* str = "123"; //文字常量区/代码段
	cout << "栈:" << &a << endl;
	cout << "堆:" << ptr << endl;
	cout << "数据段:" << &s << endl;
	printf("代码段:%p\n", str);
}

运行结果:栈和堆地址相差还是很大的,而数据段和代码段的地址相差也不小。这四个地址都分别代表了内存中的不同位置,我们接下来在打印虚表的地址,看看和哪个地址接近
在这里插入图片描述
但是现在的问题是,我们如何拿到虚表的地址,我们来分析分析
1、先取b的地址,强转成一个int* 的指针,获取到类的前4个地址,也就获得了虚表指针的地址;

Base b;
&b;
(int*)&b

2、再解引用取值,就取到了b对象头4bytes的值,这个值就是指向虚表的指针,也就是虚表的首地址

*(int*)b;

3、但是这个值是int类型的值,我们必须强转为虚表存放的类型,虚表存放的类型也就是虚函数指针,我们这里定义的虚函数是无参数无返回值的函数,此时我们就拿到了虚表的地址

//虚函数指针变量
//没有返回值,参数列表为空的指针
typedef void(*vfptr)();
vfptr* vfp = (vfptr*)(int*)&b;

运行结果:我们可以推出
在这里插入图片描述
我们发现虚表的地址更接近数据段的地址,所以虚表存放在数据段中。虚函数表本质是一个存虚函数指针的指针数组,这个数组最后面放了一个nullptr

实现原理

如何找到虚函数?
1、从对象中获取虚表指针
2、通过虚表指针找到虚表
3、从虚表中找到虚函数的地址
4、执行虚函数的指令
在这里插入图片描述
我们再通过汇编来看代码的执行
在这里插入图片描述
第一行:v中存的是v对象的指针,将v移动到eax中
第二行:[eax]就是取eax值指向的内容,这里相当于把vip对象头4个字节(虚表指针)移动到了edx
第五行:[edx]就是取edx值指向的内容,这里相当于把虚表中的头4字节存的虚函数指针移动到了eax
第六行:call eax,调用虚函数
这里可以看出满足多态的调用,不是在编译时确定的,是运行起来以后到对象的中取找的

我们来获取虚表中的虚函数,执行并打印

class Base
{
public:
	virtual void fun1()
	{
		cout << "Base::fun1()" << endl;
	}
	virtual void fun2()
	{
		cout << "Base::fun2()" << endl;
	}
private:
	int _b = 1;
};

class Derive : public Base
{
public:
	virtual void fun1()
	{
		cout << "Derive::fun1()" << endl;
	}
	virtual void fun3()
	{
		cout << "Derive::fun3()" << endl;
	}
	virtual void fun4()
	{
		cout << "Derive::fun4()" << endl;
	}
private:
	int _d = 2;
};

typedef void(*vfptr)();
void printfVftable(vfptr vtable[])
{
	cout << "虚表地址:" << vtable << endl;
	//访问虚表元素:虚函数指针
	vfptr* fptr = vtable;
	while (*fptr != nullptr)
	{
		(*fptr)();
		++fptr;
	}
}

在这里插入图片描述

多继承中的虚函数表

在多继承中,都会继承每个类的虚表
在这里插入图片描述
我们先来看看第一个虚表的地址,并执行虚表中的虚函数

class Base1 {
public:
	virtual void func1() { cout << "Base1::func1" << endl; } 
	virtual void func2() { cout << "Base1::func2" << endl; }
private: int b1;
};

class Base2 {
public:
	virtual void func1() { cout << "Base2::func1" << endl; } 
	virtual void func2() { cout << "Base2::func2" << endl; }
private: int b2;
};

class Derive : public Base1, public Base2 {
public:
	virtual void func1() { cout << "Derive::func1" << endl; } 
	virtual void func3() { cout << "Derive::func3" << endl; }
private: int d1;
};

运行结果:func2函数是使用的是第一个父类的虚函数
在这里插入图片描述
我们再来看看第二个虚表的地址,我们如何获得第二个虚表的地址呢?此时我们就必须要偏移base1这个类的大小的距离
在这里插入图片描述
我们就可以看到第二个虚表的虚函数了,并且子类的fun1函数也将第二个虚表的fun1覆盖了,但是我们发现fun3只在第一个虚表中出现,并不在第二个虚表中出现。所以可以说是,新定义的虚函数指针都会默认放到第一个虚表中,所以fun3指针只会存放第一个虚表中

多态零碎知识汇总

1、virtual关键字只在声明时加上,在类外实现时不能加
2、static和virtual是不能同时使用的
3、静态成员函数属于整个类,不能被重写,不能设置为虚函数,虚表指针是存在对象中的,通过类名是拿不到虚表指针的
4、编译时的多态性可通过函数重载和模板实现;运行时的多态性可通过虚函数实现
5、一个类的不同对象共享该类的虚表
6、虚表是在编译期间生成的
7、多继承的时候,就会可能有多张虚表
8、纯虚函数不一定是空函数,只是写函数体的意义不大
9、内联函数不能是虚函数,因为inline函数没有地址,无法把地址放到虚函数表中
10、如果存在虚函数和虚拟继承,对象的前4个字节依然是虚表指针,紧接后面的是虚基表指针
11、构造函数是不能是虚函数的,虚函数的执行依赖于虚函数表。而虚函数表在构造函数中进行初始化工作,即初始化vptr,让他指向正确的虚函数表。而在构造对象期间,虚函数表还没有被初始化,将无法进行
12、如果是普通对象,调用普通函数和虚函数的速度是一样快的;如果是引用或者指针,由于构成多态,运行调用虚函数需要到虚函数表中去查找,则普通函数更快

class Base1 { public: int _b1; }; 
class Base2 { public: int _b2; }; 
class Derive : public Base1, public Base2 { public: int _d; };
int main(){
	 Derive d; 
	 Base1* p1 = &d;  //*p1=_b1
	 Base2* p2 = &d;  //*p2=_b2
	 Derive* p3 = &d;   //*p3=_b1
	 //p1 == p3 != p2
	 return 0;
}
class A {
public:
	virtual void func(int val = 1)
	{ std::cout<<"A->"<< val <<std::endl;} 
	virtual void test(){ func();}
};
class B : public A {
	public: void func(int val=0)
	{ std::cout<<"B->"<< val <<std::endl; }
};
int main(int argc ,char* argv[]) {
	B*p = new B;
	p->test(); 
	//B->1,A类中的func才是真正的定义
	//而B类中是对func的重写,编译器看到是重写时,不看缺省值,只看定义
	return 0;
}
  • 18
    点赞
  • 39
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 7
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

WhiteShirtI

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值