【C++】継承とは?わかりやすく解説!

  • 継承ってどんな意味?
  • オーバーライドやprotectedとは?

このような疑問をプログラミングが初めての方にもわかるように、わかりやすく解説しますオブジェクト指向の3つの特徴の1つのカプセル化というのはどういうものなのでしょうか。見ていきましょう!

無料体験ができるプログラミングスクールの詳細記事

継承inheritance)とは?

継承とは、あるオブジェクト(クラス)の機能や情報を引継ぎ、新たなオブジェクト(クラス)を作成することです。 オリジナルの機能を修正したり追加したりもでき、オリジナルを保護する機能も含みます。

ここでのオブジェクトオリジナルの意味合いは、

  • オブジェクト : オブジェクト指向のオブジェクトの概念。要はクラス。
  • オリジナル : 継承の時に参照される元(親)データ。または「元」という意味。

簡単に言うと、一回使ったオブジェクト(クラス)の要素と操作を引き継げるよってことです。

例えば、Aクラスを引き継いで(継承して)Bクラスを作る。そのBクラスを引き継いで(継承して)、それに何個か変数を加えてCクラスを作る。あるいは、Dクラスの定義を少しだけ変えて(オーバーライドして)Eクラスを作る。このようなことができるようになるのが継承という機能です。

継承の仕組み

継承は、C++だとこのような構文によって定義されます。

class 新クラス名 : public 元クラス名 { ... };

元のクラスを引き継いで新しいクラスを作りますって構文ですね。例えばこんなクラスAがあったとします。

class A {
	int x;
	int y;
};

クラスA

整数型 x 整数型 y

そして、クラスAを引き継いでクラスBを作るときはこうします。

class A {
	int x;
	int y;
};

//class 新クラス名 : public 元クラス名 
class B : public A {
	int z;
};

今回はint(整数)型のzを付け足しましたが付け足さなくても大丈夫です。

クラスB

整数型 x 整数型 y

整数型 z

点線内がクラスAのメンバになっています。

車の設計図を使ったわかりやすい例

車に例えて説明します。ある車Aを作るのに設計図を作ったとします。

車Aの設計図

エンジン xxx ,タイヤ xxx , ほかの機能…

これにより車Aは作ることができるようになりました。

次に、車Aのエンジンを改良したスーパーカーBを作ることが決まりました。

その時に、スーパーカーBは車Aを改良して作るため、車Aをベースに作ることで効率的に開発を進めることができます。

そこで車Aの設計図を引き継ぎます。これが継承です。

スーパーカーBの設計図

車Aの設計図(複製)

エンジン xxx ,タイヤ xxx , ほかの機能…

スーパーカーBは車Aのエンジンを改良したいので、ほかの機能や要素はそのままで、エンジンだけ取り替えます。これは、オーバーライドと呼ばれます。

スーパーカーBの設計図

エンジン Super_Engine

車Aの設計図(複製)

エンジン xxx  ,タイヤ xxx , ほかの機能…

オーバーライドは上書きのようなもので、今回はエンジンを上書きしました。しかし、スーパーカーBを作るために車Aの設計図を上書きしてしまっては、車Aが作れなくなってしまうのでよろしくないですよね。

オーバーライドは、オリジナル(親の)クラスは上書きせず、子のクラスでのみ作用します。ゆえに、今回はスーパーカーBの設計図にのみ作用し、車Aの設計図には何ら影響はありません。なので、スーパーカーBの設計図内の車Aの設計図には(複製)とつけてあり、オリジナルには影響しないという意味を込めています。

これで、性能のいいスーパーカーBの設計図を効率的に作ることができました。

オーバーライド(override)とは?

オーバーライドとは簡単に言えば上書きです。

こちらの記事で詳しく解説しています。

https://www.jpazamu.com/override/

継承を使う際に必要な知識-protected-

