c++多态 详解

0.多态的概念:

多态的概念:相同的消息可能会送给多个不同的类别之
对象,而系统可依据对象所属类别,引发对应类别的方法,而有不同的行为。简单来说,所谓多态意指相同的消息给予不同的对象会引发不同的动作。

多态分为静态多态与动态多态。静态与动态是针对编译期间与运行期间而言的。

静态多态是编译期间就确定要调用什么了,比如函数重载,底层是将函数名与参数按照规则重新命名

动态多态是运行期间才能知道调用什么,下文主要讲解动态多态。

1.多态的要求

在c++中,多态是有严格的要求的:

1.必须通过父类的指针或者引用去调用

2.派生类必须重写基类的虚函数,要求三同(函数名、返回值、参数类型),协变可以不遵循返回值相同,但是要求返回值必须为父子类型的指针或者引用

2.多态的几种实现方式以及注意事项

先来看一看多态的运行效果: 

2.1通过父类的指针去调用

class Parent {
public:
	virtual void buyTicks() {
		cout << "成人票" << endl;
	}
private:
	string _name;
	int _age;
};
class Student : public Parent{
public:
	virtual void buyTicks() {
		cout << "学生票" << endl;
	}
};
void BuyTickes(Parent *p) {
	p->buyTicks();
}
int main() {
	Parent* par = new Parent();
	Student* stu = new Student();
	BuyTickes(par);
	BuyTickes(stu);
	return 0;
}

运行结果:

成人票
学生票

通过这个我们可以发现我们向通过基类的指针去分别指向基类与派生类,在运行时会引发两种动作。这样子的就为多态,

2.2通过父类的引用去调用

class Parent {
public:
	virtual void buyTicks() {
		cout << "成人票" << endl;
	}
private:
	string _name="ignor";
	int _age = 23;;
};
class Student : public Parent{
public:
	virtual void buyTicks() {
		cout << "学生票" << endl;
	}
};

void BuyTickes(Parent &p) {
	p.buyTicks();
}
int main() {
	Parent p;
	Student s;
	BuyTickes(p);
	BuyTickes(s);
	return 0;
}

2.3协变(可以不遵循返回值相同的要求)

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

class Parent {
public:
	virtual  A *buyTicks() {
		cout << "成人票" << endl;
		return nullptr;
	}
private:
	string _name="ignor";
	int _age = 23;;
};
class Student : public Parent{
public:
	virtual B* buyTicks() {
		cout << "学生票" << endl;
		return nullptr;
	}
};
void BuyTickes(Parent *p) {
	p->buyTicks();
}
int main() {
	Parent *p=new Parent;
	Student *s=new Student;
	BuyTickes(p);
	BuyTickes(s);
	return 0;
}

2.4注意事项:

2.4.1如果父类实现了析构函数,且父类必须将析构函数用virtual修饰,那么子类也必须实现析构函数,重写父类的析构函数。

class Parent {
public:
	virtual  void buyTicks() {
		cout << "成人票" << endl;
	}
	~Parent()  //父类没有虚函数修饰
	{
		cout << "~Parent" << endl;
	}
};
class Student : public Parent{
public:
	virtual void buyTicks() {
		cout << "学生票" << endl;
	}
	~Student()
	{
		cout << "~student" << endl;
	}
};
void BuyTickes(Parent *p) {
	p->buyTicks();
	delete p;
}
int main() {
	BuyTickes(new Parent());
	BuyTickes(new Student());
	return 0;
}

父类没有虚函数修饰,子类也就达不成重写父类的要求,那么子类的析构与父类的析构构成覆盖。此时就会发生内存泄露问题。

成人票
~Parent
学生票
~Parent

学生这里没有调用父类对象的析构函数进行析构只析构了自己,导致父类对象形成内存泄露

所以父类对象的析构函数必须使用虚函数修饰。

既然构成多态的条件是要求函数名相同,那么子类与父类的函数名也不相同啊,为什么也可以构成多态?-->原因是析构函数会被统一命名为destory的名字,故函数名相同,构成多态

2.4.2 如果父类有缺省参数,那么缺省参数也会被继承下来

