C++中的多态

C++中的多态,第1张

目录

多态的定义

多态构成的条件

析构函数的重写

抽象类

多态的作用

多态的原理


多态的定义

多态是在不同继承关系的类对象,去调用同一函数,产生了不同的行为。


多态构成的条件

1、必须通过基类的指针或者引用调用虚函数。


注意:对象不行。


2、被调用的函数必须是虚函数,且派生类的虚函数必须对基类的虚函数进行重写,注意:基类的的虚函数必须得要有virtual修饰,派生类可以不用virtual修饰,但是不建议。


重载、重定义与重写三者的区别。


这三个定义很容易混淆。


重载的条件

1、两个函数在同一作用域

2、函数名相同、参数不同

重定义的条件

1、两个函数在基类和派生类各自的作用域类

2、只需要函数名相同

3、两个基类的和派生类的同名函数不构成重写就是重定义

重写的条件

1、基类的函数必须是virtual修饰

2、重写函数必须由相同的类型、名称和参数列表(缺省值可以不同)

这个体现出了接口的继承,一旦构成多态缺省值取决于基类

参考下面这篇文章

C++继承中重载、重写、重定义的区别: - A-祥子 - 博客园 (cnblogs.com)

class Person {
public:
	virtual void Print()//虚函数
	{
		cout << "我是基类" << endl;
	}
};

class Student:public Person {
public:
	virtual void Print()//重写
	{
		cout << "我是派生类" << endl;
	}
};
void fuc(Person p)
{
	p.Print();
}
int main()
{
    Student s;
    fuc(s);//不构成多态,直接传的对象
	Person* p = new Student();//基类的指针指向派生类的对象
	p->Print();//构成多态
	return 0;
}

先将Print函数进行重写,fuc函数传参时不构成多态,不满足多态的条件中的必须是基类的指针或者引用去调用虚函数。


而下面的基类的指针指向派生类的对象,此时构成了多态。


 

析构函数的重写

这个比较特殊

先举个例子

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

class Student:public Person {
public:
	
	~Student()
	{
		cout << "~Student()" << endl;
	}
};
int main()
{
	Person* p = new Student();//基类的指针指向派生类的对象
	delete p;
	return 0;
}

发现只调用了Person的析构函数,没有调用Student的析构函数,很显然,存在内存泄漏的危险。


原因很简单,因为p是基类的指针,释放的时候当然调用他自己的析构函数。


如果是Student类型的指针,释放时当然会调用Student类型的析构,而且还会自动调用基类的析构函数。


那么现在的问题是,之前构成的多态岂不是会造成内存泄漏?

这个问题其实析构函数也可以重写,虽然不同名,但是编译器统一处理成destructor。


class Person {
public:
	
	virtual ~Person()//将析构函数也变成虚函数
	{
		cout << "~Person()" << endl;
	}
};

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

此时再次运行就不会存在内存泄漏了。


 

抽象类

纯虚函数

虚函数不给函数体,在虚函数的后面写上 =0 ,则这个函数为纯虚函数。


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


抽象类不能实例化出对象。


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

class Person {
public:
	virtual void func() = 0;//纯虚函数
};

class Student:public Person {
public:
};
int main()
{
	Person* p = new Student();//报错
	p->func();
	delete p;
	return 0;
}

 

纯虚函数的作用

1、强制派生类完成重写

2、体现出接口的继承

虚函数的继承就是一种接口继承,派生类继承的是虚函数的接口,目的是为了重写,达成多态。


看一个例子

