フリーキーズ | 独学プログラミング

C++継承とポリモーフィズム入門!初心者でも分かる実例とコード解説

リンドくん

リンドくん

たなべ先生、C++で「継承」と「ポリモーフィズム」っていう言葉をよく聞くんですけど、なんだか難しそうで...

たなべ

たなべ

確かに専門用語だと難しく感じるよね。
簡単にいえば、継承は「親から子へ特徴を受け継ぐ」ポリモーフィズムは「同じ名前でも中身が違う」っていう、身近な概念なんだよ。

C++を学んでいると、必ず出会う「継承(inheritance)」と「ポリモーフィズム(polymorphism)」という概念。
これらはオブジェクト指向プログラミングの核心とも言える重要な仕組みです。

一見難しそうに見えるかもしれませんが、実際には私たちの日常生活にも似たような概念が存在しているんです。
例えば、動物という大きなカテゴリから犬や猫が派生し、それぞれが独自の鳴き方を持っているように、プログラムでも基本的なクラスから特化したクラスを作り、それぞれが独自の動作を持てるのです。

この記事では、C++の継承とポリモーフィズムについて、基本概念から実践的な使い方まで、初心者の方でも理解できるよう段階的に解説していきます。

プログラミング学習でお悩みの方へ

HackATAは、エンジニアを目指す方のためのプログラミング学習コーチングサービスです。 経験豊富な現役エンジニアがあなたの学習をサポートします。

✓ 質問し放題

✓ β版公開中(2025年内の特別割引)

HackATAの詳細を見る

継承の基本概念 - クラスの親子関係を理解しよう

リンドくん

リンドくん

継承って具体的にはどういうことなんですか?

たなべ

たなべ

簡単に言うと、既存のクラスの特徴を引き継いで新しいクラスを作る仕組みなんだ。
例えば「動物」クラスから「犬」クラスを作る感じだね。

継承とは何か

継承(inheritance)とは、既存のクラス(親クラス、基底クラス)の機能を受け継いで、新しいクラス(子クラス、派生クラス)を作成する仕組みです。
この仕組みにより、コードの再利用性が大幅に向上し、保守性の高いプログラムを作ることができます。

継承の最大のメリットは以下の通りです。

  • コードの重複を避けられる - 共通の機能を親クラスにまとめられます
  • 機能の拡張が簡単 - 基本機能はそのままに、新しい機能を追加できます
  • 保守性が向上 - 共通部分の修正が一箇所で済みます

基本的な継承の書き方

まずは、シンプルな継承の例を見てみましょう。

#include <iostream>
#include <string>

// 基底クラス(親クラス)
class Animal
{
protected:
    std::string name;
    int age;
    
public:
    Animal(const std::string& n, int a) : name(n), age(a)
    {
        std::cout << "動物 " << name << " が生まれました" << std::endl;
    }
    
    void sleep()
    {
        std::cout << name << " が眠っています" << std::endl;
    }
    
    void eat()
    {
        std::cout << name << " が食事をしています" << std::endl;
    }
    
    std::string getName() const
    {
        return name;
    }
};

// 派生クラス(子クラス)
class Dog : public Animal
{
public:
    Dog(const std::string& n, int a) : Animal(n, a)
    {
        std::cout << "犬として特化されました" << std::endl;
    }
    
    void bark()
    {
        std::cout << name << " がワンワン吠えています" << std::endl;
    }
    
    void wagTail()
    {
        std::cout << name << " が尻尾を振っています" << std::endl;
    }
};

この例では、Animalクラスが基底クラス、Dogクラスが派生クラスになっています。
DogクラスはAnimalクラスの機能(sleep()eat()など)をすべて受け継ぎつつ、犬特有の機能(bark()wagTail())を追加しています。

アクセス指定子の理解

継承において重要なのがアクセス指定子の理解です。

  • public - どこからでもアクセス可能
  • protected - 自分のクラスと派生クラスからアクセス可能
  • private - 自分のクラスからのみアクセス可能

上の例でnameageprotectedにしているのは、派生クラス(Dog)からもアクセスできるようにするためです。
もしprivateにしてしまうと、派生クラスからアクセスできなくなってしまいます。

virtual関数とポリモーフィズムの実装

リンドくん

リンドくん

ポリモーフィズムって何ですか?名前からして難しそう...

たなべ

たなべ

ポリモーフィズムは「多態性」とも呼ばれるんだ。同じ関数名でも、オブジェクトの種類によって違う動作をする仕組みなんだよ。
これがvirtual関数で実現できるんだ。

