C++初阶学习————多态

多态的概念

通俗的说就是多种形态,具体点就是去完成某个行为,当不同的对象去完成时会产生出不同的状态。
生活中的多态例如:不同的手机抢一个红包,金额是不同的;同一家店分为不同种的会员等等
大致可以分为静态的多态和动态的多态
静态的多态:如函数重载
动态的多态是本章着重介绍的

多态的实现及定义

动态多态的定义:一个父类的指针或引用,去调用(父类或子类)同一个函数,根据传递的类不同,调用结果也不同
多态的必要条件:
1.父类的指针或引用去调用
2.子类(派生类)必须构成函数重写
函数重写:
(1).子类和父类必须构成虚函数(在函数名前加 virtual 且修饰的是非静态成员函数)
(2).父类和子类的函数必须构成三同:即返回值相同、参数相同、函数名相同

代码示例如下

class Person
{
public:

	virtual void BuyTicket()//虚函数:virtual修饰的非静态成员函数
	{
		cout << "买票 - 全价" << endl;
	}
};

class Student : public Person
{
public:

	//子类中满足:必须是虚函数,且函数名、参数、返回值与父类的都相同时就构成重写(覆盖)
	virtual void BuyTicket()
	{
		cout << "买票 - 半价" << endl;
	}
};

void Func(Person& p)
{
	p.BuyTicket();
}
int main()
{
	Person p;
	Student s;

	Func(p);
	Func(s);
	return 0;
}

在这里插入图片描述
只要以上条件少一个,就不满足多态了
假如接收传递的不是父类的指针或引用那会有两种情况:

1.若是父类的普通对象接收传递的类的,那么调用的函数一定是父类自己的函数。因为当接收传递的是父类时就是父类本身,接收传递的是子类时会发生切片,把父类的部分切给形参接收。
所以无论怎么样,明确显示了形参的类型就是父类,且传递的子类还会发生切片,所以一定调用的是父类的函数。
在这里插入图片描述
在这里插入图片描述

2.若是子类接收传递的类,那么无论是指针引用还是普通变量,都只能接收子类,因为接收父类不能切片,会存在子类的访问越界的风险;
在默认情况下调用的是子类的函数,因为形参类型是子类且构成函数隐藏,想要调用就需要指定类域
在这里插入图片描述

在这里插入图片描述

1.例外情况1

上述是标准的定义方式及多态需要满足的条件,但有个例外情况
协变:若返回值是父子关系的指针或引用,那么也可以构成多态
代码如下:

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



class Person
{
public:

	//virtual void BuyTicket()
	//virtual A* BuyTicket()
	virtual Person* BuyTicket()
	{
		cout << "买票 - 全价" << endl;
		return nullptr;
	}
};

class Student : public Person
{
public:

	//virtual void BuyTicket()
	//virtual B* BuyTicket()
	virtual Student* BuyTicket()
	{
		cout << "买票 - 半价" << endl;
		return nullptr;
	}
};

void Func(Person& p)
{
	p.BuyTicket();
}
int main()
{
	Person p;
	Student s;

	Func(p);
	Func(s);
	return 0;
}

在这里插入图片描述
在这里插入图片描述

2.析构函数的多态

析构函数在编译时会把所以析构函数名替换为 destructor(),把三同问题中,唯一函数名不同的问题解决了,只需要加上virtual 就可以构成函数重写
但是要构成析构函数的多态比较复杂,下面分三种情况介绍

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

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

};

1.普通对象的析构

Person p;
Student s;

这种类型的对象,无论是否构成多态,都会正确调用析构函数的
父类调用父类自己的;子类调用子类自己,子类中父类在子类结束作用域后自动调用父类自己的(栈帧中的先定义后析构的问题在上一节继承中有介绍)

2.动态申请对象时,父类指针接收动态内存地址
(1)调用的是父类指针,但不构成多态会发生什么?
先说结论,不构成多态会造成内存泄漏,原因如下:

Person* pp = new Person;
Person* ps = new Student;

delete pp;
delete ps;

在这里插入图片描述

