C++プログラマへの道 #4 - 継承

Last Edited: 9/10/2024

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

C++ References

継承

クラスオブジェクトのユニークな特徴の一つは、他のクラスから属性やメソッドを継承することで、 コードの重複を減らすことができる点です。以下は、継承をどのように使用するかの例です。

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;
}

namecaloriesのような属性や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 {
}

DerivedClasscommon_valueBaseClass1から来るのか、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のオーバーライドされたメソッドを使用する、動的バインディングを実装するには、BaseClassprint関数を定義するときに 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;
}

クイズ

この記事では、学習した内容を確認するためのクイズを設けます。記事のメイン部分を読んだ後に、ぜひ自分で問題を解いてみることを強くお勧めします。各問題をクリックすると答えが表示されます。

リソース