分割コンパイルと翻訳単位

こんにちは、めのんです!

前回予告したように、今回は翻訳単位について解説することにします。
翻訳単位は分割コンパイルに関わる仕様になりますので、分割コンパイルについてまずは解説し、そのあと翻訳単位を説明しようと思います。

分割コンパイルのしかた

今回は分割コンパイルを行う方法として、先日の固定長メモリプールを題材に取り上げることにします。
まずはソースコードを再掲載します。

#include <stdio.h>
#include <stddef.h>

// 処理系の最大境界調整要求を持つ型
union max_align_type
{
  long long a;
  double b;
  long double c;
  void* d;
};

// 処理系の最大調整要求
const size_t max_align_size = offsetof(struct { char a; union max_align_type b; }, b);

union node
{
  union max_align_type aligner;
  char array[256];  // 固定長配列(ここでは256バイト)
  union node* next;
};

// 固定長メモリプールの管理領域
union node memory[256];
union node *top;

// 固定長メモリプールの初期設定
void fmp_setup(void)
{
  const size_t n = sizeof(memory)/sizeof(memory[0]);
  for (size_t i = 0; i < n - 1; i++)
    memory[i].next = &memory[i + 1];
  memory[n - 1].next = NULL;
  top = &memory[0];
}

// 固定長メモリプールからメモリブロックを割り付ける。
void *fmp_alloc(void)
{
  if (top == NULL)
    return NULL;
  void *r = top->array;
  top = top->next;
  return r;
}

// 固定長メモリプールから割り付けたメモリブロックを解放する。
void fmp_free(void* p)
{
  if (p == NULL)
    return;
  ((union node*)p)->next = top;
  top = p;
}

int main(void)
{
  void *array[0x10];

  fmp_setup();

  // 16回メモリブロックを割り付ける。
  for (int i = 0; i < 0x10; i++)
  {
    printf("i = %d\n", i);
    void *p = fmp_alloc();
    array[i] = p;
    printf("p = %p top = %p\n", p,top);
  }
  // 16回メモリブロックを解放する。
  for (int i = 0; i < 0x10; i++)
  {
    printf("i = %d\n", i);
    void *p = array[i];
    fmp_free(p);
    printf("p = %p top = %p\n", p,top);
  }
  return 0;
}

main関数とそれ以外に分割する

それでは先ほどのソースファイルをmain関数とそれ以外に分割します。
main関数からtopを参照していましたので、その部分だけ取り除いた上で分割すると次のようになります。

まずはmain関数を定義するmain.cです。

// main.c
#include <stdio.h>

void fmp_setup(void);
void *fmp_alloc(void);
void fmp_free(void* p);

int main(void)
{
  void *array[0x10];

  fmp_setup();

  // 16回メモリブロックを割り付ける。
  for (int i = 0; i < 0x10; i++)
  {
    printf("i = %d\n", i);
    void *p = fmp_alloc();
    array[i] = p;
    printf("p = %p\n", p);
  }
  // 16回メモリブロックを解放する。
  for (int i = 0; i < 0x10; i++)
  {
    printf("i = %d\n", i);
    void *p = array[i];
    fmp_free(p);
    printf("p = %p\n", p);
  }
  return 0;
}

次に固定長メモリプールの関数群を定義するfmp.cです。

// fmp.c
#include <stddef.h>

// 処理系の最大境界調整要求を持つ型
union max_align_type
{
  long long a;
  double b;
  long double c;
  void* d;
};

// 処理系の最大調整要求
const size_t max_align_size = offsetof(struct { char a; union max_align_type b; }, b);

union node
{
  union max_align_type aligner;
  char array[256];  // 固定長配列(ここでは256バイト)
  union node* next;
};

// 固定長メモリプールの管理領域
union node memory[256];
union node *top;

// 固定長メモリプールの初期設定
void fmp_setup(void)
{
  const size_t n = sizeof(memory)/sizeof(memory[0]);
  for (size_t i = 0; i < n - 1; i++)
    memory[i].next = &memory[i + 1];
  memory[n - 1].next = NULL; top = &memory[0];
}

// 固定長メモリプールからメモリブロックを割り付ける。
void *fmp_alloc(void)
{
  if (top == NULL)
    return NULL;
  void *r = top->array;
  top = top->next;
  return r;
}

// 固定長メモリプールから割り付けたメモリブロックを解放する。
void fmp_free(void* p)
{
  if (p == NULL)
    return;
  ((union node*)p)->next = top;
  top = p;
}

ほとんど機械的に分離しただけですけど、1点だけ注意することがあります。
それは、main.cで

void fmp_setup(void);
void *fmp_alloc(void);
void fmp_free(void* p);

のように、固定長メモリプール関連の関数を宣言しているところです。
もし関数の宣言無しで呼び出すと、各関数は仮引数並び無しで返却値がint型の関数だとみなされてしまいます。
これは実際の定義と矛盾しますので、必ずこのように関数の宣言を行う必要があります。

分割コンパイルのしかた

ソースファイルが2つに分離されましたが、このプログラムのコンパイルのしかたを解説しないといけませんね。

まとめてコンパイル

とりあえずコンパイルするだけなら次のようにすればOKです。

gcc main.c fmp.c

このように、コンパイル時に分割したソースファイルを単に並べて指定すれば、それぞれのソースファイルがコンパイルされて結合(リンク)されます。
「-o」オプションを指定しなければ、Windowsであればa.exeが、それ以外のプラットフォームではa.outが生成されます。
もちろん「-o」オプションを付けて生成される実行ファイルの名前を指定してもかまいません。

個別にコンパイル

次のように、それぞれのソースファイルを個別にコンパイルして、最後にリンクすることもできます。

gcc -c main.c
gcc -c fmp.c
gcc main.o fmp.o

「-c」オプションを付ければ、そのソースファイルだけをコンパイルしてリンクは行いません。
この結果生成されるのはmain.oのように拡張子(サフィックス)が「.o」のファイルになります。

「.o」で終わるファイルのことを「オブジェクトファイル」といいます。
1つ以上のオブジェクトファイルをリンクすることで実行ファイルを生成することができます。
上の例では3行目でリンクを行っています。
全部コマンドは「gcc」なので簡単ですね。

なぜこのように個別にコンパイルする必要があるかというと、毎回全部コンパイルしていると時間がかかるからです。
変更があったソースファイルだけを再コンパイルするようにすれば、最小限の時間で実行ファイルを作り直すことができます。

どのソースファイルに変更があったかを自分で判断するのは大変ですので、Makefileを使うなどして自動化することもできます。
Makefileについてはまた別の機会で紹介できたらと考えています。

翻訳単位

翻訳単位というのは、簡単にいえばオブジェクトファイルを生成するのに必要な一連のソースコードです。

たとえば、main.oを生成するには、main.cとそこからインクルードされている「stdio.h」をあわせたものが翻訳単位になります。
fmp.oであれば、fmp.cとそこからインクルードされる「stddef.h」をあわせたものになります。
インクルードしたソースコードからさらに別のソースコードをインクルードしているなら、それらも翻訳単位に含まれます。

この翻訳単位という概念はすごく大事ですので、しっかり理解しておいてください。


今回の解説は以上となります。
次回は結合(リンケージ)について解説しようと思います。
どうぞご期待ください!