C++:让你玩转多态

1. 多态的概念

  • 多态(polymorphism)的概念:通俗来说,就是多种形态。
  • 多态分为编译时多态(静态多态)和运行时多态(动态多态)
  • 编译时多态(静态多态):主要就是我们前面讲的函数重载和函数模板,他们传不同类型的参数就可以调用不同的函数,通过参数不同达到多种形态,之所以叫编译时多态,是因为他们实参传给形参的参数匹配是在编译时完成的,我们把编译时一般归为静态,运行时归为动态。
  • 运行时多态,具体点就是去完成某个行为(函数),可以传不同的对象就会完成不同的行为,就达到多种形态。

2. 多态的定义及实现

2.1 多态的构成条件

多态是⼀个 继承关系 下的类对象,去调用同⼀函数,产生了不同的行为。比如Student继承了Person。Person对象买票全价,Student对象优惠买票。

实现多态还有两个必须重要条件:

  • 第一必须是基类的指针或引用,因为只有基类的指针或引用才能既指向基类对象又指向派生类对象。
  • 第⼆派生类必须对基类的虚函数完成重写/覆盖,重写或者覆盖了,基类和派
    生类之间才能有不同的函数,多态的不同形态效果才能达到。

2.2 虚函数

类成员函数前面加virtual修饰,那么这个成员函数被称为虚函数。注意非成员函数不能加virtual修饰。

代码演示:

cpp 复制代码
class student
{
public:
	virtual void Buyticket()
	{
		cout << "半价\n" << endl;
	}
};

2.3 虚函数的重写和覆盖

虚函数的重写和覆盖实际上是指:

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

cpp 复制代码
#include<iostream>
#include<string>
using namespace std;

class person
{
public:
	virtual void Buyticket()
	{
		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;
}

2.4 多态场景的⼀个选择题


问题分析

首先很多人都会认为由于继承关系,所以B对象用指针p访问test()函数的时候,仍然认为这是此时的指针类型是B,但是继承的含义不是直接拷贝下来的,而是派生类没有的再去基类访问。

而在基类中,this指针是不会变的,只要是在A类中,指针类型就是A,所以当p调用test()函数的时候,是在A类中调用的,此时this指针是 A,所以默认的函数缺省值val是A决定的,但是A和B中的func函数又构成了重写,所以又去调用派生类B的func函数,所以最终结果是B->1。
注意 :有很多不理解为什么缺省值是使用A类的,你只需要直到,虚函数重写的本质是重写虚函数的实现,跟不管你参数带的缺省值,所以缺省值是A的,然后调用B的实现,这一块是真的很反人类,但是记住就行了。

2.5 虚函数重写的一些其他问题

  • 协变(了解即可)
    派生类重写基类虚函数时,与基类虚函数返回值类型不同。即基类虚函数返回基类对象的指针或者引用派生类虚函数返回派生类对象的指针或者引用时,称为协变。协变的实际意义并不大,所以我们了解⼀下即可。
cpp 复制代码
#include<iostream>
#include<string>
using namespace std;
class A{};
class B:public A{};

class person
{
public:
	virtual A* Buyticket()
	{
		cout << "全价" << endl;
	}
};

class student :public person
{
public:
	virtual B* Buyticket()
	{
		cout << "半价" << endl;
	}
};

void Buyticket(person* p)
{
	p->Buyticket();
}

int main()
{
	person p;
	Buyticket(&p);
	return 0;
}

2.6 析构函数的重写

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

为什么要这要设计呢?

先看一段代码:

cpp 复制代码
#include<iostream>
#include<string>
using namespace std;

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

class Derived:public Base
{
public:
	Derived()
	{
		data = new int[100];
	}
	
	~Derived()
	{
		cout << "~Base()" << endl;
	}
private:
	int* data;
};

int main()
{
	Base* b = new Derived();//基类对象指向了一个派生类对象
	delete b;
	return 0;
}

这段代码如果没有给基类的析构函数写成虚函数,编译器就会报错,因为,我们是创建了一个Base对象,然后用这个基类去只想它的派生类,而在派生类中,我们在堆上开辟了一个数组,而我们的指针类型是Base的,就只会调用Base的析构,而导致资源释放不全,导致内存泄漏,如果你写成虚函数,就只会调用派生类的析构函数,就不会造成这种问题。

2.6 override 和 final关键字

C++对虚函数重写的要求比较严格,但是有些情况下由于疏忽,比如函数名写错参数

写错等导致无法构成重写,而这种错误在编译期间是不会报出的,因此C++11提供了override,可以帮助用户检测是否重写。如果我们不想让派生类重写这个虚函数,那么可以用final去修饰。

代码示例:

cpp 复制代码
#include<iostream>
#include<string>
using namespace std;

class Car
{
public:
	virtual void Drive()
	{
		cout << "舒适度" << endl;
	}
};

class Benz :public Car
{
public:
	void Drive()override
	{
		cout << "很好" << endl;
	}
};
cpp 复制代码
class Car
{
public:
	virtual void Drive()final
	{
		cout << "舒适度" << endl;
	}
};

class Benz :public Car
{
public:
	void Drive()
	{
		cout << "很好" << endl;
	}
};

如果我们写了虚函数又写了final的关键字,就会导致编译器报错。

2.7 重载/重写/隐藏的对比

3. 纯虚函数和抽象类

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

cpp 复制代码
#include<iostream>
#include<string>
using namespace std;

class Car
{
public:
	virtual void Drive()=0
	{
		cout << "舒适" << endl;
	}
};
class Benz :public Car
{
public:
	 void Drive()
	{
		cout << "操作" << endl;
	}
};
int main()
{
	Car c;
	Benz b;
	b.Drive();
	return 0;
}
cpp 复制代码
#include<iostream>
#include<string>
using namespace std;

class Car
{
public:
	virtual void Drive()=0
	{
		cout << "舒适" << endl;
	}
};
class Benz :public Car
{
public:
	 void Driver()
	{
		cout << "操作" << endl;
	}
};
int main()
{
	Car c;
	Benz b;
	b.Drive();
	return 0;
}

4. 多态的原理

4.1 虚函数表指针

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

4.2 多态的原理

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

相关推荐
qeen877 小时前
【数据结构】二叉树基本概念及堆的C语言模拟实现
c语言·数据结构·c++·
lynnlovemin7 小时前
C++高精度加减乘除算法详解
开发语言·c++·算法·高精度
minji...7 小时前
Linux 网络套接字编程(七)TCP服务端和客户端的实现——网络版本计算器
linux·运维·服务器·网络·c++·tcp/ip·udp
郝学胜-神的一滴7 小时前
epoll 反应堆模型深度拆解:从红黑树到回调闭环,手写高性能回射服务器
linux·运维·服务器·开发语言·c++·unix
小张成长计划..7 小时前
【C++】26:用哈希表封装unordered_set和unordered_map
c++·散列表
故事和你917 小时前
洛谷-算法2-4-字符串2
开发语言·数据结构·c++·算法·深度优先·动态规划·图论
cpp_25017 小时前
P3374 【模板】树状数组 1
数据结构·c++·算法·题解·洛谷·树状数组
郝学胜-神的一滴7 小时前
干货版《算法导论》 02 :算法效率核心解密
java·开发语言·数据结构·c++·python·算法
stolentime7 小时前
AT_agc061_d [AGC061D] Almost Multiplication Table题解
c++·算法·构造