虚函数和多态的定义
在《C++ 系列 -- 继承和派生》一节中讲到,基类的指针也可以指向派生类对象
#include <iostream>
using namespace std;
// 基类 People
class People{
public:
People(char *name, int age);
void display();
protected:
char *m_name;
int m_age;
};
People::People(char *name, int age): m_name(name), m_age(age){}
void People::display(){
cout<<m_name<<"今年"<<m_age<<"岁了,是个无业游民"<<endl;
}
// 派生类 Teacher
class Teacher: public People{
public:
Teacher(char *name, int age, int salary);
void display();
private:
int m_salary;
};
Teacher::Teacher(char *name, int age, int salary): People(name, age), m_salary(salary){}
void Teacher::display(){
cout<<m_name<<"今年"<<m_age<<"岁了,是一名教师,每月有"<<m_salary<<"元的收入"<<endl;
}
int main(){
People *p = new People("王志刚", 23);
p -> display();
p = new Teacher("赵宏佳", 45, 8200);
p -> display();
return 0;
}
运行结果:
王志刚今年23岁了,是个无业游民
赵宏佳今年45岁了,是个无业游民 // 预期输出:赵宏佳今年45岁了,是一名教师,每月有8200元的收入。
本例中,当基类指针 p 指向派生类 Teacher 的对象时,虽然使用了 Teacher 的成员变量,但是却没有使用它的成员函数
由此可证:通过基类指针只能访问派生类的成员变量,但是不能访问派生类的成员函数
为了解决这个问题,让基类指针能够访问派生类的成员函数,C++ 增加了虚函数 。使用虚函数非常简单,只需要在成员函数声明前面增加 virtual
关键字
#include <iostream>
using namespace std;
// 基类 People
class People{
public:
People(char *name, int age);
virtual void display(); // 声明为虚函数
protected:
char *m_name;
int m_age;
};
People::People(char *name, int age): m_name(name), m_age(age){}
void People::display(){
cout<<m_name<<"今年"<<m_age<<"岁了,是个无业游民"<<endl;
}
// 派生类 Teacher
class Teacher: public People{
public:
Teacher(char *name, int age, int salary);
virtual void display(); // 声明为虚函数
private:
int m_salary;
};
Teacher::Teacher(char *name, int age, int salary): People(name, age), m_salary(salary){}
void Teacher::display(){
cout<<m_name<<"今年"<<m_age<<"岁了,是一名教师,每月有"<<m_salary<<"元的收入"<<endl;
}
int main(){
People *p = new People("王志刚", 23);
p -> display();
p = new Teacher("赵宏佳", 45, 8200);
p -> display();
return 0;
}
运行结果:
王志刚今年23岁了,是个无业游民 // 基类指针指向派生类对象时就使用派生类的成员
赵宏佳今年45岁了,是一名教师,每月有8200元的收入 // 基类指针指向基类对象时就使用基类的成员
有了虚函数以后:
- 基类指针指向基类对象时就使用基类的成员(包括成员函数和成员变量)
- 基类指针指向派生类对象时就使用派生类的成员
我们将这种现象称为多态
C++提供多态的目的是:可以通过基类指针对所有派生类(包括直接派生和间接派生)的成员变量和成员函数进行“全方位”的访问,尤其是成员函数。如果没有多态,我们只能访问成员变量
虚函数是根据指针的指向来调用的,指针指向哪个类的对象就调用哪个类的虚函数
多态的应用场景
假设你正在玩一款军事游戏,敌人突然发动了地面战争,于是你命令陆军、空军及其所有现役装备进入作战状态
#include <iostream>
using namespace std;
// 基类:军队
class Troops
{
public:
virtual void fight() { cout << "Strike back!" << endl; }
};
// 派生类1:陆军
class Army : public Troops
{
public:
void fight() { cout << "--Army is fighting!" << endl; }
};
// 派生类1_1:99A主战坦克
class _99A : public Army
{
public:
void fight() { cout << "----99A(Tank) is fighting!" << endl; }
};
// 派生类1_2:武直10武装直升机
class WZ_10 : public Army
{
public:
void fight() { cout << "----WZ-10(Helicopter) is fighting!" << endl; }
};
// 派生类1_3:长剑10巡航导弹
class CJ_10 : public Army
{
public:
void fight() { cout << "----CJ-10(Missile) is fighting!" << endl; }
};
// 派生类2:空军
class AirForce : public Troops
{
public:
void fight() { cout << "--AirForce is fighting!" << endl; }
};
// 派生类2_1:J-20隐形歼击机
class J_20 : public AirForce
{
public:
void fight() { cout << "----J-20(Fighter Plane) is fighting!" << endl; }
};
// 派生类2_2:CH5无人机
class CH_5 : public AirForce
{
public:
void fight() { cout << "----CH-5(UAV) is fighting!" << endl; }
};
// 派生类2_3:轰6K轰炸机
class H_6K : public AirForce
{
public:
void fight() { cout << "----H-6K(Bomber) is fighting!" << endl; }
};
int main()
{
Troops *p = new Troops;
p->fight();
// 陆军
p = new Army;
p->fight();
p = new _99A;
p->fight();
p = new WZ_10;
p->fight();
p = new CJ_10;
p->fight();
// 空军
p = new AirForce;
p->fight();
p = new J_20;
p->fight();
p = new CH_5;
p->fight();
p = new H_6K;
p->fight();
return 0;
}
运行结果:
Strike back!
--Army is fighting!
----99A(Tank) is fighting!
----WZ-10(Helicopter) is fighting!
----CJ-10(Missile) is fighting!
--AirForce is fighting!
----J-20(Fighter Plane) is fighting!
----CH-5(UAV) is fighting!
----H-6K(Bomber) is fighting!
这个例子中的派生类比较多,如果不使用多态,那么就需要定义多个指针变量,很容易造成混乱;而有了多态,只需要一个指针变量 p 就可以调用所有派生类的虚函数
虚函数注意事项
-
只需要在虚函数的声明处加上 virtual 关键字,函数定义处可以加也可以不加
-
为了方便,你可以只将基类中的函数声明为虚函数,这样所有派生类中具有遮蔽关系的同名函数都将自动成为虚函数
-
当在基类中定义了虚函数时,如果派生类没有定义新的函数来遮蔽此函数,那么将使用基类的虚函数
-
只有派生类的虚函数覆盖基类的虚函数(函数原型相同)才能构成多态(通过基类指针访问派生类函数)。例如基类虚函数的原型为
virtual void func()
,派生类虚函数的原型为virtual void func(int)
,那么当基类指针 p 指向派生类对象时,语句p -> func(100)
将会出错,而语句p -> func()
将调用基类的函数 -
构造函数不能是虚函数。对于基类的构造函数,它仅仅是在派生类构造函数中被调用,这种机制不同于继承。也就是说,派生类不继承基类的构造函数,将构造函数声明为虚函数没有什么意义
-
析构函数可以声明为虚函数,而且有时候必须要声明为虚函数
构成多态的条件
- 必须存在继承关系
- 继承关系中必须有同名的虚函数,并且它们是覆盖关系(函数原型相同)
- 存在基类的指针,通过该指针调用虚函数
什么时候声明虚函数
- 看成员函数所在的类是否会作为基类
- 看成员函数在类的继承后有无可能被更改功能,如果希望更改其功能的,一般应该将它声明为虚函数
如果成员函数在类被继承后功能不需修改,或派生类用不到该函数,则不要把它声明为虚函数
纯虚函数和抽象类
可以将虚函数声明为纯虚函数,语法格式为:
virtual 返回值类型 函数名 (函数参数) = 0;
纯虚函数没有函数体,只有函数声明,在虚函数声明的结尾加上=0
,表明此函数为纯虚函数。
最后的
=0
并不表示函数返回值为0,它只起形式上的作用,告诉编译系统“这是纯虚函数”。
包含纯虚函数的类称为抽象类。之所以说它抽象,是因为它无法创建对象。原因很明显,纯虚函数没有函数体,不是完整的函数,无法调用,也无法为其分配内存空间。
抽象类通常是作为基类,让派生类去实现纯虚函数。派生类必须实现纯虚函数才能被实例化
纯虚函数和抽象类举例:
#include <iostream>
using namespace std;
// 基类1:线(未定义 area 和 volume,属于抽象类)
class Line{
public:
Line(float len);
virtual float area() = 0;
virtual float volume() = 0;
protected:
float m_len;
};
Line::Line(float len): m_len(len){ }
// 中间基类2:矩形(未定义 volume,属于抽象类)
class Rec: public Line{
public:
Rec(float len, float width);
float area();
protected:
float m_width;
};
Rec::Rec(float len, float width): Line(len), m_width(width){ }
float Rec::area(){ return m_len * m_width; }
// 派生类:长方体(已定义 area 和 volume,可以实例化)
class Cuboid: public Rec{
public:
Cuboid(float len, float width, float height);
float area();
float volume();
protected:
float m_height;
};
Cuboid::Cuboid(float len, float width, float height): Rec(len, width), m_height(height){ }
float Cuboid::area(){ return 2 * ( m_len*m_width + m_len*m_height + m_width*m_height); }
float Cuboid::volume(){ return m_len * m_width * m_height; }
// 派生类:正方体
class Cube: public Cuboid{
public:
Cube(float len);
float area();
float volume();
};
Cube::Cube(float len): Cuboid(len, len, len){ }
float Cube::area(){ return 6 * m_len * m_len; }
float Cube::volume(){ return m_len * m_len * m_len; }
int main(){
Line *p = new Cuboid(10, 20, 30);
cout<<"The area of Cuboid is "<<p->area()<<endl;
cout<<"The volume of Cuboid is "<<p->volume()<<endl;
p = new Cube(15);
cout<<"The area of Cube is "<<p->area()<<endl;
cout<<"The volume of Cube is "<<p->volume()<<endl;
return 0;
}
运行结果:
The area of Cuboid is 2200
The volume of Cuboid is 6000
The area of Cube is 1350
The volume of Cube is 3375
本例中定义了四个类,它们的继承关系为:Line --> Rec --> Cuboid --> Cube
Line 是一个抽象类,也是最顶层的基类,在 Line 类中定义了两个纯虚函数 area() 和 volume()。Line 类表示“线”,没有面积和体积,但它提前定义了这两个纯虚函数。这样的用意很明显:Line 类不需要被实例化,但是它为派生类提供了约束条件,派生类必须要实现这两个函数,完成计算面积和体积的功能,否则就不能实例化
于是在 Rec 类中,实现了 area() 函数,也就是定义了纯虚函数的函数体。但这时 Rec 仍不能被实例化,因为它没有实现继承来的 volume() 函数,volume() 仍然是纯虚函数,所以 Rec 也仍然是抽象类
直到 Cuboid 类,才实现了 volume() 函数,才是一个完整的类,才可以被实例化
抽象基类还可以实现多态
抽象基类除了约束派生类的功能,还可以实现多态
注意第 51 行代码,指针 p 的类型是 Line,但是它却可以访问派生类中的 area() 和 volume() 函数,正是由于在 Line 类中将这两个函数定义为纯虚函数;如果不这样做,51 行后面的代码都是错误的。这或许才是C++提供纯虚函数的主要目的
关于纯虚函数的几点说明
- 一个纯虚函数就可以使类成为抽象基类,但是抽象基类中除了包含纯虚函数外,还可以包含其它的成员函数(虚函数或普通函数)和成员变量
- 只有类中的虚函数才能被声明为纯虚函数,普通成员函数和顶层函数均不能声明为纯虚函数
typeid 运算符用来获取一个表达式的类型信息。类型信息对于编程语言非常重要,它描述了数据的各种属性:
-
对于基本类型(int、float 等C++内置类型)的数据,类型信息所包含的内容比较简单,主要是指数据的类型。
-
对于类类型的数据(也就是对象),类型信息是指对象所属的类、所包含的成员、所在的继承关系等。
类型信息是创建数据的模板,数据占用多大内存、能进行什么样的操作、该如何操作等,这些都由它的类型信息决定。
typeid运算符
typeid 的操作对象既可以是表达式,也可以是数据类型,下面是它的两种使用方法:
typeid( dataType )
typeid( expression )
dataType 是数据类型,expression 是表达式,这和 sizeof 运算符非常类似,只不过 sizeof 有时候可以省略括号( )
,而 typeid 必须带上括号。
typeid 会把获取到的类型信息保存到一个 type_info 类型的对象里面,并返回该对象的常引用;当需要具体的类型信息时,可以通过成员函数来提取。typeid 的使用非常灵活,请看下面的例子(只能在 VC/VS 下运行):
#include <iostream>
#include <typeinfo>
using namespace std;
class Base{ };
struct STU{ };
int main(){
// 获取一个普通变量的类型信息
int n = 100;
const type_info &nInfo = typeid(n);
cout<<nInfo.name()<<" | "<<nInfo.raw_name()<<" | "<<nInfo.hash_code()<<endl;
// 获取一个字面量的类型信息
const type_info &dInfo = typeid(25.65);
cout<<dInfo.name()<<" | "<<dInfo.raw_name()<<" | "<<dInfo.hash_code()<<endl;
// 获取一个对象的类型信息
Base obj;
const type_info &objInfo = typeid(obj);
cout<<objInfo.name()<<" | "<<objInfo.raw_name()<<" | "<<objInfo.hash_code()<<endl;
// 获取一个类的类型信息
const type_info &baseInfo = typeid(Base);
cout<<baseInfo.name()<<" | "<<baseInfo.raw_name()<<" | "<<baseInfo.hash_code()<<endl;
// 获取一个结构体的类型信息
const type_info &stuInfo = typeid(struct STU);
cout<<stuInfo.name()<<" | "<<stuInfo.raw_name()<<" | "<<stuInfo.hash_code()<<endl;
// 获取一个普通类型的类型信息
const type_info &charInfo = typeid(char);
cout<<charInfo.name()<<" | "<<charInfo.raw_name()<<" | "<<charInfo.hash_code()<<endl;
// 获取一个表达式的类型信息
const type_info &expInfo = typeid(20 * 45 / 4.5);
cout<<expInfo.name()<<" | "<<expInfo.raw_name()<<" | "<<expInfo.hash_code()<<endl;
return 0;
}
运行结果:
int | .H | 529034928
double | .N | 667332678
class Base | .?AVBase@@ | 1035034353
class Base | .?AVBase@@ | 1035034353
struct STU | .?AUSTU@@ | 734635517
char | .D | 4140304029
double | .N | 667332678
从本例可以看出,typeid 的使用非常灵活,它的操作数可以是普通变量、对象、内置类型(int、float等)、自定义类型(结构体和类),还可以是一个表达式。
本例中还用到了 type_info 类的几个成员函数,下面是对它们的介绍:
-
name() 用来返回类型的名称
-
raw_name() 用来返回名字编码算法产生的新名称
-
hash_code() 用来返回当前类型对应的 hash 值。hash 值是一个可以用来标志当前类型的整数,有点类似学生的学号、公民的身份证号、银行卡号等。不过 hash 值有赖于编译器的实现,在不同的编译器下可能会有不同的整数,但它们都能唯一地标识某个类型
遗憾的是,C++ 标准只对 type_info 类做了很有限的规定,不仅成员函数少,功能弱,而且各个平台的实现不一致。例如上面代码中的 name() 函数,nInfo.name()
、objInfo.name()
在 VC/VS 下的输出结果分别是int
和class Base
,而在 GCC 下的输出结果分别是i
和4Base
。
C++ 标准规定,type_info 类至少要有如下所示的 4 个 public 属性的成员函数,其他的扩展函数编译器开发者可以自由发挥,不做限制。
1) 原型:const char* name() const;
返回一个能表示类型名称的字符串。但是C++标准并没有规定这个字符串是什么形式的,例如对于上面的objInfo.name()
语句,VC/VS 下返回“class Base”,但 GCC 下返回“4Base”。
2) 原型:bool before (const type_info& rhs) const;
判断一个类型是否位于另一个类型的前面,rhs 参数是一个 type_info 对象的引用。但是C++标准并没有规定类型的排列顺序,不同的编译器有不同的排列规则,程序员也可以自定义。要特别注意的是,这个排列顺序和继承顺序没有关系,基类并不一定位于派生类的前面。
3) 原型:bool operator== (const type_info& rhs) const;
重载运算符“==”,判断两个类型是否相同,rhs 参数是一个 type_info 对象的引用。
4) 原型:bool operator!= (const type_info& rhs) const;
重载运算符“!=”,判断两个类型是否不同,rhs 参数是一个 type_info 对象的引用。
raw_name() 是 VC/VS 独有的一个成员函数,hash_code() 在 VC/VS 和较新的 GCC 下有效。
可以发现,不像 Java、C# 等动态性较强的语言,C++ 能获取到的类型信息非常有限,也没有统一的标准,如同“鸡肋”一般,大部分情况下我们只是使用重载过的“==”运算符来判断两个类型是否相同。
判断类型是否相等
typeid 运算符经常被用来判断两个类型是否相等。
1) 内置类型的比较
例如有下面的定义:
char *str;
int a = 2;
int b = 10;
float f;
类型判断结果为:
类型比较 | 结果 | 类型比较 | 结果 |
---|---|---|---|
typeid(int) == typeid(int) | true | typeid(int) == typeid(char) | false |
typeid(char*) == typeid(char) | false | typeid(str) == typeid(char*) | true |
typeid(a) == typeid(int) | true | typeid(b) == typeid(int) | true |
typeid(a) == typeid(a) | true | typeid(a) == typeid(b) | true |
typeid(a) == typeid(f) | false | typeid(a/b) == typeid(int) | true |
typeid 返回 type_info 对象的引用,而表达式typeid(a) == typeid(b)
的结果为 true,可以说明,一个类型不管使用了多少次,编译器都只为它创建一个对象,所有 typeid 都返回这个对象的引用。
需要提醒的是,为了减小编译后文件的体积,编译器不会为所有的类型创建 type_info 对象,只会为使用了 typeid 运算符的类型创建。不过有一种特殊情况,就是带虚函数的类(包括继承来的),不管有没有使用 typeid 运算符,编译器都会为带虚函数的类创建 type_info 对象,我们将在《C++ RTTI机制精讲(C++运行时类型识别机制)》中展开讲解。
2) 类的比较
例如有下面的定义:
class Base{};
class Derived: public Base{};
Base obj1;
Base *p1;
Derived obj2;
Derived *p2 = new Derived;
p1 = p2;
类型判断结果为:
类型比较 | 结果 | 类型比较 | 结果 |
---|---|---|---|
typeid(obj1) == typeid(p1) | false | typeid(obj1) == typeid(*p1) | true |
typeid(&obj1) == typeid(p1) | true | typeid(obj1) == typeid(obj2) | false |
typeid(obj1) == typeid(Base) | true | typeid(*p1) == typeid(Base) | true |
typeid(p1) == typeid(Base*) | true | typeid(p1) == typeid(Derived*) | false |
表达式typeid(*p1) == typeid(Base)
和typeid(p1) == typeid(Base*)
的结果为 true 可以说明:即使将派生类指针 p2 赋值给基类指针 p1,p1 的类型仍然为 Base*。
type_info 类的声明
最后我们再来看一下 type_info 类的声明,以进一步了解它所包含的成员函数以及这些函数的访问权限。type_info 类位于typeinfo
头文件,声明形式类似于:
class type_info {
public:
virtual ~type_info();
int operator==(const type_info& rhs) const;
int operator!=(const type_info& rhs) const;
int before(const type_info& rhs) const;
const char* name() const;
const char* raw_name() const;
private:
void *_m_data;
char _m_d_name[1];
type_info(const type_info& rhs);
type_info& operator=(const type_info& rhs);
};
它的构造函数是 private 属性的,所以不能在代码中直接实例化,只能由编译器在内部实例化(借助友元)。而且还重载了“=”运算符,也是 private 属性的,所以也不能赋值。