首先pp接受的时父类类型申请的动态内存,new的顺序是 :operator new + Person构造函数
当他析构时,delete的是pp这个变量,这个变量是Person类型,则调用顺序是:PP的类型的 析构函数 + operator delete,( pp->destructor() ,pp是属于person作用域的) 。
那么这里析构函数调用的就是Person自己的 ~Person()

而Student new的顺序是:先是operator new + Person的构造函数 之后是 operator new + Student的构造函数

到这里都还是正常的,但是当delete ps时,就出问题了
析构时的顺序:ps类型的 析构函数 + operator delete ,而ps类型是Person,那么调用的就是父类的析构函数+delete(Person作用于的ps->destructor(),以及Person的delete)
所以会造成内存泄漏

(2)调用的是父类的指针,且构成多态(满足函数重写)
在析构函数前加上virtual 即可
在这里插入图片描述

3.动态申请对象时,子类指针接收动态内存地址
首先子类不能接收 父类类型申请的动态内存地址,因为会发生越界等意想不到的问题
那么就是子类指针接收子类对象

Student* ps = new Student;
delete ps;

这时不会构成多态,并且会正确调用析构函数的,因为ps所属类型是Student的,那么构造时就会先构造继承的Person部分,再构造Student自己的;析构时会先调用Student自己的,继承中父类部分发现所在子类的作用域结束了,会自动调用Person的析构
(编译时会发现调用两个destructor(),但所属作用域是不同的)

总结:
1.普通类型,无论是否构成多态都会正确调用
2.动态申请时,若不构成多态,则需要申请类型和指针的类型保持一致(否则父类的指针接收子类的内存时会在析构时发生内存错误),若构成多态,则会正确调用
也就是指针指向什么类型的动态内存,就调用什么类型的destructor()

3.例外情况2

由于析构函数反映出的问题,可能是为了防止实际工程中发生内存错误,C++增加了一个例外,析构函数构成多态的条件:
父类必须写virtual,子类可以不用写,会继承父类中虚函数的属性,子类中同样的函数也就有了虚函数属性,就构成了子类(派生类)虚函数重写

大佬设计初衷可能是,防止 当父类加上virtual 构成虚函数,但是犹豫一些疏漏等原因并没有构成多态,没调用到子类的析构函数,就会造成内存泄漏的场景。但同时也衍生出一些小问题,代码如下:

class Person
{
public:
	//例外:父类必须写virtual ,子类可以不用写,因为他把属性继承下来了
	virtual void BuyTicket()
	{
		cout << "买票 - 全价" << endl;
	}
	virtual ~Person()
	{
		cout << "~Person()" << endl;
	}
};

class Student : public Person
{
public:
	//virtual void BuyTicket()
	void BuyTicket()//例外: 虽然子类 没写virtual ,但是他把父类的虚函数属性继承下来,所以也算虚函数
	{
		cout << "买票 - 半价" << endl;
	}
	virtual ~Student()
	{
		cout << "~Student()" << endl;
	}

};

void Func(Person& p)
{
	p.BuyTicket();
}
int main()
{
	Person p;
	Student s;

	Func(p);
	Func(s);
	return 0;
}

在这里插入图片描述
由于可以允许 父类必须写virtual而 子类可以不写virtual,又衍生出一个例外:
在这里插入图片描述

这也是因为,子类继承了父类所的虚函数所有的属性,父类这里的函数是公有的,所以到了子类这里就是公有了

建议自己写的时候按标准规范来写,肯定不会出错,代码想表达的意思还明确

final和override

1.final(防止被继承)

假如想要类不能被继承,该如何实现?
方法1:

class A
{
private:
	A(int a = 0)
		:_a(a)
	{}
public:
	static A CreateOBJ(int a = 0)
	{
		//new
		return A(a);
	}
protected:
	int _a;

};

class B : public A
{
	
};

把父类的构造函数设置成私有,当子类实例化对象时,先会调用父类的构造函数,但这里把他设置为私有就掉不成了,达到了不能继承的目的,但这种方法比较麻烦,代码可读性也不好

方法2:final关键字(C++11中新增加的)