class Parent {
public:
	virtual  void buyTicks(int num=1) {
		cout << "A:" << num << endl;
	}

};
class Student : public Parent{
public:
	virtual void buyTicks(int num=0) {
		cout << "B:" << num << endl;
	}
};
void BuyTickes(Parent *p) {
	p->buyTicks();
	delete p;
}
int main() {
	BuyTickes(new Parent());
	BuyTickes(new Student());
	return 0;
}

A:1
B:1

父类的缺省参数为1 那么子类也会继承父类的缺省参数(只有当发生多态的时候,才会继承父类的缺省参数)

2.4.3通过父类函数去调用也构成多态

class Parent {
public:
	virtual  void buyTicks(int num=1) {
		cout << "A:" << num << endl;
	}
	void func() {
		buyTicks();
	}

};
class Student : public Parent{
public:
	virtual void buyTicks(int num=0) {
		cout << "B:" << num << endl;
	}
};
int main() {
	Student s;
	s.func();
	return 0;
}

这个输出结果是神马?

B:1

来看一看调用逻辑,在main函数里面创建了一个派生类对象,派生类去调用父类的函数 func

由于func不是虚函数,并且派生类也没有完成重写,所以func是一个普通的函数,在派生类中,把父类的func继承了下来,所以派生类也拥有func函数,故可以调用func,func函数里面有个隐藏的参数为,this指针,所以我们s.func时,进入func函数,在func里面去调用buyTicks,实际上是用Parent* this去调用的,也满足多态的要求。

所以会发生多态,上文中也提到过,会把父类函数的缺省值继承下来,所以输出结果为B:1

class Parent {
public:
	virtual  void buyTicks(int num=1) {
		cout << "A:" << num << endl;
	}
	void func() {
		buyTicks();
	}

};
class Student : public Parent{
public:
	virtual void buyTicks(int num=0) {
		cout << "B:" << num << endl;
	}
	void func() {
		buyTicks();
	}
};
int main() {
	Student s;
	s.func();
	return 0;
}

这里的Student的func函数是对父类的覆盖,所以func不满足多态

输出的结果为B:0

2.4.5小总结

综上:如果满足多态,看指向对象的类型,调用这个类型的成员函数

           如果不满足多态, 看调用者类型,调用这个类型的成员函数

        解释:满足多态的情况下,我们定义Parent * p= new student,这个会调用Student的发生重写的函数

                           我们定义Parent *p =new Parent ,这个会调用Parent中被重写的函数

                不满足多态的情况下,我们定义Student *s =new Student,这个会调用Student的函数

                                                        我们定义Parent *s =new Student,这个会调用parent的函数

3.底层实现--虚函数表

为什么能够发生多态?底层都做了那些工作去支持我们的多态?

3.1汇编代码的变化

先来看看汇编代码:

不构成多态的反汇编代码:

可以发现多态实则是底层的汇编代码的改变,也可以看出是否构成多态,是在编译期间就确定了,确定后,满足多态的汇编被修改为第一张照片,不满足的话,就原封不动。

3.2引入虚函数表

那么这段汇编代码究竟干了一些什么事情?

先来看一道题目:

class Base {
public:
	virtual void Func1() {
		cout << "func1" << endl;
	}
private:
	char _c;
};
int main() {
	cout << sizeof(Base) << endl;
	return 0;
}

这个打印出来的结果是什么?--> 8

class Base {
public:
	void Func1() {
		cout << "func1" << endl;
	}
private:
	char _c;
};
int main() {
	cout << sizeof(Base) << endl;
	return 0;
}

这个打印出来的结果是什么? -->1

打印结果为什么不一样?????

用虚函数修饰,就会产生虚函数表,不论是否发生多态

引入虚函数表

可以看到,当我们声明虚函数时,会产生虚函数表,子类继承父类,同时也会将父类的虚函数表拷贝到子类的虚函数表处。如果发生多态,则子类会将自己的虚函数覆盖掉父类拷贝过来的虚函数

重写又称为覆盖  ,重写是语法层的概念,覆盖是底层的概念。

