本文结合黑马程序员、C语言中文网以及《C++ Primer》对多态进行了总结
多态是C++面向对象三大特性之一。
多态分为两类
静态多态: 函数重载 和 运算符重载属于静态多态,复用函数名
动态多态: 派生类和虚函数实现运行时多态
静态多态和动态多态区别:
静态多态的函数地址早绑定 - 编译阶段确定函数地址
动态多态的函数地址晚绑定 - 运行阶段确定函数地址
在C++语言中,基类必须将它的两种成员函数区分开来:一种是基类希望其派生类进行覆盖的函数;另一种是基类希望派生类直接继承而不要改变的函数。对于前者,基类通常将其定义为虚函数(virtual)。当我们使用指针或者引用调用虚函数时,该调用将被动态绑定。根据引用或指针所绑定的对象类型不同,该调用可能执行基类的版本,也可能执行某个派生类的版本。
借用C语言中文网的一个例子,要看懂这个例子,我们首先应该明白一个前提:基类指针可以指向一个派生类对象,但派生类指针不能指向基类对象。基类的指针指向派生类的对象,指向的是派生类中基类的部分。所以只能操作派生类中从基类中继承过来的数据和基类自身的数据。
//基类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< display();
p = new Teacher("赵宏佳", 45, 8200);
p -> display();
return 0;
}
运行结果:
王志刚今年23岁了,是个无业游民。
赵宏佳今年45岁了,是个无业游民。
导致错误输出的原因是,调用函数 display() 被编译器设置为基类中的版本,这就是所谓的静态多态,或静态链接 ——函数调用在程序执行前就准备好了。有时候这也被称为早绑定,因为 display() 函数在程序编译期间就已经设置好了。
当在display()前加上virtual关键字后,就可以避免这个错误。因为此时,编译器看的是指针的内容,而不是它的类型。
1.只需要在虚函数的声明处加上virtual关键字,函数定义处可以加也可以不加
2.virtual只能出现在类内部的声明语句之前而不能用于类外部的函数定义。如果基类把一个函数声明成虚函数,则该函数在派生类中隐式地也是虚函数。
3.派生类经常(但不总是)覆盖它继承的虚函数。如果派生类没有覆盖其基类中的某个虚函数,则该虚函数的行为类似于其他的普通成员,派生类会直接继承其在基类中的版本
4.构造函数不能是虚函数(
从vptr角度解释
虚函数的调用是通过虚函数表来查找的,而虚函数表由类的实例化对象的vptr指针(vptr可以参考C++的虚函数表指针vptr)指向,该指针存放在对象的内部空间中,需要调用构造函数完成初始化。如果构造函数是虚函数,那么调用构造函数就需要去找vptr,但此时vptr还没有初始化!
)
//基类
class Base{
public:
Base();
~Base();
protected:
char *str;
};
Base::Base(){
str = new char[100];
cout<<"Base constructor"<
运行结果:
Base constructor
Derived constructor
Base destructor
-------------------
Base constructor
Derived constructor
Derived destructor
Base destructor
发现在delete指向派生类的父类指针时没有释放派生类对象占用的内存,造成了内存泄漏。因为这里的析构函数是非虚函数,通过指针访问非虚函数时,编译器会根据指针的类型来确定要调用的函数,pb是基类的指针,所以不管它指向基类的对象还是派生类的对象,始终都是调用基类的析构函数。
更改代码
class Base{
public:
Base();
virtual ~Base();
protected:
char *str;
};
运行结果:
Base constructor
Derived constructor
Derived destructor
Base destructor
-------------------
Base constructor
Derived constructor
Derived destructor
Base destructor
将基类的析构函数声明为虚函数后,派生类的析构函数也会自动成为虚函数。编译器根据指针的指向来选择函数。在实际开发中,应该将基类的析构函数声明为虚函数。
纯虚函数声明语法: virtual 返回值类型 函数名 (函数参数) = 0;
纯虚函数没有函数体,只有函数声明。包含纯虚函数的类称为抽象类,它无法实例化,也无法创建对象。抽象类通常是作为基类,让派生类去实现纯虚函数。派生类必须实现纯虚函数才能被实例化。
#include
using namespace std;
//线
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){ }
//矩形
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; }
//长方体
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 "<area()<volume()<area()<volume()<
继承关系为:Line --> Rec --> Cuboid --> Cube,一直到Cuboid实现了所有的纯虚函数,才是一个完整的类,才可以被实例化。
还有一个细节:指针 p 的类型是 Line,但是它却可以访问派生类中的 area() 和 volume() 函数,正是由于在 Line 类中将这两个函数定义为纯虚函数。
在刚刚学习纯虚函数的时候我有一个疑问:既然在基类中无法实现纯虚函数,那为何要在基类中声明它呢?通过查阅网上的资料,我做了一个总结。
在很多情况下,基类本身生成对象是不合情理的。例如,动物作为一个基类可以派生出老虎、孔雀等子类,但动物本身生成对象明显不合常理。
纯虚函数的意义,让所有的类对象(主要是派生类对象)都可以执行纯虚函数的动作,但类无法为纯虚函数提供一个合理的默认实现。所以类纯虚函数的声明就是在告诉子类的设计者,"你必须提供一个纯虚函数的实现,但我不知道你会怎样实现它"。个人感觉纯虚函数还可以防止不应被实例化的基类被示例化为对象,可以提高安全性。
前面介绍了虚函数,而编译器之所以可以找到虚函数,是因为在创建对象时额外地增加了虚函数表。如果一个类包含了虚函数,那么在创建该类的对象时就会额外地增加一个数组,数组中的每一个元素都是虚函数的入口地址。
//People类
class People{
public:
People(string name, int age);
public:
virtual void display();
virtual void eating();
protected:
string m_name;
int m_age;
};
People::People(string name, int age): m_name(name), m_age(age){ }
void People::display(){
cout<<"Class People:"< display();
p = new Student("王刚", 16, 84.5);
p -> display();
p = new Senior("李智", 22, 92.0, true);
p -> display();
return 0;
}
在对象的开头位置有一个指针 vfptr,指向虚函数表,并且这个指针始终位于对象的开头位置。
基类的虚函数在 vtable 中的索引(下标)是固定的,不会随着继承层次的增加而改变,派生类新增的虚函数放在 vtable 的最后。如果派生类有同名的虚函数遮蔽(覆盖)了基类的虚函数,那么将使用派生类的虚函数替换基类的虚函数,这样具有遮蔽关系的虚函数在 vtable 中只会出现一次。
p -> display(),在编译器内部会发生类似下面的转换( *( *(p+0) + 0 ) )(p),( *(p+0) + 0 )
也就是 display() 的地址,( *( *(p+0) + 0 ) )(p)
也就是对 display() 的调用,这里的 p 就是传递的实参,它会赋值给 this 指针。