1.多态概念及介绍
多态是C++中一个重要的知识点,与类的继承相关联。简单来说,多态运用于执行某一个行为时,会对不同类型的对象采取不同的处理方法。我们生活中有着许多简单与多态相关的例子,例如购物时拥有会员这一身份的人购物能够享受购物折扣,又如购票的学生优惠,军人优先等等。
2.多态的定义与实现
多态的定义与实现,是在类的继承前提下完成的,且构成多态还需要满足两个特定的条件。
- 子类(派生类)必须完成父类(基类)虚函数的重写
- 虚函数调用时传递的参数必须是父类的指针或者引用
2.1虚函数
多态的实现,虚函数是其中一个必不可少的点。虚函数是由关键词:virtual所修饰的成员函数,需要注意关键词virtual在继承中也出现过,但需要将两者区分开,在继承中virtual是用于解决菱形继承的,用于修饰继承时的父类。
2.2虚函数的重写
虚函数的重写是子类对父类中虚函数的重新定义,可看做是将父类中的虚函数覆盖,虚函数的重写需要满以下条件
- 子类函数名与父类虚函数的函数名、返回值、以及参数相同
class A
{
public:
virtual void test()
{
cout << "class A" << endl;
}
};
class B : public A
{
public:
virtual void test()//加关键词virtual的重写
{
cout << "class B" << endl;
}
};
class C: public A
{
public:
void test()//不加关键词virtual的重写
{
cout << "class C" << endl;
}
};
对于子类完成对父类虚函数的重写,实际上关键词virtual不加时重写依然生效,我们可以看做子类继承了父类中的虚函数。但即便如此,不加virtual的写法是不规范的,正常情况下我们在写多态时最好写上关键词virtual修饰成员函数。
2.21两类特殊的虚函数重写
在虚函数的重写中,存在两种特殊的情况,即便不满足前面我们所说的函数名,返回值,以及参数都相同的条件,依然可以构成对父类虚函数的重写。
1.协变(返回值不同)
协变是当子类重写父类虚函数时函数返回值不同时的情况,我们称之为协变,这种特殊情况只存在于当父类虚函数的返回值是一个继承关系中的父类的指针或者引用时才能触发,此时子类虚函数的返回值也应当是对应继承关系中的子类的指针或者引用。
2.析构函数的虚重写 (函数名不同)
虚构函数的虚重写是发生在对类的析构函数进行虚重写的情况下的,这种情况下,子类虚重写函数的函数名与父类虚函数不同。
class A
{
public:
virtual ~A()
{}
};
class B: public A
{
public:
virtual ~B()
{}
};
虽然基类与派生类析构函数名字不同,看起来违背了重写的规则,实则不然,这里可以理解为编译器对析构函数的名称做了特殊处理,编译后析构函数的名称统-处理成相同的destructor,编译器对析构函数的这种处理只存在于继承关系中,而多态的成立是在继承关系的前提下完成的。
对于继承关系中,是很推荐将基类的析构函数写为虚函数的,让派生类重写基类的虚析构函数,形成多态的关系,这对于某些场景下是很有用的。
例如基类A类对象类型的指针指向一个派生类对象B的内存空间,等到销毁new开辟的内存时,如果基类与派生类没有形成析构函数的多态关系,则在释放指针p空间时会调用A类的析构函数,而不是B类的析构函数,若果想让指向何种类型类就调用那个类对应析构函数释放空间就可以让派生类重写基类的析构函数,形成多态关系。
2.3关键词override和final
2.31override
关键词override是用于修饰虚函数的重写,在虚函数的后面加上关键词override就可以检测是否对完成对父类虚函数的重写,如果没有完成虚函数的重写则编译不能通过,起到一个提醒的作用。
有一点需要我们注意的是,关键词override不能胡乱使用,如果加在不可能不存在多态关系的类成员函数后面反而会起到相反的效果,不仅没有起到检测是否完成虚函数的重写,还可能因此导致变成一个小bug。
2.32final
关键词final可用于修饰虚函数,加在虚函数的后面,表示改虚函数不能够被重写,如果被重写则不编译不能通过。此外也可用于修饰类,表示该类不能被继承。
2.4重载、重写(覆盖)、重定义(隐藏)的区分
对于函数的重载:
- 重载的函数必须在同一作用域
- 函数名必须相同,参数不同、与函数返回值相同与否无关
对于重写(覆盖),也就是虚重写,发生在两个类的继承关系中
- 重写的作用域是在派生类中
- 重写的函数名,返回类型、返回值都必须相同,除了协变和析构函数重写两种特殊情况
对于重定义,也是发生在两个类的继承关系中
- 作用域是基类和其派生类
- 重定义只需要函数名相同就构成,也就是说在一个继承关系中,派生类与基类的同名函数如果没有构成重写那就构成了重定义
2.5构成多态的函数参数类型
前面我们说过,要构成多态的关系除了满足前面我们说的派生类完成基类的虚函数重写,还需要满足调用虚函数时的函数参数,只有当参数是基类的指针或者引用时才可以形成多态的关系。
class A
{
public:
virtual void test()
{
cout << "class A" << endl;
}
};
class B:public A
{
public:
virtual void test()
{
cout << "class B" << endl;
}
};
void test1(A a)
{
a.test();
}
void test2(A& a)//基类引用
{
a.test();
}
void test3(A* a)//基类指针
{
a->test();
}
以上有三种传参调用虚函数的方式,我们可以知道只有后面两种满足函数参数类型。
对于不满足多态的虚函数调用:
其调用的虚函数类型只与函数定义的形参的类型有关,无论函数传递的实参是基类还是派生类,其都会去调用函数形参对应的类的虚函数。
而对于满足多态关系的函数调用虚函数:
其调用的虚函数类型与函数定义形参类型无关,只与实际传递的实参类型有关,如传递派生类的对象,其就会去调用派生类中重写的虚函数;传递对象为基类,则会去调用基类的虚函数。
3.抽象类
抽象类是一个含有纯虚函数的类,也叫做接口类,抽象类不能够实例化出对象,其继承的派生类在重写基类的纯虚函数前同样是不能够实例化出对象的。
3.1存虚函数
存虚函数是指在虚函数的基础上,再在其函数后面加上 = 0 的一个虚函数。
3.2抽象类的意义
抽象类的另一个名称也叫做接口类,其作用主要就是用于继承关系中基类所定义的接口,以便为了让派生类重写基类的虚函数,其最终目的是实现多态的关系。与非多态关系的继承不同的是,抽象类继承的接口,需要派生类去另外实现,而非多态关系的继承可以继承使用基类的成员函数。所以如果不是为了实现多态,一般不要去定义抽象类。
4.多态的原理
4.1虚函数表
在多态中,类的成员变量实际上隐藏着一个用于存储虚函数地址的函数指针数组成员变量,这个隐藏的成员变量叫做:虚函数指针表(Virtual Function Table),简称虚函数表,多态的实现正是靠虚函数表的存在实现的。
class A
{
public:
virtual void test()
{
cout << "class A" << endl;
}
};
class B:public A
{
protected:
int _b = 0;
public:
virtual void test()
{
cout << "class B" << endl;
}
};
观察以上代码,我们会发现对象a、b中都存在一个类中未定义的成员变量:_vfptr,这个变量就是我们所提到的虚函数表,本质是一个存放函数指针的数组, 其中b对象中的_vfptr实际上是继承基类A中所得到的。
为什么两个对象的虚函数表存放函数地址不同?
仔细观察我们会发现,b继承的虚函数表中所存放的虚函数地址与a中是不同的,这是因为由于派生类对基类的虚函数实现了重写,派生类将自己的虚函数地址替换掉了基类中的虚函数地址,覆盖了基类的虚函数。
这也解释了为什么多态中可以对不同对象有着不同的处理,虚函数表中存放的函数地址可用于访问调用不同派生类、基类的虚函数。当满足多态关系时,当调用虚函数传递派生类的对象,其就会通过虚函数表去调用派生类中重写的虚函数;传递对象为基类,则会去调用基类的虚函数。
4.2同一个类中的虚函数表
在多态中,对于同一个类中的成员变量--虚函表,是该类每个对象公共的,虚函数表存放着各个对象所重写的虚函数地址,每个不同的对象都是通过同一张虚函数表来调取各自的虚函数的。而对于不同的类,自然是用各自不同的虚函数表
4.3派生类的虚函数表
在继承关系中的虚函数表,对于基类中被派生类重写的虚函数,派生类会将自己重写的虚函数的地址替换掉基类的虚函数地址,而对于基类中未被重写的虚函数,则会被继承保留下来。
class A
{
public:
virtual void func1()
{
cout << "A::func1()" << endl;
}
virtual void func2()
{
cout << "A::func2()" << endl;
}
};
class B :public A
{
public:
virtual void func1()
{
cout << "B::func1()" << endl;
}
};
如果派生类中有自己独有的虚函数,其虚函数的地址也会被存放在派生类的虚函数表中。只不过在编译器中通过监控调试我们一般是看不到的,但其确实存在。特别地,如果是多继承关系中派生类拥有自己独有虚函数,其虚函数地址应当被存放在第一个继承基类的虚函数表中,这个第一个继承基类指的是从左往右写在派生类第一个继承关系的基类。