ポリモーフィズムとは

ポリモーフィズム(多態性)とは、同じインターフェースを持ちながら、オブジェクトの実際の型に応じて異なる動作をする仕組みです。
これにより、柔軟で拡張性の高いプログラムを作ることができます。

C++ではvirtual関数を使ってポリモーフィズムを実現します。
virtual関数を使うことで、実行時にオブジェクトの実際の型を判断し、適切な関数を呼び出すことができます。

virtual関数の実装例

先ほどの動物の例を拡張して、ポリモーフィズムを実装してみましょう。

#include <iostream>
#include <string>
#include <vector>
#include <memory>

class Animal
{
protected:
    std::string name;
    int age;
    
public:
    Animal(const std::string& n, int a) : name(n), age(a) {}
    
    // virtual関数 - 派生クラスでオーバーライド可能
    virtual void makeSound()
    {
        std::cout << name << "が何かの音を出しています" << std::endl;
    }
    
    virtual void move()
    {
        std::cout << name << "が移動しています" << std::endl;
    }
    
    // 仮想デストラクタも重要
    virtual ~Animal() = default;
    
    std::string getName() const { return name; }
};

class Dog : public Animal
{
public:
    Dog(const std::string& n, int a) : Animal(n, a) {}
    
    // virtual関数をオーバーライド
    void makeSound() override
    {
        std::cout << name << "がワンワン吠えています" << std::endl;
    }
    
    void move() override
    {
        std::cout << name << "が4本足で走っています" << std::endl;
    }
};

class Cat : public Animal
{
public:
    Cat(const std::string& n, int a) : Animal(n, a) {}
    
    void makeSound() override
    {
        std::cout << name << "がニャーニャー鳴いています" << std::endl;
    }
    
    void move() override
    {
        std::cout << name << "が軽やかに歩いています" << std::endl;
    }
};

class Bird : public Animal
{
public:
    Bird(const std::string& n, int a) : Animal(n, a) {}
    
    void makeSound() override
    {
        std::cout << name << "がチュンチュン歌っています" << std::endl;
    }
    
    void move() override
    {
        std::cout << name << "が空を飛んでいます" << std::endl;
    }
};

ポリモーフィズムの威力を実感する

ここからがポリモーフィズムの真骨頂です。
異なる動物たちを同じ方法で扱えるようになります。

int main()
{
    // 異なる動物のオブジェクトを作成
    std::vector<std::unique_ptr<Animal>> animals;
    
    animals.push_back(std::make_unique<Dog>("ポチ", 3));
    animals.push_back(std::make_unique<Cat>("ミケ", 2));
    animals.push_back(std::make_unique<Bird>("ピーちゃん", 1));
    
    std::cout << "=== 動物たちの行動 ===" << std::endl;
    
    // 同じコードで異なる動物を扱える!
    for (const auto& animal : animals)
    {
        std::cout << "\n" << animal->getName() << "の行動:" << std::endl;
        animal->makeSound();  // それぞれの動物に応じた音が出る
        animal->move();       // それぞれの動物に応じた移動をする
    }
    
    return 0;
}

このコードを実行すると、以下のような出力が得られます。

=== 動物たちの行動 ===

ポチの行動:
ポチがワンワン吠えています
ポチが4本足で走っています

ミケの行動:
ミケがニャーニャー鳴いています
ミケがしなやかに歩いています

ピーちゃんの行動:
ピーちゃんがチュンチュン歌っています
ピーちゃんが空を飛んでいます

重要なのは、ループの中で同じコード(animal->makeSound()animal->move())を書いているにも関わらず、それぞれの動物に応じた異なる動作が実行されていることです。これがポリモーフィズムの威力なのです。

抽象クラスとインターフェース

純粋仮想関数の活用

ここからは純粋仮想関数virtual 関数名() = 0;)について詳しく見てみましょう。
純粋仮想関数を持つクラスは抽象クラスと呼ばれ、直接インスタンス化することができません。

// これは抽象クラス
class AbstractVehicle
{
public:
    // 純粋仮想関数 - 派生クラスで必ず実装が必要
    virtual void start() = 0;
    virtual void stop() = 0;
    virtual void accelerate() = 0;
    
    // 通常の仮想関数 - デフォルト実装を提供
    virtual void honk()
    {
        std::cout << "プップー!" << std::endl;
    }
    
    virtual ~AbstractVehicle() = default;
};

class Car : public AbstractVehicle
{
public:
    void start() override
    {
        std::cout << "エンジンスタート!" << std::endl;
    }
    
