このブログ記事では、C++における継承の概念を紹介します。

継承
クラスオブジェクトのユニークな特徴の一つは、他のクラスから属性やメソッドを継承することで、 コードの重複を減らすことができる点です。以下は、継承をどのように使用するかの例です。
class MenuItem {
public:
string name;
float calories;
void print () {
cout << name << " has " << calories << " kcals." << endl;
}
};
class Drink : public MenuItem {
public:
float ml;
void kcals_per_ml () {
cout << "kcals/ml: " << calories/ml << "." << endl;
}
};
int main () {
Drink lemonade;
lemonade.name = "Lemonade";
lemonade.calories = 105;
lemonade.ml = 250;
lemonade.print(); // => Lemonade has 105 kcals.
lemonade.kcals_per_ml(); // => kcals/ml: 0.42.
return 0;
}
name
やcalories
のような属性やprint
メソッドを定義しなくても、Drink
オブジェクトはMenuItem
を継承することで
それらにアクセスできます。継承されるクラスをベースクラス(または親クラス)と呼び、ベースクラスから継承されたクラスを
派生クラス(または子クラス)と呼びます。派生クラスからさらに継承されたクラスを作成することもできます。
class HotDrink : public Drink {
public:
float temperature;
void serving_temperature () {
cout << "Temperature: " << temperature << "." << endl;
}
};
int main () {
HotDrink hot_chocolate;
hot_chocolate.name = "Hot Chocolate";
hot_chocolate.carolies = 154;
hot_chocolate.ml = 200;
hot_chocolate.temperature = 70;
hot_chocolate.print(); // => Hot Chocolate has 154 kcals.
hot_chocolate.kcals_per_ml(); // => kcals/ml: 0.77.
hot_chocolate.serving_temperature(); // => Temperature: 70.
return 0;
}
HotDrink
クラスはDrink
クラスを継承し、Drink
クラスはMenuItem
クラスを継承しています。
したがって、HotDrink
クラスのオブジェクトは、Drink
およびMenuItem
のすべてのメンバー変数や関数にアクセスできます。
コンストラクタとデストラクタ
派生クラスでコンストラクタを定義すると、派生クラスのコンストラクタ内で特に指定がない限り、ベースクラスのデフォルトコンストラクタが自動的に実行されます。
class BaseClass {
BaseClass () {
cout << "BaseClass Constructor" << endl;
}
};
class DerviedClass : public BaseClass {
DerivedClass () {
cout << "DerivedClass Constructor" << endl;
}
};
int main () {
DerviedClass derived;
// => BaseClass Constructor
// DerivedClass Constructor
return 0
}
ベースクラスにデフォルトコンストラクタが設定されていない場合、派生クラスがベースクラスのパラメータ付きコンストラクタを明示的に使用しない限り、 コンパイルエラーになります。
class BaseClass {
public:
string name;
BaseClass (string name) {
cout << "BaseClass Param Constructor" << endl;
}
};
class DerviedClass : public BaseClass {
DerivedClass () : BaseClass("default") {
cout << "DerivedClass Constructor" << endl;
}
};
int main () {
DerviedClass derived;
// => BaseClass Param Constructor
// DerivedClass Constructor
return 0
}
デストラクタはコンストラクタと同様に動作しますが、派生クラスのデストラクタが先に実行されます。
class BaseClass {
public:
string name;
BaseClass (string name) {
cout << "BaseClass Constructor" << endl;
}
~BaseClass () {
cout << "BaseClass Destructor" << endl;
}
};
class DerviedClass : public BaseClass {
DerivedClass () : BaseClass("default") {
cout << "DerivedClass Constructor" << endl;
}
~DerivedClass () {
cout << "DerivedClass Destructor" << endl;
}
};
int main () {
DerviedClass derived;
// => BaseClass Constructor
// DerivedClass Constructor
// Derived Class Destructor
// BaseClass Destructor
return 0
}
多重継承
C++では、1つのクラスだけでなく、複数のクラスから継承することができます。ただし、複数の親クラスに同じ名前の属性やメソッドがある場合、曖昧さが生じます。
class BaseClass1 {
public:
int value;
void print() {
cout << "BaseClass1" << endl;
}
};
class BaseClass2 {
public:
int value;
void print() {
cout << "BaseClass2" << endl;
}
};
class DerivedClass : public BaseClass1, public BaseClass2 {
public:
};
int main () {
DerivedClass derived;
cout << derived.value << endl; // => err
derived.print(); // => err
return 0;
}
メンバー変数や関数をDerivedClass
内でオーバーロードするか、どの親クラスの実装を使用するかを::
を使って指定することで、
曖昧さを解決できます。
int main () {
DerivedClass derived;
cout << derived.BaseClass1::value << endl; // => BaseClass 1 value
derived.BaseClass2::print(); // => BaseClass2
return 0;
}
曖昧さを解決するために、::
を使って派生クラスで関数をオーバーライドすることもできます。
親クラスが共通の祖父母クラスを共有している場合にも曖昧さが生じることがあります。
class GrandParentClass {
public:
int common_value;
};
class BaseClass1 : public GrandParentClass {
}
class BaseClass2 : public GrandParentClass {
}
class DerivedClass : public BaseClass1, public BaseClass2 {
}
DerivedClass
のcommon_value
がBaseClass1
から来るのか、BaseClass2
から来るのかは曖昧です。
これを解決するために、親クラスが祖父母クラスを継承する際にvirtual
キーワードを使用できます。
class GrandParentClass {
public:
int common_value;
};
class BaseClass1 : virtual public GrandParentClass {
}
class BaseClass2 : virtual public GrandParentClass {
}
class DerivedClass : public BaseClass1, public BaseClass2 {
}
virtual
キーワードを使用することで、DerivedClass
は曖昧さなく祖父母クラスのメンバーを継承できます。
この方法を使用すると、DerivedClass
のコンストラクタは自動的にGrandParentClass
のデフォルトコンストラクタを
実行します。そのため、祖父母クラスや親クラスのパラメータ付きまたはデフォルトのコンストラクタを使用したい場合は、
::
で指定する必要があります。ご覧のように、多重継承は曖昧さを解決するために複雑さを伴い、しばしば批判されます。
動的バインディング
以下の例では、派生クラスのオーバーライドされたメソッドが呼び出されると予想されるかもしれません。
class BaseClass {
public:
int a;
void print () {
cout << a << endl;
};
BaseClass (int a) : a(a) {};
};
class DerivedClass : public BaseClass {
public:
int b;
void print () {
cout << a << ", " << b << endl;
};
DerivedClass (int a, int b) : BaseClass(a), b(b) {};
};
int main () {
BaseClass* array[] = {
new BaseClass(1),
new BaseClass(2),
new DerivedClass(3,4),
new DerivedClass(5,6)
};
for (int i = 0; i < 4; i++) {
array[i] -> print();
} // => 1 2 3 5 not 1 2 3 4 5 6
for (int i = 0; i < 4; i++) {
delete array[i];
}
return 0;
}
しかし、すべてのオブジェクトがBaseClass.print()
を実行します。これは、配列がBaseClass
へのポインタだからです。
デフォルトでは、メソッドは静的にバインドされ、どのメソッドを使用するかはコンパイル時に宣言されたポインタの型によって決まります。
DerivedClass
のオーバーライドされたメソッドを使用する、動的バインディングを実装するには、BaseClass
でprint
関数を定義するときに
virtual
キーワードを使用します。
class BaseClass {
public:
int a;
virtual void print () {
cout << a << endl;
};
BaseClass (int a) : a(a) {};
};
int main () {
BaseClass* array[] = {
new BaseClass(1),
new BaseClass(2),
new DerivedClass(3,4),
new DerivedClass(5,6)
};
for (int i = 0; i < 4; i++) {
array[i] -> print();
} // => 1 2 3 4 5 6
for (int i = 0; i < 4; i++) {
delete array[i];
}
return 0;
}
動的バインディングは、実行時にオブジェクトの型を決定するため、プログラムがより柔軟になりますが、実行速度が遅くなるというデメリットもあります。
仮想デストラクタ
以下のコードを実行すると、BaseClass
のデストラクタのみが実行されることに気付くでしょう。
これは、デストラクタが静的にバインドされているためです。
class BaseClass {
public:
int a;
void print () {
cout << a << endl;
};
BaseClass (int a) : a(a) {};
~BaseClass () {
cout << "BaseClass Destructor" << endl;
}
};
class DerivedClass : public BaseClass {
public:
int b;
void print () {
cout << a << ", " << b << endl;
};
DerivedClass (int a, int b) : BaseClass(a), b(b) {};
~DerivedClass () {
cout << "DerivedClass Destructor" << endl;
}
};
int main () {
BaseClass* array[] = {
new BaseClass(1),
new BaseClass(2),
new DerivedClass(3,4),
new DerivedClass(5,6)
};
for (int i = 0; i < 4; i++) {
array[i] -> print();
}
for (int i = 0; i < 4; i++) {
delete array[i];
} // => Only BaseClass Destructors are executed
return 0;
}
DerivedClass
がデストラクタ内で解放する必要があるポインタ属性を持っている場合、
上記の静的バインディングはメモリリークにつながる可能性があります。
このような場合、デストラクタには動的バインディングを使用することが重要です。
class BaseClass {
public:
int a;
void print () {
cout << a << endl;
};
BaseClass (int a) : a(a) {};
virtual ~BaseClass () {
cout << "BaseClass Destructor" << endl;
}
};
int main () {
BaseClass* array[] = {
new BaseClass(1),
new BaseClass(2),
new DerivedClass(3,4),
new DerivedClass(5,6)
};
for (int i = 0; i < 4; i++) {
array[i] -> print();
}
for (int i = 0; i < 4; i++) {
delete array[i];
} // => Only BaseClass Destructors & DerivedClass Destructors are executed
return 0;
}
クイズ
この記事では、学習した内容を確認するためのクイズを設けます。記事のメイン部分を読んだ後に、ぜひ自分で問題を解いてみることを強くお勧めします。各問題をクリックすると答えが表示されます。
リソース
- Portfolio Courses. 2022. Introduction To Inheritance | C++ Tutorial. YouTube.
- Portfolio Courses. 2022. Multilevel Inheritance | C++ Tutorial. YouTube.
- Portfolio Courses. 2022. How Constructors Work With Inheritance | C++ Tutorial. YouTube.
- Portfolio Courses. 2022. Introduction To Destructors In Inheritance | C++ Tutorial. YouTube.
- Portfolio Courses. 2022. Multiple Inheritance Deep Dive | C++ Tutorial. YouTube.
- Portfolio Courses. 2022. Dynamic Binding (Polymorphism) With The Virtual Keyword | C++ Tutorial. YouTube.
- Portfolio Courses. 2022. Virtual Destructors | C++ Tutorial. YouTube.