c++:多态

一、初识多态

多态的含义就是:一个对象或事物在不同情况有多种表达状态,在c++中的多态中,分为编译时多态和运行时多态,我们这里讲的是运行时多态,运行时多态要在要在延续是在继承体系中,一个函数在不同对象的调用实现的不同的结果

比如,我们把动物的叫声作为基类,猫和狗分别作为它的派生类,在调用动物叫声的函数中,如果是猫对象调用就是"喵喵喵',狗对象调用就是"汪汪汪",就是某个函数在不同的对象调用发生不同的结果,然而这个函数被称为虚函数,下面会详细讲

二、多态的条件

虚函数

cpp 复制代码
class Person 
{
public:、
virtual void BuyTicket() { cout << "买票全价" << endl;}
};

<font size="4" color="b">就是跟虚继承一样的关键词virtual 加在函数返回类型的前面,需要构成多态首先就是基类要有虚函数,并且调用函数要是基类的指针和引用

虚函数的重写

在虚函数被调用时,要在派生类里也写了这个虚函数且关键词virtual在派生类的重写可以省略,并且 函数返回值相同(协变除外,后面会讲),函数名相同,形参类型相同,才构成重写/覆盖

满足这两个条件,就能构成多态

三、多态的感受

在实际生活中,普通人买票全价,学生买票半价学生又属于普通人所以可以构成继承,然后在继承体系中,不同人买票得到的结果又不同,所以买票函数可以构成多态

