my.code(); Logomy.code();
C++-5.プロジェクトの分割とビルド

my.code(); Logomy.code();

  • C++
    • Sandbox
    • 0.C++の世界へようこそ
    • 1.型システムと制御構造
    • 2.データ集合とモダンな操作
    • 3.ポインタとメモリ管理
    • 4.関数と参照渡し
    • 5.プロジェクトの分割とビルド
    • 6.クラスの基礎
    • 7.クラスを使いこなす
    • 8.継承とポリモーフィズム
    • 9.テンプレート
    • 10.STL ①:コンテナ
    • 11.STL ②:アルゴリズムとラムダ式
    • 12.RAIIとスマートポインタ
  • JavaScript
    • Sandbox
    • 0.JavaScriptへようこそ
    • 1.基本構文とデータ型
    • 2.制御構文
    • 3.関数とクロージャ
    • 4.'this'の正体
    • 5.オブジェクトとプロトタイプ
    • 6.クラス構文
    • 7.配列とイテレーション
    • 8.非同期処理①: Promise
    • 9.非同期処理②: Async/Await
  • Python
    • Sandbox
    • 0.環境構築と基本思想
    • 1.基本構文とデータ型
    • 2.リスト、タプル、辞書、セット
    • 3.制御構文と関数
    • 4.モジュールとパッケージ
    • 5.オブジェクト指向プログラミング
    • 6.ファイルの入出力とコンテキストマネージャ
    • 7.例外処理
    • 8.ジェネレータとデコレータ
  • Ruby
    • Sandbox
    • 0.rubyの世界へようこそ
    • 1.基本構文とデータ型
    • 2.制御構造とメソッド定義
    • 3.すべてがオブジェクト
    • 4.コレクション (Array, Hash, Range)
    • 5.ブロックとイテレータ
    • 6.クラスとオブジェクト
    • 7.モジュールとMix-in
    • 8.Proc, Lambda, クロージャ
    • 9.標準ライブラリの活用
    • 10.テスト文化入門
    • 11.メタプログラミング入門
  • Rust
    • Sandbox
    • 0.Rustの世界へようこそ
    • 1.基本構文と「不変性」
    • 2.関数と制御フロー
    • 3.所有権
    • 4.借用とスライス
    • 5.構造体とメソッド構文
    • 6.列挙型とパターンマッチ
    • 7.モジュールシステムとパッケージ管理
    • 8.コレクションと文字列
    • 9.エラーハンドリング
    • 10.ジェネリクスとトレイト
    • 11.ライフタイム
  • TypeScript
    • Sandbox
    • 0.TypeScriptへようこそ
    • 1.基本的な型と型推論
    • 2.オブジェクト、インターフェース、型エイリアス
    • 3.関数の型定義
    • 4.型を組み合わせる
    • 5.ジェネリクス
    • 6.クラスとアクセス修飾子
    • 7.非同期処理とユーティリティ型
my.code(); Logomy.code();

環境構築不要、その場で実践。

ut-code / my-code

Copyright © 2026 ut.code();

my.code(); について
コード実行環境についてAI質問機能についてライセンスお問い合わせ
ut.code(); について
公式ウェブサイト公式 𝕏 アカウント

第5章: プロジェクトの分割とビルド

これまでの章では、すべてのコードを1つの .cpp ファイルに記述してきました。しかし、プログラムが大規模で複雑になるにつれて、このアプローチは現実的ではなくなります。コードの可読性が低下し、少しの変更でもプログラム全体の再コンパイルが必要になり、開発効率が大きく損なわれるからです。

この章では、プログラムを複数のファイルに分割し、それらを効率的に管理・ビルドする方法を学びます。これは、小さなプログラムから一歩進み、本格的なソフトウェア開発を行うための重要なステップです。

ヘッダファイルとソースファイル

C++では、プログラムをヘッダファイルとソースファイルという2種類のファイルに分割するのが一般的です。

  • ヘッダファイル (.h または .hpp): 「宣言」を置く場所です。クラスの定義、関数のプロトタイプ宣言、定数、テンプレートなどを記述します。他のファイルに対して「何ができるか(インターフェース)」を公開する役割を持ちます。
  • ソースファイル (.cpp): 「実装」を置く場所です。ヘッダファイルで宣言された関数の具体的な処理内容などを記述します。ヘッダファイルが公開したインターフェースを「どのように実現するか」を記述する役割を持ちます。

なぜ分割するのか? 🤔

  1. 関心の分離: インターフェース(何ができるか)と実装(どうやるか)を分離することで、コードの見通しが良くなります。他の開発者はヘッダファイルを見るだけで、その機能の使い方がわかります。
  2. コンパイル時間の短縮: ソースファイルを変更した場合、再コンパイルはそのファイルだけで済みます。プロジェクト全体を再コンパイルする必要がないため、大規模なプロジェクトでは開発サイクルが劇的に速くなります。
  3. 再利用性の向上: よく使う関数やクラスをまとめておけば、別のプロジェクトでそのファイルをインクルードするだけで簡単に再利用できます。

