C++之多态

一.多态的概念

去完成某个行为,当不同的对象完成时,会产生不同的状态。

二.多态的定义及实现

1)多态的构成条件

多态的前提是继承,只有满足继承关系,才能形成多态。
在继承中要构成多态还有两个条件:
a. 必须通过基类的指针或者引用调用虚函数;
b. 被调用的函数必须是虚函数,且派生类必须对基类的虚函数进行重写;

2)多态的代码演示

#include<iostream>
#include<string>
using namespace std;

class Person{
public:
	Person(string name="person")
		:_name(name)
	{}
	//多态要保证有虚函数
	virtual void show()
	{
		cout << "name:" << _name << endl;
	}
protected:
	string _name;
};

//多态的前提是继承
class Teacher :public Person{
public:
	Teacher(string name="teacher",int TNo=11111)
	{
		_name = name;
		_TNo = TNo;
	}
	//多态中,子类要重写继承自父类的虚函数
	virtual void show()
	{
		cout << "name:" << _name << "   TNo:" << _TNo << endl;
	}
protected:
	int _TNo;
};

//多态要通过基类的指针或者引用调用虚函数
void fun(Person& p)
{
	p.show();
}

//非多态
void fun2(Person p)
{
	p.show();
}
void test()
{
	Person p("cxp");
	Teacher t("t1", 66666);
	cout << "多态:" << endl;
	fun(p);
	fun(t);
	cout << "非多态:" << endl;
	fun2(p);
	fun2(t);
}

int main()
{
	test();
	return 0;
}

运行结果:
在这里插入图片描述
因为是通过基类的引用调用,所以满足多态的条件,
而通过对象调用时,不满足多态的条件,只是简单的传值。

(3)虚函数及虚函数重写

a.虚函数:

被virtual关键字修饰的类成员函数
虚函数的实际使用中,在子类中可以不加virtual关键字,但最好加上,这样看起来比较明确。
定义虚函数就是为了实现多态,如果不实现多态就不需要定义虚函数
一个类中如果没有定义任何成员变量,且只有一个虚函数,这个类的大小为4个字节,因为类中有一个虚表指针,它指向虚表,虚表中存放虚函数指针。

虚函数常见的一些疑难点
  1. 虚函数和inline函数
    inline函数不能声明为虚函数,因为内联函数会被展开,它没有函数地址,而虚函数需要在虚表中存储自己的函数地址

  2. 虚函数和static
    虚函数不能是static的,因为static修饰的函数没有this指针

b.虚函数的重写:

子类定义了一个父函数接口完全相同的函数,子类和父类的函数名相同,参数列表也相同,只是函数体中的内容发生了改变。

虚函数重写的两个例外:
第一种:协变
返回值类型可以不同,但是返回值类型必须是有继承关系的指针或者引用;

协变代码演示:

class A{
};
class B :public A{
};

class Parent{
public:
	virtual A* fun(){
		return new A;
	}
};

class Son :public Parent{
public:
	virtual B* fun(){
		return new B;
	}
};

第二种:析构函数的重写
当通过父类的指针调用子类对象时,如果释放父类的指针指向的空间仅仅是释放了父类的资源,子类的资源不会被释放,会造成内存泄漏,因此析构函数要加上virtual关键字进行重写。

代码演示:
析构函数不加virtual关键字:

class Person{
public:
	 ~Person(){
		cout << "~Person()" << endl;
	}
};

class Student :public Person{
public:
	~Student(){
		delete[] ptr;
		cout << "~Student()" << endl;
	}
private:
	char* ptr = new char[100];
};

void test()
{
	Person* p = new Student;
	delete p;

}

运行结果:
在这里插入图片描述
仅仅调用了父类的析构函数

加上virtual关键字构成重写:

class Person{
public:
	 virtual ~Person(){
		cout << "~Person()" << endl;
	}
};

class Student :public Person{
public:
	virtual ~Student(){
		delete[] ptr;
		cout << "~Student()" << endl;
	}
private:
	char* ptr = new char[100];
};

void test()
{
	Person* p = new Student;
	delete p;

}

运行结果:
在这里插入图片描述
释放基类指针所指对象的资源时,会释放子类的资源和父类的资源

(4)final关键字

用法一:
class 类名 final:
一个类的类名后面加上final关键字之后,这个类将不能被继承;
示例:

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

报错:
在这里插入图片描述

用法二:
virtual 虚函数名(参数列表) final:
表示这个虚函数将不能被重写

示例:

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

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

报错:
在这里插入图片描述