class A final  //不可被继承 C++11
{
private:
	A(int a = 0)
		:_a(a)
	{}
public:
	static A CreateOBJ(int a = 0)
	{
		//new
		return A(a);
	}

	virtual void f()final
	{
		cout << "f()" << endl;
	}
protected:
	int _a;

};



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

在父类名后面加final,代表该类不能被继承
在父类中的虚函数后面加final,代表该虚函数不能被继承

2.override(检查重写)

override关键字的使用方式如下:

class A   
{
public:
	virtual void f() 
	{
		cout << "f()" << endl;
	}
protected:
	int _a;

};



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

加在派生类虚函数后面,检查派生类虚函数是否重写了基类某个虚函数,如果没有重写编译报错。

重写、重载、重定义的区别
在这里插入图片描述

抽象类(纯虚函数)

定义方式:

class flower//抽象类用于定义生活中 无法具体化的事物
{
public:
	virtual void kind() = 0;			//纯虚函数 实现没意义,因为他不可实例化

	/*virtual void kind() = 0
	{
		cout << "种类:" << endl;
	}*/


	void func()
	{
		cout << "func" << endl;
	}
};

class rose :public flower
{
public:
	virtual void kind()
	{
		cout << "种类:玫瑰" << endl;
	}


};

int main()
{
	//flower* sp = new flower;
	flower* p = new rose;
	p->kind();
	p->func();
	
}
在虚函数的后面写上 =0 ,则这个函数为纯虚函数。
包含纯虚函数的类叫做抽象类(也叫接口类),抽象类不能实例化出对象。
派生类继承后也不能实例化出对象,只有重写纯虚函数,派生类才能实例化出对象。
纯虚函数规范了派生类必须重写,另外纯虚函数更体现出了接口继承

由于抽象类不能实例化出对象,也就调不到他的函数,就算实现出功能也没有意义,所以一般抽象类只声明不实现。

多态的原理

虚函数表

先看一段代码:

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


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

在这里插入图片描述
若按普通类计算应该为:int _b 0 ~ 3 内存对齐数为最大对齐数是4 所以应该共占4字节
而实际情况是,除了int _b还有一个虚函数指针_vfpr(我这里是32位的)占4字节,0-3 + 4-7 共8字节并且满足内存对齐
在这里插入图片描述

这里用最开始的全价票半价票的代码来解释一下

class Person
{
public:

	virtual void BuyTicket()
	{
		cout << "买票 - 全价" << endl;
	}
protected:
	int _a;
};

class Student : public Person
{
public:

	virtual void BuyTicket()
	{
		cout << "买票 - 半价" << endl;
	}
protected:
	int _b;
};



void Func(Person& p)
{
	p.BuyTicket();
}

int main()
{
	Person p;
	Student s;

	Func(p);
	Func(s);
	return 0;
}

在这里插入图片描述

在这里插入图片描述
(汇编下的调用)

虚表指针是指向虚表的,而虚函数表本质是一个存虚函数的指针数组

对为什么是父类的指引或引用调用的解释

再来解释一下为什么只能是父类的指针或引用调用

首先必须是父类,假如若是子类,就只能接收子类的对象了,因为他不能对父类进行反向切片,会越界
其次是指针或引用,访问的是传递进来的对象本身的父类部分,而传值传参是一个临时对象,实例化对象本身在编译时就确定了函数地址了

	Person p1 = s;
	Person& p2 = s;

在这里插入图片描述
再例如:假如父类对象可以把子类的虚表切下来保存,那么析构的时候就会混乱了

在这里插入图片描述
并且:
不满足多态时,编译时确定地址
满足多态时,会在运行时到虚表中去查找再调用
所以普通对象调用速度相对较快

注意:虚表中的虚函数指针只是指向虚函数的地址,而虚函数和类中普通函数一样,都是存在公共区(代码段)这里说的只是vs环境下的情况

这里就合理解释了前面的例外情况2,为什么子类私有的虚函数也可以调用,因为他们是去虚函数表了去查找调用虚函数,虚函数表分不出分私有公有,并且调用的接口属于父类,父类中是公有

