北大C++程序设计编程作业答案+解析·魔兽世界之二:装备

本篇文章我们着重讲解本系列课程final大作业的前置作业之二:魔兽世界之二:装备

1. 前置知识

对于本题所需要的前置知识,大家需要对前几章的知识点有一定的认识,不太明白的童鞋,可以参考前几章编程习题的讲解文章:

  • 运算符重载
  • 继承
  • 多态
  • 输入输出和模板
  • 标准模板库STL

2. 思路解析:数据结构

首先,推荐大家阅读上一篇习题的分析,循序渐进地构建这个final大作业的代码和思路。这题和前一题的区别主要是介绍了Warrior和Weapon的特点,但是为了给final大作业做铺垫,这里我把Weapon具体的特点也融合到本题之中:

sword的攻击力是使用者当前攻击力的20%(去尾取整)。

bomb的攻击力是使用者当前攻击力的40%(去尾取整),但是也会导致使用者受到攻击,对使用者的攻击力是对敌人取整后的攻击力的1/2(去尾取整)。Bomb一旦使用就没了。

arrow的攻击力是使用者当前攻击力的30%(去尾取整)。一个arrow用两次就没了。

所以如果只针对这题,没必要引入Weapon类,大家可以参考这个版本的代码:wow_weapons_submit_ver1.cpp。但为了更好的练习,文章后面都把Weapon的攻击力特点作为题目要求之一。总的来说,这题相比前一题来说,难度并不大,仅仅只是增加了Warrior持有Weapon的情况:

dragon 可以拥有一件武器。编号为n的dragon降生时即获得编号为 n%3 的武器。dragon还有“士气”这个属性,是个浮点数,其值为它降生后其司令部剩余生命元的数量除以造dragon所需的生命元数量。

ninja可以拥有两件武器。编号为n的ninja降生时即获得编号为 n%3 和 (n+1)%3的武器。

iceman有一件武器。编号为n的iceman降生时即获得编号为 n%3 的武器。

lion 有“忠诚度”这个属性,其值等于它降生后其司令部剩余生命元的数目。

wolf没特点。

请注意,在以后的题目里,武士的士气,生命值,忠诚度在其生存期间都可能发生变化,都有作用,武士手中的武器随着使用攻击力也会发生变化。

所以对比上题,我们首先需要新增Warrior类用来表示战士这个属性,其他具体的类,比如dragon,ninjia作为派生类继承Warrior类。另外就是Weapon类,同样作为所有武器的父类,其他武器类,比如sword都是其子类。到这里,我们分析了Warrior和Weapon类的继承关系,剩下的就是他们所需要存储的数据域。

对于Warrior类,它应该存储所有战士共有的成员属性:

  • 编号 - id
  • 类型 - t
  • 战斗力 - p
  • 生命值 - m
  • 初始武器库 - w_init

对于每个Warrior的派生类,这里就不再赘述,大家可以通过源码来看看他们各自有哪些成员变量。相似地,对于Weapon类和其派生类,大家同样可以源码来进行理解如何构建它们的数据结构。这里需要注意一点,我们不需要存储每个武器的伤害,因为武器的伤害是根据使用者的攻击力进行实时计算的。

3. 代码解析:数据结构

首先,对于Commander和WorldWarcraft类,整体的数据结构没有太大变化,这里就skip了,详细情况大家可以看看源码。对于Warrior类,大家可以看看这里成员变量的类型,这里需要注意武器库存储的是Weapon的指针,而不是Weapon实例,后面我们会讲解为什么会这样做:

※ 为了方便理解整体的代码思路,本篇讲解的代码尽量和final的保持一致,或指出两者的不同之处,大家可以比对两个版本的源码来观察代码构建过程的取舍思路。

// 源码中还有一个额外的成员变量 - W_lib,这个是final战斗中才会用到,大家可以skip掉
class Warrior {
protected:
    const size_t id; // ID
    const Warrior_enum t; // Type
    const size_t p; // power
    size_t m; // Life points.
    // https://www.geeksforgeeks.org/pointer-array-array-pointer/
    Weapon* (*W_init) = nullptr; // Initialized weapons.
	// 方法略
};

// 这里每个Warrior派生类的OUT_FORMAT是用来输出printf的字符串格式,比如dragon的:
// "It has a %s,and its m is %ld, p is %ld\n";
class Dragon: public Warrior {
    const static char* OUT_FORMAT;
    double morale;
	// 方法略
};

class Ninjia: public Warrior {
    const static char* OUT_FORMAT;
	// 方法略
};

class Iceman: public Warrior {
    const static char* OUT_FORMAT;
	// 方法略
};

class Lion: public Warrior {
    const static char* OUT_FORMAT;
    size_t loyalty;
	// 方法略
};

// 注意Wolf类没有任何成员变量,主要因为它没有初始武器,也没有特殊的数值单位
class Wolf: public Warrior {
	// 方法略
};