オブジェクト指向にはカプセル化というものがありますね。これは、オブジェクトの情報を保護する機能なのですが、実はそのカプセル化のせいで継承先のクラスからデータを呼び出せない状況が生まれたりするんです。例えばこのようなとき、

#include <iostream>
using namespace std;

class A {

	int a;

public:

//aに値を代入するメンバ関数
	void set_a(int _a) { a = _a; }

//aの値を取得するメンバ関数
	int get_a() { return a; }

};

class B : public A {

public:

//aの値を表示するメンバ関数
	void show_a() { cout << a << endl; }//×privateなメンバaにアクセスできない

};

int main() {

//クラスBのbを作成
	B b;

//bのaに10を代入 ×コンパイルエラー
	b.a = 10; //カプセル化によりprivateメンバのアクセス不可

//bのaに10を代入
	b.set_a(10);

//bのaの値を表示
	cout << b.get_a() << endl;

//bのaの値を表示 ×コンパイルエラー
	b.show_a(); //メンバ関数定義が間違っているため

}

main文から見ていきましょう。

//クラスBのbを作成
	B b;

ここは皆さん大丈夫でしょうか。クラスB型のインスタンスbを作成しています。

//bのaに10を代入 ×コンパイルエラー
	b.a = 10; //カプセル化によりprivateメンバのアクセス不可

これはそもそもprivateなメンバ(b.a)にアクセスしようとしているのでカプセル化で弾かれますね。

//bのaに10を代入
	b.set_a(10);

b.set_a(10)はb.a=10と意味的には同じですが、publicなメンバ関数を経由してaの値に代入しているのでOKです。

//bのaの値を表示
	cout << b.get_a() << endl;

これも同じ理由でOKです。

//bのaの値を表示 ×コンパイルエラー
	b.show_a(); //メンバ関数定義が間違っているため

最後にb.show_a()これはなぜいけないのでしょうか。publicなメンバ関数(show_a)にアクセスしているのでOKだと思われるかもしれません。main文でのこの書き方は間違ってはいません。しかし、クラスBのメンバ関数show_a()の定義自体が間違っているためにコンパイルに通りません。

//aの値を表示するメンバ関数
	void show_a() { cout << a << endl; }//×privateなメンバaにアクセスできない

ダメな理由は、「子クラスから親クラスのprivateメンバへアクセスすることはできないから」です。クラスBからクラスAのprivateメンバint型 aにアクセスしようとしていますね。これはできません。子クラスから見ると親クラスのprivateメンバはカプセル化によって保護されてしまいます。

じゃあpublicにするか?という話ですが。否です。これだとカプセル化の意味がなくなります。全部publicにしてしまったら何の意味もありません。

そこで登場するのがこの「protected」です。

class A {

//protectedで保護!継承先からアクセスできるように!
protected:

	int a;
 ...
};

これは、継承先(子クラス)からならアクセスできるようにするよって意味です。もちろん元のクラス自身(ここでいうとクラスA)はprivateやpublic同様にアクセスできます。しかしmain文やほかの関係ないクラスからはカプセル化により保護されアクセスができなくなります。privateとpublicを継承の観点からいいとこどりをしたようなものです。

では早速この「protected」を使用して先ほどのソースコードを正しいものにしてみましょう。

#include <iostream>
using namespace std;

class A {

//protectedで保護!継承先からアクセスできるように!
protected:

	int a;

public:

//aに値を代入するメンバ関数
	void set_a(int _a) { a = _a; }

//aの値を取得するメンバ関数
	int get_a() { return a; }

};

class B : public A {

public:

//aの値を表示するメンバ関数
	void show_a() { cout << a << endl; }//OK

};

int main() {

//クラスBのbを作成
	B b;

//bのaに10を代入 ×コンパイルエラー
	b.a = 10; //カプセル化によりprivateメンバのアクセス不可

//bのaに10を代入
	b.set_a(10);

//bのaの値を表示
	cout << b.get_a() << endl;

//bのaの値を表示
	b.show_a(); //OK

}

