オブジェクト指向のC++プログラムを書いていると「抽象クラス」や「純粋仮想関数」という言葉によく出会います。
どちらも「継承」と深く関係する概念ですが、仕組みや使いどころをきちんと理解しておかないと、エラーに悩まされたり、設計が分かりにくくなったりします。
この記事では、純粋仮想関数と抽象クラスの関係や、実際の使い方をサンプルコード付きで丁寧に解説します。
純粋仮想関数とは何か
純粋仮想関数の基本イメージ

純粋仮想関数とは、「派生クラスで必ずオーバーライドさせるための、実装を持たない仮想関数」のことです。
宣言の形はvirtual 戻り値 型名(引数) = 0;のように、関数宣言の末尾に= 0を付けます。
ポイントは「実装がない」「派生クラスで必ず定義させる」という2点です。
これにより、「このクラスはインターフェース(約束事)だけを示し、具体的な処理は派生クラスに任せる」という設計ができます。
純粋仮想関数の宣言例
#include <iostream>
using namespace std;
// 図形の共通インターフェースを表すクラス
class Shape {
public:
// 純粋仮想関数: 描画する処理の「約束」だけを示す
virtual void draw() const = 0;
};
この時点ではShapeクラスにはdraw()の実装がありません。
「Shapeを継承するクラスは、必ずdraw()を実装してください」というメッセージをコンパイラに伝えている状態です。
抽象クラスとの関係と違い
抽象クラスとは

「抽象クラス」とは、少なくとも1つ以上の純粋仮想関数を持つクラスのことです。
C++の言語仕様として「このクラスは抽象クラスです」と宣言するキーワードがあるわけではなく、純粋仮想関数が1つでもあると自動的に抽象クラス扱いになります。
抽象クラスと純粋仮想関数の関係
関係を整理すると、次のようになります。
- 純粋仮想関数
→ 実装を持たない仮想関数で、派生クラスに実装を強制するもの - 抽象クラス
→1つ以上の純粋仮想関数を持つため、自分自身はインスタンスを作れないクラス
つまり「純粋仮想関数を持っているから、そのクラスは抽象クラスになる」という関係です。
抽象クラスの特徴(インスタンス化できない)
純粋仮想関数を持つ抽象クラスは、次の制約があります。
- 抽象クラスそのものを
newしたり、スタック上にインスタンスを作ることはできません - しかし、ポインタや参照型としては扱えるため、「共通の型」として非常に便利です
#include <iostream>
using namespace std;
class Shape {
public:
virtual void draw() const = 0; // 純粋仮想関数
};
int main() {
// Shape s; // コンパイルエラー: 抽象クラスのインスタンスは作れない
Shape* p = nullptr; // ポインタならOK (まだ何も指していない)
cout << "抽象クラスのポインタは定義できます。" << endl;
return 0;
}
抽象クラスのポインタは定義できます。
純粋仮想関数の書き方と基本ルール
基本的な構文
純粋仮想関数は、通常の仮想関数に= 0をつけるだけです。
class Base {
public:
virtual void func() = 0; // 純粋仮想関数
};
この= 0は「0を代入している」わけではなく、「この関数は純粋仮想関数である」という特別な記号だと理解してください。
実装を持たない、とは限らない
実は、純粋仮想関数であっても、クラス定義の外側で実装を書くことは可能です。
ただし、それでも「純粋仮想」であることは変わらず、そのクラスは抽象クラスのままです。
#include <iostream>
using namespace std;
class Base {
public:
virtual void show() = 0; // 宣言は純粋仮想
};
// クラスの外で実装を書くこともできる
void Base::show() {
cout << "Base::show() のデフォルト実装です。" << endl;
}
class Derived : public Base {
public:
// 必ずオーバーライドは必要
void show() override {
cout << "Derived::show() です。" << endl;
// 必要なら Base の実装を呼び出せる
Base::show();
}
};
int main() {
Derived d;
d.show();
return 0;
}
Derived::show() です。
Base::show() のデフォルト実装です。
このように「インターフェースとしては必須だが、共通処理も提供したい」ときに使うテクニックです。
実際の現場では多くありませんが、仕様として知っておくと混乱を防げます。
実例で理解する|抽象クラスと純粋仮想関数
図形クラスを例にした継承構造

