C++ 発展編 3日目

継承

継承の基本的な考え方と利点

C++における継承とは、既存のクラス(基底クラスと呼びます)が持つメンバー(変数や関数)を、別のクラス(派生クラスと呼びます)に引き継がせる仕組みです
これにより、基底クラスの機能を再利用したり、拡張したりすることが可能になります

クラスの継承は、「is-a(~である)」という関係性を表現するのに適しています
これは、派生クラスが基底クラスの一種である、あるいは派生クラスが基底クラスの部分集合である、という意味合いを持ちます
例えば、「犬は動物である」という関係は、動物クラスを基底クラス、犬クラスを派生クラスとすることで表現できます

継承を利用することで、以下の利点があります

  1. コード再利用性の向上
    既存の基底クラスの機能やデータを派生クラスで利用できるため、同じようなコードを何度も書く必要がなくなり、コードの重複を避け、開発効率を大幅に向上させることができます
  2. 保守性の向上
    クラス間の関係性が明確になることで、コードの理解が容易になります
    また、基底クラスの機能に変更があった場合、その変更が派生クラスにも自動的に反映されるため、システムのメンテナンスが容易になります
  3. 設計の柔軟性
    新しいクラスを派生させることで、既存の設計を拡張したり、特定の機能を変更したりすることが可能になります
    これにより、コードの柔軟性が高まり、将来的な変更にも対応しやすくなります

派生クラスの定義

クラスの継承を行うには、クラス定義の際に、コロン(:)とアクセス修飾子(アクセス指定子ともいう)を使って基底クラスを指定します
アクセス修飾子には、public, protected, privateのいずれかを指定し、基底クラスのメンバーが派生クラス内でどのようにアクセス可能になるかを制御します

class 派生クラス名 : アクセス修飾子 基底クラス名 {
    // 派生クラスのメンバー
};

例えば、以下のように書くと、Animalクラスを基底クラスとして、Dogクラスを派生クラスとして定義できます

// 基底クラス
class Animal {
  public:
    void eat() {
      std::cout << "I’m eating." << std::endl;
    }
};

// 派生クラス
class Dog: public Animal {
  public:
    void bark() {
      std::cout << "Woof!" << std::endl;
    }
};

int main() {
  Dog dog;
  dog.eat(); // 基底クラスのメンバー関数を呼び出す
  dog.bark(); // 派生クラスのメンバー関数を呼び出す
  return 0;
}

この場合、DogクラスはAnimalクラスのメンバー関数であるeat()を引き継いでいるため、Dogクラスのオブジェクトはeat()を呼び出すことができます
また、Dogクラスは自分自身のメンバー関数であるbark()を持っています
これは、DogクラスがAnimalクラスの機能を拡張していることを示しています

クラス継承を使ったサンプルプログラムをもう一つ以下に示します
Personクラスを基底クラスとして、StudentクラスとTeacherクラスを派生クラスとして定義します
Personクラスには名前と年齢を持ち、Studentクラスには学校と学年を持ち、Teacherクラスには教科と給料を持ちます

// 基底クラス
class Person {
  protected:
    std::string name;
    int age;
  public:
    Person(std::string n, int a) {
      name = n;
      age = a;
    }
    void show() {
      std::cout << "Name: " << name << std::endl;
      std::cout << "Age: " << age << std::endl;
    }
};

// 派生クラス
class Student: public Person {
  private:
    std::string school;
    int grade;
  public:
    Student(std::string n, int a, string s, int g): Person(n, a) {// 基底クラスのコンストラクタへ引数を渡す
      school = s;
      grade = g;
    }
    void show() {
      Person::show(); // 基底クラスのメンバー関数を呼び出す
      std::cout << "School: " << school << std::endl;
      std::cout << "Grade: " << grade << std::endl;
    }
};

// 派生クラス
class Teacher: public Person {
  private:
    std::string subject;
    int salary;
  public:
    Teacher(std::string n, int a, std::string sub, int sal): Person(n, a) {// 基底クラスのコンストラクタへ引数を渡す
      subject = sub;
      salary = sal;
    }
    void show() {
      Person::show(); // 基底クラスのメンバー関数を呼び出す
      std::cout << "Subject: " << subject << std::endl;
      std::cout << "Salary: " << salary << std::endl;
    }
};
int main(){
    Teacher tch("Yamada Taro",26,"情報",290000);
    Student stu("Suzuki Jiro",11,"abc小学校",6);

    tch.show();
    stu.show();

    return 0;
}