而对于Weapon类的数据结构相对简单,这里也不再赘述:

// 这里注意武器耐久度的类型和其表示的范围,和相应的耐久度数值
// 注意这一版的源码有damage成员变量的,但是根据后续的题目,这个变量是不需要的
class Weapon {
protected:
    const Weapon_enum t; // id
    int durability; // -1 no durability, 0 broken, >= 1 normal.
	// 方法略
};

class Sword : public Weapon {
	// 方法略
};

class Bomb : public Weapon  {
	// 方法略
};

class Arrow : public Weapon {
	// 方法略
};

3. 思路解析:算法/成员函数

我们首先来分析一下Weapon的成员函数,因为只需要实现一个功能:当Warrior使用某种武器时的伤害,根据之前题意的描述,我们只需要把Warrior类的攻击力当作参数传入伤害计算函数即可,就是需要注意计算误差,比如取攻击力的40%,不能写成 p * 0.4,需要用分数进行表示:p * 4 / 10.0,之后就是每种武器都有自己的伤害计算成员函数,所以Warrior类里面需要定义一个相关的虚函数,即下面的每个的派生类对其进行具体的实现。

分析完Weapon类需要的成员函数,接下来就是Warrior类所需要的成员函数。针对本题,Warrior类有两个特点:

  1. 每个Warrior生成时,会有初始武器库;
  2. 生成后,Warrior需要报告相关的属性和武器;

所以,在构造函数里面我们需要初始化武器库,然后也需要一个生成信息输出虚函数,之后每个派生类进行重写。

4. 代码解析:算法/成员函数

同样我们先来看看Weapon类的代码:

class Weapon {
	// 成员变量略
public:
    Weapon( Weapon_enum t_, size_t dur_ ) : t( t_ ), durability( dur_ ) {}

    virtual ~Weapon() {}

    // -------------------------------------------------------
    // getter and setter
    // -------------------------------------------------------

    virtual size_t getDamage( size_t p_ ) = 0;
};

除了一些基本的构造和析构函数,我们看到这里定义了一个计算伤害的虚函数:

virtual size_t getDamage( size_t p_ ) = 0;

所以我们需要在派生类里面实现这个方法,比如sword:

class Sword : public Weapon {
public:
    Sword() : Weapon( sword, -1 ) {}

    size_t getDamage( size_t p_ ) override;
};

// sword的攻击力是使用者当前攻击力的20%(去尾取整)。
size_t Sword::getDamage( size_t p_ ) {
    return std::floor( p_ * 2 / 10.0 );
}

分析完Weapon类的代码实现,接下来就是Warrior类:

class Warrior {
	// 成员变量略
    Weapon* get_weapon( Weapon_enum t_, size_t p_ );
public:
    virtual ~Warrior() {
        // 注意收回W_init分配的内存空间,防止内存溢出
        delete [] W_init;
    }

    virtual void print() = 0;
};

同样,这里有输出生成信息的虚函数:

virtual void print() = 0;

那么同样地,派生类必须实现这个方法,同时我们需要在构造函数里面初始化武器库,比如dragon:

class Dragon: public Warrior {
    const static char* OUT_FORMAT;
    double morale;

public:
    Dragon( size_t id_, size_t m_, size_t p_, size_t n_, size_t m_remained )
    : Warrior( id_, dragon, p_, m_ ), morale( ( double ) m_remained / m_ ) {
        // dragon 可以拥有一件武器。编号为n的dragon降生时即获得编号为 n%3 的武器。
        // 分配一个长度的Weapon指针数组
        W_init = new Weapon*[ 1 ]; 
        // 获取n%3的武器,这里的get_weapon()大家可以看成一个API,传入需要武器的类型,获取该武器的一个实例对象
        // 参数p_在final版源码是不需要的
        W_init[ 0 ] = get_weapon( static_cast<Weapon_enum>( n_ % 3 ), p_ ); 
    }

    // 生成输出函数
    void print() override;
};

const char* Dragon::OUT_FORMAT = "It has a %s,and it's morale is %.2f\n";
void Dragon::print()  {
    printf(
        OUT_FORMAT,
        WEAPON_NAMES[ W_init[ 0 ]->get_type() ],
        morale
    );
}

另外,Ninja使用bomb不会受到反噬这个机制,笔者选择在战斗逻辑里面实现,不过大家也可以放到Ninja类里面实现,就看大家怎么选择了。

5. 项目代码

※ 以下习题答案全部通过OJ,使用编译器为:G++(9.3(with c++17))。

※ 因为本题是课程final大作业的前置作业,所以最终代码会针对后面题目的要求进行修改。之所以这么做是让大家能从一个大项目的角度来看待整个作业,而不是仅仅是一个作业,所以我们会着重于模块化,可读性,可维护性,因此那种把整个代码放在一个文件的情况在这个作业是不会出现的。但是为了应对作业提交的要求(提交只能是一个文件),这里都会提供一个可供提交的版本,大家可以对比提交版本和最终版本的区别,看看我们究竟是从一个小项目一步步创建一个大型项目的,这个能力在后期大家项目中也是非常有用的能力之一。