分割の例

簡単な足し算を行う関数を別のファイルに分割してみましょう。

まず、関数の「宣言」をヘッダファイルに記述します。

ファイルを編集:math_utils.h
// 関数の宣言を記述するヘッダファイル

// この関数が他のファイルから参照されることを示す
int add(int a, int b);

次に、この関数の「実装」をソースファイルに記述します。

ファイルを編集:math_utils.cpp
// 関数の実装を記述するソースファイル

#include "math_utils.h" // 対応するヘッダファイルをインクルード

int add(int a, int b) {
    return a + b;
}

最後に、main関数を含むメインのソースファイルから、このadd関数を呼び出します。

ファイルを編集:math_app.cpp
#include <iostream>
#include "math_utils.h" // 自作したヘッダファイルをインクルード

int main() {
    int result = add(5, 3);
    std::cout << "The result is: " << result << std::endl;
    return 0;
}
ブラウザ上で動作するcppの実行環境です。
左上の実行ボタンを押して、このページ内のmath_app.cppmath_utils.cppに書かれている内容を実行します。
The result is: 8

ここで注目すべき点は、math_app.cppがadd関数の具体的な実装を知らないことです。math_utils.hを通じて「intを2つ受け取ってintを返すaddという関数が存在する」ことだけを知り、それを利用しています。

インクルードガード

複数のファイルから同じヘッダファイルがインクルードされる状況はよくあります。例えば、A.hがB.hをインクルードし、ソースファイルがA.hとB.hの両方をインクルードするような場合です。

もしヘッダファイルに何の対策もしていないと、同じ内容(クラス定義や関数宣言)が複数回読み込まれ、「再定義」としてコンパイルエラーが発生してしまいます。

ファイルを編集:A.h
#include "B.h" // B.hをインクルード

// A.hの内容
ファイルを編集:B.h
class B {
    // Bクラスの内容
};
ファイルを編集:bad_include_app.cpp
#include "A.h"
#include "B.h" // B.hが二重にインクルードされる

