クラスの継承の基本
概要
最終更新日:2020/03/02
C++でよく使われる継承の基本的な内容、使い方について書いた記事です。
この記事は以下の内容を知りたい方に向けて書いています。
- 継承とは何か知りたい
- 継承の使い方を知りたい
- protectedの役割を知りたい
- 継承を使った際のコンストラクトデストラクタの挙動が知りたい
- アップキャストは何かを知りたい
継承とは
継承とは既存のクラスを使用して新しいクラスを作成する機能のことです。
この機能を使用することで、既存のクラスのメンバを最初から持っている
新しいクラスを作成することができるようになり、
より効率的なプログラミングを行うことができます。
名称について
継承を行う際に継承元になるクラスと継承先になるクラスは
以下の記述している通りいくつかの名称で呼ばれていますが
全て同じ意味ですので、書籍や資料で違う単語で書かれたとしても
気にしなくて大丈夫です。
継承元 |
基本クラス、基底クラス、ベースクラス、スーパークラス、親クラス |
継承先 |
派生クラス、サブクラス、子クラス |
これ以降の継承の名称については継承元を基底クラス、派生クラスで統一したいと思います。
継承の目的
継承の目的は既存のクラスを再利用してプログラミングを効率化することです。
例えばゲームで敵のクラスを作成する場合、大抵は敵の種類分のクラスを作成します。
その敵クラスのメンバの中には同じ意図のメンバが複数存在します。
継承を使用しない場合は全てのクラスで同じメンバの宣言を行う必要がありますが、
継承を使用した場合、共通のメンバを持つ基底クラスを定義すれば、
そのクラスを継承するだけで継承先のクラスではそれらのメンバを宣言する必要がなくなります。
このような複数のクラスに共通するメンバを1つのクラスにまとめることを汎化(はんか)といいます。
継承の実装方法
継承は既存のクラスの情報をそのまま新しいクラスに利用する機能です。
その為、継承を行うには最低二つのクラスが必要となります。
継承の書式は派生クラス名の後ろに「: public 基底クラス名」を記述します。
※「: public 基底クラス名」のpublicの部分はprivateなど設定を行えますが、
基本はpublicで問題がないので、現状はpublicと覚えてください。
// 書式
// 基底クラス
class 基底クラス名
{
};
// 派生クラス
class 派生クラス名 : public 基底クラス名
{
};
// 具体例
// 基底クラス
class ElectricalApplianceStore
{
public:
void PrintEarnings(); // 売り上げ表示
int Earnings; // 売り上げ金
};
// 売り上げ表示
void ElectricalApplianceStore::PrintEarnings()
{
printf("売り上げ金額 = %d円\n", Earnings);
}
// 派生クラス
class BranchStore : public ElectricalApplianceStore
{
};
int main()
{
BranchStore osaka;
osaka.Earnings = 1000000;
osaka.PrintEarnings();
return 0;
}
表示結果:
売り上げ金額 = 1000000円
上のコードでは基底のElectricalApplianceStoreを継承したBranchStoreクラスを
インスタンス化してメンバを使用しています。
このように基底のメンバを派生で使用することができます。
protected
継承の機能を深く利用する上で大切な機能がアクセス指定子の「protected」です。
protectedを指定すると基底クラスと派生クラスでメンバの使用が可能です。
この特性を生かして派生先で使用したいメンバは基底でprotected宣言しておきます。
// 基底
class ElectricalApplianceStore
{
public:
void PrintEarnings(); // 売り上げ表示
protected:
int Earnings; // 売り上げ金
};
// 売り上げ表示
void ElectricalApplianceStore::PrintEarnings()
{
printf("売り上げ金額 = %d円\n", Earnings);
}
// 派生
class BranchStore : public ElectricalApplianceStore
{
public:
BranchStore(int earnings);
};
BranchStore::BranchStore(int earnings)
{
Earnings = earnings;
}
int main()
{
BranchStore osaka(1000000);
osaka.Earnings = 1000000; // エラー
osaka.PrintEarnings();
return 0;
}
上のコードはmain関数内のosaka.Earningsに値を代入の場所でエラーになります。
これはprotectedの公開範囲が基底と派生先のクラス内でのみ可能で、
外部からのアクセスが不可能という証明しています。
privateの注意点
継承を行う上での注意点としてprivateの有効範囲があります。
privateの有効範囲はあくまでそのクラス内になりますので
継承先でprivate設定したメンバの使用をすることはできません。
// 基底
class ElectricalApplianceStore
{
public:
void PrintEarnings(); // 売り上げ表示
private:
int Earnings; // 売り上げ金
};
// 売り上げ表示
void ElectricalApplianceStore::PrintEarnings()
{
printf("売り上げ金額 = %d円\n", Earnings);
}
// 派生
class BranchStore : public ElectricalApplianceStore
{
public:
BranchStore(int earnings);
};
// 売り上げ表示
void ElectricalApplianceStore::PrintEarnings()
{
printf("売り上げ金額 = %d円\n", Earnings);
}
// 継承先
class BranchStore : public ElectricalApplianceStore
{
public:
BranchStore(int earnings);
};
BranchStore::BranchStore(int earnings)
{
Earnings = earnings; // エラー
}
int main()
{
BranchStore osaka(1000000);
osaka.PrintEarnings();
return 0;
}
上のコードでは派生クラスであるBranchStoreのコンストラクタでエラーが発生します。
これはEarningsがprivate設定になっているため、BranchStoreで
Earningsにアクセスする権限がないためです。
このようにprivate設定をする場合は派生先で使用しないように気を付けてください。
コンストラクタとデストラクタ
基底クラスに定義されているコンストラクタとデストラクタは
派生クラスに継承されません。
コンストラクタとデストラクタは定義しているクラスの名前が
メンバ関数名になるので、基底クラスは基底クラスの
派生クラスは派生クラスのコンストラクタ、デストラクタの宣言が必要です。
コンストラクタ、デストラクタの呼び出し順番
コンストラクタとデストラクタの継承は行われませんが、
基底クラスにコンストラクタ、デストラクタが定義されている場合
派生クラスの生成、破棄のタイミングで基底クラスのコンストラクタ、
デストラクタが自動的に呼び出されるようになっています。
// 基底クラス
class Base
{
public:
Base();
~Base();
};
Base::Base()
{
printf("Baseコンストラクタ\n");
}
Base::~Base()
{
printf("Baseデストラクタ\n");
}
// 派生クラス
class Super : public Base
{
public:
Super();
~Super();
};
Super::Super()
{
printf("Superコンストラクタ\n");
}
Super::~Super()
{
printf("Superデストラクタ\n");
}
int main()
{
Super* test = new Super; // コンストラクタ呼び出し
delete test; // デストラクタ呼び出し
}
実行結果:
Baseコンストラクタ
Superコンストラクタ
Superデストラクタ
Baseデストラクタ
上のコードから分かるようにコンストラクタとデストラクタの呼び出される順番は
コンストラクタが基底クラス→派生クラスで
デストラクタは派生クラス→基底クラスの順番で呼び出されます。
この順番はしっかりと覚えておいてください。
引数を持つコンストラクタの呼び出し
コンストラクタは関数のオーバーロードが可能となっていますので、
基底クラスに複数のコンストラクタを持っている可能性があります。
その場合、派生クラスは基底クラスのコンストラクタの選択をする必要があります。
コンストラクタの指定方法
コンストラクタの指定は派生コンストラクタ定義部分で
引数の「)」と関数開始の「{」の間に「:」をつけて
その後ろに基底クラスのコンストラクタ名を指定して()の中に引数を指定します。
そうするとその引数の内容に当てはまるコンストラクタが呼び出されます。
この「:」から始まる部分をイニシャライザ(初期化子)と呼びます。
// 書式
クラス名::関数名(引数) : 基底クラス(初期値)
{
}
// 具体例
class Base
{
public:
Base();
Base(int val);
int Value;
};
Base::Base()
{
printf("Baseコンストラクタ その1\n");
}
Base::Base(int value)
{
Value = value;
printf("Baseコンストラクタ その2 Value = %d\n", Value);
}
class Super : public Base
{
public:
Super(int value02);
Super(int value, int value02);
int Value02;
};
Super::Super(int value02)
{
Value02 = value02;
printf("Superコンストラクタ その1 Value02 = %d\n", Value02);
}
Super::Super(int value01, int value02) : Base(value01)
{
Value02 = value02;
printf("Superコンストラクタ その2 Value02 = %d\n", Value02);
}
int main()
{
Super* super01 = new Super(10);
printf("\n");
Super* super02 = new Super(20, 300);
delete super01;
delete super02;
}
実行結果:
Baseコンストラクタ その1
Superコンストラクタ その1 Value02 = 10
Baseコンストラクタ その2 Value = 20
Superコンストラクタ その2 Value02 = 300
上のコードでは、変数super01のBaseのコンストラクタは引数一つを指定しており、
変数super02のBaseコンストラクタは引数二つを指定しています。
このように基底クラスのコンストラクタが複数あった場合でも、
派生クラスの用途に合わせて使い分けることが可能です。
アップキャスト
アップキャストとは派生クラスのポインタを基底クラスポインタにキャストすることです。
ポインタへのキャストは同じデータ型のポインタしか代入できないようになっていますが、
以下の絵のように「派生クラスは基底クラスのメンバを内包している」という
約束事があるからこそ特別にキャストが認められています。
ただし、キャストが完了した基底クラスでは派生クラスのメンバは使用できません。
class AnimalBase
{
public:
int AnimalType;
};
class Cat : public AnimalBase
{
public:
int CatKind;
};
int main()
{
Cat tama;
// CatクラスをAnimalクラスにアップキャストする
AnimalBase* animal = &tama;
animal->mAnimalType = 100;
// AnimalBaseにキャストしているのでCatのメンバは使えない
animal->CatKind = 1;
return 0;
}
上の例ではCatクラスのtamaをアップキャストしてAnimalBaseのanimalに
アップキャストをして代入しています。
この処理によって、tamaはAnimalBaseクラスとして扱われますので、
「animal->CatKind = 1;」という処理はエラーになります。
これは、AnimalBaseクラスにはCatKind変数がないからです。