c++的特性——多态

目录

概念

多态实现条件

虚函数

虚函数的重写/覆盖

练习题

析构函数的重写

override和final关键字

重载/隐藏/重载的区别

纯虚函数和抽象类

多态

虚函数表指针

多态的原理

动态绑定与静态绑定

虚函数表总结


前面学习了C++的三个特性中的两个特性,今天我们将学习它的最后一个特性------多态。

概念

多态,实际上就是多种形态,多态分为编译时多态(静态多态)和运行时多态(动态多态),

静态多态就是函数重载和函数模板。因为它们实参传给形参的参数匹配是在编译时完成的·。

动态多态就是传不同对象给函数就会完成不同的行为。

比如买票的行为,学生买的是学生票,普通人买的是全价票,军人优先买票。

复制代码
#include<iostream>
using namespace std;
class Person
{
public:
	virtual void func()
	{
		cout << "全价购票" << endl;
	}
};
class Student:public Person
{
public:
	virtual void func()
	{
		cout << "学生优惠票" << endl;
	}
};
class Soldier:public Person
{
public:
	virtual void func()
	{
		cout << "优先购票" << endl;
	}
};
void BuyTicket(Person& person)
{
	person.func();
}
int main()
{
	Student John;
	BuyTicket(John);
	Soldier Amy;
	BuyTicket(Amy);
	return 0;
}

在上面我们发现Student类和Solider类都继承了Person类,但是它们中都有一个一模一样的成员函数,而且父类的函数并没有被隐藏,这是为什么呢?下面我们就来学习多态实现的条件。

多态实现条件

有两个必需条件:

1.基类的指针或者引用必须调用虚函数

2.被调用的函数必须是虚函数,并且完成了虚函数的重写/覆盖。

虚函数

类中成员函数前面用virtual修饰的为虚函数。注意:非成员函数不能用virtual修饰。

虚函数的重写/覆盖

虚函数的重写/覆盖:派⽣类中有⼀个跟基类完全相同的虚函数(即派⽣类虚函数与基类虚函数的返回值类型、函数名字、参数列表完全相同),称派⽣类的虚函数重写了基类的虚函数。
注意:在重写基类虚函数时,派⽣类的虚函数在不加virtual关键字时,虽然也可以构成重写(因为继承后基类的虚函数被继承下来了在派⽣类依旧保持虚函数属性),但是该种写法不是很规范,不建议这样使⽤,不过在考试选择题中,经常会故意买这个坑,让你判断是否构成多态。

练习题

以下程序输出结果是什么()
A: A->0 B: B->1 C: A->1 D: B->0 E: 编译出错 F: 以上都不正确

复制代码
class A
{
public:
virtual void func(int val = 1){ std::cout<<"A->"<< val <<std::endl;}
virtual void test(){ func();}
};
class B : public A
{
public:
void func(int val = 0){ std::cout<<"B->"<< val <<std::endl; }
};
int main(int argc ,char* argv[])
{
B*p = new B;
p->test();
return 0;
}

答案:B 解析:在test函数中调用了基态指针(*this)调用了虚函数(func),虽然派生类B类中func函数,没有virtual,但是还是和基类的func还是构成重写,所以存在多态。在test函数中A类指针指向的func函数,但是是B对象指向test函数,所以会调用B中的实现方式。所以val=1。

析构函数的重写

像以下这段代码

复制代码
class A
{
public:
	~A()
	{
		cout << "~A()" << endl;
	}
};
class B : public A
{
public:
	~B()
	{
		cout << "~B()" << endl;
	}
};
int main(int argc, char* argv[])
{
	A* a1 = new A;
	A* a2 = new B;
	delete a1;
	delete a2;
	return 0;
}

运行结果:

派生类 B 可能会分配额外的资源,如动态内存、文件句柄、网络连接等。如果只调用基类 A 的析构函数,而没有调用派生类 B 的析构函数,这些资源将无法被正确释放,从而导致内存泄漏或其他资源管理问题。我们发现在delete p2时,只调用了A的析构函数,没有调用B的析构函数。 但如果这两个类中存在多态关系就可以完全释放,只有将基类的析构函数声明为虚函数,才能保证在通过基类指针删除派生类对象时,能够正确地调用派生类的析构函数,进而先完成派生类的析构操作,再完成基类的析构操作,确保对象能够被完全销毁。那如何构成多态关系呢?

首先需要虚函数重写:

基类的析构函数为虚函数,此时派生类只要定义了,无论是否加virtual关键字,都与基类的析构函数构成重写,虽然基类与派生类析构函数名字不同,但实际上编译器对析构函数的名称做了特殊处理,编译后析构函数的名称统一处理为destructor,所以基类的析构函数加了virtual修饰,派生类的析构函数就构成了重写。