次のような構造を考えます。
- 抽象クラス
Shape
→ 純粋仮想関数draw()を持ち、図形を描画する「約束」だけを定義 - 具体的な図形クラス
Circle、Rectangle
→ 各自のdraw()を実装する
#include <iostream>
#include <vector>
#include <memory> // smart pointer を使うため
using namespace std;
// 抽象クラス: 図形の共通インターフェース
class Shape {
public:
// 純粋仮想関数: 描画処理のインターフェース
virtual void draw() const = 0;
// 仮想デストラクタ(派生クラスをポインタで扱うためには必須)
virtual ~Shape() = default;
};
// 具象クラス: 円
class Circle : public Shape {
public:
void draw() const override {
cout << "○ 円を描画します。" << endl;
}
};
// 具象クラス: 長方形
class Rectangle : public Shape {
public:
void draw() const override {
cout << "□ 長方形を描画します。" << endl;
}
};
int main() {
// Shape は抽象クラスなのでインスタンス化できない
// Shape s; // コンパイルエラー
// Shape のポインタ(スマートポインタ)の配列で、さまざまな図形を一括管理
vector<unique_ptr<Shape>> shapes;
shapes.push_back(make_unique<Circle>());
shapes.push_back(make_unique<Rectangle>());
// ポリモーフィズム: 同じ draw() 呼び出しでも、実際の型に応じて処理が変わる
for (const auto& s : shapes) {
s->draw();
}
return 0;
}
○ 円を描画します。
□ 長方形を描画します。
この例では、Shape型のポインタを通して、実際にはCircleやRectangleのdraw()が呼ばれています。
これが、抽象クラス+純粋仮想関数を使う最大の目的である「ポリモーフィズム」の実現です。
どんなときに純粋仮想関数を使うか
インターフェースを明示したいとき

「使う側は共通のインターフェースだけを知っていればよい」という状況で、純粋仮想関数は非常に有用です。
例えば、入出力ストリームを扱う共通のクラスIStreamを作り、そこにread()やwrite()を純粋仮想関数として定義しておきます。
実際の実装はFileStreamやNetworkStreamが担い、使う側はIStream*として扱えば、ファイルかネットワークかを意識せず同じコードで処理できます。
共通処理と「必ず実装させたい」処理を分けたいとき
抽象クラスは、純粋仮想関数だけでなく、通常のメンバ関数やメンバ変数も持てます。
そのため「共通処理は抽象クラス側で実装しつつ、一部だけは各派生クラスに任せる」という設計もよく行われます。
例として、ゲームのキャラクターを表すCharacterクラスを考えます。
#include <iostream>
using namespace std;
// 抽象クラス: キャラクターの基本
class Character {
public:
// テンプレートメソッド的な共通処理
void attack() {
// 共通の前処理
cout << "攻撃準備中..." << endl;
// キャラごとに異なる攻撃方法
doAttack();
// 共通の後処理
cout << "攻撃終了。" << endl;
}
virtual ~Character() = default;
protected:
// 純粋仮想関数: ここだけ派生クラスに任せる
virtual void doAttack() = 0;
};
// 派生クラス: 戦士
class Warrior : public Character {
protected:
void doAttack() override {
cout << "剣で斬りつけた!" << endl;
}
};
// 派生クラス: 魔法使い
class Mage : public Character {
protected:
void doAttack() override {
cout << "火の玉の魔法を唱えた!" << endl;
}
};
int main() {
Warrior w;
Mage m;
cout << "戦士の攻撃:" << endl;
w.attack();
cout << endl;
cout << "魔法使いの攻撃:" << endl;
m.attack();
return 0;
}
戦士の攻撃:
攻撃準備中...
剣で斬りつけた!
攻撃終了。
魔法使いの攻撃:
攻撃準備中...
火の玉の魔法を唱えた!
攻撃終了。
このように「共通の流れは抽象クラスで定義し、一部の処理だけを純粋仮想関数で派生クラスに強制する」ことができます。
よくある注意点と設計のコツ
デストラクタは仮想関数にしておく
抽象クラスをポインタ経由で扱う場合、デストラクタを仮想関数にしておかないと、delete時に未定義動作を招く可能性があります。
抽象クラスでは、かならず仮想デストラクタを宣言する習慣をつけておきましょう。
class Base {
public:
virtual ~Base() = default; // 仮想デストラクタ
virtual void f() = 0;
};
すべての純粋仮想関数を実装しないと、まだ抽象クラス
派生クラスで、基底クラスの純粋仮想関数を1つでも未実装のままにすると、その派生クラスも抽象クラスになり、インスタンス化できません。
コンパイルエラーの原因としてよく出てくるので、「どの純粋仮想関数を実装したか」を常に意識するとよいです。
まとめ
純粋仮想関数は「派生クラスに実装を強制するための、実装を持たない仮想関数」であり、それを1つ以上含むクラスが「抽象クラス」となります。
抽象クラスはインスタンス化できませんが、ポインタや参照を通じて、さまざまな派生クラスを「共通の型」として扱えるようになります。
これにより、ポリモーフィズムを自然に使える設計が可能になります。
インターフェースを明確にしたいときや、共通処理と個別実装をきれいに分けたいときに、純粋仮想関数と抽象クラスを積極的に活用すると、拡張しやすく読みやすいC++コードを書くことができます。
