如果知道我会死在哪里,那我将永远不去那个地方 -查理 芒格
前言
多态涵盖的内容还是比较广泛的,总体可归纳如下图所示,可知多态主要分为静态多态和动态多态。两者的分类依据为多态的决定时机,静态多态由编译期决定,而动态多态由运行期决定。
静态多态
静态多态又分为函数重载和函数模板两种类型。
01、函数重载
普通函数重载
函数重载是指在同一个作用域内,名称相同但是参数列表(参数的类型、数量、顺序)不同的一组函数。编译器会根据函数调用时提供的参数类型和数量,自动选择匹配的函数版本进行调用。如下皆是函数重载的示例,
//参数类型不同
int add(int a, int b) {
return a + b;
}
double add(double a, double b) {
return a + b;
}
std::string add(char a, char b) {
std::string dest = std::string(1,a)+std::string(1,b);
return dest;
}
//基于参数数量和类型的多态
int sum(int a, int b) {
return a + b;
}
double sum(double a) {
return a;
}
void sum() {
std::cout << "No arguments passed. \n";
}
成员函数重载
成员函数除了支持如上的重载方式外,依据成员函数是否为const也分别为函数重载。const的用法总结可参考历史文章。
class People
{
public:
People(int age):m_age(age){}
int getAge()const
{
return m_age;
}
int getAge()
{
return m_age;
}
private:
int m_age{0};
};
02、函数模板
随着C++11新特性的出现,函数模板的实现方式也出现了新的方式,可以分为旧式模板和新特性模板。
旧式函数模板
通过使用template关键字进行模板函数的声明和定义,如下即为函数参数类型不同的重载的函数模板形式的实现。
template <typename T>
T add(T a, T b) {
return a + b;
}
注意,
1.模板函数的定义必须要被使用该模板的地方所知晓。所以函数模板一般将定义和声明同时置于头文件中;2.函数的模板类型T的推导必须具有唯一性,否则编译失败,例如如上的add函数使用方式如下,会出现编译报错, “T add(T,T)”: 模板 参数“T”不明确。
新特性模板
其实函数模板完全是基于类型推导而来,依据函数实参类型来推到类型T,但是C++11以来auto具有自动类型推导的作用,同时函数参数类型自C++20来支持了auto类型,故完全可以使用auto来代替template关键字的作用,书写基于新特性的函数模板
//如下代码需要C++20方可编译通过
auto add(auto a, auto b) {
return a + b;
}
动态多态
动态多态允许我们使用一种通用的方式来处理不同的数据类型。当一个基类指针或引用指向一个派生类对象时,便可以通过这个基类指针调用派生类中重写的函数,实现在运行时的多态。由此可知,动态多态需要有三要素:
1. 继承:要有基类和子类,甚至是多个子类
2. 虚函数:基类内应有虚函数,子类最好要重写(override)虚函数
3. 指针或引用:指向子类对象的基类指针或引用
动态多态可以简单的认为是继承+虚函数实现。
01、继承
C++继承方面的资料多如牛毛,不必再次多言。仅结合自己的经历谈谈菱形继承和禁止继承。
菱形继承
如下示例代码,作为菱形继承的简单示例,菱形继承的根本特征为:存在继承自同一个类的两个子类,又有一个类多继承自这两个子类,便会导致菱形继承,出现指代不明的现象。
class Parent
{
protected:
int i_p{0};
};
class SonA:public Parent
{
private:
int i_son_a{2};
};
class SonB:public Parent
{
private:
int i_son_b{4};
};
class GrandSon :public SonA, public SonB
{
};
针对如上菱形继承的问题,可以通过虚继承的方式得到解决,示例代码如下:
class Parent
{
public:
int i_p{0};
};
//注意此处的virtual
class SonA: virtual public Parent
{
private:
int i_son_a{2};
};
//注意此处的virtual
class SonB:virtual public Parent
{
private:
int i_son_b{4};
};
class GrandSon : public SonA, public SonB
{
private:
int i_gs{10};
};
当然,最好的方法时修改基类Parent,让其功能尽可能的小,拆成两个父类,减少继承自同一类的可能,从根本杜绝菱形继承。
禁止继承
如果想要明确表明某个类不能被继承,可以通过final关键字
class Parent final
{
public:
int i_p{0};
};
class SonA: public Parent//报错
{
private:
int i_son_a{2};
};
02
虚函数
函数前通过virtual关键字表明函数为虚函数。如下
class Parent
{
public:
virtual void run(){}
private:
int i_p{0};
};
1. 子类重写虚函数时最好有override关键字标识,以敦促编译器为我们进行函数名称、参数个数和类型的检查。
2. 如果一个类必须要有一个虚函数,那么整个虚函数应该时析构函数,即常说的虚析构。
虚函数涉及到内容纷繁复杂,依次简述如下:
虚表指针和虚函数表
借助指向子类的基类指针或引用可以触发多态其根本是由于虚函数表和虚表指针的作用。
虚表指针指向虚函数表,虚表指针是含有虚函数的类的对象必有的一个由编译其生成指针。
而虚函数表由编译器生成,每个含有虚函数的类均有一个虚函数表,同时一个类的虚函数表只有一个,与类的实例化对象数量并无关联。
虚函数表为类内所有虚函数的函数指针所组成的表,所以当子类重写虚函数时,子类的虚函数表内含有的虚函数指针为重写后的函数指针,也即子类和父类的虚函数表并不是同一个。
纯虚函数
纯虚函数即虚函数后加上=0标识,如此处的run函数即为纯虚函数
class Parent
{
public:
virtual void run()=0;
private:
};
纯虚函数强制子类重写该方法;多用于设计模式中的模板方法。
含有纯虚函数的类为虚基类,虚基类不能用于声明对象
禁止重写虚方法
从父类继承的虚方法默认为虚函数,当不希望该虚方法被子类重写时,可以使用final关键字注明,禁止该虚方法被重写。