派生クラスのオブジェクトを生成する際には、まず基底クラスのコンストラクタが呼び出され、基底クラス部分が初期化されます
C++では、メンバー初期化リスト (: 基底クラス名(引数)) を使用して基底クラスのコンストラクタに引数を渡すことができます
以下の行がメンバ初期化リストを使った派生クラスのコンストラクタです

Student(std::string n, int a, string s, int g): Person(n, a) 
Teacher(std::string n, int a, std::string sub, int sal): Person(n, a)

アクセス修飾子(アクセス指定子)

アクセス修飾子は、メンバ関数やメンバ変数に指定する場合と、継承時に指定する場合があり、制御の対象が異なります

  1. メンバ関数やメンバ変数に指定する場合
    そのクラス自身のメンバーが、クラスの外部からどのようにアクセスできるかを定義することにより、データの隠蔽やインターフェースの公開を制御することが可能になります
アクセス修飾子特徴
publicクラスの内部からも外部からも自由にアクセスできます
これは、クラスのインターフェースとして公開したい機能やデータに対して使用されます
protectedそのクラス自身と、そのクラスから派生した派生クラスのメンバ関数内からアクセスできます
クラスの外部からは直接アクセスできません
privateそのクラス自身のメンバ関数内からのみアクセスできます
クラスの外部からも、派生クラスからも直接アクセスすることはできません
これは、クラスの内部実装の詳細を完全に隠蔽し、データの不正な変更を防ぐために使用されます
  1. 継承時に指定する場合
    基底クラスのメンバーが、派生クラス内部でどのようなアクセスレベルになるかを定義します
    言い換えれば、基底クラスのメンバーが、派生クラスから見てpublic、protected、privateのどれに「格下げ」されるかを決定します
基底クラスのメンバのアクセスレベルpublic継承protected継承private継承
publicpublicprotectedprivate
protectedprotectedprotectedprivate
private継承されない継承されない継承されない

private メンバーは、いかなる継承形式でも派生クラスから直接アクセスできません
基底クラスのpublicまたはprotectedなメンバー関数を介して間接的にアクセスすることは可能です
継承時のアクセス修飾子によって、派生クラスが基底クラスのメンバーを「どう見なすか」が変わります

それぞれサンプルプログラムから詳しく見ていきましょう

  1. public 継承
class Derived : public Base {
    // ...
};

基底クラスのpublicメンバーは、派生クラスでもpublicとしてアクセス可能で、派生クラスのオブジェクトを通じて外部からアクセスできます
基底クラスのprotectedメンバーは、派生クラスでもprotectedとしてアクセス可能で、派生クラスのメンバー関数内からアクセスできます
また、その派生クラスをさらに継承したクラスからもアクセスできます

最も一般的な継承形式であり、基底クラスのインターフェースをそのまま派生クラスに引き継ぎたい場合に用います

#include <iostream>

class Base {
public:
    int public_var;
protected:
    int protected_var;
private:
    int private_var; // privateなので派生クラスからは直接アクセス不可

public:
    void public_func() {
        std::cout << "Base::public_func()" << std::endl;
    }
protected:
    void protected_func() {
        std::cout << "Base::protected_func()" << std::endl;
    }
};

class DerivedPublic : public Base {
public:
    void test_access() {
        public_var = 10;        // OK: Base::public_var は public
        protected_var = 20;     // OK: Base::protected_var は protected
        // private_var = 30;    // エラー: Base::private_var は private

        public_func();          // OK: Base::public_func() は public
        protected_func();       // OK: Base::protected_func() は protected
    }
};

int main() {
    DerivedPublic dp;
    dp.public_var = 100;     // OK: 派生クラスのpublic_var(Baseから継承)はpublic
    std::cout << "call public_func()" << std::endl;
    dp.public_func();        // OK: 派生クラスのpublic_func(Baseから継承)はpublic

    // dp.protected_var = 200; // エラー: protected_var は protected
    // dp.protected_func();    // エラー: protected_func は protected

    std::cout << "call test_access()" << std::endl;
    dp.test_access();       // // OK: test_accessはpublic
    return 0;
}
実行結果
call public_func()
Base::public_func()
call test_access()
Base::public_func()
Base::protected_func()
  1. protected 継承
class Derived : protected Base {
    // ...
};

基底クラスのpublicメンバーは、派生クラス内でprotectedとしてアクセス可能で、派生クラスのメンバー関数内からアクセスできます
基底クラスのprotectedメンバーは、派生クラス内でもprotectedとしてアクセス可能です
派生クラスのオブジェクトからは、基底クラスのpublicメンバーには直接アクセスできません
さらに、その派生クラスを継承したクラスからは基底クラスのメンバーにアクセスできます