※ 由于篇幅所限,这里只会给到Warrior和Weapon类、成员变量/函数的定义,具体实现请见github相关文件。

提交版本代码链接

此模块完整代码链接

Final大作业完整代码链接

// -------------------------------------------------------
// Weapon class
// -------------------------------------------------------

class Weapon {
protected:
    const Weapon_enum t; // Type id
    int durability; // -1 no durability, 0 broken, >= 1 normal.

public:
    Weapon( Weapon_enum t_, size_t dur_ ) : t( t_ ), durability( dur_ ) {}

    virtual ~Weapon() {}

    Weapon_enum getType() const;

    // -------------------------------------------------------
    // getter and setter
    // -------------------------------------------------------

    virtual size_t getDamage( size_t p_ ) = 0;
};

class Sword : public Weapon {
public:
    Sword() : Weapon( sword, -1 ) {}

    size_t getDamage( size_t p_ ) override;
};

class Bomb : public Weapon  {
public:
    Bomb() : Weapon( bomb, 1 ) {}

    size_t getDamage( size_t p_ ) override;
};

class Arrow : public Weapon {
public:
    Arrow() : Weapon( arrow, 2 ) {}

    size_t getDamage( size_t p_ ) override;
};

// -------------------------------------------------------
// Warrior class
// -------------------------------------------------------

// https://en.cppreference.com/w/cpp/language/abstract_class
class Warrior {
protected:
    const size_t id; // ID
    const Warrior_enum t; // Type
    const size_t p; // power
    size_t m; // Life points.
    // https://www.geeksforgeeks.org/pointer-array-array-pointer/
    Weapon* (*W_init) = nullptr; // Initialized weapons.

    Warrior( size_t id_, Warrior_enum t_, size_t p_, size_t m_ ) : id( id_ ), t( t_ ), p( p_ ), m( m_ ) {}

    Weapon* get_weapon( Weapon_enum t_, size_t p_ );

public:
    virtual ~Warrior() {
        delete [] W_init;
    }

    virtual void print() = 0;
};

class Dragon: public Warrior {
    const static char* OUT_FORMAT;
    double morale;

public:
    Dragon( size_t id_, size_t m_, size_t p_, size_t n_, size_t m_remained )
    : Warrior( id_, dragon, p_, m_ ), morale( ( double ) m_remained / m_ ) {
        W_init = new Weapon*[ 1 ];
        W_init[ 0 ] = get_weapon( static_cast<Weapon_enum>( n_ % 3 ), p_ );
        W_lib.push( W_init[ 0 ] );
    }

    void print() override;
};

class Ninjia: public Warrior {
    const static char* OUT_FORMAT;

public:
    Ninjia( size_t id_, size_t m_, size_t p_, size_t n_ )
    : Warrior( id_, ninja, p_, m_ ) {
        W_init = new Weapon*[ 2 ];
        W_init[ 0 ] = get_weapon( static_cast<Weapon_enum>( n_ % WEAPON_NUM ), p_ );
        W_init[ 1 ] = get_weapon( static_cast<Weapon_enum>( ( n_ + 1 ) % WEAPON_NUM ), p_ );
        W_lib.push( W_init[ 0 ] );
        W_lib.push( W_init[ 1 ] );
    }

    void print() override;
};

class Iceman: public Warrior {
    const static char* OUT_FORMAT;

public:
    Iceman( size_t id_, size_t m_, size_t p_, size_t n_ )
    : Warrior( id_, iceman, p_, m_ ) {
        W_init = new Weapon*[ 1 ];
        W_init[ 0 ] = get_weapon( static_cast<Weapon_enum>( n_ % WEAPON_NUM ), p_ );
        W_lib.push( W_init[ 0 ] );
    }

    void print() override;
};

class Lion: public Warrior {
    const static char* OUT_FORMAT;
    size_t loyalty;

public:
    Lion( size_t id_, size_t m_, size_t p_, size_t m_remained )
    : Warrior( id_, lion, p_, m_ ), loyalty( m_remained ) {}

    void print() override;
};

class Wolf: public Warrior {

public:
    Wolf( size_t id_, size_t m_, size_t p_ ) : Warrior( id_, wolf, p_, m_ ) {}

    void print() override;
};

上一章:标准模板库STL(二)
下一章:魔兽世界之三:开战

6. 参考资料

  1. C++程序设计
  2. pixiv illustration: メイド服を着せられるお嬢様

7. 免责声明

※ 本文之中如有错误和不准确的地方,欢迎大家指正哒~
※ 此项目仅用于学习交流,请不要用于任何形式的商用用途,谢谢呢;


在这里插入图片描述

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值