閉じる

C言語のプロトタイプ宣言とは?分割コンパイルで関数を他ファイルから呼ぶ

C言語では、関数を別ファイルに分けて再利用するために、呼び出し元が関数の型情報を事前に知る必要があります。

その役割を果たすのがプロトタイプ宣言です。

本記事では、プロトタイプ宣言の基本と分割コンパイルの手順、典型的なエラーと対処法を、初心者の方にも分かりやすい順序で丁寧に解説します。

プロトタイプ宣言の基本

C言語のプロトタイプ宣言とは

プロトタイプ宣言は、関数の名前、戻り値の型、引数の型情報をコンパイラに知らせる宣言です。

呼び出し元ファイルはこの宣言を読むことで、正しい型チェックが受けられ、リンク時に関数定義へと結び付けられます。

通常はヘッダファイルに書き、利用側のソースで#includeします。

例として、次のように記述します。

C言語
// プロトタイプ宣言の例
int add(int a, int b);
double average(const int *values, size_t len);

ここでintdoubleが戻り値の型、かっこ内が引数の型と名前です。

Cでは関数宣言にexternを明示しなくても外部結合の宣言として扱われます。

最もシンプルなプロトタイプ宣言

C言語
#include <stdio.h>

// プロトタイプ宣言
void hello(void);
int add(int a, int b);

int main(void) {
    hello();
    int result = add(5, 3);
    printf("結果: %d\n", result);
    return 0;
}

// 関数定義
void hello(void) {
    printf("こんにちは!\n");
}

int add(int a, int b) {
    return a + b;
}
実行結果
こんにちは!
結果: 8

プロトタイプ宣言をしない場合、hello関数とadd関数を使っているmain関数より前に定義しないといけません。

ですが、この例だと事前にプロトタイプ宣言を行うことで関数が存在することを知れているため、コンパイルエラーにならずに実行できます。

宣言と定義の違い

宣言は「こういう関数がある」と知らせるだけで、定義は「中身を実装して提供する」ことです。

分割コンパイルでは、宣言をヘッダに、定義を実装ファイルに分けます。

以下の表で要点を整理します。

項目宣言定義
書式int add(int a, int b);int add(int a, int b) { return a + b; }
役割型を知らせる、呼び出し側に渡す実際の処理を提供する
個数複数あってよいプログラム全体で1個だけ
置き場ヘッダ(.h)が基本実装(.c)
リンクなしあり(オブジェクト間で解決)

定義をヘッダに書くと多重定義になりやすいため、基本は宣言のみをヘッダに置きます。

なぜ必要か(型チェックとリンク)

  • 型チェック: プロトタイプがあると、引数や戻り値の型が検査され、不一致をコンパイル時に検出できます。
  • リンク: 呼び出し側は宣言だけ知っていればよく、実体は別オブジェクトファイルにあってもリンク時に結びつきます。
  • C99以降では暗黙の関数宣言が禁止され、宣言なしの呼び出しはエラーになります。信頼できるビルドには-Wall -Wextra -Werror -std=c17などのオプションを使います。

複数ファイルで関数を使う手順

ヘッダファイル(.h)にプロトタイプを書く

ヘッダは、型が分かる最小限の宣言だけを書く場所です。

必要な型を使うなら、その型のための#includeもヘッダ側で行います。

C言語example.h
// example.h
#ifndef EXAMPLE_H_INCLUDED      // includeガード開始
#define EXAMPLE_H_INCLUDED

#include <stddef.h>             // size_t を使うので必要

// ここに「宣言」だけを書く
int add(int a, int b);
double average(const int *values, size_t len);

#endif // EXAMPLE_H_INCLUDED    // includeガード終了

実装(.c)に関数定義を書く

実装ファイルは関数の中身を書きます。

内部だけで使う補助関数はstaticでファイルスコープに限定します。

C言語example.c
// example.c
#include "example.h"  // 宣言と定義の不一致を防ぐため、必ず自分のヘッダを先頭に
#include <assert.h>   // 任意: デバッグ用

// 内部利用の補助関数は static でファイル内限定にする
static int clamp_to_int_range(long v) {
    if (v > 2147483647L) return 2147483647;
    if (v < -2147483648L) return -2147483648;
    return (int)v;
}

int add(int a, int b) {
    long sum = (long)a + (long)b; // オーバーフロー対策の一例
    return clamp_to_int_range(sum);
}

double average(const int *values, size_t len) {
    assert(values != NULL || len == 0);
    if (len == 0) return 0.0;
    long long total = 0;
    for (size_t i = 0; i < len; ++i) {
        total += values[i];
    }
    return (double)total / (double)len;
}

利用側(.c)でヘッダをincludeする

呼び出し側はヘッダを#includeし、通常どおり関数を呼び出します。

ヘッダを必ずインクルードしてから呼び出すのがポイントです。

C言語
// main.c
#include <stdio.h>
#include "example.h"  // ここから add と average の型が分かる

int main(void) {
    int a = 3, b = 4;
    int s = add(a, b);
    int data[] = {1, 2, 3, 4, 7};
    double avg = average(data, sizeof(data) / sizeof(data[0]));

    printf("add(%d, %d) = %d\n", a, b, s);
    printf("average = %.2f\n", avg);
    return 0;
}

分割コンパイルとリンク(gccの例)

分割コンパイルでは、それぞれの.cをオブジェクト.oにし、最後にリンクします。

Shell
# 1. コンパイル(オブジェクト化)
gcc -std=c17 -Wall -Wextra -Werror -O2 -c example.c
gcc -std=c17 -Wall -Wextra -Werror -O2 -c main.c

