🐥
C++ 練習問題
Q3:以下のようなソースコードを作成せよ
親クラスの名前をPlayerとする
- 垂直ジャンプと表示されるpublicな関数を作成する。関数名はjumpとする
Playerを継承したSuperPlayerクラスを作成する
- 弾道ジャンプと表示されるpublicな関数を作成する。関数名はjumpとする
main()内に作成したクラスを用いて以下のコードを書く
- Player型のポインタ変数を2つ作成する
- 1つ目には動的に生成した親クラスのオブジェクトを格納する
- 2つ目には動的に生成した子クラスのオブジェクトを格納する
- それぞれのオブジェクトからjump()を呼び出し、コンソールコマンドに以下の表示をさせる
class Player
{
public:
virtual void Jump()
{
std::cout << "垂直ジャンプ\n";
}
virtual ~Player()
{
}
};
class SuperPlayer : public Player
{
public:
virtual void Jump() override
{
std::cout << "弾道ジャンプ\n";
}
virtual ~SuperPlayer() override
{
}
};
int main()
{
Player* p1 = new Player();
Player* p2 = new SuperPlayer();
p1->Jump();
p2->Jump();
delete p1;
delete p2;
}
Q4:以下のようなソースコードを作成せよ
親クラスの名前をObjectとする、このクラスは抽象クラスとする
- メンバ関数Motion()を作成する。この関数は純粋仮想関数とする
このクラスを継承したCharacter,Bullet,Coinクラスを作成して各々のMotion()関数をオーバーライドして以下の処理を記述する
- CharacterクラスのMotion()には「移動と回転ができる」という文字列を表示させる処理を記述
- BulletクラスのMotion()には「直線移動ができる」という文字列を表示させる処理を記述
- CoinクラスのMotion()には「その場で回転し続ける」いう文字列を表示させる処理を記述
main()内に作成したクラスを用いて以下のコードを書く
- Object型のポインタ変数を3つ作成してそれぞれには動的に作成したオブジェクト(Character,Bullet,Coin)を格納する
- それぞれのオブジェクトからMotion()関数を呼び出し、コンソールコマンドに以下の表示をさせる
class Object
{
public:
virtual void Motion() = 0;
virtual ~Object() = 0; // 純粋仮想デストラクタは宣言とは別に定義も書くこと
};
class Character : public Object
{
public:
virtual void Motion() override
{
std::cout << "移動と回転ができる\n";
}
virtual ~Character() override
{
}
};
class Bullet : public Object
{
public:
virtual void Motion() override
{
std::cout << "直線移動ができる\n";
}
virtual ~Bullet() override
{
}
};
class Coin : public Object
{
public:
virtual void Motion() override
{
std::cout << "その場で回転し続ける\n";
}
virtual ~Coin() override
{
}
};
int main()
{
Object* character = new Character();
Object* bullet = new Bullet();
Object* coin = new Coin();
character->Motion();
bullet->Motion();
coin->Motion();
delete character;
delete bullet;
delete coin;
}
Discussion
ここで示された例題にはよくある誤りが含まれています。
SuperPlayer
のオブジェクトをPlayer
としてアクセスしてもよいですが、SuperPlayer
のオブジェクトをPlayer
としてdelete
しようとした結果は言語仕様としては未定義となっており、実際の挙動としてはSuperPlayer
のオブジェクトが正しく後始末されないという形になることが多いです。 (もうひとつの例題のObject
も同様です。)これはやりがちな誤りであるにもかかわらずコンパイル時にも実行時にも検出することが困難 (検出されてエラーになったりはしない) で、一見して正しく動いているように見えて黙って急にトラブルを引き起こすので気づき難い問題です。
対処方法としては基底クラスのデストラクタを仮想関数として定義するべきです。
気づき難い誤りである一方では、あまりに典型的な誤りなので大抵の入門書では説明されていると思います。 手を動かして学ぶのは大事ですが、入門書程度は読んでから取り組むことをお勧めします。
C++ の言語仕様には数多くの「未定義」が含まれているので動作して確かめたことがいつもそうなるとは限らず、ちゃんとした解説を読まずに正しく使うのが難しいです。 念のために申し上げておきますが、手を動かして学ぶということを否定するわけではありません。 その前に基本的な解説くらいは読んでおいて欲しいというくらいのことです。
余談ですが C++ 用語としては親クラスやスーパークラスといった表現はあまり好ましくありません。 すでに知られていたそれらの用語が誤解を生じやすいという理由で C++ では意図的に新しく基底クラス (base class) という用語を創作して採用した経緯があります。
用語を言語仕様に合わせていない解説は言語仕様 (または言語仕様に基づいた解説) を参照せずに書かれた可能性が高く、その信憑性には疑問が生じます。
ご指摘ありがとうございます。指摘して頂いた部分はすぐに修正しました。
すごく基本的なミスをしてしまったので恥ずかしいです。
継承の用語についてですが基底クラスが正確な用語だとは初めて知りました。
Webサイトでは基底クラス(親クラス、スーパークラス)のように説明されていたので・・・
用語を併記してあったのなら親切な解説ですね。
オブジェクトの構造としては基底クラス (のオブジェクト) を派生クラス (のオブジェクト) が内包するような形になります。 そのような視点で見た時に内側のほうを「親」とは捉えにくいので誤解の元になるのです。 実際、包含関係 (メンバとそのメンバを持つクラス) の場合はクラスのほうを「親」と称する事例が多いでしょう。
質問サイトなどではクラススコープ内にクラス定義があるようなクラスのことを親クラスと称している事例もよく見られますし、初心者の使う「親クラス」が何を意味しているのかというのは文脈で察しないといけないのでしんどいです。 (具体的なコードが書いてあるときならわかるので困らないのですが。)
「スーパー」という用語についても「上位の」というニュアンスがありますが、一般的に派生クラスのほうが多機能なので機能の少ないほうをスーパーと呼ばなければならないのが混乱の元になるという判断があったようです。
確かに派生クラスが基底クラスを包み込みとイメージした場合、基底側を親とは認識し辛いですね、勉強になります。
参考までにC++を学んだ時に使用した本を教えて下さい。現在私は「優しいC++」、「独習C++」を使用しています。
また今後C++を勉強するうえで注意すべき部分は何でしょうか?
私は古い世代です。 古い入門書で基本を理解した後は仕様改定について調べたり、必要に応じて仕様書を読んだりして身に付けたので現代的な入門書でどれが好ましいかといった知識を持っていません。 私が持っている書籍の内で最も新しいものでも C++03 相当のものです。
今となっては役に立たないということはお断りしておきますが具体的には「注解 C++リファレンスマニュアル」や「詳説C++ 第2版」といったもので学びました。
言語仕様の解説というわけではありませんが C++ の設計者自身が C++ の歴史や思想について著した「C++の設計と進化」は現在の仕様に繋がる背景がわかるので (理解というよりも) 納得するのに役立ちました。
通して読んで学ぶようなものではありませんがリファレンスマニュアルとしてとても有用なものとして cppreference.com があります。 言語仕様が日本語で整理されていて規格改定でどのように変化したのかもわかりやすく記載されています。 疑問が生じたらまずはこれを参考にするとよいでしょう。
C++ を学ぶ上で注意すべきと私が考えるのは先述したように「未定義」についてです。 C++ は未定義を踏みやすい (そしてそれがその場では気づけない) という事情があります。 実験で確かめようとしてはならない (確かめてもあてにならない) ということは意識する必要があると思います。
「未定義」の部分は怖いところもありますが引き続き頑張っていきます。ありがとうございました。