©️

C++ クラスの継承

2024/10/03に公開

継承とカプセル化

派生クラス(サブクラス・子クラス)は基底クラス(スーパークラス・親クラス)のプライベートメンバへアクセスできない

スーパークラスでプライベートメンバへのアクセスを提供するパブリックまたはプロテクトなメソッドを用意し、サブクラスはこのメソッドを呼び出すことで、間接的にプライベートメンバへアクセスする

サンプルコード

class Base {
private:
    int privateMember;
protected:
    int protectedMember;
public:
    void setPrivateMember(int value) {
        privateMember = value;
    }
    int getProtectedMember() {
        return protectedMember;
    }
};

class Derived : public Base {
public:
    void accessMembers() {
        setPrivateMember(10); // パブリックメソッド経由でプライベートメンバにアクセス
        std::cout << getProtectedMember() << std::endl; // プロテクトメンバに直接アクセス
    }
};

自動ポインタ変換

自動ポインタ変換とは、プログラミング言語において、ある型のポインタを別の型のポインタに、プログラマが明示的にキャストしなくても、コンパイラが自動的に変換する機能

サンプルコード

#include <iostream>

class Base {
public:
    void print() {
        std::cout << "Base class" << std::endl;
    }
};

class Derived : public Base {
public:
    void print() {
        std::cout << "Derived class" << std::endl;
    }
};

int main() {
    Base base;
    Derived derived;

    // 派生クラスのポインタを基底クラスのポインタに変換
    Base* basePtr = &derived;
    basePtr->print(); // Base classが出力される

    // 基底クラスのポインタを派生クラスのポインタに変換(エラー)
    // Derived* derivedPrt = &base; // error: invalid conversion from ‘Base*’ to ‘Derived*’

    // 基底クラスのポインタを派生クラスのポインタに変換するにはキャストが必要(安全ではない)
    Derived* derivedPrt = (Derived *)&base;
    derivedPrt->print(); // Derived classが出力される
    return 0;
}

継承のアクセス指定子

継承のアクセス指定子と呼ぶのは一般的ではないかもです
↓コードでpublicとしている箇所のことです

class Derived : public Base {
  • public継承:
    • 基底クラスのpublicなメンバを、サブクラスでもpublicとして継承する
    • 最も一般的な継承の形態
  • protected継承:
    • 基底クラスのpublicおよびprotectedなメンバを、サブクラスでもprotectedとして継承する
    • 派生クラスとその派生クラスでしかアクセスできない
  • private継承:
    • 基底クラスのpublicなメンバも、サブクラスではprivateなメンバとして扱われる
    • 基底クラスとの関係を隠蔽したい場合に用いる

多重継承

1つのクラスが複数の基底クラスから機能を継承する
多重継承でよく発生する問題として、ダイヤモンド継承問題がある

サンプルコード

#include <iostream>

class Animal {
public:
    void eat() {
        std::cout << "食べる" << std::endl;
    }
};

class Flyable {
public:
    void fly() {
        std::cout << "飛ぶ" << std::endl;
    }
};

// AnimalとFlyableの両方を継承
class Bird : public Animal, public Flyable {
public:
    void sing() {
        std::cout << "さえずる" << std::endl;
    }
};

int main() {
    Bird bird;
    bird.eat();
    bird.fly();
    bird.sing();
    return 0;
}

ダイヤモンド継承問題

あるクラスが複数の経路で同じ基底クラスを継承する場合、どの基底クラスのメンバ関数が呼び出されるのかが曖昧になる問題

ダイヤモンド継承問題のサンプルコード

class A {
public:
    void foo() { std::cout << "A::foo()" << std::endl; }
};

class B : public A {};
class C : public A {};

class D : public B, public C {};

int main() {
    D d;
    d.foo(); // どちらのfoo()が呼ばれるのか?
}

ダイヤモンド継承問題の解決策

  • 仮想基底クラス:
    • 基底クラスを仮想基底クラスとして宣言することで、重複した基底クラスのインスタンスが1つにまとめられる
  • スコープ解決演算子
    • どの基底クラスのメンバ関数を呼び出すのかを明示的に指定する
// 仮想基底クラスの例
class A {
public:
    void foo() { std::cout << "A::foo()" << std::endl; }
};

class B : virtual public A {};
class C : virtual public A {};

// スコープ解決演算子の例
d.A::foo(); // 明確にAクラスのfoo()を呼び出す

メンバ関数の多重定義(オーバーロード)

派生クラスの関数は、基底クラスの同じ名前の関数を多重定義しない
基底クラスの同じ名前の関数を隠ぺいする
隠ぺいされたメソッドを呼ぶにはクラスを指定する

#include <iostream>

class Calculator {
public:
    int add(int a, int b) {
        return a + b;
    }
};

class AdvancedCalculator : public Calculator {
public:
    int add(int a, int b, int c, int d) {
        return a + b + c + d;
    }
};

int main() {
    AdvancedCalculator calc;
    // std::cout << calc.add(1, 2) << std::endl; // 基底クラスのaddを呼ぶとコンパイルエラー
    std::cout << calc.Calculator::add(1, 2) << std::endl; // 隠ぺいされたメソッドを呼ぶにはクラスを指定する
    std::cout << calc.add(1, 2, 3, 4) << std::endl; // サブクラスのaddを呼ぶ
}

Discussion