# 2. リンク(実行ファイル生成)
gcc example.o main.o -o app

1行でまとめても構いません。

Shell
gcc example.c main.c -o app

includeガードで重複includeを防ぐ

同じヘッダを複数回インクルードすると、重複宣言やマクロ再定義の問題になります。

includeガードを使って一度だけ読み込ませます。

C言語
#ifndef EXAMPLE_H_INCLUDED
#define EXAMPLE_H_INCLUDED
/* ヘッダの内容 */
#endif

一部の環境では#pragma onceも使えますが、移植性の観点からは従来のガードの方が確実です。

よくあるミスとエラー対処

宣言と定義の不一致(引数型/戻り値)

宣言と定義で型が異なると、コンパイラはconflicting typesを報告します。

C言語
// bad.h
int add(double x, double y);  // 宣言: double

// bad.c
#include "bad.h"
int add(int a, int b) {       // 定義: int
    return a + b;
}

コンパイル例:

Shell
gcc bad.c -c
実行結果
bad.c:3:5: error: conflicting types for 'add'

ヘッダを必ず実装側でもインクルードし、両者を同じ宣言から共有すると不一致を未然に防げます。

暗黙の宣言はエラー(C99以降)

宣言なしで関数を呼ぶと、C99以降はエラーです。

C言語
// implicit.c
#include <stdio.h>

int main(void) {
    printf("%d\n", add(1, 2)); // add の宣言がない
    return 0;
}

コンパイル例と出力:

Shell
gcc implicit.c -c
実行結果
implicit.c:5:18: error: implicit declaration of function 'add' is invalid in C99

必ず対応ヘッダを#includeするか、少なくとも先頭に正しいプロトタイプを書いてから呼び出してください。

多重定義や重複シンボルのリンクエラー

ヘッダに関数の「定義」を書くと、インクルードした各.cにコピーされ、リンク時にmultiple definitionになります。

C言語
// bad.h (やってはいけない)
int add(int a, int b) { return a + b; } // 定義を書いてしまっている

リンク時の出力例:

text
/usr/bin/ld: main.o: in function `add': multiple definition of `add'; example.o: first defined here
collect2: error: ld returned 1 exit status

対処は定義を.cへ移し、ヘッダには宣言だけを残すことです。

static関数は他ファイルから呼べない

staticを付けたファイルスコープ関数は内部結合になり、他ファイルからリンクできません。

C言語
// example.c
static int secret(void) { return 42; }

このsecretをヘッダにint secret(void);と宣言して他ファイルから呼ぶと、リンクエラーになります。

text
undefined reference to `secret'

他ファイルから使いたいならstaticを外し、ヘッダにプロトタイプを置きます。

ヘッダに定義を書かない(宣言のみ)

ヘッダは宣言だけを書くのが基本です。

例外的にstatic inline関数をヘッダに置くテクニックもありますが、初心者のうちは避け、「宣言は.h、定義は.c」を徹底しましょう。

小さなサンプル構成

ファイル構成(example.h/example.c/main.c)

3ファイルに分けた最小構成です。

C言語
// example.h
#ifndef EXAMPLE_H_INCLUDED
#define EXAMPLE_H_INCLUDED

#include <stddef.h>

// 足し算のプロトタイプ
int add(int a, int b);

// 配列の平均値のプロトタイプ
double average(const int *values, size_t len);

#endif // EXAMPLE_H_INCLUDED
C言語
// example.c
#include "example.h"

// 内部専用の補助関数
static int clamp_to_int_range(long v) {
    if (v > 2147483647L) return 2147483647;
    if (v < -2147483648L) return -2147483648;
    return (int)v;
}

int add(int a, int b) {
    long sum = (long)a + (long)b;
    return clamp_to_int_range(sum);
}

double average(const int *values, size_t len) {
    if (len == 0) return 0.0;
    long long total = 0;
    for (size_t i = 0; i < len; ++i) {
        total += values[i];
    }
    return (double)total / (double)len;
}
C言語
// main.c
#include <stdio.h>
#include "example.h"

int main(void) {
    int x = 3, y = 4;
    int s = add(x, y);

    int a[] = {1, 2, 3, 4, 10};
    double avg = average(a, sizeof(a) / sizeof(a[0]));

    printf("add(%d, %d) = %d\n", x, y, s);
    printf("average = %.2f\n", avg);
    return 0;
}

ビルドコマンド(gcc -c/-o)

分割コンパイルとリンクを段階的に行います。

Shell
# コンパイル
gcc -c example.c
gcc -c main.c

# リンク
gcc example.o main.o -o app

ワンライナーも可能です。

Shell
gcc example.c main.c -o app

実行と動作確認

実行して結果を確認します。

Shell
./app
実行結果
add(3, 4) = 7
average = 4.00

呼び出し側はヘッダをインクルードするだけで、別ファイルの関数を安全に使えることが確認できます。

まとめ

プロトタイプ宣言は、コンパイル時の型チェックとリンク時の結合を成立させるC言語の要です。

基本原則は「宣言はヘッダ、定義は実装ファイル、利用側はヘッダをインクルード」です。

includeガードで重複を防ぎ、C99以降は暗黙の宣言が許されない点にも注意してください。

宣言と定義の不一致、多重定義、staticの可視性などの落とし穴を避ければ、分割コンパイルによる保守性と再利用性の高いコードが実現できます。

この記事を書いた人
エーテリア編集部
エーテリア編集部

プログラミングの基礎をしっかり学びたい方向けに、C言語の基本文法から解説しています。ポインタやメモリ管理も少しずつ理解できるよう工夫しています。

クラウドSSLサイトシールは安心の証です。

URLをコピーしました!