これまでの章では、すべてのコードを1つの .cpp
ファイルに記述してきました。しかし、プログラムが大規模で複雑になるにつれて、このアプローチは現実的ではなくなります。コードの可読性が低下し、少しの変更でもプログラム全体の再コンパイルが必要になり、開発効率が大きく損なわれるからです。
この章では、プログラムを複数のファイルに分割し、それらを効率的に管理・ビルドする方法を学びます。これは、小さなプログラムから一歩進み、本格的なソフトウェア開発を行うための重要なステップです。
C++では、プログラムをヘッダファイルとソースファイルという2種類のファイルに分割するのが一般的です。
.h
または .hpp
): 「宣言」を置く場所です。クラスの定義、関数のプロトタイプ宣言、定数、テンプレートなどを記述します。他のファイルに対して「何ができるか(インターフェース)」を公開する役割を持ちます。.cpp
): 「実装」を置く場所です。ヘッダファイルで宣言された関数の具体的な処理内容などを記述します。ヘッダファイルが公開したインターフェースを「どのように実現するか」を記述する役割を持ちます。簡単な足し算を行う関数を別のファイルに分割してみましょう。
まず、関数の「宣言」をヘッダファイルに記述します。
次に、この関数の「実装」をソースファイルに記述します。
最後に、main
関数を含むメインのソースファイルから、このadd
関数を呼び出します。
ここで注目すべき点は、math_app.cpp
がadd
関数の具体的な実装を知らないことです。math_utils.h
を通じて「int
を2つ受け取ってint
を返すadd
という関数が存在する」ことだけを知り、それを利用しています。
複数のファイルから同じヘッダファイルがインクルードされる状況はよくあります。例えば、A.h
がB.h
をインクルードし、ソースファイルがA.h
とB.h
の両方をインクルードするような場合です。
もしヘッダファイルに何の対策もしていないと、同じ内容(クラス定義や関数宣言)が複数回読み込まれ、「再定義」としてコンパイルエラーが発生してしまいます。
この問題を解決するのがインクルードガードです。インクルードガードは、ヘッダファイルの内容が1つの翻訳単位(ソースファイル)内で一度しか読み込まれないようにするための仕組みです。
プリプロセッサディレクティブである #ifndef
, #define
, #endif
を使います。
# MATH_UTILS_H // もし MATH_UTILS_H が未定義なら
# MATH_UTILS_H // MATH_UTILS_H を定義する
// --- ヘッダファイルの中身 ---
int add(int a, int b);
// -------------------------
# // MATH_UTILS_H
MATH_UTILS_H
は未定義なので、#define
が実行され、中身が読み込まれます。MATH_UTILS_H
は既に定義されているため、#ifndef
から #endif
までのすべてが無視されます。マクロ名 (MATH_UTILS_H
) は、ファイル名に基づいて一意になるように命名するのが慣習です。
より現代的で簡潔な方法として #pragma once
があります。多くのモダンなコンパイラがサポートしています。
# once
#
std::string to_upper(const std::string& str);
この一行をヘッダファイルの先頭に書くだけで、コンパイラがそのファイルが一度しかインクルードされないように処理してくれます。特別な理由がない限り、現在では #pragma once
を使うのが主流です。
複数のソースファイル(.cpp
)は、それぞれがコンパイルされてオブジェクトファイル(.o
や .obj
)になります。その後、リンカがこれらのオブジェクトファイルと必要なライブラリを結合して、最終的な実行可能ファイルを生成します。
この一連の作業をビルドと呼びます。ファイルが増えてくると、これを手動で行うのは非常に面倒です。そこで、ビルド作業を自動化するビルドシステムが使われます。
先ほどの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
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
だけを再生成し、再リンクするため、ビルド時間が短縮されます。
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 はクロスプラットフォーム開発におけるデファクトスタンダードです。Calculator
というクラスを作成してください。このクラスは、加算、減算、乗算、除算のメンバ関数を持ちます。
Calculator.h
: Calculator
クラスの定義を記述します。Calculator.cpp
: 各メンバ関数の実装を記述します。practice12_1.cpp
: Calculator
クラスのインスタンスを作成し、いくつかの計算を行って結果を表示します。これらのファイルをg++で手動ビルドして、プログラムを実行してください。