override和final关键字

C++对虚函数重写的要求比较严格,但有些情况下由于疏忽,比如函数名写错参数等导致无法构成重写,而这种错误在编译期间是不会报出,只有在程序运行时没有得到预期结果才debug,会得不偿失,因此C++11提供override来帮助用户检测是否冲重写。如果不想让派生类重写这个虚函数,那么可以用final去修饰。

override关键字演示:override加在派生类虚函数后面。

复制代码
class Car {
public:
	virtual void Dirve()
	{}
};
class Benz :public Car
{
public:
	virtual void Drive()override//与基类的虚函数名称不同,不构成重写
//报错:错误	C3668	"Benz::Drive": 包含重写说明符"override"的方法没有重写任何基类方法
	{
		cout << "Benz" << endl;
	}
};
int main()
{
	return 0;
}

final关键字演示:加在基类成员函数名后面

复制代码
class Car {
public:
	//错误	C3248	"Car::Drive" : 声明为"final"的函数无法被"Benz::Drive"重写	
	virtual void Drive()final
	{}
};
class Benz :public Car
{
public:
	virtual void Drive()
	{
		cout << "Benz" << endl;
	}
};
int main()
{
	return 0;
}

重载/隐藏/重载的区别

重载:

1.两个函数作用在同一作用域。

2.函数名相同,参数不同,参数的类型或者个数不同,返回值可同可不同。
重写/覆盖:

1.两个函数分别在继承体系中的父类和子类,在不同作用域

2.函数名、参数,返回值都必须相同,协变例外(基类虚函数返回基类对象的指针或者引

⽤,派⽣类虚函数返回派⽣类对象的指针或者引⽤时,称为协变。)

3.两个函数必须是虚函数
隐藏:

1.两个函数分别在继承体系的父类和子类中,在不同作用域

2.函数名相同/父类和子类的成员变量相同

3.两个函数只要不构成重写,就是隐藏

纯虚函数和抽象类

纯虚函数:

1.在虚函数的后面加上"=0"。

2.不需要定义实现,只要声明即可。

3.可以强制派生类重写虚函数,因为不重写就实例化不出对象。

抽象类:包含纯虚函数的类。抽象类不能实例化出对象,如果派生类继承后不重写纯虚函数,那么派生类也是抽象类。

复制代码
class Car {
public:
	virtual void Drive()=0
	{}
};
class Benz :public Car
{
public:
	virtual void Drive()
	{
		cout << "Benz" << endl;
	}
};
class BMW :public Car
{
public:
	virtual void Drive()
	{
		cout << "BMW" << endl;
	}
};
int main()
{
	//Car car;
	//抽象类不能实例化出对象
	//但是如果派生类重写纯虚函数,派生类可以实例化出对象
	Benz benz;
	BMW bmw;
	Car* pBenz = new Benz;
	pBenz->Drive();
	Car* pBMW = new BMW;
	pBMW->Drive();
	return 0;
}

派生类中不重写虚函数,派生类也是抽象类。

复制代码
class Car {
public:
	virtual void Drive()=0
	{}
};
class Benz :public Car
{
public:
	
};
class BMW :public Car
{
public:
	virtual void Drive()
	{
		cout << "BMW" << endl;
	}
};
int main()
{
	Benz benz;
//报错:Benz类中没有重写抽象类中的纯虚函数,Benz也是抽象类
//故Benz不能实例化出对象
	BMW bmw;
	return 0;
}

多态

虚函数表指针

下⾯编译为32位程序的运⾏结果是什么()

A. 编译报错 B. 运⾏报错 C. 8 D. 12

复制代码
class Base
{
public:
virtual void Func1()
{
cout << "Func1()" << endl;
}
protected:
int _b = 1;
char _ch = 'x';
};
int main()
{
Base b;
cout << sizeof(b) << endl;
return 0;
}

答案是D,如果我们按正常计算都应该是8byte,但12byte是怎么来的呢?

当我们调试,在监视窗口可以看到b对象多了一个成员变量(_vfptr)。

对象中的这个指针就是虚函数表指针。一格含有虚函数的类中都至少有一个虚函数表指针,因为一个类所有虚函数的地址要放进这个类对象的虚函数表中。

多态的原理