引き続きb.a=10はコンパイルエラーになってくれています。このように、継承先からなら親クラスのメンバにアクセスできるよってのが「protected」という機能になります

ちなみに実行結果はこうなります。

10
10

継承を使うメリットについて解説

継承の仕組みが分かったところで、継承を使うことのメリットについて見ていきましょう。

継承のメリットは同じ記述を再び書かなくてよいという点

下でも紹介しますが、RPG風なこのようなクラスがあったとします。

class human {
	string name;
	int Level;
	int HP;
	int MAX_HP;
	int ATK;
	int DEF;
public:
	human(string _name, int _Level);
	void print_status();
};

上から名前、レベル、体力、体力の上限値、攻撃力、防御力、デフォルトコンストラクタ、コンストラクタ、ステータス表示関数ですね。RPGによくあるやつです。定義をちょっと省略していますが気にしないでください。

もう一個魔法使いのクラスwizardを作ります。

class wizard :public human {
	string name;
	int Level;
	int HP;
	int MAX_HP;
	int ATK;
	int DEF;
	int MP;
	int MAX_MP;
public:
	wizard() {};
	wizard(string _name, int _Level);
	void print_status();
};

なんか似ていません?同じ人間同士なんだし継承で表せるのでは?と考えるべきです。魔法使いが人間なのかという点は疑問ですが。

ではHumanの定義にprotectedを追加して継承してみましょう。その際マジックポイントMPが追加されていることを忘れずに。

class human {
protected:
	string name;
	int Level;
	int HP;
	int MAX_HP;
	int ATK;
	int DEF;
public:
	human(string _name, int _Level);
	void print_status();
};
//コンストラクタ定義は省略
//ステータス表示関数定義も省略

class wizard :public human {
	int MP;
	int MAX_MP;
public:
	wizard(string _name, int _Level);
	void print_status();//オーバーライド<br>
};
//コンストラクタ定義は省略
//ステータス表示関数定義も省略<br>

wizardクラスの記述量が大幅に減りましたね。これが大きなメリットです。

RPG風のプログラムを継承で作る!

最初の3行は標準入出力や文字列型の使用、名前空間の定義などで、おまじないと思ってくれて構いません。一気にソースコード書かれても混乱すると思いますので、下に1対1で解説を書いています。わからないところがあれば照らし合わせてみてください。

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

class human {
protected:
	string name;
	int Level;
	int HP;
	int MAX_HP;
	int ATK;
	int DEF;
public:
	human(string _name, int _Level);
	void print_status();
};

human::human(string _name, int _Level)
{
	Level = _Level;
	name = _name;
	MAX_HP = 20 + 4 * Level;
	HP = MAX_HP;
	ATK = 10 + 2 * Level;
	DEF = 4 + 2 * Level;
}

void human::print_status()
{
	cout << name << "  Lv" << Level << "\n";
	cout << "HP " << HP << "/" << MAX_HP << "\n";
	cout << "Attack " << ATK << "  Defense " << DEF << "\n\n";
}

class wizard :public human {
	int MP;
	int MAX_MP;
public:
	wizard(string _name, int _Level);
	void print_status();
};

wizard::wizard(string _name, int _Level) :human(_name, _Level)
{
	MAX_MP = 30 + 5 * Level;
	MP = MAX_MP;
}

void wizard::print_status()
{
	cout << name << "  Lv" << Level << "\n";
	cout << "HP " << HP << "/" << MAX_HP << "  MP " << MP << "/" << MAX_MP << "\n";
	cout << "Attack " << ATK << "  Defense " << DEF << "\n\n";
}


int main() {
	human Daniel("ダニエル", 80);
	Daniel.print_status();
	wizard Nao("なおまる", 100);
	Nao.print_status();
}

上から解説していきます。

humanクラス定義

まずはhuman(人間)の性質を定義します。