基底クラスの機能を派生クラスの内部実装として隠蔽したい場合に用います

#include <iostream>

class Base {
public:
    int public_var;
protected:
    int protected_var;
private:
    int private_var;

public:
    void public_func() {
        std::cout << "Base::public_func()" << std::endl;
    }
protected:
    void protected_func() {
        std::cout << "Base::protected_func()" << std::endl;
    }
};

class DerivedProtected : protected Base {
public:
    void test_access() {
        public_var = 10;        // OK: Base::public_var は protected になった
        protected_var = 20;     // OK: Base::protected_var は protected
        public_func();          // OK: Base::public_func() は protected になった
        protected_func();       // OK: Base::protected_func() は protected
    }
};

class GrandChildFromProtected : public DerivedProtected {
public:
    void test_grandchild_access() {
        public_var = 100;       // OK: DerivedProtected で protected になったため、さらに継承した GrandChildからもアクセス可能
        protected_var = 200;    // OK
        public_func();          // OK
        protected_func();       // OK
    }
};


int main() {
    DerivedProtected dp;
    // dp.public_var = 100;     // エラー: public_var は protected になったため、外部からアクセス不可
    // dp.public_func();        // エラー: public_func は protected になったため、外部からアクセス不可
    std::cout << "call DerivedProtected::test_access" << std::endl;
    dp.test_access();

    GrandChildFromProtected gcp;
    std::cout << "call GrandChildFromProtected::test_grandchild_access" << std::endl;
    gcp.test_grandchild_access(); // OK
    return 0;
}
実行結果
call DerivedProtected::test_access
Base::public_func()
Base::protected_func()
call GrandChildFromProtected::test_grandchild_access
Base::public_func()
Base::protected_func()
  1. private 継承
class Derived : private Base {
    // ...
};

基底クラスのpublicメンバーは、派生クラス内でprivateとしてアクセス可能で、派生クラスのメンバー関数内からのみアクセスできます
基底クラスのprotectedメンバーは、派生クラス内でもprivateとしてアクセス可能です
派生クラスのオブジェクトからは、基底クラスのメンバーには一切直接アクセスできません
また、この派生クラスをさらにpublicで継承しても、基底クラスのメンバーにはアクセスできません

基底クラスの機能を派生クラスの内部実装として完全に隠蔽したい場合に用います

#include <iostream>

class Base {
public:
    int public_var;
protected:
    int protected_var;
private:
    int private_var;

public:
    void public_func() {
        std::cout << "Base::public_func()" << std::endl;
    }
protected:
    void protected_func() {
        std::cout << "Base::protected_func()" << std::endl;
    }
};

class DerivedPrivate : private Base {
public:
    void test_access() {
        public_var = 10;        // OK: Base::public_var は private になった
        protected_var = 20;     // OK: Base::protected_var は private になった
        public_func();          // OK: Base::public_func() は private になった
        protected_func();       // OK: Base::protected_func() は private になった
    }
};

class GrandChildFromPrivate : public DerivedPrivate {
public:
    void test_grandchild_access() {
        // public_var = 100;     // エラー: DerivedPrivate で private になったため、さらに継承した GrandChild からはアクセス不可
        // protected_var = 200;    // エラー
        // public_func();          // エラー
    }
};

int main() {
    DerivedPrivate dp;
    // dp.public_var = 100;     // エラー: public_var は private になったため、外部からアクセス不可
    // dp.public_func();        // エラー: public_func は private になったため、外部からアクセス不可

    dp.test_access();   // OK: DerivedPrivate::test_accessはpublicであるため外部からアクセス可能

    return 0;
}
実行結果
Base::public_func()
Base::protected_func()

抽象クラスと仮想関数

仮想関数

仮想関数とは、基底クラスで定義され、派生クラスでその処理内容を自由に変更(オーバーライド)できると宣言された関数のことです
継承によって派生クラスは基底クラスの機能を引き継ぎつつ、独自の機能を追加することができます
しかし、時には既存の機能の処理自体を変更したい場合もあります
全ての関数の処理を変更されては困るため、基底クラスの設計者は「この関数は派生クラスで処理を変更しても良い」と明示的に宣言するために仮想関数を使用します
派生クラスでは、仮想関数をオーバーライドして動作を変更します

仮想関数は基底クラスでは以下のように記述します
基底クラスの仮想関数にはvirtualをつけ、派生クラスのオーバーライドする関数にはoverrideを付加します

