閉じる

【C++】クラスの継承入門|仕組みと使い方をわかりやすく解説

オブジェクト指向の中心的な考え方の1つが「継承」です。

C++でもクラス継承を使うことで、既存のクラスを土台に機能を拡張したり、共通処理をまとめて再利用しやすくなります。

本記事では、C++におけるクラス継承の基本的な仕組みと使い方を、図解とサンプルコードを交えながら入門者向けに解説します。

C++のクラス継承とはなにか

継承のイメージ

継承とは、既にあるクラス(親クラス)を基にして、新しいクラス(子クラス)を定義する仕組みです。

親クラスが持っているメンバ変数やメンバ関数を、そのまま子クラスでも利用できるようになります。

ここで覚えておきたい用語を整理します。

  • 親クラス(基底クラス、Base class)
  • 子クラス(派生クラス、Derived class)
  • 親クラスの機能を子クラスが受け継ぐことを「継承する」と表現します。

クラス継承を使うと、以下のような場面で役立ちます。

  • 共通処理を親クラスにまとめて、重複コードを減らしたいとき
  • 型の階層(動物 → 犬・猫、図形 → 円・四角 など)を表現したいとき
  • ポリモーフィズム(多態性)を使って、共通インターフェースで扱いたいとき

基本的な継承の書き方

継承の基本構文

C++でクラスを継承するときの基本構文は次のようになります。

C++
class 基底クラス名 {
    // 基底クラスのメンバ
};

class 派生クラス名 : public 基底クラス名 {
    // 派生クラスのメンバ
};

ポイントは「: public 基底クラス名」の部分で、ここでどのクラスからどのように継承するかを指定しています。

まずは最もよく使うpublic継承に絞って解説します。

最小のサンプルコード

C++
#include <iostream>
#include <string>

// 人を表す基底クラス
class Person {
protected:
    std::string name;  // 名前

public:
    // コンストラクタ
    Person(const std::string& name)
        : name(name) {}

    // 自己紹介するメンバ関数
    void sayHello() const {
        std::cout << "こんにちは、" << name << "です。" << std::endl;
    }
};

// 学生を表す派生クラス
// Personをpublic継承しています
class Student : public Person {
private:
    int grade;  // 学年

public:
    // コンストラクタ
    // 基底クラス(Person)のコンストラクタも呼び出します
    Student(const std::string& name, int grade)
        : Person(name),  // 基底クラス部分の初期化
          grade(grade)   // 派生クラス独自メンバの初期化
    {}

    // 学生としての動作を表すメンバ関数
    void study() const {
        std::cout << name << "は勉強中です。学年: " << grade << std::endl;
    }
};

int main() {
    Student s("Taro", 3);

    // 基底クラスのメンバ関数をそのまま利用できます
    s.sayHello();

    // 派生クラスで追加したメンバ関数も使えます
    s.study();

    return 0;
}
実行結果
こんにちは、Taroです。
Taroは勉強中です。学年: 3

この例では、StudentはPersonを継承しているので、Personが持つnamesayHello()をそのまま利用できます。

一方で、gradestudy()のように、Student独自のメンバも追加できます。

アクセス指定子と継承の関係

public / protected / private の復習

クラスのメンバには、次の3種類のアクセス指定があります。

  • public: どこからでもアクセス可能
  • protected: そのクラスと派生クラスからのみアクセス可能
  • private: そのクラスの内部からのみアクセス可能

継承ではとくにprotectedが重要になります。

派生クラスからはprotectedメンバにアクセスできますが、外部(クラスの外)からはアクセスできない、という性質が、基底クラスの実装を守りつつ派生クラスで活用したいときに便利です。

継承時の指定子: public / protected / private 継承

継承には3種類の指定方法があります。

  • class Derived : public Base
  • class Derived : protected Base
  • class Derived : private Base

ここでは詳細には踏み込みすぎず、入門ではpublic継承を使うと覚えておくとよいです。

public継承は、「AはBの一種である」という関係を表現するときに使います。

代表的なアクセスの変化を表でまとめます。

基底クラスのメンバpublic継承後(外部から)protected継承後(外部から)private継承後(外部から)
publicpublicのままアクセス不可アクセス不可
protectedアクセス不可アクセス不可アクセス不可
privateアクセス不可アクセス不可アクセス不可

外部から見えるアクセス範囲はこのようになります。

実際には派生クラス内部からどう見えるかも変わりますが、最初は「public継承なら基底クラスのpublicはそのままpublicとして使える」という部分だけ押さえておくと理解しやすいです。

基底クラスのコンストラクタと派生クラス

コンストラクタ呼び出しの流れ

継承したクラスを生成する際、最初に基底クラスのコンストラクタが呼ばれ、そのあとに派生クラスのコンストラクタが実行されます。