(5)override关键字

虚函数 override:强制重写父类的一个虚函数
在子类当中使用,用来检查子类是否重写了父类中的虚函数,如果没有重写就会报错。

使用示例:
使用override但是没有重写

class Base
{
public:
	virtual void func1()
	{
		cout << "Base::func1()" << endl;
	}
};

class Derive : public Base
{
public:
	virtual void func1() override;
};

编译报错:
在这里插入图片描述

override正确使用示例:

class Base
{
public:
	virtual void func1()
	{
		cout << "Base::func1()" << endl;
	}
};

class Derive : public Base
{
public:
	virtual void func1() override;
};

void Derive::func1()
{
	cout << "Derive::func1()" << endl;
}

使用了override的虚函数进行重写后编译通过


三.纯虚函数和抽象类

(1)纯虚函数

虚函数=0
函数体为0的虚函数称为纯虚函数。
示例:

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

(2)抽象类

包含纯虚函数的类就是抽象类(也叫接口类),无论这个类中是否有其他的函数。

抽象类不能用来实例化对象,当派生类继承了一个抽象类时,当将其中的纯虚函数全部实现之后,才能用来实例化对象;

抽象类的使用场景:
定义一些公用的规范化的接口,各个子类根据自己不同的需求,重写纯虚函数。

(3)接口继承和实现继承

虚函数的继承是一种接口继承,派生类继承的是基类虚函数的接口,目的是为了重写,达成多态,继承的是接口。
普通函数的继承是一种实现继承,派生类继承了基类函数,可以使用函数,继承的是函数的实现。

四.多态的原理

(1)虚表

a、每个有虚函数的对象除了自身的成员,还会有一个虚表指针成员__vfptr(这个成员指向虚表),虚表中存放当前对象中的虚函数指针。
b、虚表是一个虚函数指针数组,虚表中不存放普通函数指针。
c、子类会继承父类的虚表,但子类中的虚表和父类是独立的。
d、子类的虚表中,如果有重写的虚函数,对应的函数指针也会被子类的虚函数指针覆盖;
e、虚表指针是一个指向函数指针数组的指针,是一个二级指针;
f、虚表指针放在对象中,但是虚表并不放在对象中,一般存放在代码段(vs)。
g、虚表在编译期确定;
h、一个类实例化后的不同对象使用的是同一张虚表;
这个特性可以通过代码验证:

#include <iostream>
#include <cstdio>

using namespace std;

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

class Base2
{
public:
	virtual void func3()
	{
		cout << "Base2::func3()" << endl;
	}
	virtual void func4()
	{
		cout << "Base2::func4()" << endl;
	}
};

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

	virtual void func3()
	{
		cout << "Derive::func3()" << endl;
	}

	virtual void func5()
	{
		cout << "Derive::func5()" << endl;
	}
};

typedef void(*VFPTR)();		//定义虚函数指针类型

//虚表指针指向一个虚函数指针数组,是一个二级指针
void PrintVirtualTable(VFPTR* head)
{
	可能死循环,因为编译器有时在虚表最后面没有放nullptr,
	//生成->清理解决方案,编译即可
	while (*head != nullptr)
	{
		(*head)();
		++head;
	}
}

void test()
{
	Derive d;
	Derive d2;
	Derive d3;

	VFPTR* p = (VFPTR*)(*(int*)(&d));
	printf("%p\n", p);

	p = (VFPTR*)(*(int*)(&d2));
	printf("%p\n", p);

	p = (VFPTR*)(*(int*)(&d3));
	printf("%p\n", p);

}

int main()
{
	test();

	return 0;
}

测试结果如下:
在这里插入图片描述
经过验证,所有对象的虚表指针是同一个虚表指针,也就是用的是同一张虚表

虚表存放的位置:
visual studio验证:代码段
验证过程:

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

void fff(){}
void test()
{
	Base b;
	int x;	//栈上
	int *p = new int;	//堆上
	static int s = 0;	//数据段
	void(*fptr)();
	fptr = fff;		//代码段
	typedef void (*vfptr)();	//定义一个函数指针
	/*
		拿到对象的首地址,将其解析为int*类型,再进行解引用,就拿到了前四个字节的内容(虚表指针),
		得到的数据是地址的十进制形式,是一个整数值,再将数据进行类型强转,强转为函数指针的指针
	*/
	vfptr* ptr= (vfptr*)(*(int*)(&b));

	cout << "栈:" << &x << endl;
	cout << "堆:" << p << endl;
	cout << "数据段:" << &s << endl;
	cout << "代码段:" << fptr << endl;
	cout << "虚表:" << ptr << endl;
	
	delete p;
}

