このブログ記事では、C言語(C++)におけるモジュール化について紹介します。

Cの大規模プロジェクトに取り組む際、コードが非常に大きくなると、関連するモジュールに分割したくなる場面に直面します。 この記事では、モジュール化の動機と、C(およびC++)でそれをどのように実現できるかについて説明します。
なぜモジュール化が必要か
前述のように、コードが非常に大きくなると、開発者がスクロールしてコード全体を把握することが難しくなります。 このような場合、モジュール化が役立ちます。さらに大きな単一ファイルで作業していると、たとえ小さな変更しか加えない場合でも、 コード全体を再コンパイルしなければなりません。モジュール化を行うことで、関連するファイルだけを再コンパイルすることで済むようになります。
たとえば、これまでにC++でAdjacencyMatrix
やBTree
のようなさまざまなカスタムデータ型を扱ってきました。
もしそれらをすべてmain
関数が存在する1つのファイルに含めると、これらのオブジェクトが提供する抽象化の利点を活かすことが難しくなります。
また、隠れた実装の小さな変更がプログラムの大部分を再コンパイルさせる原因になる可能性があります。この問題はCでも同様で、
理想的には構造体やその構造体と一緒に動作する関数を別のファイルに定義したいと考えるでしょう。
モジュール化の方法
C(およびC++)には正式なモジュールの定義はありませんが、ヘッダーファイルとソースファイルを使用して「モジュール化」を実現することができます。
ヘッダーファイル(.h
)には、関数や型の宣言が含まれ、他のコードに対するインターフェースとして機能します。ソースコード(.c
や.cpp
)には、
宣言された関数や型の実装が含まれます。以下は連結リストのヘッダーファイルの例です。
typedef struct {
int data;
struct Node *next;
} Node;
void printLinkedList (Node *head);
Node *prependNode(Node *head, int data);
ヘッダーファイルは、関数や型を宣言するだけで、定義はしません。(技術的には定義もできますが、変更が加えられるたびにそのモジュールを使用するすべてのコードファイルを再コンパイルする必要があります。)
また、ヘッダーファイルには機能を説明するドキュメントを含めることもできます。ソースコードでは、#include
マクロを使用して宣言をコピーし、定義を提供します。
#include "<stdio.h>"
#include "LinkedList.h"
void printLinkedList (Node *head) {
Node *current = head;
while (current != NULL) {
printf("%d -> ", current -> data);
current = current -> next;
}
printf("\n");
return;
};
Node *prependNode(Node *head, int data) {
Node *newNode = malloc(sizeof(Node));
// ... IMPLEMENTATIONS ...
return head;
};
上記では標準ライブラリを使用するときに使用してきた<>
ではなく、""
を使用しています。
これは、""
がヘッダーファイルが同じディレクトリ内にあることを示すためです。
#include
マクロは指定されたヘッダーファイルに置き換わるため、
同じヘッダーファイルを複数回使用すると再宣言の問題が発生する可能性があります。
この問題を防ぐために、以下のようにインクルードガードを使用します。
#ifndef LINKEDLIST_H
#define LINKEDLIST_H
typedef struct {
int data;
struct Node *next;
} Node;
void printLinkedList (Node *head);
Node *prependNode(Node *head, int data);
#endif
インクルードガードは、ユニークな変数LINKEDLIST_H
がすでに定義されている場合に宣言をスキップするためのマクロです。
これにより、同じヘッダーファイルが再使用されることを防ぎ、同じ関数や型が再宣言されることを防ぎます。
これでヘッダーファイルを#include "LINKEDLIST.h"
のようにしてメインソースコードに使用し、関数や型を使用できます。
オブジェクトファイル
モジュールを使用するためにソースコードをコンパイルする場合、コードをオブジェクトファイル(.o
)にコンパイルします。
オブジェクトファイルには機械語とシンボルテーブルが含まれますが、それ自体では実行できません。
シンボルテーブルは、リンク時(組み立て時)にメインソースコードが関数定義の場所を特定するために使用されます。
オブジェクトファイルを作成するには、-c
フラグを使用します。
we can use the -c
flag.
> gcc -c LinkedList.c
> gcc -c main.c
> ls
main.c main.h main.o LinkedList.c LinkedList.h LinkedList.o
上をリンクして実行するには、オブジェクトファイルをリストし、実行可能ファイルを指定します。
> gcc main.o LinkedList.o
> ls
a.out main.c main.h main.o LinkedList.c LinkedList.h LinkedList.o
> ./a.out
デフォルトでは、実行可能ファイルの名前はa.out
ですが、-o
フラグを使用して変更できます。
たとえば、gcc -o <name> main.o LinkedList.o
のようにします。C++のg++
でも同じことが適用されます。
静的ライブラリと動的ライブラリ
オブジェクトファイルを「モジュール」として使用できますが、関連するオブジェクトファイルをすべてリストしてそれらを実行可能ファイルにコンパイルするのは面倒です。 特に、複数の場所で使用されるオブジェクトファイルが多数ある場合、それらを把握して毎回リストするのは困難です。 そのため、通常、オブジェクトファイルをまとめてアーカイブし、ライブラリを作成します。
静的ライブラリは、ライブラリ内のすべての宣言と定義がコンパイル時に実行可能ファイルにコピーされるライブラリです。
静的ライブラリはar rcs lib_<name>.a <name>.o ...
を使用して作成できます。静的ファイルは通常、
lib
という接頭辞と.a
拡張子を持ちます。その後、-L.
および-l
フラグを使用して静的ライブラリをリンクできます。
たとえば、gcc main.o -L. -l<name>
のようにします。この場合、.
はライブラリが同じディレクトリにあることを示します。
動的ライブラリまたは共有ライブラリは、宣言と定義が必要に応じて実行時に動的に参照されるライブラリです。
このため、通常、より軽量な実行可能ファイルになります。動的ライブラリを作成するには、
オブジェクトファイルを作成するときに-fPIC
フラグを使用し、その後に-shared
フラグを使用します。
たとえば、gcc -shared lib_<name>.so <name>.o ...
のようにします。共有ライブラリは通常、.so
(共有オブジェクトファイル)拡張子を持ち、
-L.
および-l
フラグを使用してリンクします。
結論
この記事では、なぜモジュール化が必要なのか、ヘッダーファイルとオブジェクトファイルを使用してどのようにモジュール化を実現するのか、 そしてオブジェクトファイルから静的または共有ライブラリを作成する方法について説明しました。モジュール化により、 大規模なコードベース、特に共同開発環境での作業が容易になります。練習として、データ構造に関する過去の記事に戻り、 それらをモジュール化したりライブラリを作成したりしてみることをお勧めします。
リソース
- Jain, A. 2024. Static Library vs Dynamic Library: Understanding the Differences. Medium.
- Jordan, K. 2021. C "Modules" - Tutorial on .h Header Files, Include Guards, .o Object Code, & Incremental Compilation. YouTube.