class 基底クラス名{
public:
    virtual 戻り値の型 仮想関数名(引数); // 仮想関数
}

//仮想関数の定義 
戻り値の型 基底クラス名::仮想関数名(引数){  // 仮想関数の定義にはvirtualは書かない 

}

一方、派生クラスで仮想関数をオーバーライトするには以下のように記述します

class 派生クラス名::public 基底クラス名{
public:
    戻り値の型 仮想関数名(引数) override; // オーバーライド 
}

// オーバーライドした関数の定義
戻り値の型 派生クラス名::仮想関数名(引数){ // 定義にはoverrideは書かない

}

オーバーライドしたサンプルプログラムを以下に示します

#include <iostream>

// 基底クラス
class Animal {
public:
    // 仮想関数として宣言
    virtual void makeSound() const {
        std::cout << "動物が鳴いています。" << std::endl;
    }

    // 仮想デストラクタ
    virtual ~Animal() {
        std::cout << "Animal デストラクタが呼ばれました。"<< std::endl;
    }
};

// 派生クラス1
class Dog : public Animal {
public:
    // 基底クラスの仮想関数をオーバーライド
    void makeSound() const override {
        std::cout << "ワンワン! "<< std::endl;
    }

    ~Dog() override {
        std::cout << "Dog デストラクタが呼ばれました。" << std::endl;
    }
};

// 派生クラス2
class Cat : public Animal {
public:
    // 基底クラスの仮想関数をオーバーライド
    void makeSound() const override {
        std::cout << "ニャーニャー!" << std::endl;
    }

    ~Cat() override {
        std::cout << "Cat デストラクタが呼ばれました。" << std::endl;
    }
};

int main() {
    Animal* a1 = new Dog(); // 基底クラスのポインターに派生クラスのオブジェクトを代入
    Animal* a2 = new Cat(); // 基底クラスのポインターに派生クラスのオブジェクトを代入
    Animal* a0 = new Animal(); // 基底クラスのポインターに基底クラスのオブジェクトを代入

    std::cout << "--- 犬の場合 ---" << std::endl;
    a1->makeSound(); // ワンワン:DogクラスのmakeSound()が呼び出される

    std::cout << "--- 猫の場合 ---" << std::endl;
    a2->makeSound(); // ニャーニャー:CatクラスのmakeSound()が呼び出される

    std::cout << "--- 通常のAnimalの場合 ---" << std::endl;
    a0->makeSound(); // 動物が鳴いています。:AnimalクラスのmakeSound()が呼び出される

    std::cout << "--- 犬の削除 ---" << std::endl;
    delete a1;  // 仮想デストラクタにより、Dogのデストラクタも呼ばれる

    std::cout << "--- 猫の削除 ---" << std::endl;
    delete a2;  // 仮想デストラクタにより、Catのデストラクタも呼ばれる

    std::cout << "--- 通常のAnimalの削除 ---" << std::endl;
    delete a0;  // 仮想デストラクタにより、Animalのデストラクタも呼ばれる

    return 0;
}
実行結果
--- 犬の場合 ---
ワンワン! 
--- 猫の場合 ---
ニャーニャー!
--- 通常のAnimalの場合 ---
動物が鳴いています。
--- 犬の削除 ---
Dog デストラクタが呼ばれました。
Animal デストラクタが呼ばれました。
--- 猫の削除 ---
Cat デストラクタが呼ばれました。
Animal デストラクタが呼ばれました。

makeSound()とデストラクタが仮想関数として定義されています
仮想関数で定義したデストラクタは仮想デストラクタと呼びます
仮想デストラクタは、基底クラスのポインタを使って、派生クラスのオブジェクトをdeleteする際、派生クラスのデストラクタも確実に呼び出されるようにするための機能です

Animal* a1 = new Dog(); // 基底クラスのポインターに派生クラスのオブジェクトを代入
Animal* a2 = new Cat(); // 基底クラスのポインターに派生クラスのオブジェクトを代入
Animal* a0 = new Animal(); // 基底クラスのポインターに基底クラスのオブジェクトを代入

動的生成されたDogやCatオブジェクトをAnimalポインタで指すようにしています
仮想関数の性質により、Animalポインタ->makeSound();は、ポインタが実際に指しているオブジェクトのmakeSound()が呼び出されています

純粋仮想関数と抽象クラス

