C言語では、関数を別ファイルに分けて再利用するために、呼び出し元が関数の型情報を事前に知る必要があります。
その役割を果たすのがプロトタイプ宣言です。
本記事では、プロトタイプ宣言の基本と分割コンパイルの手順、典型的なエラーと対処法を、初心者の方にも分かりやすい順序で丁寧に解説します。
プロトタイプ宣言の基本
C言語のプロトタイプ宣言とは
プロトタイプ宣言は、関数の名前、戻り値の型、引数の型情報をコンパイラに知らせる宣言です。
呼び出し元ファイルはこの宣言を読むことで、正しい型チェックが受けられ、リンク時に関数定義へと結び付けられます。
通常はヘッダファイルに書き、利用側のソースで#include
します。
例として、次のように記述します。
// プロトタイプ宣言の例
int add(int a, int b);
double average(const int *values, size_t len);
ここでint
やdouble
が戻り値の型、かっこ内が引数の型と名前です。
Cでは関数宣言にextern
を明示しなくても外部結合の宣言として扱われます。
最もシンプルなプロトタイプ宣言
#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
もヘッダ側で行います。
// 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
でファイルスコープに限定します。
// 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
し、通常どおり関数を呼び出します。
ヘッダを必ずインクルードしてから呼び出すのがポイントです。
// 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
にし、最後にリンクします。
# 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行でまとめても構いません。
gcc example.c main.c -o app
includeガードで重複includeを防ぐ
同じヘッダを複数回インクルードすると、重複宣言やマクロ再定義の問題になります。
includeガードを使って一度だけ読み込ませます。
#ifndef EXAMPLE_H_INCLUDED
#define EXAMPLE_H_INCLUDED
/* ヘッダの内容 */
#endif
一部の環境では#pragma once
も使えますが、移植性の観点からは従来のガードの方が確実です。
よくあるミスとエラー対処
宣言と定義の不一致(引数型/戻り値)
宣言と定義で型が異なると、コンパイラはconflicting typesを報告します。
// bad.h
int add(double x, double y); // 宣言: double
// bad.c
#include "bad.h"
int add(int a, int b) { // 定義: int
return a + b;
}
コンパイル例:
gcc bad.c -c
bad.c:3:5: error: conflicting types for 'add'
ヘッダを必ず実装側でもインクルードし、両者を同じ宣言から共有すると不一致を未然に防げます。
暗黙の宣言はエラー(C99以降)
宣言なしで関数を呼ぶと、C99以降はエラーです。
// implicit.c
#include <stdio.h>
int main(void) {
printf("%d\n", add(1, 2)); // add の宣言がない
return 0;
}
コンパイル例と出力:
gcc implicit.c -c
implicit.c:5:18: error: implicit declaration of function 'add' is invalid in C99
必ず対応ヘッダを#include
するか、少なくとも先頭に正しいプロトタイプを書いてから呼び出してください。
多重定義や重複シンボルのリンクエラー
ヘッダに関数の「定義」を書くと、インクルードした各.c
にコピーされ、リンク時にmultiple definitionになります。
// bad.h (やってはいけない)
int add(int a, int b) { return a + b; } // 定義を書いてしまっている
リンク時の出力例:
/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
を付けたファイルスコープ関数は内部結合になり、他ファイルからリンクできません。
// example.c
static int secret(void) { return 42; }
このsecret
をヘッダにint secret(void);
と宣言して他ファイルから呼ぶと、リンクエラーになります。
undefined reference to `secret'
他ファイルから使いたいならstatic
を外し、ヘッダにプロトタイプを置きます。
ヘッダに定義を書かない(宣言のみ)
ヘッダは宣言だけを書くのが基本です。
例外的にstatic inline
関数をヘッダに置くテクニックもありますが、初心者のうちは避け、「宣言は.h、定義は.c」を徹底しましょう。
小さなサンプル構成
ファイル構成(example.h/example.c/main.c)
3ファイルに分けた最小構成です。
// 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
// 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;
}
// 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)
分割コンパイルとリンクを段階的に行います。
# コンパイル
gcc -c example.c
gcc -c main.c
# リンク
gcc example.o main.o -o app
ワンライナーも可能です。
gcc example.c main.c -o app
実行と動作確認
実行して結果を確認します。
./app
add(3, 4) = 7
average = 4.00
呼び出し側はヘッダをインクルードするだけで、別ファイルの関数を安全に使えることが確認できます。
まとめ
プロトタイプ宣言は、コンパイル時の型チェックとリンク時の結合を成立させるC言語の要です。
基本原則は「宣言はヘッダ、定義は実装ファイル、利用側はヘッダをインクルード」です。
includeガードで重複を防ぎ、C99以降は暗黙の宣言
が許されない点にも注意してください。
宣言と定義の不一致、多重定義、static
の可視性などの落とし穴を避ければ、分割コンパイルによる保守性と再利用性の高いコードが実現できます。