class Person {
public:
	virtual void func(int val = 1)
	{
		cout << "Person " << val<func();
	delete p;
	return 0;
}

上面代码最终输出的是 Student 1,而不是Student 0,充分func函数是把整个接口给继承过来了,但是呢,并不继承函数体。


所以,如果不实现多态,不要把函数定义成虚函数

多态的作用

接口重用!!!

相同的接口实现不同的方法,有点和函数重载、模板相似(静态多态),我们现在所说的多态是动态的多态,举个例子,IO设备,通过系统的接口找到他们自己的方法(驱动),而在我们用户层,只需要用一个接口就可以对不同的设备进行 *** 作,非常方便,其实就是多态的思想(Linux系统层面是用C写的,没有多态,其实是用的函数指针)。


既然这样有好处,很方便,C++就引入多态。


C++多态的两种形式 - 云+社区 - 腾讯云 (tencent.com)

多态的原理

虚函数表指针

class Person {
public:
	virtual void func()
	{}
private:
	int _num = 0;
};
int main()
{
	Person p;
	cout << sizeof(p) << endl;//输出8
	return 0;
}

可以看到大小为8,原因是有一个虚函数表指针_vfptr

虚函数表是一个指针数组,里面存着虚函数的地址,而虚函数表指针 _vfptr就是指向这个指针数组

虚函数表里的最后一个元素为nullptr,这样就可以知道虚函数表里有几个虚函数地址。


class Person {
public:
	virtual void func1()
	{}
	virtual void func2()
	{}
	void func3()
	{}
private:
	int _num = 0;
};

class Student:public Person {
public:
	virtual void func2()
	{}
};
int main()
{
	Person p;
	Student s;
	return 0;
}

func2被重写了

可以看到func2的地址发生了变化,由于没有重写func1,func1的地址直接继承过来了,所以func1的地址是不变的,func3不是虚函数,所以不再虚函数表里面。


所以说,多态的原理就是,在运行过程中找到指向对象的虚表中查看需要调用的虚函数的地址,并进行调用。


 

vs下的调试窗口会屏蔽掉派生类自己的虚函数的地址

例如

class Person {
public:
	virtual void func1()
	{
		cout << "func1()" << endl;
	}
	virtual void func2()
	{
		cout << "func2()" << endl;
	}
	void func3()
	{}
private:
	int _num = 0;
};

class Student:public Person {
public:

	virtual void func2()
	{
		cout << "func2()" << endl;
	}
	virtual void func4()//派生类自己的虚函数
	{
		cout << "func4()" << endl;
	}
};

注意:

如果基类有虚函数,派生类不会生成自己的虚表,会继承基类的虚表,如果基类没有虚函数,派生类有,派生类自己生成虚表。


这里派生类的虚表只出现两个虚函数地址,但是实际上虚表里面应该存储三个虚函数地址,即派生类自己的虚函数地址监视窗口不显示。


打印虚表内虚函数地址的函数

typedef void(*VFPTR)();//函数指针
void PrintVfptr(VFPTR* vfptr)
{
	int index = 0;
	while (vfptr[index])
	{
		printf("vfptr[%d] = %p -> ", index, vfptr[index]);
		vfptr[index]();
		index++;
	}
}
int main()
{
	Person p;
	Student s;
	PrintVfptr((VFPTR*)(*(int*)&p));//取出前四个字节并强转成指向指针数组的指针,注意是二级指针,且类型是函数指针类型
	cout << endl;
	PrintVfptr((VFPTR*)(*(int*)&s));
	return 0;
}

在vs下,虚函数表的指针在对象的地址空间的前四个字节。


只需要取出这前四个字节,然后进行类型转换。


可以看到,其实虚函数表中有func4的地址。


派生类的大小

派生类的大小如何计算呢?

派生类的大小 = 基类的大小 + 自己本身的大小。


注意注意:这里是分别单独计算,然后直接求和,单独计算的时候需要考虑内存对齐,求和之后不用考虑对齐。


举个例子:

class Person {
public:
	virtual void func1()
	{}
	virtual void func2()
	{}
	void func3()
	{}
private:
	char_num = 0;
};

class Student:public Person {
public:
	virtual void func2()
	{}
	virtual void func4()
	{}
private:
	int c;
};

Person的大小为8字节,包括虚函数表指针4字节+char类型一个字节,最终内存对齐一下就是8字节,关于内存对齐的知识看这篇文章

看了这篇自定义数据类型讲解还不会,可以放下你手中的键盘了k

而单独的Student的大小为 int类型4个字节,此时千万别把虚函数表指针再算一次,上面已经说过一次了,因为基类有虚函数,派生类不用自己生成虚函数表指针,所以大小就为4

总的大小就为8+4 = 12字节。


欢迎分享,转载请注明来源:内存溢出

原文地址: http://outofmemory.cn/langs/607134.html

(0)
打赏 微信扫一扫 微信扫一扫 支付宝扫一扫 支付宝扫一扫
上一篇 2022-04-14
下一篇 2022-04-14

发表评论

登录后才能评论

评论列表(0条)

保存