复制代码
class Person {
public:
virtual void BuyTicket() { cout << "买票-全价" << endl; }
private:
string _name;
};
class Student : public Person {
public:
virtual void BuyTicket() { cout << "买票-打折" << endl; }
private:
string _id;
};
class Soldier: public Person {
public:
virtual void BuyTicket() { cout << "买票-优先" << endl; }
private:
string _codename;
};
void Func(Person* ptr)
{
// 这⾥可以看到虽然都是Person指针Ptr在调⽤BuyTicket
// 但是跟ptr没关系,⽽是由ptr指向的对象决定的。
ptr->BuyTicket();
}
int main()
{
// 其次多态不仅仅发⽣在派⽣类对象之间,多个派⽣类继承基类,重写虚函数后
// 多态也会发⽣在多个派⽣类之间。
Person ps;
Student st;
Soldier sr;
Func(&ps);
Func(&st);
Func(&sr);
return 0;
}

从底层结构的角度,Func函数中ptr->BuyTicket(),是如何让ptr指向Student对象调用Student::BuyTicket()的呢?我们知道如果想要找到函数,那么就要有函数的地址,但是这次我们不再是编译时通过调用对象确定函数的地址了,在上面我们谈到类中有虚函数的类的成员变量就会多一个虚函数表指针,所以我们在运行时通过虚函数表指针就可以确定虚函数的地址,这样就实现了指针或引用指向基类就调用基类的函数,指向派生类就调用派生类对应的虚函数。

在图中我们发现每个对象中的虚函数指针都是不同的,那么就可以区分该调用哪个对象的虚函数。

动态绑定与静态绑定

静态绑定:对于非虚函数,在编译时确定调用函数地址。

动态绑定:对于虚函数,在运行时到指定对象的虚函数表中找到调用函数的地址。

复制代码
//BuyTicket()是虚函数,编译在运行时到ptr的虚函数表中确定调用函数地址
ptr->BuyTicket();
00DA4931  mov         eax,dword ptr [ptr]  
00DA4934  mov         edx,dword ptr [eax]  
00DA4936  mov         esi,esp  
00DA4938  mov         ecx,dword ptr [ptr]  
00DA493B  mov         eax,dword ptr [edx]  
00DA493D  call        eax  
00DA493F  cmp         esi,esp  
00DA4941  call        __RTC_CheckEsp (0DA1302h) 
//BuyTicket()不是虚函数,编译器直接确定调用地址
ptr->BuyTicket();
003D3BB1  mov         ecx,dword ptr [ptr]  
003D3BB4  call        Person::BuyTicket (03D168Bh) 

虚函数表总结

1.基类对象的虚函数表中存放基类所有虚函数的地址。同类型的对象共用一张虚表,不同对象各自有独立的虚表。

2.派生类由两部分组成,继承下来的基类和自己的成员,一般情况下,继承下来的基类中有虚函数表指针,自己就不会在生成虚函数表指针。但是继承下来的基类部分虚函数表指针和基类对象的虚函数表指针不是同一个。

3.派生类中重写的基类的虚函数,派生类的虚函数表中对应的虚函数就会被覆盖成派生类重写的虚函数地址。

4.派生类的虚函数表包含:(1)基类的虚函数表地址,(2)派生类重写的虚函数地址完成覆盖,(3)派生类自己的虚函数地址

5.虚函数表本质是一个存虚函数指针的指针数组。

6.虚函数和普通函数一样,编译后是一段指令,都是存放在代码段,只是虚函数表地址存到虚表中。

7.虚函数表存在哪里C++标准没有规定。但是可以通过代码对比验证。vs下存在代码段。

复制代码
class Base {
public:
    virtual void func1() { cout << "Base func1" << endl; }
    virtual void func2() { cout << "Base func2" << endl; }
};

class Derived : public Base {
public:
    void func1() override { cout << "Derived func1" << endl; }
    void func2() override { cout << "Derived func2" << endl; }
};
int main()
{
    Base b;
    Derived d;
    return 0;
}
相关推荐
ChiaWei Lee3 分钟前
【C语言】深入理解指针(三):C语言中的高级指针应用
c语言·开发语言
最后一个bug3 分钟前
教你快速理解linux中的NUMA节点探测是干什么用的?
linux·c语言·开发语言·arm开发·嵌入式硬件
Chiyamin16 分钟前
C++面向对象速览(三)
c++
Tadecanlan20 分钟前
[C++面试] 智能指针面试点(重点)续4
开发语言·c++·面试
Chiyamin21 分钟前
C++面向对象速览(一)
c++
GOTXX27 分钟前
BoostSiteSeeker项目实战
前端·c++·后端·mysql·搜索引擎·项目实战·boost
快乐点吧31 分钟前
【Word】批注一键导出:VBA 宏
开发语言·c#·word
胡乱儿起个名1 小时前
C++的指针数组、数组指针和指针数组指针
开发语言·c++
kill bert1 小时前
第32周Java微服务入门 微服务基础
java·开发语言·微服务
学c真好玩1 小时前
4.1-python操作wrod/pdf 文件
开发语言·python·pdf