    void stop() override
    {
        std::cout << "エンジンストップ" << std::endl;
    }
    
    void accelerate() override
    {
        std::cout << "アクセル踏み込み" << std::endl;
    }
};

class Bicycle : public AbstractVehicle
{
public:
    void start() override
    {
        std::cout << "ペダル漕ぎ始め" << std::endl;
    }
    
    void stop() override
    {
        std::cout << "ブレーキをかける" << std::endl;
    }
    
    void accelerate() override
    {
        std::cout << "ペダルを早く漕ぐ" << std::endl;
    }
    
    // honk()をオーバーライド
    void honk() override
    {
        std::cout << "チリンチリン!" << std::endl;
    }
};

抽象クラスを使うことで、派生クラスが必ず実装すべき機能を強制できます。
これにより、設計者の意図を明確に示し、実装漏れを防ぐことができます。

継承とポリモーフィズムのベストプラクティス

リンドくん

リンドくん

継承とポリモーフィズム、だんだん分かってきました!でも、使う時に気をつけることはありますか?

たなべ

たなべ

使い方を間違えると複雑になりすぎることもあるんだ。
いくつかの重要なポイントを押さえておこう。

適切な継承設計のポイント

1. "is-a"関係を意識する

継承は「AはBの一種である」という関係の時に使います。
例えば、「犬は動物の一種である」は自然ですが、「車は道路の一種である」は不自然ですよね。

// 良い例:"Dog is-a Animal"
class Animal { /* ... */ };
class Dog : public Animal { /* ... */ };

// 悪い例:"Car has-a Engine"(これは継承ではなくコンポジション)
class Engine { /* ... */ };
class Car : public Engine { /* ... */ };  // 不適切

// 正しくはこう
class Car
{
private:
    Engine engine;  // コンポジション(has-a関係)
public:
    // ...
};

2. 仮想デストラクタを忘れずに

基底クラスのポインタで派生クラスのオブジェクトを削除する場合、仮想デストラクタが必要です。

class Base
{
public:
    virtual ~Base() = default;  // 重要!
    virtual void func() = 0;
};

class Derived : public Base
{
public:
    ~Derived() override
    {
        std::cout << "Derivedのデストラクタ" << std::endl;
    }
    
    void func() override { /* ... */ }
};

3. override キーワードの活用

C++11以降では、overrideキーワードを使って意図を明確にしましょう。

class Base
{
public:
    virtual void process() { /* ... */ }
};

class Derived : public Base
{
public:
    void process() override  // overrideで意図を明確に
    {
        // ...
    }
    
    // タイプミスもコンパイルエラーで検出される
    // void proces() override;  // エラー!基底クラスにない
};

パフォーマンスの考慮

仮想関数には若干のオーバーヘッドがあります。しかし、現代のコンパイラは非常に賢く、多くの場合で最適化してくれます。
premature optimization(早すぎる最適化)は避け、まずは設計の清潔さを重視しましょう。

// パフォーマンスが重要でない場合は、設計の美しさを優先
class GameObject
{
public:
    virtual void update() = 0;
    virtual void render() = 0;
    virtual ~GameObject() = default;
};

// ゲームのメインループで毎フレーム呼ばれる場合は検討が必要
// しかし、まずは動くものを作ってから最適化を考える

まとめ

リンドくん

リンドくん

継承とポリモーフィズム、最初は難しく感じましたが、実例を見ると分かりやすいですね!

たなべ

たなべ

そうだね!概念を理解して実際にコードを書いてみることが一番の学習方法だよ。
最初は簡単な例から始めて、だんだん複雑なものに挑戦していこう。

C++の継承とポリモーフィズムは、オブジェクト指向プログラミングの核心的な概念です。
これらを理解することで、より柔軟で保守性の高いプログラムを作成できるようになります。

今回学んだ重要なポイント

  • 継承は既存のクラスの機能を受け継いで新しいクラスを作る仕組み
  • ポリモーフィズムは同じインターフェースで異なる動作を実現する仕組み
  • virtual関数を使ってポリモーフィズムを実装する
  • 純粋仮想関数で抽象クラスを作り、派生クラスに実装を強制できる
  • 適切な設計により、拡張性と保守性を両立できる

プログラミングの学習は、概念を理解するだけでなく、実際に手を動かして体験することが最も重要です。
今回の例を参考に、ぜひ自分なりのクラス設計に挑戦してみてください。

この記事をシェア

関連するコンテンツ