分割コンパイルとリンク


関数のプロトタイプ宣言

次のプログラムを見て欲しい。

#include <stdio.h>
#include <math.h>

int main(void)
{
    double x, y;

    scanf("%lf", &x);
    y = rt3(x);
    printf("%f\n", y);

    return 0;
}

double rt3(double a)
{
    double x, new_x;

    x = a;

    while (1) {
        new_x = x - (x*x*x - a) / (3*x*x);
        if (fabs(new_x - x) < 1e-10) break;
        x = new_x;
    }

    return new_x;
}
proto1.c

rt3はニュートン法を使って与えられた引数の3乗根を計算する 関数だが、このプログラムをコンパイルすると、

proto1.c:15: error: conflicting types for 'rt3'
proto1.c:9: error: previous implicit declaration of 'rt3' was here

のようにエラーになってしまう。これは、C言語が古い設計のせいなのか 上から下に順にコンパイルしていくために発生している。 関数を含んだ部分を正しくコンパイルするためには、 「関数の引数と戻り値の型」が必要である。 このプログラムでは関数rt3の定義がmain よりも後にあるために、mainの中でrt3を呼び出すときに rt3の引数と戻り値の型が分からずに困ってしまうことになる。 C言語では分からない場合は勝手に「int型」と仮定して 先に進むようになっているが、その後実際の定義では引数も戻り値も double型だったことが判明したので、そこでエラーになっている。

これを解決するには、

#include <stdio.h>
#include <math.h>

double rt3(double a)
{
    double x, new_x;

    x = a;

    while (1) {
        new_x = x - (x*x*x - a) / (3*x*x);
        if (fabs(new_x - x) < 1e-10) break;
        x = new_x;
    }

    return new_x;
}

int main(void)
{
    double x, y;

    scanf("%lf", &x);
    y = rt3(x);
    printf("%f\n", y);

    return 0;
}
proto2.c

のように順序を入れ換えてしまえば簡単だが、

などの場合はそれでは解決できない。そこで、「プロトタイプ宣言」と 呼ばれる、関数の中身は書かずに引数と戻り値の型だけ記述する方法が 用意されている。

戻り値の型 関数名(引数の型, 引数の型, ...);

のように書く。上の例では、

double rt3(double);

mainの前に挿入して、

#include <stdio.h>
#include <math.h>

double rt3(double);

int main(void )
{
    double x, y;

    scanf("%lf", &x);
    y = rt3(x);
    printf("%f\n", y);

    return 0;
}

double rt3(double a)
{
    double x, new_x;

    x = a;

    while (1) {
        new_x = x - (x*x*x - a) / (3*x*x);
        if (fabs(new_x - x) < 1e-10) break;
        x = new_x;
    }

    return new_x;
}
proto3.c

のようにすればエラーは無くなる。

分割コンパイルとリンク

プログラムが大きくなると、一つの.cファイルに全ての 関数を記述していると編集が大変になるので、複数のファイルに分割する ことが出来る。

上の例題を2つのファイルに分割してみる。

#include <stdio.h>

double rt3(double);

int main(void)
{
    double x, y;

    scanf("%lf", &x);
    y = rt3(x);
    printf("%f\n", y);

    return 0;
}
main1.c

#include <math.h>

double rt3(double a)
{
    double x, new_x;

    x = a;

    while (1) {
        new_x = x - (x*x*x - a) / (3*x*x);
        if (fabs(new_x - x) < 1e-10) break;
        x = new_x;
    }

    return new_x;
}
rt3.c

これをコンパイルするには、次のようにする。

% cc -c main1.c
% cc -c rt3.c
% cc -o hoge main1.o rt3.o

1行目でmain1.cをコンパイルしてmain1.oを、 2行目でrt3.cをコンパイルしてrt3.oを生成している。 これは「オブジェクトファイル」と呼ばれるもので、 「実行ファイル」になる前段階の不完全なプログラムである。 ほぼコンパイルは済んでいるが、一部未定義の呼び出し部分があって、 そこが空欄になっているようなものと思えばよい。 3行目でオブジェクトファイルを「リンク」し、実行ファイルhogeを 生成している。「リンク」とは、複数のオブジェクトファイルを結合し、 空欄になっている未解決の呼び出し部分を互いに埋めて完全な実行ファイルに する作業を言う。

このように分割しておくと、編集が楽になるだけでなく、 ある一つのファイルだけ変更した場合は残りのファイルはコンパイル する必要が無いので、コンパイル時間の節約にもなる。

ヘッダファイルを使う

上の例の場合、main1.cの中にrt3のプロトタイプ宣言が 含まれている。定義が別ファイルにあるのでプロトタイプ宣言が必須となるが、 rt3という関数のより完全な「部品化」をするために、 次のようにすることが多い。ヘッダファイルrt3.hを用意して、

double rt3(double);
rt3.h

mainは次のようにする。

#include <stdio.h>
#include "rt3.h"

int main(void)
{
    double x, y;

    scanf("%lf", &x);
    y = rt3(x);
    printf("%f\n", y);

    return 0;
}
main2.c

のようにする。

#include <stdio.h>等の正体

本体は/usr/include/stdio.hにある。 中身はprintfscanfなどのプロトタイプ宣言。

printf.oscanf.oなどのオブジェクトファイルは /usr/lib/libc.aというファイルの中に入っており、 リンク時に必要ならその中から取り出されて実行ファイルに結合される。

また、/usr/include/math.hにはsqrtlogの プロトタイプ宣言が記述されている。 sqrt.olog.oなどは、 /usr/lib/libm.aというファイルの中に入っており、 これは自動的にリンクはされず、リンクするときは-lmを付ける 必要がある。


分割コンパイルとリンク