class human {
protected:
	string name;
	int Level;
	int HP;
	int MAX_HP;
	int ATK;
	int DEF;
public:
	human(string _name,int _Level);
	void print_status();
};

要素(メンバ変数)

name 名前
Level レベル
HP Hit point=体力
MAX_HP HPの最大値
ATK Attack=攻撃力
DEF Defense=防御力

初期設定(コンストラクタ)

human(string _name,int _Level) 仮引数に、string(文字列)型の「_name」とint(整数)型の_Levelを取り、名前を格納し、レベル(_Level)に応じたステータスを初期設定するプログラムが下で組まれている。
human::human(string _name,int _Level) 
{
	Level = _Level;
	name = _name;
	MAX_HP = 20 + 4 * Level; //Lv0で20→Lv1で24...
	HP = MAX_HP;
	ATK = 10 + 2 * Level;
	DEF = 4 + 2 * Level;
}

操作(メンバ関数)

void print_status() void(戻り値なし)型のキャラクターのステータスを表示する関数。
void human::print_status()
{
	cout << name << "  Lv" << Level << "\n"; //名前 LV??
	cout << "HP " << HP << "/" << MAX_HP << "\n"; //HP ??/???
	cout << "Attack " << ATK << "  Defense " << DEF << "\n\n"; //Attack ??  Def..
}

humanのクラスを継承したwizardクラスを定義

humanの要素をすべて受け継いでいます。

class wizard :public human {
	int MP;
	int MAX_MP;
public:
	wizard(string _name, int _Level);
	void print_status();
};

追加した要素(メンバ変数)

MP Magic point=魔力
MAX_MP MPの最大値

初期設定(コンストラクタ)

wizard(string _name, int _Level) humanの仮引数2個のコンストラクタを引き継ぎ(引き継ぐというよりは先にhumanのコンストラクタを起動させてから)新しくMPとMAX_MPも算出して代入。
wizard::wizard(string _name,int _Level):human(_name,_Level)
{
	MAX_MP = 30 + 5 * Level;
	MP = MAX_MP;
}

 オーバーライドしたメンバ関数

void print_status() wizardクラスに対応(MPなどに対応)したステータス表示の関数。
void wizard::print_status()
{
	cout << name << "  Lv" << Level << "\n";
	cout << "HP " << HP << "/" << MAX_HP << "  MP "<<MP<<"/"<<MAX_MP<<"\n";
	cout << "Attack " << ATK << "  Defense " << DEF << "\n\n";
}

メイン文

human Daniel(“ダニエル”,80) humanクラスのインスタンス「Daniel」を作成。名前ダニエル,Lv80
wizard Nao(“なおまる”,100) wizardクラスのインスタンス「Nao」を作成。名前なおまる,Lv100
int main() {
	human Daniel("ダニエル", 80);
	Daniel.print_status();
	wizard Nao("なおまる", 100);
	Nao.print_status();
}

実行結果

こんな感じに出ますね。

ここで重要なのは、humanクラスの要素を引き継いだwizardクラスが承によって組まれていることです。また、Nao(wizardクラス)には、MPが追加されていることも重要です。

つまり、

「class wizard :public human」

クラスhumanを引継ぎ、新たなクラスwizardを作成

これがこの記事の継承というものに相当します。

なお、これはC++で書かれたソースコードなのでほかの言語とは異なる部分が多少ありますが、概念が伝われば問題ないです。

最後に

オブジェクト指向の要素の一つである「継承」ですが、ただ単に引継ぎの機能ということがお分かりいただけたでしょうか。

これがすごく使い勝手がいいもので、なければならない機能なんですよね。

最初はオブジェクト指向プログラミング言語には、こんな機能があるんだな程度でいいと思います。あとは、直接そういう言語に触れてみるとさらに理解が深まると思います。

最後まで読んでいただきありがとうございます。

オブジェクト指向のメリットとは?例に例えてわかりやすく解説!

コメントを残す

メールアドレスが公開されることはありません。 * が付いている欄は必須項目です