覆盖父类方法


在〈继承共同行为〉中,Roleto_string被继承了,然而,你也许会想要SwordsManMagician各自的to_string,可以有类名称作为前置,这个需求可以借由在各自的类中定义to_string来达成。例如:

#include <iostream>
#include <string> 
using namespace std;

class Role {
    string name;   // 角色名称
    int level;     // 角色等级
    int blood;     // 角色血量

public:
    Role(string name, int level, int blood)
     : name(name), level(level), blood(blood) {}

    string to_string() {
        return "(" + 
            this->name + ", " + 
            std::to_string(this->level) + ", " + 
            std::to_string(this->blood) + 
        ")";
    };
};

class SwordsMan : public Role {
public:
    using Role::Role;

    void fight() {
        cout << "挥剑攻击" << endl;
    }

    string to_string() {
        return "SwordsMan" + Role::to_string();
    };
};

class Magician : public Role {
public:
    using Role::Role;

    void fight() {
        cout << "魔法攻击" << endl;
    }

    void cure() {
        cout << "魔法治疗" << endl;
    }

    string to_string() {
        return "Magician" + Role::to_string();
    };
};

int main() { 
    SwordsMan swordsMan("Justin", 1, 1000);
    Magician magician("Magician", 1, 800);

    swordsMan.fight();
    magician.fight();
    magician.cure();

    cout << swordsMan.to_string() << endl;
    cout << magician.to_string() << endl;

    return 0;
}

在范例中,to_string方法获取Role::to_string的调用结果,并加上各自的前置名称后返回,Role::to_string这样的调用,会隐含地传入目前的this,作为Role::to_string中的this

这一次虽然同样是swordsMan.to_string()magician.to_string()调用,然而使用了子类各自的定义,父类的to_string定义被覆盖(hide),因此执行结果会是:

挥剑攻击
魔法攻击
魔法治疗
SwordsMan(Justin, 1, 1000)
Magician(Magician, 1, 800)

在子类中若要调用父类构造函数或者是父类方法,在其他语言中,会有super之类的关键字可以用,然而 C++ 必须使用父类名称,在简单的情境中,写死父类名称或许不是什么问题,然而,在更复杂的情况,多个方法都得调用父类方法时,写死一大堆父类名称,可能就是个问题,如果父类名称在编写时又比较复杂,问题可能就更大。

一个缓解的方式是以using定义别名。例如:

class SwordsMan : public Role {
    using super = Role;

public:
    using Role::Role;

    void fight() {
        cout << "挥剑攻击" << endl;
    }

    string to_string() {
        return "SwordsMan" + super::to_string();
    };
};

这么一来,未来若真的要修改父类名称,可以只在一个地方修改。

实际上就以上的需求,你也可以在SwordsManMagician中定义一个desc来完成相同的任务,那么以相同名称遮敝父类方法的意义何在呢?

就这边来说,在还没遮敝同名方法方法前,swordsMan.to_string()magician.to_string()在编译时期,就绑定了调用的方法会是Role中定义的to_string方法,在遮敝同名方法之后,编译时绑定的版本,就是各自类中定义的to_string方法。

也就是就这边的范例来说,遮敝同名方法之目的,是要在编译时期,视实例的类型来绑定对应的方法版本。

如果遮敝了to_string,然后你这么调用呢?

SwordsMan swordsMan("Justin", 1, 1000);

Role &role = swordsMan;
cout << role.to_string() << endl; // 显示 (Justin, 1, 1000)

首先,因为继承会有 is-a 的关系,也就是SwordsMan是一种Role,当=左边类型是一种右边类型时,编译器允许隐含的类型转换,因此Role role = swordsMan可以通过编译。

接下来role.to_string()调用时,由于编译器在编译时期只知道role的类型是Role,虽然role实际上参考了swordsMan,然而编译时期能绑定的就是Roleto_string定义,因此执行的结果会是来自Roleto_string定义,而不是SwordsManto_string定义。

如果想在编译时期,不管实例实际上是哪种类型,一律视调用方法时的变量类型来决定调用的版本,这个行为就会是你要的,在这种情况下,若有个函数或方法,想要操作实例继承而来,或者是本身定义的方法,会透过模版来达成。例如:

template <typename T>
void printInfo(T &t) {
    cout << t.to_string() << endl;
}

因为是模版,实际上会依调用时指定的实例类型,重载出对应类型的版本,该调用哪个版本,是编译时期就决定的事,就代码本身而言,是以父类定义的行为来看待实例的操作,是一种多态(polymorphism)的实现,由于这种多态实现是编译时期就可以达成方法绑定,亦被称为编译时期多态(compile-time polymorphism)。

当然,printInfo模版可以适用任何具有to_string方法的实例,不用是Role或其子类的实例,因而可用来实现结构类型系统(structural type system)。

如果想在执行时期,看看实际上实例是何种类型,并采用各自类型的to_string定义,该方法必须设定为virtual,这之后再来说明了。


展开阅读全文