多态(C++)

多态的概念

概念

多态即多种形态,当不同对象去执行同一个行为时,会产生不同的状态

多态的定义及实现

多态的构成条件

  1. 必须通过基类的指针/引用去调用虚函数( virtual所修饰)
  2. 被调用的函数必须是虚函数,并且派生类必须对基类的虚函数进行重写

在这里插入图片描述

class Person
{
public:
	virtual void Buyticket()
	{
		cout << "Person:买票-全价" << endl;
	}
};

class Student:public Person
{
public:
	virtual void Buyticket()
	{
		cout << "Student:买票-半价" << endl;
	}
};

//多态调用
void Display(Person& p)
{
	p.Buyticket();
}

int main()
{
	Person pn;
	Display(pn);

	Student st;
	Display(st);
	return 0;
}

在这里插入图片描述

普通调用:和调用对象的类型有关
多态调用:和指针/引用指向的对象有关

class Person
{
public:
	virtual void Buyticket()
	{
		cout << "Person:买票-全价" << endl;
	}
};

class Student:public Person
{
public:
	virtual void Buyticket()
	{
		cout << "Student:买票-半价" << endl;
	}
};

//普通调用
void Display(Person p)
{
	p.Buyticket();
}

int main()
{
	Person pn;
	Display(pn);
	Student st;
	Display(st);
	return 0;
}

在这里插入图片描述

虚函数

虚函数:被 virtual修饰的类成员函数称作虚函数

class Person
{
public:
	virtual void Buyticket()
	{
		cout << "Person:买票-全价" << endl;
	}
};

虚函数的重写

虚函数的重写:派生类中有一个和基类完全相同的虚函数,称派生类的虚函数重写了基类的虚函数

虚函数重写的两个例外

  1. 协变(基类与派生类虚函数返回值类型不同)
    派生类重写基类虚函数时,与基类虚函数返回值类型不同。也就是基类虚函数返回基类对象的指针/引用,派生类虚函数返回派生类对象的指针/引用
class Person
{
public:
	~Person()
	{
		cout << "~Person()" << endl;
		delete[] _p;
	}
protected:
	int* _p = new int[10];
};

class Student :public Person
{
public:
	~Student()
	{
		cout << "~Student()" << endl;
		delete[] _s;
	}
protected:
	int* _s = new int[10];
};

int main()
{
	Person pn;
	Student st;
	return 0;
}

在这里插入图片描述

  1. 析构函数的重写(基类与派生类析构函数名不同)
    先观察如果析构函数不设置为虚函数
class Person
{
public:
	~Person()
	{
		cout << "~Person()" <<endl;
		delete[] _p;
	}
protected:
	int* _p = new int[10];
};

class Student :public Person
{
public:
	~Student()
	{
		cout << "~Student()" << endl;
		delete[] _s;
	}
protected:
	int* _s = new int[20];
};

int main()
{
	Person pn;
	Student st;
	return 0;
}

在这里插入图片描述

运行起来似乎没有什么问题,做如下修改,结果又是如何

int main()
{
	Person* ptr1 = new Person;
	Person* ptr2 = new Student;

	delete ptr1;
	delete ptr2;
	return 0;
}

在这里插入图片描述

由运行结果可知,发生了内存泄漏,Student的资源并没有释放;delete的行为是:使用指针调用析构函数,由于是普通调用,函数与调用对象的类型有关,所以造成了内存泄漏;如果将函数设置为虚函数结构会怎么样呢?

class Person
{
public:
	virtual ~Person()
	{
		cout << "~Person()" << endl;
		delete[] _p;
	}
protected:
	int* _p = new int[10];
};

class Student :public Person
{
public:
	virtual ~Student()
	{
		cout << "~Student()" << endl;
		delete[] _s;
	}
protected:
	int* _s = new int[20];
};

在这里插入图片描述

内存泄漏的问题完美地解决

虽然函数名不同,这里其实是构成了虚函数的重写,编译器对析构函数的名称做了处理,编译后的析构函数的名称统一处理成 destructor

  1. 子类虚函数可以不加 virtual修饰(不推荐)

C++11override和final

  1. final:修饰虚函数,表示该虚函数不能被重写
  2. override:检查派生类虚函数是否重写了某类的某个虚函数,如果没有重写编译报错