int main() {
    [[maybe_unused]] B b; // Bクラスを使う

    return 0;
}
ブラウザ上で動作するcppの実行環境です。
左上の実行ボタンを押して、このページ内のbad_include_app.cppに書かれている内容を実行します。
In file included from bad_include_app.cpp:2:
B.h:1:7: error: redefinition of 'class B'
    1 | class B {
      |       ^
In file included from A.h:1,
                 from bad_include_app.cpp:1:
B.h:1:7: note: previous definition of 'class B'
    1 | class B {
      |       ^

この問題を解決するのがインクルードガードです。インクルードガードは、ヘッダファイルの内容が1つの翻訳単位(ソースファイル)内で一度しか読み込まれないようにするための仕組みです。

伝統的なインクルードガード

プリプロセッサディレクティブである #ifndef, #define, #endif を使います。

#ifndef MATH_UTILS_H // もし MATH_UTILS_H が未定義なら
#define MATH_UTILS_H // MATH_UTILS_H を定義する

// --- ヘッダファイルの中身 ---
int add(int a, int b);
// -------------------------

#endif // MATH_UTILS_H
  • 最初のインクルード: MATH_UTILS_H は未定義なので、#define が実行され、中身が読み込まれます。
  • 2回目以降のインクルード: MATH_UTILS_H は既に定義されているため、#ifndef から #endif までのすべてが無視されます。

マクロ名 (MATH_UTILS_H) は、ファイル名に基づいて一意になるように命名するのが慣習です。

#pragma once

より現代的で簡潔な方法として #pragma once があります。多くのモダンなコンパイラがサポートしています。

#pragma once

#include <string>

std::string to_upper(const std::string& str);

この一行をヘッダファイルの先頭に書くだけで、コンパイラがそのファイルが一度しかインクルードされないように処理してくれます。特別な理由がない限り、現在では #pragma once を使うのが主流です。

プロジェクトのビルド

複数のソースファイル(.cpp)は、それぞれがコンパイルされてオブジェクトファイル(.o や .obj)になります。その後、リンカがこれらのオブジェクトファイルと必要なライブラリを結合して、最終的な実行可能ファイルを生成します。

この一連の作業をビルドと呼びます。ファイルが増えてくると、これを手動で行うのは非常に面倒です。そこで、ビルド作業を自動化するビルドシステムが使われます。

手動でのビルド (g++)

先ほどのmath_app.cppとmath_utils.cppを例に、g++コンパイラで手動ビルドする手順を見てみましょう。

# 1. 各ソースファイルをコンパイルしてオブジェクトファイルを生成する (-c オプション)
g++ -c math_app.cpp -o main.o
g++ -c math_utils.cpp -o math_utils.o

# 2. オブジェクトファイルをリンクして実行可能ファイルを生成する
g++ main.o math_utils.o -o my_app

# 3. 実行する
./my_app

または、以下のように1回のg++コマンドで複数ソースファイルのコンパイルとリンクを同時に行うこともできます。

g++ math_app.cpp math_utils.cpp -o my_app
./my_app

Makefileによる自動化

makeは、ファイルの依存関係と更新ルールを記述したMakefileというファイルに従って、ビルドプロセスを自動化するツールです。

以下は、非常にシンプルなMakefileの例です。

# コンパイラを指定
CXX = g++
# コンパイルオプションを指定
CXXFLAGS = -std=c++17 -Wall

# 最終的なターゲット(実行可能ファイル名)
TARGET = my_app

# ソースファイルとオブジェクトファイル
SRCS = math_app.cpp math_utils.cpp
OBJS = $(SRCS:.cpp=.o)

# デフォルトのターゲット (makeコマンド実行時に最初に実行される)
all: $(TARGET)

# 実行可能ファイルの生成ルール
$(TARGET): $(OBJS)
    $(CXX) $(CXXFLAGS) -o $(TARGET) $(OBJS)

# オブジェクトファイルの生成ルール (%.o: %.cpp)
# .cppファイルから.oファイルを作るための汎用ルール
%.o: %.cpp
    $(CXX) $(CXXFLAGS) -c $< -o $@

# 中間ファイルなどを削除するルール
clean:
    rm -f $(OBJS) $(TARGET)

このMakefileがあるディレクトリで、ターミナルからmakeと入力するだけで、必要なコンパイルとリンクが自動的に実行されます。math_app.cppだけを変更した場合、makeはmain.oだけを再生成し、再リンクするため、ビルド時間が短縮されます。

CMakeによるモダンなビルド管理

Makefileは強力ですが、OSやコンパイラに依存する部分があり、複雑なプロジェクトでは管理が難しくなります。

CMakeは、MakefileやVisual Studioのプロジェクトファイルなどを自動的に生成してくれる、クロスプラットフォーム対応のビルドシステムジェネレータです。CMakeLists.txtという設定ファイルに、より抽象的なビルドのルールを記述します。

# CMakeの最低要求バージョン
cmake_minimum_required(VERSION 3.10)

# プロジェクト名を設定
project(MyAwesomeApp)

# C++の標準バージョンを設定
set(CMAKE_CXX_STANDARD 17)
set(CMAKE_CXX_STANDARD_REQUIRED ON)

# 実行可能ファイルを追加
# add_executable(実行ファイル名 ソースファイル1 ソースファイル2 ...)
add_executable(my_app math_app.cpp math_utils.cpp)

このCMakeLists.txtを使ってビルドする一般的な手順は以下の通りです。

# 1. ビルド用の中間ファイルを置くディレクトリを作成し、移動する
mkdir build
cd build

# 2. CMakeを実行して、ビルドシステム(この場合はMakefile)を生成する
cmake ..

# 3. make (または cmake --build .) を実行してビルドする
make

# 4. 実行する
./my_app

CMakeは、ライブラリの検索、依存関係の管理、テストの実行など、大規模プロジェクトに必要な多くの機能を備えており、現在のC++開発における標準的なツールとなっています。

この章のまとめ

  • プロジェクトの分割: プログラムは「宣言」を記述するヘッダファイル (.h) と、「実装」を記述するソースファイル (.cpp) に分割することで、保守性や再利用性が向上します。
  • インクルードガード: ヘッダファイルの多重インクルードによる再定義エラーを防ぐために、#pragma once や #ifndef/#define/#endif を使用します。
  • ビルドシステム: 複数のファイルをコンパイル・リンクするプロセスを自動化するために、make や CMake といったツールが使われます。特に CMake はクロスプラットフォーム開発におけるデファクトスタンダードです。

練習問題1: 電卓クラスの分割

Calculator というクラスを作成してください。このクラスは、加算、減算、乗算、除算のメンバ関数を持ちます。

  • Calculator.h: Calculatorクラスの定義を記述します。
  • Calculator.cpp: 各メンバ関数の実装を記述します。
  • practice6_1.cpp: Calculatorクラスのインスタンスを作成し、いくつかの計算を行って結果を表示します。

これらのファイルをg++で手動ビルドして、プログラムを実行してください。

ファイルを編集:Calculator.h
ファイルを編集:Calculator.cpp
ファイルを編集:practice6_1.cpp
#include <iostream>
#include "Calculator.h"

int main() {
    Calculator calc;

    std::cout << "3 + 5 = " << calc.add(3, 5) << std::endl;
    std::cout << "10 - 2 = " << calc.subtract(10, 2) << std::endl;
    std::cout << "4 * 7 = " << calc.multiply(4, 7) << std::endl;
    std::cout << "20 / 4 = " << calc.divide(20, 4) << std::endl;
    return 0;
}
ブラウザ上で動作するcppの実行環境です。
左上の実行ボタンを押して、このページ内のpractice6_1.cppCalculator.cppに書かれている内容を実行します。
3 + 5 = 8
10 - 2 = 8
4 * 7 = 28
20 / 4 = 5
前のページ« 関数と参照渡し
次のページクラスの基礎 »