cpp 复制代码
class Person
{
public:
	virtual void buyticket()
	{
		cout << "票价-全价" << endl;
	}
};
	class student : public Person
	{
	public:
	 	void buyticket()
		{
			cout << "票价-半价"<< endl;
		}
		
	};
	int main()
	{
  Person p;
 student st1;
 Person* p1 = &p;
    Person* p2 = &st1;
	p1->buyticket();
	p2->buyticket();

所以结果的呈现调用谁的虚函数是看基类指针或引用的对象是谁,如上p1是基类指针,指向的是基类对象,所以调用基类的buyticket函数,p2是基类指针,指向的是派生类对象,所以调用派生类的buyticket

并且调用的实际过程是:基类虚函数声明加基类指针或引用指向的对象的虚函数实现部分。我们把上面函数改一下

cpp 复制代码
class Person
{
public:
	virtual void buyticket(int val=0)
	{
		cout << "票价-全价" << val << endl;
	}
};
	class student : public Person
	{
	public:
	 	void buyticket(int val=1)
		{
			cout << "票价-半价"  << val << endl;
		}
		
	};
	int main()
	{
  Person p;
 student st1;
 Person* p1 = &p;
    Person* p2 = &st1;
	p1->buyticket();
	p2->buyticket();

所以由此得出的结论是正确的

面这道题更能让你理解:
以下程序输出结果是什么()
A: A->0 B:B->1 C:A->1 D:B->0 E:编译出错 F:以上都不正确

cpp 复制代码
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()
{
B*p = new B;

p->test();
return 0;
}

这道题是选B,我相信很多小伙伴写错了,这道题能复习一些继承的知识还有理解多态结果的实现过程。首先test函数先会在B的类域找,没找到才会去基类找,找到了之后,在基类test的函数中形参是 A* this,所以调用的func是 this->func(),首先this是基类类型的指针,而且调用的函数是虚函数,并且完成了虚函数的重写,因为派生类有这个虚函数,所以构成多态, 因为this指针指向的是B对象所以调用它的虚函数,所以函数的实现部分就是B类的,但函数的声明是拿基类的所以val的缺省值是1,所以这道题的答案是B

四、析构函数的重写

cpp 复制代码
class A
{
public:
	 ~A()
	{
		cout << "~A()" << endl;
	}
};

class B : public A {
public:
	~B()
	{
		cout << "~B()->delete:" << _p << endl;
		delete _p;
	}
protected:
	int* _p = new int[10];
};
int main()
{
	A* p1 = new A;
	A* p2 = new B;
	// 只有派⽣类	Student	的析构函数重写了	Person	的析构函数,下⾯的
	//调用的析构-> A->destructor(),//是虚函数并且是基类指针 构成多态条件
	delete p1;
	delete p2;
}

如果这里的基类的析构函数不写成虚函数的话,这里的运行结果是这样的:

第一个~A()没问题是析构掉A对象,但第二个析构掉B对象就有问题了,根据我们在继承的学习中,析构函数会先析构掉派生类,在调用基类的析构去析构掉基类,但这里能看出是直接调用基类的析构

其实在这里基类和派生类的析构函数构成隐藏,祖师爷在这里把继承体系中的析构函数做了特殊化处理实际在编译器显示析构函数名都是destructor,这里p2的调用相当于A->destructor(),所以它直接调用基类的析构去了,我们知道,析构函数分为两步: 先析构掉数据,在operator delete掉在堆上创建对象的资源,但这里只是释放掉派生类中基类的资源,派生类自己的独有部分的资源还没释放,这就造成了内存泄漏
所以要正常调用B对象的析构函数,那就要将析构函数弄成虚函数,让它们构成重写,所以再次调用的话,就不看类型了,只看它指向的对象,所以会调用B的析构,就能正常释放资源

五、纯虚函数和抽象函数

在虚函数的后⾯写上=0,则这个函数为纯虚函数,纯虚函数不需要定义实现(实现没啥意义因为要被
派⽣类重写,但是语法上可以实现),只要声明即可。包含纯虚函数的类叫做抽象类,抽象类不能实例
化出对象,如果派⽣类继承后不重写纯虚函数,那么派⽣类也是抽象类。纯虚函数某种程度上强制了
派⽣类重写虚函数,因为不重写实例化不出对象。

cpp 复制代码
class Car
{
public:
	virtual void Drive() = 0;
	virtual void fun() = 0;
};
class Benz :public Car
{
public:
	void fun()
	{
		cout << "Benz舒适" << endl;
	}
	void Drive()
	{
		cout << "Benz舒适" << endl;
	}
};
int main() {
	Benz b;//纯虚函数不能被实例化,并且它的派生类强制要有重写,否则派生类对象也不能实例化
	Car* c = &b;
	c->Drive();
}

因为车是个抽象的东西,所以它的虚函数可以写成纯虚函数, Car c 会报错,如果派生类不重写虚函数的话,定义的派生类对象也会报错

协变

派⽣类重写基类虚函数时,与基类虚函数返回值类型不同。即基类虚函数返回基类对象的指针或者引
⽤,派⽣类虚函数返回派⽣类对象的指针或者引⽤时,称为协变。协变的实际意义并不⼤,所以我们
了解⼀下即可。

六、重载/隐藏/重写的对比

七、编译多态和运行多态的区别

编译多态又叫静态多态,它处理是在编译和连接的时候就call 函数的地址,函数的重载和函数模板就是典型的例子,典型的重载输入输出流如cin>>不同的类型的值

运行时的多态就是在编译连接的时候不进行直接call 调用函数的地址,它是在运行时去找对象的地址进行操作,

八、多态的原理

有了上面对多态的概念与基本使用有了相应的了解,现在我们来了解一下为什么能调用相应行为的虚函数,我们先来一道题引出虚函数表指针

下⾯编译为32位程序的运⾏结果是什么()
A. 编译报错 B.运⾏报错C.8 D.12

cpp 复制代码
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;
}

根据内存对齐我们能轻松得出b的大小为8字节后选C,但是这道题选D,所以成员变量就不止这两个,那还有四个字节是什么呢?没错,是接下来要讲的虚函数表指针

虚函数表指针

上⾯题⽬运⾏结果12bytes,除了_b和_ch成员,还多⼀个__vfptr放在对象的前⾯(注意有些平台可能
会放到对象的最后⾯,这个跟平台有关),对象中的这个指针我们叫做虚函数表指针(v代表virtual,f代
表function)。⼀个含有虚函数的类中都⾄少都有⼀个虚函数表指针,因为⼀个类所有虚函数的地址要
被放到这个类对象的虚函数表中,虚函数表也简称虚表。

这个指针的类型是函数指针数组,它里面是储存虚函数地址

多态是如何实现的

满⾜多态条件后,底层不再是编译时通过调⽤对象确定函数的地址,⽽是运⾏时到指向的对象的虚表中确定对应的函数的地址,这样就实现了指针或引⽤指向基类就调⽤基类的虚函数,指向派⽣类就调⽤派⽣类对应的虚函数。

比如上面原先写的买票的相关多态,不同对象调用的虚表不一样,指向Person对象的指针或引用就调用Person的虚表,在虚表的里面能找到对应虚函数地址,然后再去调用函数

cpp 复制代码
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;
}