重载,覆盖,隐藏的对比

在这里插入图片描述

抽象类

概念

在虚函数的后面加上=0,称这个函数为纯虚函数,包含纯虚函数的类称作抽象类,抽象类不能实例化对象;派生类继承后也不能实例化对象,只有重写纯虚函数,派生类才能实例化对象;纯虚函数规定了派生类必须重写

class Car
{
public:
	virtual void Drive()=0;
};

int main()
{
	Car c;
	return 0;
}

在这里插入图片描述

纯虚函数不可以实例化

class Car
{
public:
	virtual void Drive() = 0;
};

class NIO : public Car
{
public:
	virtual void Drive()
	{
		cout << "安全驾驶" << endl;
	}
};

int main()
{
	NIO et7;
	et7.Drive();
	return 0;
}

在这里插入图片描述

接口继承和实现继承

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

多态的原理

虚函数表

class Base
{
public:
	virtual void Test()
	{
		cout << "Test()" << endl;
	}
private:
	int _a = 0;
};

int main()
{
	Base b;
	cout << sizeof(b) << endl;
	return 0;
}

按照以往的方式计算 Base类的大小:类中成员变量只有 int类型大小四个字节,成员函数 Test不占空间,所以类的大小应该是四个字节

运行结果如下

在这里插入图片描述

运行结果和预算的结果不一样,这是怎么回事呢?难道是成员函数也要计算大小吗?接下来,通过监视一探究竟

在这里插入图片描述

原来对象类中除了成员变量之外,还有_vfptr,也就是接下来要学习的虚函数表指针,想要学习指针,就先来了解虚函数表

虚函数表:用来存放虚函数,就如上面[0]中存放的就是Test的地址;本质就是函数指针数组

在这里插入图片描述

class Car
{
public:
	virtual void Test1()
	{
		cout << "Car:Test1()" << endl;
	}

	virtual void Test2()
	{
		cout << "Car:Test2()" << endl;
	}
private:
	int _a = 0;
};

class NIO : public Car
{
public:
	virtual void Test1()
	{
		cout << "NIO:Test1()" << endl;
	}

private:
	int _b = 0;
};

int main()
{
	Car c;
	NIO et7;
	return 0;
}

在这里插入图片描述

完成重写的虚函数,表函数表对应位置覆盖成重写的虚函数

多态的原理

int main()
{
	Car c;
	NIO et7;
    
    //多态调用
	Car* ptr = &c;
	ptr->Test1();
	ptr = &et7;
	ptr->Test1();

    //普通调用
	ptr = &c;
	ptr->Test2();
	ptr = &et7;
	ptr->Test2();
	return 0;
}

运行结果如下

在这里插入图片描述

普通调用由调用对象类型决定:Test2()函数不是虚函数,调用类型是Car;多态调用由指针/引用指向的对象决定:Test1()函数是虚函数,两次调用指向的对象都不同

再通过汇编进行观察

在这里插入图片描述

普通调用在编译时就确定好的(静态);多态调用在编译时是不确定的(动态)运行之后在,根据调用对象指向的类型,在表函数表中找到对应的函数进行调用

在这里插入图片描述

动态绑定与静态绑定

  1. 静态绑定,在程序编译期间就已经确定程序的行为,也称静态多态
  2. 动态绑定,在程序运行期间,根据具体的类型确定程序具体的行为,调用具体的函数,也称动态多态

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

单继承中的虚函数表

观察下列代码

class Car
{
public:
	virtual void test1()
	{
		cout << "Car test1()" << endl;
	}

	virtual void test2()
	{
		cout << "Car test2()" << endl;
	}

private:
	int _a;
};

class NIO:public Car
{
public:
	virtual void test1()
	{
		cout << "NIO test1()" << endl;
	}

	virtual void test3()
	{
		cout << "NIO test3()" << endl;
	}
	
private:
	int _b;
};

int main()
{
	Car c;
	NIO et7;
	return 0;
}

在这里插入图片描述

通过监视窗口能够发现:在派生类对象et7中重写了test1,继承了test2,但是本身的test3却没有,在内存窗口看到一个未知的地址,可以猜测是test3函数,现在就是想办法将其打印出来进行验证