上のサンプルでは、基底クラスのAnimal自身もオブジェクトとして作成することができました
しかし、「犬」や「猫」の具体的なオブジェクトは作ることができますが、「動物」という漠然としたオブジェクトを作ったとしても、どのようにふるまうかは明確でない場合があります
このような状況を表現するため、導入されたのが、純粋仮想関数抽象クラスという概念です

純粋仮想関数

純粋仮想関数とは、基底クラスでその関数の「宣言」のみを行い、「実装」を持たない仮想関数のことです
派生クラスにその関数の実装を「義務付ける」ために使用されます

構文としては、仮想関数の宣言の末尾に= 0を付け加えます

class クラス名{
public:
    virtual 戻り値の型 関数名() = 0;    
}

例えば、以下のように宣言します

class Animal {
public:
    virtual void makeSound() = 0; // 純粋仮想関数
};

この= 0は、この関数は基底クラスでは未実装であり、派生クラスが実装を提供しなければならないという決まりのようなものです

抽象クラス

抽象クラスとは、少なくとも一つの純粋仮想関数を持つクラスのことをいいます
抽象クラスは、一般的な概念や共通の性質を表現するために使われるため、そのままではオブジェクトを作成できません
抽象クラスから派生した具体的なクラスで、純粋仮想関数をオーバーライドして実装しなければなりません

仮想関数で提示したサンプルプログラムを抽象クラスと純粋仮想関数で書き換えたサンプルプログラムを以下に示します

#include <iostream>

// 抽象クラス Animal
class Animal {
public:
    // 純粋仮想関数として宣言
    virtual void makeSound() const = 0; 

    // 仮想デストラクタは引き続き必要
    virtual ~Animal() {
        std::cout << "Animal デストラクタが呼ばれました。\n";
    }
};

// 派生クラス Dog
class Dog : public Animal {
public:
    // 純粋仮想関数 makeSound() をオーバーライドして実装
    void makeSound() const override {
        std::cout << "ワンワン!\n";
    }

    ~Dog() override {
        std::cout << "Dog デストラクタが呼ばれました。\n";
    }
};

// 派生クラス Cat
class Cat : public Animal {
public:
    // 純粋仮想関数 makeSound() をオーバーライドして実装
    void makeSound() const override {
        std::cout << "ニャーニャー!\n";
    }

    ~Cat() override {
        std::cout << "Cat デストラクタが呼ばれました。\n";
    }
};

int main() {
    // 抽象クラスは直接インスタンス化できない!
    // 抽象クラスへのポインタや参照は宣言できる
    // Animal genericAnimal; // これはコンパイルエラーになります!

    Animal* a1 = new Dog(); // 抽象クラスのポインターに派生クラスのオブジェクトを代入
    Animal* a2 = new Cat(); // 抽象クラスのポインターに派生クラスのオブジェクトを代入

    // Animal* a0 = new Animal(); // これはコンパイルエラーになります!

    std::cout << "--- 犬の場合 ---" << std::endl;
    a1->makeSound(); // ワンワン:DogクラスのmakeSound()が呼び出される

    std::cout << "--- 猫の場合 ---" << std::endl;
    a2->makeSound(); // ニャーニャー:CatクラスのmakeSound()が呼び出される

    std::cout << "--- 犬の削除 ---" << std::endl;
    delete a1;  // 仮想デストラクタにより、Dogのデストラクタも呼ばれる

    std::cout << "--- 猫の削除 ---" << std::endl;
    delete a2;  // 仮想デストラクタにより、Catのデストラクタも呼ばれる

    return 0;
}
実行結果
-- 犬の場合 ---
ワンワン!
--- 猫の場合 ---
ニャーニャー!
--- 犬の削除 ---
Dog デストラクタが呼ばれました。
Animal デストラクタが呼ばれました。
--- 猫の削除 ---
Cat デストラクタが呼ばれました。
Animal デストラクタが呼ばれました。

仮想関数と純粋仮想関数の使い分け

  • 仮想関数
    基底クラスにデフォルトの実装があるが、派生クラスでその動作を変更できることを許可したい場合に用います
    基底クラスのオブジェクトも直接インスタンス化可能です
  • 純粋仮想関数
    基底クラスでは具体的な実装がなく、その役割は「インターフェースを定義」する場合に使用します
    派生クラスにその実装を強制し、基底クラスの直接インスタンス化を禁止します
    これにより、設計者は「このクラスはあくまで概念であり、具体的な振る舞いは派生クラスに任せる」という意図を明確にできます

コメント

この記事へのコメントはありません。

関連記事

C++ 13日目:ルートボタン

Python 応用編 2日目

C言語(応用編)~2日目~

PAGE TOP