对于重写(覆盖)的演示

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

	virtual void Func2()
	{
		cout << "Base::Func2()" << endl;
	}
	void Func3()
	{
		cout << "Base::Func3()" << endl;
	}
private:
	int _b;
};


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

int main()
{
	Base b;
	Derive d;
	return 0;
}

在这里插入图片描述
子类继承时把父类的虚函数表拷贝了一份,之后子类的func1重写后把地址覆盖到原来func1的位置上
由于这个示例是在vs环境上演示的,子类能显示出来的虚函数表有多少个虚函数取决于父类的虚函数表有多少个虚函数。实际子类虚函数的数量不一定和父类对应 (如上述代码)

打印虚表

1.单继承

通过前面的 一些调试截图不难发现:
1.对象中第一个变量是指针(虚表指针),指针大小占4字节(64位8字节),
2.虚表实际上是一个函数指针数组
所以可以利用这两点来达到打印虚表的目的,具体代码如下:


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

	virtual void Func2()
	{
		cout << "Base::Func2()" << endl;
	}
	void Func3()
	{
		cout << "Base::Func3()" << endl;
	}
public:

private:
	int _b;
};


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

typedef void(*VF_PTR)();	//VF_PTR代表 函数指针  

void print(VF_PTR* table)   //函数指针数组 VF_PTR table[] 等价,只是做参数时数组退化成指针,形参接的是首元素地址 
{
	虚表指针存的是虚表首元素地址,首元素又是个虚函数指针,所以这里可以也理解为二级指针
	for(int i = 0;table[i] != nullptr;i++)
	{
		printf("vft[%d] : %p -> ",i,table[i]);
		VF_PTR f = table[i];
		f();
	}

}

int main()
{
	Base b;
	Derive d;
	print( (VF_PTR*)(*(int*)&b) );		
	cout << endl;	
	print( (VF_PTR*)(*(int*)&d) );
	虚表指针是对象b的首元素,也就是前4个字节,所以这里强转一下int接收再解引用,
	对象b就变成int b了,这时再次转成函数指针数组就可以了 
	return 0;
}

在这里插入图片描述

在这里插入图片描述

  • 子类是把父类的虚表拷贝了 一份,并把子类重写的虚函数覆盖到原先父类虚函数在虚表的位置上
    在vs上的监视窗口并没有显示全,显示的元素个数只取决于父类虚表的个数,也算是一个小bug

  • 利用虚表调用虚函数无论是私有还是公有,都可以调的到,说明虚表是不分私有公有的,但是常规对象调用时,是不可以调得到的

2.多继承

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;
};
int main()
{
	Derive d;

	print( (VF_PTR*)(*(void**)&d) );
	cout << endl;

	//Base2 b2;
	//print( (VF_PTR*)(  *(void**)( (char*)&d + sizeof(Base1) ) ));
	
	Base2* p = &d;
	print( (VF_PTR*)( *(void**)p ) );
或者 
	print( (VF_PTR*)*(Base2**)p );
	解引用是取到虚表指针本身,因为要传递的是虚表指针的值
	return 0;
}

在这里插入图片描述
通过打印虚表可以看出:多继承派生类的未重写的虚函数放在第一个继承基类部分的虚函数表中

注意:
多继承base1是先继承的,所以base1的虚表指针是在整体对象的前4个字节,比较好取出,而Base2的虚表指针需要找到Base2的首地址,在取出前4个字节才是Base2的虚表指针
同时在取字节数时不要忘了分清当前系统位数,例如32位指针类型占4字节,正好与前4个字节对应

这里来解释一下为什么要强转二级指针再解引用,而直接传p就不行。明白原理的请略过
在这里插入图片描述

菱形继承、菱形虚拟继承

实际中我们不建议设计出菱形继承及菱形虚拟继承,一方面太复杂容易出问题,另一方面这样的模型,访问基类成员有一定得性能损耗。所以菱形继承、菱形虚拟继承我们的虚表我们就不看了,一般我们也不需要研究清楚,因为实际中很少用。如果好奇心比较强的佬们,可以去看下面的两篇链接文章。

  1. C++ 虚函数表解析
  2. C++ 对象的内存布局
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值