逆に、オブジェクトが破棄されるときは、派生クラスのデストラクタ → 基底クラスのデストラクタの順に呼ばれます。

サンプルコードで流れを確認してみます。

C++
#include <iostream>

class Base {
public:
    Base() {
        std::cout << "Baseのコンストラクタ" << std::endl;
    }

    ~Base() {
        std::cout << "Baseのデストラクタ" << std::endl;
    }
};

class Derived : public Base {
public:
    Derived() {
        std::cout << "Derivedのコンストラクタ" << std::endl;
    }

    ~Derived() {
        std::cout << "Derivedのデストラクタ" << std::endl;
    }
};

int main() {
    Derived d;  // ここでコンストラクタが呼ばれる

    return 0;   // mainを抜けるとデストラクタが呼ばれる
}
実行結果
Baseのコンストラクタ
Derivedのコンストラクタ
Derivedのデストラクタ
Baseのデストラクタ

このように、生成は「親 → 子」、破棄は「子 → 親」の順になります。

基底クラスのコンストラクタを明示的に呼び出す

基底クラスが引数付きコンストラクタしか持たない場合、派生クラスのコンストラクタからメンバ初期化リストで明示的に呼び出す必要があります。

C++
#include <iostream>
#include <string>

class Person {
protected:
    std::string name;

public:
    // デフォルトコンストラクタは定義していません
    Person(const std::string& name)
        : name(name) {
        std::cout << "Personのコンストラクタ: " << name << std::endl;
    }
};

class Student : public Person {
private:
    int grade;

public:
    // StudentのコンストラクタからPersonのコンストラクタを呼び出します
    Student(const std::string& name, int grade)
        : Person(name),  // 基底クラス部分
          grade(grade) { // 派生クラス部分
        std::cout << "Studentのコンストラクタ: grade = " << grade << std::endl;
    }
};

int main() {
    Student s("Hanako", 2);
    return 0;
}
実行結果
Personのコンストラクタ: Hanako
Studentのコンストラクタ: grade = 2

基底クラスにデフォルトコンストラクタが無い場合、派生クラス側で必ず明示的に呼び出す必要がある、という点を押さえておきましょう。

メンバ関数のオーバーライド(上書き)

「共通インターフェース+振る舞いの違い」を表現する

継承の大きな目的の1つは、共通のメソッド名を保ちながら、中身の実装を派生クラスごとに変えることです。

これを「オーバーライド」(override)と呼びます。

C++
#include <iostream>

class Animal {
public:
    void speak() const {
        std::cout << "動物が何か音を出します。" << std::endl;
    }
};

class Dog : public Animal {
public:
    // 基底クラスと同じ名前・同じ引数の関数を定義し直す
    void speak() const {
        std::cout << "ワン!" << std::endl;
    }
};

class Cat : public Animal {
public:
    void speak() const {
        std::cout << "ニャー!" << std::endl;
    }
};

int main() {
    Animal a;
    Dog d;
    Cat c;

    a.speak();  // 基底クラスの実装
    d.speak();  // Dogの実装
    c.speak();  // Catの実装

    return 0;
}
実行結果
動物が何か音を出します。
ワン!
ニャー!

この例のように、同じspeak()というメソッド名でも、クラスごとに異なる振る舞いをさせることができます

virtual と override でより安全なオーバーライド

C++では、本格的に継承を活用するときにはvirtualoverrideを使うのが一般的です。

これはポリモーフィズムの説明とセットになるため、本記事では概要にとどめます。

  • 基底クラス側でvirtualを付けておくと、その関数は仮想関数になり、ポインタや参照経由で呼び出したときに実行時の型に応じて正しい実装が呼び出されるようになります。
  • 派生クラス側でoverrideを付けると、本当に基底クラスの関数を正しくオーバーライドしているかコンパイラがチェックしてくれます。

簡単な例を示します。

C++
#include <iostream>

class Animal {
public:
    // 仮想関数として宣言
    virtual void speak() const {
        std::cout << "Animal: ..." << std::endl;
    }
};

class Dog : public Animal {
public:
    // オーバーライドであることを明示
    void speak() const override {
        std::cout << "Dog: ワン!" << std::endl;
    }
};

void makeItSpeak(const Animal& animal) {
    // 参照経由でも、実際の型に応じたspeakが呼ばれます
    animal.speak();
}

int main() {
    Animal a;
    Dog d;

    makeItSpeak(a);  // Animal::speak
    makeItSpeak(d);  // Dog::speak

    return 0;
}
実行結果
Animal: ...
Dog: ワン!

ここで登場した「参照経由での呼び出し」や「実行時の型に応じた振る舞い」は、ポリモーフィズム(多態性)の話題になりますので、継承に慣れてきたら次のステップとして学ぶと理解が深まります。

is-a関係を意識した継承設計

「〜は〜の一種である」かどうかを考える

