继承共同行为


子类继承父类,可用来避免重复的行为,不过并非为了避免重复定义行为就使用继承,滥用继承而导致程序维护上的问题时有所闻,如何正确判断使用继承的时机,以及继承之后如何活用多态,才是学习继承时的重点。

无论如何,先来看看行为重复是怎么一回事,假设你在正开发一款 RPG(Role-playing game)游戏,一开始设定的角色有剑士与魔法师。首先你定义了剑士类:

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

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

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

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

接着你为魔法师定义类:

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

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

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

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

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

你注意到什么呢?因为只要是游戏中的角色,都会具有角色名称、等级与血量,类中也都为名称、等级与血量定义了取值方法与设值方法,MagicianSwordsMan有许多代码重复了。

重复在程序设计上,就是不好的讯号。举个例子来说,如果要将namelevelblood改为其他名称,那就要修改SwordsManMagician两个类,如果有更多类具有重复的代码,那就要修改更多类,造成维护上的不便。

如果要改进,可以把相同的代码提升(Pull up)为父类:

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) + 
        ")";
    };
};

这个类在定义上没什么特别的新语法,只不过是将SwordsManMagician中重复的代码复制过来。接着SwordsMan可以如下继承Role

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

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

在定义SwordsMan类时,:指出会SwordsMan会扩充Role的行为,:右边的public表示,会以公开的方式继承Role,这表示继承而来的Role成员,权限控制最大是public,也就是Role继承而来的相关成员维持既有的权限控制。

在继承类时,还可以在:右边指定protectedprivate,表示继承而来的Role成员权限控制最大是protectedprivate,例如若:右边指定privateRoleprotectedpublic成员在子类中,权限就会被限缩为private

继承时设定的权限默认会套用至各个成员,然而,可以使用using指出哪些成员要维持父类中设定之权限。例如,若父类P中有publicpublicMemberprotectedprotectedMember

class D : private P {
public:
    using P::publicMember;    // 维持 public

protected:
    using P::protectedMember; // 维持 protected
};

如果继承时没有指定publicprotectedprivate,若子类定义时使用struct,那默认就是public继承,若子类定义时使用class,那默认就是private继承。

定义类时,protected成员,是表示只能被子类访问。

在方才的代码中,SwordsMan定义了构造函数,构造时指定的namelevelblood指定给Role的构造函数,SwordsMan也定义了自己的fight方法。

类似地,Magician可以如下继承Role类:

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

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

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

如何看出确实有继承了呢?以下简单的程序可以看出:

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

... 方才的 Role、SwordsMan、Magician 代码

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

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

    cout << "SwordsMan" << swordsMan.to_string() << endl;
    cout << "Magician" << magician.to_string() << endl;

    return 0;
}

虽然SwordsManMagician并没有定义to_string方法,但从Role继承了,所以可以直接使用,执行的结果如下:

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

继承的好处之一,就是若要将namelevelblood等值域改名为其他名称,那就只要修改Role就可以了,继承Role的子类无需修改。

SwordsManMagician中定义了构造函数,并调用了父类Role构造函数,实际上构造函数本体没写什么,在这种情况下,你可能会想直接继承Role定义的构造流程,这可以透过using指定父类名称来达到,例如:

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

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

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

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

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

这么一来,SwordsMan("Justin", 1, 1000)Magician("Magician", 1, 800)的构造流程,就直接走Role中相同签署的构造流程了,不过,就继承意义而言,这才是实质地继承了构造函数,不过这种方式,不能继承默认、复制与移动构造函数,若需要这些构造函数,子类必须自行定义。

在面向对象中,继承是个双面刃,想判断继承的运用是否正确,有许多角度可以探讨,最基本的,就是看看父子类是否为「是一种(is-a)」的关系,就上例来说,SwordsMan是一种RoleMagician是一种Role,符合最低限度的关系。

就这边的范例说,构造子类实例时,会先执行父类构造函数,接着是子类构造函数,而解构的时候相反,会先执行子类析构函数,接着才是父类析构函数。


展开阅读全文