本文へスキップ
BecomeCoder

C++コース · 第18章 実務のC++ ― 分離とモダンな型 · レッスン71

ヘッダと実装の分離 ― 宣言と定義・.h と .cpp

ローカル実施

導入

1つの .cpp ファイルにすべてを書き続けると、プログラムが大きくなるほど見通しが悪くなります。実務の C++ では、外部に見せる「宣言」をヘッダファイル(.h)に、実際の処理である「定義」を実装ファイル(.cpp)に分けて書きます。

この回は読み物です。ブラウザの学習環境は単一ファイルなので、複数ファイルへの分離は手元の環境(g++ / clang など)で試してください。

説明

まず「宣言」と「定義」の違いを区別します。宣言は「名前と型(プロトタイプ)だけ」、定義は「実際の中身(実体)」です。

double circleArea(double r);          // 宣言:名前・引数・戻り値の型だけ

double circleArea(double r) {         // 定義:中身(実体)がある
    return 3.14159 * r * r;
}

ヘッダファイルには宣言だけを置き、複数の .cpp ファイルから #include して共有します。第17章の namespace を使い、宣言・定義の両方を名前空間の中に入れます。

// geometry.h(宣言だけを置くヘッダファイル)
#pragma once   // 二重インクルード防止(今どきはこれで十分)

namespace geometry {
    double circleArea(double r);      // 宣言のみ。中身はここには書かない
}
// geometry.cpp(定義=実体を置く実装ファイル)
#include "geometry.h"

namespace geometry {
    double circleArea(double r) {     // ClassName::method() 同様、名前空間::関数名で定義
        return 3.14159 * r * r;
    }
}
// main.cpp(geometry.h を include して使う側)
#include <iostream>
#include "geometry.h"
using namespace std;

int main() {
    cout << geometry::circleArea(2.0) << endl;   // 12.5664
    return 0;
}

#pragma once は「このファイルは一度しか読み込まない」という合図です。同じヘッダを複数の .cpp から #include しても、二重に取り込まれて壊れることを防ぎます。古いコードでは #ifndef GEOMETRY_H / #define GEOMETRY_H / #endif という書き方(インクルードガード)も使われますが、意味は同じです。

クラスも同じ考え方で分離します。ヘッダにはクラス定義(メンバの一覧)を、メンバ関数の実体は .cpp に クラス名::関数名(){ ... } の形で書きます。

// shape.h
#pragma once

namespace shapes {
    class Circle {
    public:
        Circle(double r);        // コンストラクタの宣言
        double area() const;     // メンバ関数の宣言
    private:
        double radius;
    };
}
// shape.cpp
#include "shape.h"

namespace shapes {
    Circle::Circle(double r) : radius(r) {}     // コンストラクタの定義

    double Circle::area() const {               // メンバ関数の定義
        return 3.14159 * radius * radius;
    }
}
flowchart LR
  h["geometry.h(宣言)"]
  h --> mcpp["main.cpp"]
  h --> gcpp["geometry.cpp"]
  mcpp --> mo["main.o(コンパイル)"]
  gcpp --> go["geometry.o(コンパイル)"]
  mo --> link["リンク"]
  go --> link
  link --> exe["実行ファイル"]

各 .cpp ファイルは「コンパイル単位」として個別にコンパイルされ、.o(オブジェクトファイル)になります。最後に「リンカ」がすべての .o を集めて1つの実行ファイルにまとめます。関数を呼ぶ側(main.cpp)は、コンパイルの時点では geometry.h の宣言さえ見えていれば十分で、実体はリンクの段階で結びつけられます。

この仕組みを支えるのが ODR(One Definition Rule/1定義規則) です。「同じものの定義(実体)は、実行ファイル全体でちょうど1つでなければならない」というルールで、宣言(ヘッダに置く)はいくつの .cpp に取り込まれても構いませんが、定義(実体)を誤ってヘッダに書いて複数の .cpp から #include すると、リンク時に「二重定義」エラーになります(inline 関数やテンプレートは例外です)。

まとめ

ヘッダファイル(.h)には宣言、実装ファイル(.cpp)には定義を置きます。#pragma once(またはインクルードガード)で二重インクルードを防ぎます。各 .cpp は個別にコンパイルされて .o になり、リンカが1つの実行ファイルにまとめます。ODR(1定義規則)は「実体は1つだけ」という約束です。次のレッスンでは、こうして整理した設計の中で使う、複数の型を1つで扱う道具を見ていきます。

次のレッスンへ →