継承を設計するときは、「AはBの一種である(is-a)」と言えるかどうかを常に意識することが重要です。

  • DogはAnimalの一種である → 継承は自然
  • StudentはPersonの一種である → 継承は自然
  • エンジンは車の一種である → 不自然
  • 車はエンジンの一種である → これも不自然

このように、「〜は〜の一種である」という日本語にしてみて、違和感がなければpublic継承を使う候補になります。

逆に、データの構成要素を表したい場合(車がエンジンを持つ、PCがCPUを持つ など)は、継承ではなく「メンバとして持つ」コンポジションを使うほうが適切です。

継承を使うメリット・デメリット

継承にはメリットも多い一方で、設計を誤るとコードが複雑になりがちです。

入門段階で知っておきたいポイントを整理します。

観点メリット注意点(デメリット)
再利用性共通部分を基底クラスにまとめて再利用できる無理な共通化はかえって分かりにくくなる
拡張性新しい派生クラスを追加しやすい基底クラスの変更が階層全体に影響しやすい
可読性is-a関係が明確ならコードの意図が伝わりやすい抽象度が高くなりすぎると理解に時間がかかる
柔軟性ポリモーフィズムと組み合わせると柔軟な設計が可能複雑な継承階層や多重継承はバグの温床になりやすい

「とりあえず継承しておけばよい」という発想は危険です。

最初はシンプルなクラス設計から始めて、明らかに共通化したい部分が見えてきた段階で継承を検討するとよいです。

継承の練習用サンプル: 図形クラス

図形のクラス階層を作ってみる

最後に、簡単な練習として、図形クラスの継承例を見てみます。

ここでは入門に合わせて、virtualの詳しい話には踏み込みすぎず、「共通メンバを持つ基底クラス+具体的な派生クラス」という形を体験してみます。

C++
#include <iostream>

// 図形の基底クラス
class Shape {
protected:
    int x;  // 図形の位置(X座標)
    int y;  // 図形の位置(Y座標)

public:
    Shape(int x, int y)
        : x(x), y(y) {}

    // 共通のインターフェース(ここでは単純に情報を表示するだけ)
    void showPosition() const {
        std::cout << "位置: (" << x << ", " << y << ")" << std::endl;
    }
};

// 円を表す派生クラス
class Circle : public Shape {
private:
    int radius;  // 半径

public:
    Circle(int x, int y, int radius)
        : Shape(x, y),     // 基底クラス部分の初期化
          radius(radius) { // 派生クラス独自メンバの初期化
    }

    void draw() const {
        std::cout << "Circleを描画します。半径: " << radius << std::endl;
    }
};

// 長方形を表す派生クラス
class Rectangle : public Shape {
private:
    int width;   // 幅
    int height;  // 高さ

public:
    Rectangle(int x, int y, int width, int height)
        : Shape(x, y),
          width(width),
          height(height) {
    }

    void draw() const {
        std::cout << "Rectangleを描画します。幅: " << width
                  << ", 高さ: " << height << std::endl;
    }
};

int main() {
    Circle c(10, 20, 5);
    Rectangle r(0, 0, 8, 4);

    std::cout << "Circle:" << std::endl;
    c.showPosition();  // 基底クラスのメンバ関数
    c.draw();          // Circle独自のメンバ関数

    std::cout << std::endl;

    std::cout << "Rectangle:" << std::endl;
    r.showPosition();  // 基底クラスのメンバ関数
    r.draw();          // Rectangle独自のメンバ関数

    return 0;
}
実行結果
Circle:
位置: (10, 20)
Circleを描画します。半径: 5

Rectangle:
位置: (0, 0)
Rectangleを描画します。幅: 8, 高さ: 4

このサンプルでは、Shapeが図形としての共通する性質(位置情報)を持ち、CircleとRectangleが具体的な図形として機能を拡張しています。

基底クラスのshowPosition()はどちらの派生クラスからも利用できるので、重複コードを避けつつ一貫したインターフェースを提供できます。

まとめ

C++におけるクラス継承は、「既存クラスをベースに、新しいクラスを作るための仕組み」です。

public継承を使うと、親クラスのpublicメンバをそのまま子クラスでも利用でき、共通機能の再利用や「〜は〜の一種である」という関係の表現がしやすくなります。

基底クラスのコンストラクタ呼び出し、protectedメンバの扱い、メンバ関数のオーバーライドなどの基本を押さえたうえで、Dog/AnimalやShape/Circleのような小さな例から練習していくと理解が深まりやすいです。

まずはシンプルな継承関係で「共通部分を親に集約する」感覚をつかみ、その先でvirtualやポリモーフィズムへと学びを広げていくと良いでしょう。

継承・ポリモーフィズム・抽象化

クラウドSSLサイトシールは安心の証です。

URLをコピーしました!