理解覆盖:如果不发生多态,即,重写的条件不成立,单纯的指派生类继承父类,那么子类同时也会将父类的函数的地址拷贝出来。如果发生多态,那么子类会重新生成一个子类函数的地址,去覆盖掉拷贝出来的父类函数的地址。所以成为覆盖。那么在调用时,如果为父类对象,就去调用父类对象(从父类的虚函数表里面去找),子类对象就去调用子类对象(去子类的虚函数表里去找)。那为什么是父类的指针或者引用呢?指针和引用既可以指相父类对象 ,也可以指向子类对象(切片)。综上也就解释了为什么要使用父类对象 ,以及 需要完成重写。

虚函数表本质是一个虚函数指针数组

_vfptr的类型为void ** ,用于存放虚函数指针

即,虚函数表也是有地址的,为什么不把虚函数指针写在虚函数表的头(_vfptr存放00121ac3,而不去搞个指针数据),就是把虚函数表变为一个指针,而不是指针数组?因为重写的虚函数不止一个。

为了更为详细的观察

子类的buyTicks重写了父类的,所以虚函数指针也发生了变化,但是func的虚函数指针还是都指向同一个地址

虚函数表本质是虚函数指针数组

也就是说 虚函数表指针中存放着虚函数表,虚函数表为虚函数指针数组所指向的

在虚函数表内,谁先声明,谁就在第一个位置

再回过头来看看,这样满足多态吗?为什么不能支持多态?

class Parent {
public:
	 virtual void buyTicks(int num=1) {
		cout << "成人票" << endl;
	}
	 virtual void Func(int num = 1) {
		 cout << "成人票" << endl;
	 }
};
class Student : public Parent{
public:
	virtual void buyTicks(int num=0) {
		cout << "学生票" << endl;
	}
};
void BuyTicks(Parent p) {  //通过父类对象去调用,而不是父类指针或者引用
	p.buyTicks();
}
int main() {
	Parent p1;
	Student s;
	BuyTicks(p1);
	BuyTicks(s);
	return 0;
}

如果是父类对象的话能实现多态吗?

不能  为什么?

父类指针和父类引用的切片是将指针指向了子类,并对子类进行切片 ,这个时候子类的虚表还在,不会被修改,因为只是单纯的改变了父类指针的指向

那么如果是对象呢?子类给父类的切片 ,子类成员会拷贝给父类,会调用拷贝构造,那这个时候,虚表会不会拷贝过去?如果不会,那么父类对象的虚表里面永远都是父类对象,但是能拷贝虚表吗?不能,因为拷贝后可能就乱了,一个父类对象的虚表到底是父类对象的虚表还是子类对象的虚表?

 那么我们给出一个父类对象,它的虚表我们不确定是子类的还是父类的,也许刚开始是父类,后来子类切片,父类对象的虚表就变为了子类对象的虚表

所以对象的切片只拷贝成员不拷贝虚表

只有虚函数才会被存进虚表内

3.3打印虚函数表(只针对vs有效,因为vs编译器在虚函数表的末尾添加了nullptr)

class Parent {
public:
	 virtual void buyTicks(int num=1) {
		cout << "Parent->buyTicks" << endl;
	}
	 virtual void Func(int num = 1) {
		 cout << "Parent->Func" << endl;
	 }
};
class Student : public Parent{
public:
	virtual void buyTicks(int num=0) {
		cout << "Student->buyTicks" << endl;
	}
};
void BuyTicks(Parent p) {
	p.buyTicks();
}



typedef void(*VF_PTR)();
void Print_VFPTR(VF_PTR * ptr) {
	for (int i = 0; ptr[i] != nullptr;i++) {
		cout << "[" << i << "] " <<  ptr[i] << endl;
	}
}
int main() {
	Parent p;
	Student s;
	Print_VFPTR((VF_PTR*)(*(void**)&p));
	cout << endl;
	Print_VFPTR((VF_PTR*)*(void**) &s);
	return 0;
}

 3.3.1有时候监视窗口不太准确

class Parent {
public:
	 virtual void buyTicks(int num=1) {
		cout << "Parent->buyTicks" << endl;
	}
	 virtual void Func(int num = 1) {
		 cout << "Parent->Func" << endl;
	 }
};
class Student : public Parent{
public:
	virtual void buyTicks(int num=0) {
		cout << "Student->buyTicks" << endl;
	}
	void Func1() {};
	virtual void Func2() {};
};

