オブジェクト指向のC++では、既存のクラスを基に新しいクラスを作り、共通の振る舞いを共有しつつ差異だけを拡張する継承が重要です。
ここでは、継承の基本から、仮想関数・アクセス制御・コンストラクタの扱い、多重継承・抽象クラスまで、実用的なコード例とともに丁寧に解説します。
C++継承の基礎:継承とは何か・メリットとIS-A関係
目的と効果:コード再利用・拡張性・ポリモーフィズム
継承は、あるクラス(基底クラス)の性質を受け継いだ新しいクラス(派生クラス)を定義する仕組みです。
重複コードを減らすだけでなく、共通のインターフェースを持ちつつ具体的な振る舞いを差し替える拡張性が得られます。
特に、基底型のポインタや参照を通じて派生型の実装を呼び分けるポリモーフィズム(多態性)により、疎結合かつ柔軟な設計が可能になります。
用語整理:基底クラスと派生クラスの違い
基底クラスは共通の性質(データと振る舞い)を提供する土台です。
派生クラスはその土台を引き継ぎ、必要に応じて機能を追加したり、仮想関数をオーバーライドして挙動を変更します。
C++では継承関係はクラス定義の冒頭で宣言され、基底→派生の順に構築・破棄が行われます。
継承を使うべきケース/合成(コンポジション)との比較
継承は「XはYである(IS-A)」が明確なときに用います。
例えば「円は図形である」は自然です。
一方、「車はエンジンを持つ(HAS-A)」は合成(コンポジション)で表現します。
合成は実装の入れ替えやテスト容易性に優れます。
曖昧な場合はまず合成を選び、必要が明確なときに継承に切り替えるのが安全です。
基底クラスと派生クラスの定義方法
public継承の基本構文と書き方
public継承は「IS-A」を表現する標準的な継承です。
基底のpublicインターフェースをそのまま公開し、ポリモーフィズムの前提になることが多いです。
#include <iostream>
#include <memory>
#include <vector>
#include <string>
#include <cmath>
// 基底クラス:図形
class Shape {
public:
// ポリモーフィズムのために仮想デストラクタを宣言
virtual ~Shape() { std::cout << "Shape::~Shape()\n"; }
// 派生で実装を差し替える仮想関数
virtual double area() const = 0; // 純粋仮想:抽象クラスにする
virtual void draw() const { // 既定実装を持つ仮想関数
std::cout << "Drawing a generic shape.\n";
}
};
// 派生クラス:長方形
class Rectangle : public Shape { // public継承
double w{};
double h{};
public:
Rectangle(double w, double h) : w(w), h(h) {}
double area() const override { return w * h; }
void draw() const override { std::cout << "Drawing rectangle " << w << "x" << h << "\n"; }
};
// 派生クラス:円
class Circle final : public Shape { // final:これ以上継承させない
double r{};
public:
explicit Circle(double r) : r(r) {}
double area() const override { return 3.141592653589793 * r * r; }
void draw() const override { std::cout << "Drawing circle r=" << r << "\n"; }
};
int main() {
std::vector<std::unique_ptr<Shape>> shapes;
shapes.emplace_back(std::make_unique<Rectangle>(3, 4));
shapes.emplace_back(std::make_unique<Circle>(2));
double total = 0.0;
for (const auto& s : shapes) {
s->draw(); // 動的束縛:派生のdrawが呼ばれる
total += s->area(); // 純粋仮想の実装(派生)へ
}
std::cout << "Total area = " << total << "\n";
}
Drawing rectangle 3x4
Drawing circle r=2
Total area = 18.5664
Shape::~Shape()
Shape::~Shape()
メンバー関数とデータメンバーの継承ルール
非staticのデータメンバーは派生クラスのオブジェクトの内部に物理的に含まれます。
privateメンバーは派生から直接アクセスできませんがオブジェクトの一部です。
public/protectedメンバー関数は派生でも利用可能で、仮想関数ならオーバーライドできます。
コンストラクタとデストラクタ、代入演算子は「継承されず」、派生で必要に応じて定義・宣言します(ただし後述のusingによるコンストラクタ継承の例外あり)。
オーバーライド対象を意識した基底クラス設計
基底クラスは、派生が差し替えるべき関数にvirtualを付与し、必要であれば純粋仮想にして抽象化します。
デストラクタは多態的に扱う可能性があるなら必ずvirtualにします。
非仮想関数はテンプレートメソッドの要領で挙動の骨組みを提供し、詳細を仮想関数に委ねると拡張が容易です。
アクセス指定子と継承モード:public/protected/privateの違い
メンバーの可視性:public・protected・privateの使い分け
publicは外部API、protectedは派生にだけ見せたい実装詳細、privateはクラス内部のみに限定します。
protectedの濫用は派生に内部実装を漏らし、結合度を高めます。
できる限りpublicの小さなインターフェース+privateな実装、必要最小限のprotectedという方針が堅実です。
継承指定子(public/private/protected継承)がアクセスに与える影響
継承時の指定子により、基底のpublic/protectedが派生クラスの中からどう見えるか、さらに「派生型の外部」にどう公開されるかが変わります。
次の表は「派生クラスの外側から見た公開状態」をまとめたものです。
基底での可視性 | public継承での外部公開 | protected継承での外部公開 | private継承での外部公開 |
---|---|---|---|
public | publicのまま | 非公開(隠蔽) | 非公開(隠蔽) |
protected | 非公開(派生内部のみ) | 非公開(派生内部のみ) | 非公開(派生内部のみ) |
private | 非公開(継承されるがアクセス不可) | 非公開 | 非公開 |
派生クラスの内部からの見え方は次のとおりです。
public継承では基底のpublic/protectedにアクセス可能、privateには不可。
protected継承・private継承でも派生内部からはpublic/protectedにアクセス可能ですが、外部には露出しません。
IS-Aを表す場合はpublic継承以外を原則避けます。
フレンドとprotectedの安全な活用
friendはクラスの実装に強く結合するため乱用は禁物です。
テスト用ユーティリティやシリアライザなど限定的な相手にだけ付与し、可能ならpublic APIの拡充や非メンバー関数+名前空間による分離を検討します。
protectedメンバーは派生の拡張に必要な最小集合に止め、直接のデータメンバー公開ではなくprotectedのゲッター・セッター(不変条件を保つ)を使うと安全です。
#include <iostream>
class Base {
int secret_ = 42; // private
protected:
void set_secret(int v) { secret_ = v; } // 派生が触れる最小API
int get_secret() const { return secret_; }
public:
virtual ~Base() = default;
};
class Derived : public Base {
public:
void tweak() {
// secret_ へ直接アクセスは不可(private)
// set_secret/get_secret を通じて不変条件を保つ
set_secret(get_secret() + 1);
}
};
int main() {
Derived d;
d.tweak();
// d.secret_ や d.set_secret(…) は外部から不可
std::cout << "OK\n";
}
OK
仮想関数とオーバーライド:ポリモーフィズム入門
virtual・override・finalの書き方
virtualは基底で宣言します。
派生ではoverrideを必ず付け、シグネチャの不一致をコンパイラに検出させます。
さらに派生を禁止したい最終クラスや最終メンバーにはfinalを付けます。
struct B {
virtual void f(int) {}
virtual void g() final {} // gはこれ以上オーバーライド不可
};
struct D final : B { // D自体も継承不可
void f(int) override {} // 正しいオーバーライド
// void g() override {} // エラー: finalの再定義
};
仮想デストラクタの必要性と安全な破棄
基底を通じてdeleteする可能性があるなら、デストラクタは必ずvirtualにします。
そうでないと未定義動作を招き、派生のリソースが解放されないことがあります。
#include <iostream>
struct BadBase {
~BadBase() { std::cout << "~BadBase\n"; } // 仮想でない
};
struct BadDerived : BadBase {
~BadDerived() { std::cout << "~BadDerived\n"; }
};
struct GoodBase {
virtual ~GoodBase() { std::cout << "~GoodBase\n"; } // 仮想
};
struct GoodDerived : GoodBase {
~GoodDerived() override { std::cout << "~GoodDerived\n"; }
};
int main() {
BadBase* p1 = new BadDerived();
delete p1; // 未定義動作(~BadDerivedが呼ばれない可能性)
GoodBase* p2 = new GoodDerived();
delete p2; // 安全:派生→基底の順に呼ばれる
}
~GoodDerived
~GoodBase
現代C++ではunique_ptr/shared_ptrなどのスマートポインタと組み合わせると破棄漏れをさらに防げます。
#include <iostream>
#include <memory> // std::unique_ptr を使用するために必要
struct BadBase {
~BadBase() { std::cout << "~BadBase\n"; } // 仮想でない
};
struct BadDerived : BadBase {
~BadDerived() { std::cout << "~BadDerived\n"; }
};
struct GoodBase {
virtual ~GoodBase() { std::cout << "~GoodBase\n"; } // 仮想
};
struct GoodDerived : GoodBase {
~GoodDerived() override { std::cout << "~GoodDerived\n"; }
};
int main() {
// BadBase* p1 = new BadDerived();
// delete p1; // 未定義動作(~BadDerivedが呼ばれない可能性)
// std::unique_ptr に置き換え
std::unique_ptr<BadBase> p1 = std::make_unique<BadDerived>();
// スコープの終わりに自動的に delete が呼ばれる
// この場合も、~BadBase のデストラクタしか呼ばれないため、依然として未定義動作
// GoodBase* p2 = new GoodDerived();
// delete p2; // 安全:派生→基底の順に呼ばれる
// std::unique_ptr に置き換え
std::unique_ptr<GoodBase> p2 = std::make_unique<GoodDerived>();
// スコープの終わりに自動的に delete が呼ばれる
// 仮想デストラクタにより、~GoodDerived が正しく呼ばれるため安全
}
~GoodDerived
~GoodBase
~BadBase
動的束縛とvtableの基本理解
仮想関数の呼び出しは通常、実行時に実オブジェクトの型に基づき解決されます(動的束縛)。
多くの実装は各型にvtable(仮想関数テーブル)を持ち、ポインタ経由で関数ポインタを辿ります。
最適化によりインライン化される場合もありますが、設計上は「仮想呼び出しはコストがある」ことを意識し、必要な箇所に限定するのが良いです。
コンストラクタ/デストラクタと基底クラス初期化
初期化リストでの基底クラスコンストラクタ呼び出し
派生は必ず基底のコンストラクタを最初に呼び出します。
明示しない場合は基底のデフォルトコンストラクタが選ばれます。
#include <iostream>
#include <string>
class Person {
std::string name_;
public:
explicit Person(std::string name) : name_(std::move(name)) {
std::cout << "Person(" << name_ << ")\n";
}
virtual ~Person() { std::cout << "~Person\n"; }
const std::string& name() const { return name_; }
};
class Employee : public Person {
int id_{};
public:
Employee(std::string name, int id)
: Person(std::move(name)) // 基底を初期化リストで
, id_(id) {
std::cout << "Employee(id=" << id_ << ")\n";
}
~Employee() override { std::cout << "~Employee\n"; }
};
int main() {
Employee e("Alice", 1001);
}
Person(Alice)
Employee(id=1001)
~Employee
~Person
using宣言によるコンストラクタ継承
基底のコンストラクタ群を派生に引き継ぐにはusingを使います。
独自の追加メンバー初期化が必要なときは、メンバーの既定値と組み合わせます。
#include <string>
#include <iostream>
struct Url {
std::string s;
explicit Url(std::string u) : s(std::move(u)) {}
};
struct HttpEndpoint {
Url url;
explicit HttpEndpoint(Url u) : url(std::move(u)) {}
};
struct SecureEndpoint : HttpEndpoint {
using HttpEndpoint::HttpEndpoint; // コンストラクタ継承
bool tls{true}; // 追加メンバーは既定値で
};
int main() {
SecureEndpoint ep(Url{"https://example.com"});
std::cout << ep.url.s << " tls=" << std::boolalpha << ep.tls << "\n";
}
https://example.com tls=true
注意点として、コンストラクタ継承は「そのまま基底の引数を受け取るだけ」です。
追加の前処理・検証が必要なら、派生で明示的なコンストラクタを定義してください。
コピー/ムーブと継承:=default・=deleteの指針
継承階層では資源管理の一貫性を保つため、特殊メンバ関数の方針を明確にします。
ムーブ可能にしたい場合、基底・派生ともにムーブの安全性を満たす設計(所有権の一意性、ポインタ無効化)にして、=defaultで自動生成を利用します。
コピー禁止にしたい場合は=deleteを使います。
#include <iostream>
#include <memory>
struct BaseBuf {
std::unique_ptr<int[]> buf;
BaseBuf() : buf(new int[10]) {}
virtual ~BaseBuf() = default;
BaseBuf(const BaseBuf&) = delete; // コピー禁止
BaseBuf& operator=(const BaseBuf&) = delete;
BaseBuf(BaseBuf&&) noexcept = default; // ムーブ許可
BaseBuf& operator=(BaseBuf&&) noexcept = default;
};
struct DerivedBuf : BaseBuf {
int extra = 0;
// 基底がムーブ可能なので、派生も=defaultでよい
DerivedBuf() = default;
DerivedBuf(DerivedBuf&&) noexcept = default;
DerivedBuf& operator=(DerivedBuf&&) noexcept = default;
};
int main() {
DerivedBuf d1;
DerivedBuf d2 = std::move(d1); // ムーブ
std::cout << "moved\n";
}
moved
多重継承と抽象クラス(インターフェース)の基礎
純粋仮想関数でインターフェースを定義する
抽象クラスは少なくとも一つの純粋仮想関数を持ち、インスタンス化できません。
インターフェース用途ならデータメンバーを持たず、仮想デストラクタを宣言します。
#include <iostream>
#include <string>
struct IPrintable {
virtual ~IPrintable() = default;
virtual void print() const = 0;
};
struct ILogger {
virtual ~ILogger() = default;
virtual void log(const std::string& msg) = 0;
};
class User : public IPrintable, public ILogger {
std::string name_;
public:
explicit User(std::string n) : name_(std::move(n)) {}
void print() const override { std::cout << "User(" << name_ << ")\n"; }
void log(const std::string& msg) override { std::cout << "[LOG] " << msg << "\n"; }
};
int main() {
User u("Bob");
IPrintable& p = u;
ILogger& l = u;
p.print();
l.log("created");
}
User(Bob)
[LOG] created
多重継承の注意点:ダイヤモンド継承とvirtual継承
ダイヤモンド継承では同じ基底が二重に含まれてしまい、曖昧性や重複状態が問題になります。
virtual継承で共有基底を一つに統合します。
#include <iostream>
// 共有したい基底
struct Animal {
int age = 0;
virtual ~Animal() = default;
};
// 二系統が同じAnimalを仮想継承
struct Mammal : virtual Animal {};
struct Marine : virtual Animal {};
// 交差する派生
struct Whale : Mammal, Marine {
void birthday() { ++age; } // Animal::age は1つに統合される
};
int main() {
Whale w;
w.birthday();
std::cout << w.age << "\n"; // 曖昧性なしでアクセス可能
}
1
virtual継承はオブジェクトレイアウトやコンストラクタの複雑性を増し、間接参照のコストも発生します。
必要が明確な場合のみに限り、代替として合成や委譲も検討します。
設計ベストプラクティス:最小の公開APIと依存の分離
継承階層は浅く、小さく保ちます。
public継承は厳密なIS-Aが成り立つ関係に限定し、振る舞いの再利用だけが目的なら合成・委譲を優先します。
基底のpublic APIは最小限に抑え、protectedは慎重に付与します。
仮想関数にはoverrideを徹底し、テスト容易性のために依存はインターフェース(抽象クラス)にぶら下げ、実装とは疎結合に保ちます。
合成との対比コード例
#include <iostream>
#include <memory>
// 継承でなく合成:エンジンを持つ車
class Engine {
public:
void start() const { std::cout << "Engine start\n"; }
};
class Car { // Car is-a Engine ではないので継承しない
std::unique_ptr<Engine> engine_;
public:
Car() : engine_(std::make_unique<Engine>()) {}
void drive() {
engine_->start(); // 委譲
std::cout << "Driving...\n";
}
};
int main() {
Car c;
c.drive();
}
Engine start
Driving...
このように「持っている(HAS-A)」関係は合成が適切で、交換可能性やテストの差し替えも容易になります。
まとめ
継承は「既存のクラスを基に新しいクラスを作る」強力な道具ですが、使いどころと作法が品質を左右します。
IS-Aが明確なときにpublic継承を選び、基底には仮想デストラクタを用意し、差し替えたい振る舞いにはvirtualを付与して派生ではoverrideを徹底します。
アクセス制御はpublicを最小限に、protectedは慎重に、privateで実装詳細を隠蔽します。
コンストラクタは初期化リストで基底を確実に初期化し、必要に応じてusingで継承できます。
コピー/ムーブの方針は=defaultと=deleteで明示し、資源管理の整合性を保ちます。
多重継承やvirtual継承は有効な場面もありますが複雑性が高く、まずは合成・委譲を検討するのが安全です。
これらの原則を押さえれば、拡張しやすく保守しやすい継承設計が実現できます。