上面了解到虚函数表本质是函数指针数组,接下来通过函数指针数组将虚函数表打印出来

typedef void(*_vfptr)();

void Print_vfptr(_vfptr vft[])
{
	for (int i = 0; vft[i] != nullptr; i++)
	{
		printf("[%d]:%p->", i, vft[i]);
		vft[i]();
	}
	cout << endl;
}

通过将对象进行取址再强转 int*获取前四个字节,在类型转换 _vfptr*传递给函数进行打印

	Print_vfptr((_vfptr*)*(int*)&c);
	Print_vfptr((_vfptr*)*(int*)&et7);

完整代码如下

class Car
{
public:
	virtual void test1()
	{
		cout << "Car test1()" << endl;
	}

	virtual void test2()
	{
		cout << "Car test2()" << endl;
	}

private:
	int _a;
};

class NIO:public Car
{
public:
	virtual void test1()
	{
		cout << "NIO test1()" << endl;
	}

	virtual void test3()
	{
		cout << "NIO test3()" << endl;
	}

private:
	int _b;
};

typedef void(*_vfptr)();

void Print_vfptr(_vfptr vft[])
{
	for (int i = 0; vft[i] != nullptr; i++)
	{
		printf("[%d]:%p->", i, vft[i]);
		vft[i]();
	}
	cout << endl;
}

int main()
{
	Car c;
	Print_vfptr((_vfptr*)*(int*)&c);

	NIO et7;
	Print_vfptr((_vfptr*)*(int*)&et7);

	return 0;
}

在这里插入图片描述

打印结果和预期一致

在这里插入图片描述

多继承中的虚函数表

class NIO
{
public:
	virtual void test1()
	{
		cout << "NIO test1()" << endl;
	}

	virtual void test2()
	{
		cout << "NIO test2()" << endl;
	}

private:
	int _a;
};

class XPENG
{
public:
	virtual void test1()
	{
		cout << "XPENG test1()" << endl;
	}

	virtual void test2()
	{
		cout << "XPENG test2()" << endl;
	}

private:
	int _b;
};

class NEA :public NIO, public XPENG
{
public:
	virtual void test1()
	{
		cout << "NEA test1()" << endl;
	}

	virtual void test3()
	{
		cout << "NEA test3()" << endl;
	}

private:
	int _c;
};

int main()
{
	NIO et7;
	XPENG p7;
	NEA car;
	return 0;
}

在这里插入图片描述

通过监视会发现与上面类似的疑问,派生类 NEA对象本身的虚函数存放在第一个基类 NIO中还是第二个基类 XPENG中呢?

接下来再次通过函数指针数组进行验证

typedef void(*_vfptr)();

void Print_vfptr(_vfptr vft[])
{
	for (int i = 0; vft[i] != nullptr; i++)
	{
		printf("[%d]:%p->", i, vft[i]);
		vft[i]();
	}
	cout << endl;
}

class NIO
{
public:
	virtual void test1()
	{
		cout << "NIO test1()" << endl;
	}

	virtual void test2()
	{
		cout << "NIO test2()" << endl;
	}

private:
	int _a;
};

class XPENG
{
public:
	virtual void test1()
	{
		cout << "XPENG test1()" << endl;
	}

	virtual void test2()
	{
		cout << "XPENG test2()" << endl;
	}

private:
	int _b;
};

class NEA :public NIO, public XPENG
{
public:
	virtual void test1()
	{
		cout << "NEA test1()" << endl;
	}

	virtual void test3()
	{
		cout << "NEA test3()" << endl;
	}

private:
	int _c;
};

int main()
{
	NIO et7;
	Print_vfptr((_vfptr*)*(int*)&et7);
	XPENG p7;
	Print_vfptr((_vfptr*)*(int*)&p7);
    
	NEA car;
	//NIO虚函数表
	Print_vfptr((_vfptr*)*(int*)&car);
	XPENG* ptr = &p7;
	//XPENG虚函数表
	Print_vfptr((_vfptr*)*(int*)ptr);
	return 0;
}

在这里插入图片描述

打印结果显示:派生类本身的虚函数是存放在第一个基类的虚函数表中的

在这里插入图片描述

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值