C++ 多态(动态多态)

  本文结合黑马程序员、C语言中文网以及《C++ Primer》对多态进行了总结

多态的基本概念

多态是C++面向对象三大特性之一。


多态分为两类
静态多态: 函数重载 和 运算符重载属于静态多态,复用函数名
动态多态: 派生类和虚函数实现运行时多态


静态多态和动态多态区别:
静态多态的函数地址早绑定 - 编译阶段确定函数地址
动态多态的函数地址晚绑定 - 运行阶段确定函数地址

成员函数、继承与虚函数(virtual)

  在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;
}

C++ 多态(动态多态)_第1张图片

   在对象的开头位置有一个指针 vfptr,指向虚函数表,并且这个指针始终位于对象的开头位置。

  基类的虚函数在 vtable 中的索引(下标)是固定的,不会随着继承层次的增加而改变,派生类新增的虚函数放在 vtable 的最后。如果派生类有同名的虚函数遮蔽(覆盖)了基类的虚函数,那么将使用派生类的虚函数替换基类的虚函数,这样具有遮蔽关系的虚函数在 vtable 中只会出现一次。

  p -> display(),在编译器内部会发生类似下面的转换( *( *(p+0) + 0 ) )(p),( *(p+0) + 0 )也就是 display() 的地址,( *( *(p+0) + 0 ) )(p)也就是对 display() 的调用,这里的 p 就是传递的实参,它会赋值给 this 指针。

你可能感兴趣的:(C++,语法,c++,开发语言)