动态绑定与静态绑定

1.对不满⾜多态条件(指针或者引⽤+调⽤虚函数)的函数调⽤是在编译时绑定,也就是编译时确定调⽤
函数的地址,叫做静态绑定。
2.满⾜多态条件的函数调⽤是在运⾏时绑定,也就是在运⾏时到指向对象的虚函数表中找到调⽤函数
的地址,也就做动态绑定。

九、虚函数表


基类对象的虚函数表中存放基类所有虚函数的地址。同类型的对象共⽤同⼀张虚表,不同类型的对
象各⾃有独⽴的虚表,所以基类和派⽣类有各⾃独⽴的虚表。

cpp 复制代码
class Base {
public:
	virtual void func1() { cout << "Base::func1" << endl; }
	virtual void func2() { cout << "Base::func2" << endl; }
	void func5() { cout << "Base::func5" << endl; }
protected:
	int a = 1;
};
class Derive : public Base
{
public:
	// 重写基类的func1
		virtual void func1() { cout << "Derive::func1" << endl; }
	virtual void func3() { cout << "Derive::func1" << endl; }
		void func4() { cout << "Derive::func4" << endl; }
	protected:
		int b = 2;
	};

int main()
{
	Base b;
	Base b1;
	Derive d;
}

可以看到b和b1的虚函数地址一样

派⽣类由两部分构成,继承下来的基类和⾃⼰的成员,⼀般情况下,继承下来的基类中有虚函数表
指针,⾃⼰就不会再⽣成虚函数表指针。但是要注意的这⾥继承下来的基类部分虚函数表指针和基类对象的虚函数表指针不是同⼀个,就像基类对象的成员和派⽣类对象中的基类对象成员也独⽴的。

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

派⽣类的虚函数表中包含,(1)基类的虚函数地址,(2)派⽣类重写的虚函数地址完成覆盖,派⽣类
⾃⼰的虚函数地址三个部分。

可以看到,派生类d中的基类的虚表中的func2的函数地址相同,那就证明了是派生类直接继承基类的虚表,但是虚函数重写的会被派生类中的对应函数覆盖,比如func1的虚函数地址就是派生类自己的,所以这也是为什么调用函数时,看指向的对象类型就能调用它的对应的虚函数

但是可以看到虚函数表里并没有派生类自己的虚函数func3()的地址,因为它目前不是任何类的基类,所以它虽然加了virtual,但不是虚函数,它没有构成重写,所以vs就暂时隐藏了它

虚函数表本质是⼀个存虚函数指针的指针数组,⼀般情况这个数组最后⾯放了⼀个0x00000000标
记。(这个C++并没有进⾏规定,各个编译器⾃⾏定义的,vs系列编译器会再后⾯放个0x00000000
标记,g++系列编译不会放)

虚函数存在哪的?虚函数和普通函数⼀样的,编译好后是⼀段指令,都是存在代码段的,只是虚函
数的地址⼜存到了虚表中。
• vs下是存在代码段(常量区)

相关推荐
Lumbrologist1 小时前
【C++】零基础入门 · 第 18 节:互斥锁与线程同步
java·开发语言·c++
tangchao340勤奋的老年?1 小时前
C++ OpenGL显示地图
c++·opengl
plainGeekDev1 小时前
Fragment 手动跳转 → Navigation 组件
android·java·kotlin
plainGeekDev1 小时前
XML 主题 → Compose Material3 主题
android·java·kotlin
I Promise341 小时前
C++ 多线程编程:从入门到实战
开发语言·c++
武子康1 小时前
Java-14 深入浅出 MyBatis 插件机制深度解析:四大对象拦截与动态代理原理
java·后端
搜佛说1 小时前
sfsDb 和 SQLite、InfluxDB “硬碰硬”的底层性能与技术架构对比
jvm·架构·sqlite
小楼v1 小时前
Kafka消息队列安装步骤及从0入门到基础核心掌握
java·kafka·消息队列·教程·安装
邪修king1 小时前
C++map_set封装 : 红黑树底层迭代器以及仿函数的运用
android·c语言·数据结构·c++·b树