s的虚函数表内缺少一项Func2。

我们使用打印来看

可以发现可以打印出来,这样就证实了监视窗口有时候并不准确。

3.4虚表(虚函数表)是何时形成的,以及何时进行初始化?并且虚表存放在哪里?

注意区分虚函数表以及虚函数表指针

虚表是在编译的时候就形成了,只不过只存在虚函数表指针,它指向的虚函数表还没有初始化

虚函数表的初始化,是在构造函数的时候(在初始化列表内)进行初始化

存放在哪里? ---一会测试一下

3.5多继承下,形成的多个虚表该怎么理解?原理是什么?

下面代码运行的结果是什么?

class Base1 {
public:
	 virtual void Func(int num=1) {
		cout << "Base1->Func" << endl;
	}
	 virtual void Func1(int num = 1) {
		 cout << "Base1->Func1" << endl;
	 }
};
class Base2 {
	virtual void Func() {
		cout << "Base2->Func" << endl;
	}
	virtual void Func1() {
		cout << "Base2->Func1" << endl;
	}
};
class Derive : public Base1, public Base2 {
public:
};
typedef void(*VF_PTR)();
void Print_VFPTR(VF_PTR * ptr) {
	for (int i = 0; ptr[i] != nullptr;i++) {
		cout << "[" << i << "] " <<  ptr[i] << endl;
	}
}
int main() {
	Derive d;
	Base1* b1 = &d;
	Base2* b2 = &d;
	Derive* b3 = &d;
	cout << b1 << endl;
	cout << b2 << endl;
	cout << b3 << endl;
	return 0;
}

我们会发现b1==b3!=b2

说明b1、b3都指向同一个位置

上图为图解。

因为Derive是先继承的Base1,先继承谁,谁的虚表就在前面,后面根据继承顺序排序。


3.6指针修正

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;
	int bb;
};

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

// 用程序打印虚表
typedef void(*VF_PTR)();

//void PrintVFTable(VF_PTR table[])
void PrintVFTable(VF_PTR* table)
{
	for (int i = 0; table[i] != nullptr; ++i)
	{
		printf("[%d]:%p->", i, table[i]);
		VF_PTR f = table[i];
		f();
	}
	cout << endl;
}

int main()
{
	Derive d;
	PrintVFTable((VF_PTR*)(*(void**)&d));
	Base2* ptr2 = &d;
	PrintVFTable((VF_PTR*)(*(void**)(ptr2)));

	return 0;
}

这俩明明都是调用的func1,但是为什么它的地址不同?

我们转到汇编代码去看一看

4.思考问题:

1. 什么是多态?

2. 什么是重载、重写(覆盖)、重定义(隐藏)?

3. 多态的实现原理?

4. inline函数可以是虚函数吗?答:可以,不过编译器就忽略inline属性,这个函数就不再是 inline,因为虚函数要放到虚表中去。

5. 静态成员可以是虚函数吗?答:不能,因为静态成员函数没有this指针,使用类型::成员函数 的调用方式无法访问虚函数表,所以静态成员函数无法放进虚函数表。

6. 构造函数可以是虚函数吗?答:不能,因为对象中的虚函数表指针是在构造函数初始化列表 阶段才初始化的。

7. 析构函数可以是虚函数吗?什么场景下析构函数是虚函数?答:可以,并且最好把基类的析 构函数定义成虚函数。

8. 对象访问普通函数快还是虚函数更快?答:首先如果是普通对象,是一样快的。如果是指针 对象或者是引用对象,则调用的普通函数快,因为构成多态,运行时调用虚函数需要到虚函 数表中去查找。

9. 虚函数表是在什么阶段生成的,存在哪的?答:虚函数表是在编译阶段就生成的,一般情况 下存在代码段(常量区)的。

10. C++菱形继承的问题?虚继承的原理?注意这里不要把虚函数表和虚基 表搞混了。

11. 什么是抽象类?抽象类的作用?答:包含抽象函数的类就是抽象类,抽象函数的定义为=0,抽象类强制重写了虚函数,另外抽 象类体现出了接口继承关系。 

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

蠢 愚

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

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

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

打赏作者

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

抵扣说明:

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

余额充值