运行结果:
在这里插入图片描述
虚表的地址离代码段变量的地址最近,所以虚表的位置是代码段。

在g++中,虚表的位置也是在代码段
在这里插入图片描述

虚函数存放的位置
虚函数和普通函数一样,函数体都是放在代码段;

(2)多态原理:

a、获取对象首地址,放入eax寄存器中;
b、从对象的首地址开始获取4byte内容,存入edx寄存器中,实际存入的是虚表的首地址;
c、从虚表的首地址开始,获取虚表中的一个虚函数指针,存入eax寄存器中;
d、调用函数指针所指的函数。

(3)静态绑定和动态绑定
静态绑定又称为前期绑定(早绑定),在程序编译期间确定了程序的行为,也称为静态多态,比如:函数重载,模板
动态绑定又称后期绑定(晚绑定),是在程序运行期间,根据具体拿到的类型确定程序的具体行为,调用具体的函数,也称为动态多态(运行时多态)。

五.单继承和多继承关系的虚函数表

在虚表中,空指针表示后面没有虚函数指针,空指针表示虚表内容结束。

(1)单继承虚函数打印

代码示例:

class Base {
public:
	virtual void func1() { cout << "Base::func1" << endl; }
	virtual void func2() { cout << "Base::func2" << endl; }
private:
	int a;
};
class Derive :public Base {
public:
	virtual void func1() { cout << "Derive::func1" << endl; }
	virtual void func3() { cout << "Derive::func3" << endl; }
	virtual void func4() { cout << "Derive::func4" << endl; }
private:
	int b;
};

typedef void(*vfptr)();
//可能死循环,因为编译器有时在虚表最后面没有放nullptr,生成->清理解决方案,编译即可
void PrintTable(vfptr table[])
{
	vfptr* p = table;	//p指向虚表的起始位置
	//当*p不是虚表结束标志nullptr时,调用虚函数
	while (*p != nullptr)
	{
		(*p)();
		p++;
	}
}
void test()
{
	Base b;
	vfptr* vb = (vfptr*)(*((int*)&b));
	cout<<"Base vfptr:"<<endl;
	PrintTable(vb);
	Derive d;
	vfptr* vd = (vfptr*)(*((int*)&d));
	cout<<"Derive vfptr:"<<endl;
	PrintTable(vd);
}

运行结果:
在这里插入图片描述

(2)多继承虚函数打印

多继承中有几个直接父类就会有几个虚表。
子类新定义的虚函数,其虚函数指针存放在第一个直接父类的虚表中。

代码演示:

#include <iostream>
#include <cstdio>

using namespace std;

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

class Base2
{
public:
	virtual void func3()
	{
		cout << "Base2::func3()" << endl;
	}
	virtual void func4()
	{
		cout << "Base2::func4()" << endl;
	}
};

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

	virtual void func3()
	{
		cout << "Derive::func3()" << endl;
	}

	virtual void func5()
	{
		cout << "Derive::func5()" << endl;
	}
};

typedef void(*VFPTR)();		//定义虚函数指针类型

//虚表指针的本质是一个虚函数指针数组,也就是一个二级指针
void PrintVirtualTable(VFPTR* head)
{
	可能死循环,因为编译器有时在虚表最后面没有放nullptr,
	//生成->清理解决方案,编译即可
	while (*head != nullptr)
	{
		(*head)();
		++head;
	}
}

void test()
{
	Derive d;

	VFPTR* p2 = (VFPTR*)(*(int*)(&d));
	cout << "第1张虚表的内容如下:" << endl;
	PrintVirtualTable(p2);
	cout << "第2张虚表的内容如下:" << endl;
	p2 = (VFPTR*)(*(int*)((char*)(&d) + sizeof(Base1)));
	PrintVirtualTable(p2);
}

int main()
{
	test();

	return 0;
}

运行结果:
在这里插入图片描述

多继承打印存在的疑惑

关于多继承的虚表打印,我不知道为什么第二个虚表的首地址为什么相对于对象的地址偏移了sizeof(Base1)个字节
原因在于:多继承关系下的派生类对象的对象模型是先存储第一个类的虚表指针,再存储第一个类的自定义成员,后面存储第二个类的虚表指针和第二个类的自定义成员,以此类推,最后存储自身成员。所以要想找到第二个虚表指针,需要进行第一个虚表大小的偏移。

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值