閉じる

【C言語】 プロトタイプ宣言とは?初心者にも直感で分かる書き方と実例集

C言語で関数を正しく使うためには、「プロトタイプ宣言」の理解が欠かせません。

プロトタイプ宣言は、関数の「名前」「引数の型」「戻り値の型」をコンパイラに教えるための宣言です。

この記事では、初心者の方でも直感的に理解できるように、図解とサンプルコードを交えながら、プロトタイプ宣言の基本から実践的な書き方、つまずきやすいポイントまでを丁寧に解説していきます。

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

プロトタイプ宣言の基本構文と意味

プロトタイプ宣言とは、コンパイラに対して「このあと、こういう関数が出てきますよ」と事前に教えるための宣言です。

C言語のソースコードは、上から順番にコンパイルされていきます。

そのため、コンパイラが関数の定義本体を読む前に、その関数を呼び出すコードが登場する場合、先にプロトタイプ宣言を書いておく必要があります。

プロトタイプ宣言の基本構文は次のようになります。

  • 戻り値の型
  • 関数名
  • 引数の型と名前の一覧
  • 最後に;

という形で表現され、具体的な記述は次のようになります。

C言語
// 基本的なプロトタイプ宣言の例
int add(int a, int b);      // 戻り値: int, 引数: int型が2つ
double average(double x, double y);  // 戻り値: double, 引数: double型が2つ
void print_message(void);   // 戻り値なし(void), 引数なし(void)

この宣言を見れば、コンパイラも人間も「どんな関数なのか」をひと目で理解できます。

プロトタイプ宣言は関数の「仕様書」のようなものだと考えると分かりやすいです。

宣言と定義の違いを初心者向けに解説

C言語では、「宣言(declaration)」「定義(definition)」は厳密に区別されます。

初心者が混乱しやすいポイントなので、ここで整理しておきます。

宣言は、「この名前のものが、こういう型で存在しますよ」と知らせるだけの情報です。

プロトタイプ宣言は、まさにこの「宣言」の一種です。

一方、定義は、「実際にメモリや処理内容を用意すること」を意味します。

関数の定義では、波かっこ{ }の中に処理内容を書き、コンパイラに実際の実行手順を教えます。

宣言だけ:

C言語
// 宣言(プロトタイプ宣言)だけで、中身はない
int add(int a, int b);   // ← 宣言

定義(本体付き):

C言語
// 関数の定義(本体付き)
int add(int a, int b) {  // ← 定義
    return a + b;
}

宣言と定義をまとめると、次のような違いがあります。

項目宣言(prototypeなど)定義(function body)
役割存在と型を知らせる実際の処理を記述する
本体{ }なしあり
回数複数回あってもよい1回だけ(重複はエラー)
メモリ確保しないする

プロトタイプ宣言は「宣言」であり、本体を伴う「定義」ではないという点を必ず意識しておきましょう。

main関数より前にプロトタイプ宣言が必要な理由

Cコンパイラはソースコードを上から順に読みながら、識別子(変数名や関数名)の情報を蓄えます。

コンパイラがmain関数をコンパイルしている時点で、呼び出される関数についての情報がまだなければ、その関数の引数や戻り値の型が分からない状態になってしまいます。

次のようなコードを考えてみます。

C言語
#include <stdio.h>

int main(void) {
    int result = add(3, 4);  // addの情報がまだない状態で呼び出している
    printf("%d\n", result);
    return 0;
}

// ここでようやく定義
int add(int a, int b) {
    return a + b;
}

このコードでは、main関数の中でaddを呼び出していますが、コンパイラはまだaddという関数がどのような引数と戻り値を持つのかを知りません。

現代的なCコンパイラでは、「暗黙の宣言」は基本的に禁止されており、次のようなエラーや警告が出ます。

  • 「関数addの暗黙的な宣言」
  • 「未宣言の識別子を使用しています」など

そこで、mainより前にプロトタイプ宣言を書いておき、コンパイラに先に情報を伝える必要があります。

C言語
#include <stdio.h>

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

int main(void) {
    int result = add(3, 4);  // ここではaddの型情報が分かっている
    printf("%d\n", result);
    return 0;
}

// ここで定義
int add(int a, int b) {
    return a + b;
}

このように書けば、コンパイラはmainを読むときにaddの情報をすでに知っているため、正しくコンパイルできます。

プロトタイプ宣言の書き方

戻り値と引数を含むプロトタイプ宣言の書き方

プロトタイプ宣言は、「戻り値の型」「関数名」「引数の型と並び」が一目で分かるように書くことが大切です。

一般形は次のようになります。

C言語
戻り値の型 関数名(引数の型1 引数名1, 引数の型2 引数名2, ...);

具体例をいくつか示します。

C言語
// 最大値を求める関数
int max(int a, int b);

// 2次元座標の距離を計算する関数
double distance(double x1, double y1, double x2, double y2);

// 成功なら1、失敗なら0を返す関数
int save_to_file(const char *filename, const char *text);

ここで重要なのは、引数のが必ず書かれていることです。

C言語では、関数の引数の数と型が正しく一致していないと、メモリ破壊や予期しない動作につながります。

プロトタイプ宣言によって、コンパイラが引数のチェックを行えるようになります。

引数名は省略することもできますが、読みやすさのために基本的には書くことをおすすめします。

C言語
// 引数名を省略したプロトタイプ宣言(合法だが読みづらい)
int max(int, int);

void関数のプロトタイプ宣言の例

戻り値がない関数は、戻り値の型としてvoidを指定します。

たとえば、画面にメッセージを表示するだけの関数などが該当します。

C言語
// メッセージを表示するだけの関数
void print_hello(void);

// ログを書き出す関数
void write_log(const char *message);

// 配列の内容を表示する関数
void print_array(const int *array, int size);

戻り値がvoidの関数では、関数内でreturn;とだけ書いて処理を途中で終了させることはできますが、return 値;のように値を返すことはできません。

C言語
#include <stdio.h>

// プロトタイプ宣言
void print_hello(void);

int main(void) {
    print_hello();
    return 0;
}

// 定義
void print_hello(void) {
    printf("Hello, C language!\n");
}
実行結果
Hello, C language!

引数なしの関数を宣言する時の注意点

C言語では、引数がない関数のプロトタイプ宣言は、必ずvoidと明示して書くことが推奨されています。

正しい形:

C言語
void do_something(void);   // ← 「引数はない」と明示

一方で、次のように()の中を空にした書き方も文法上は存在します。

C言語
void do_something();       // ← 古いスタイル

しかし、この書き方は「引数の情報がない(不特定)」という意味になり、コンパイラによっては引数チェックを正しく行えません。

これは古いK&Rスタイルの名残であり、現代のC(特にC99以降)では避けるべき書き方です。

「引数がない」ことをはっきりさせるために(void)と書く、と覚えておきましょう。

ヘッダファイルに書くプロトタイプ宣言の基本

複数のソースファイルにまたがって関数を利用する場合、プロトタイプ宣言はヘッダファイル(.h)に書くのが一般的です。

基本的なスタイルは次のようになります。

myfunc.h:

C言語
#ifndef MYFUNC_H
#define MYFUNC_H

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

#endif  // MYFUNC_H

myfunc.c:

C言語
#include <stdio.h>
#include "myfunc.h"   // 自分自身のヘッダをインクルードするのが基本

int add(int a, int b) {       // 関数の定義
    return a + b;
}

void print_result(int value) {  // 関数の定義
    printf("Result: %d\n", value);
}

main.c:

C言語
#include "myfunc.h"  // ここでも同じヘッダをインクルード

int main(void) {
    int result = add(5, 7);   // プロトタイプ宣言の情報を元にチェックされる
    print_result(result);
    return 0;
}

このように、ヘッダファイルにプロトタイプ宣言を集約し、必要なソースファイルから#includeすることで、宣言の重複を避けつつ安全に関数を共有できます。

プロトタイプ宣言の実例集

int型を返す関数のプロトタイプ宣言と実装例

int型を返す関数は最も基本的なパターンです。

例として、2つの整数のうち大きい方を返すmax関数を考えてみます。

C言語
#include <stdio.h>

// プロトタイプ宣言
int max(int a, int b);   // 2つのintを受け取り、大きい方のintを返す

int main(void) {
    int x = 10;
    int y = 20;

    int m = max(x, y);   // maxの戻り値をint型の変数に受け取る
    printf("max(%d, %d) = %d\n", x, y, m);

    return 0;
}

// 関数の定義
int max(int a, int b) {
    if (a > b) {
        return a;        // aが大きければaを返す
    } else {
        return b;        // それ以外はbを返す
    }
}
実行結果
max(10, 20) = 20

プロトタイプ宣言と定義のシグネチャ(戻り値の型、引数の型と順番)が完全に一致していることに注目してください。

複数引数を持つ関数のプロトタイプ宣言と実例

複数の引数を持つ関数でも、プロトタイプ宣言の基本は変わりません。

引数をカンマで区切って列挙するだけです。

次は、箱の体積を計算する関数の例です。

C言語
#include <stdio.h>

// プロトタイプ宣言
double calc_volume(double width, double height, double depth);

int main(void) {
    double w = 2.0;
    double h = 3.5;
    double d = 4.0;

    double v = calc_volume(w, h, d);
    printf("Volume = %.2f\n", v);

    return 0;
}

// 関数の定義
double calc_volume(double width, double height, double depth) {
    return width * height * depth;
}
実行結果
Volume = 28.00

ここでは、引数の「意味」が分かるような名前を付けているため、プロトタイプ宣言を読むだけでもどんな計算をしているのかがイメージしやすくなっています。

ポインタを使う関数のプロトタイプ宣言サンプル

ポインタを使う関数は、値そのものではなく「アドレス」を渡すことで、呼び出し元の変数を書き換えることができます。

代表的な例として、2つの整数を入れ替えるswap関数を見てみましょう。

C言語
#include <stdio.h>

// プロトタイプ宣言
void swap(int *a, int *b);  // int型へのポインタを2つ受け取る

int main(void) {
    int x = 5;
    int y = 10;

    printf("Before: x = %d, y = %d\n", x, y);
    swap(&x, &y);   // 変数xとyのアドレスを渡す
    printf("After : x = %d, y = %d\n", x, y);

    return 0;
}

// 関数の定義
void swap(int *a, int *b) {
    int temp = *a;  // *aで、ポインタaが指す先の値を取得
    *a = *b;        // bが指す値をaが指す場所に代入
    *b = temp;      // 退避しておいた値をbが指す場所に代入
}
実行結果
Before: x = 5, y = 10
After : x = 10, y = 5

プロトタイプ宣言においても、ポインタであることを*を付けて明確に示す必要があります。

int aint *aは、まったく別の型として扱われます。

配列を引数に取る関数のプロトタイプ宣言

C言語では、配列を関数に渡すとき、実際には「配列の先頭要素へのポインタ」が渡されます。

そのため、プロトタイプ宣言では次のような書き方をします。

C言語
// どちらもほぼ同じ意味
int sum_array(const int array[], int size);   // 配列風の書き方
int sum_array(const int *array, int size);    // ポインタ風の書き方

実際の例を見てみましょう。

C言語
#include <stdio.h>

// プロトタイプ宣言
int sum_array(const int array[], int size);

int main(void) {
    int data[] = {1, 2, 3, 4, 5};
    int n = sizeof(data) / sizeof(data[0]);

    int sum = sum_array(data, n);  // 配列名は先頭要素へのポインタに変換される
    printf("Sum = %d\n", sum);

    return 0;
}

// 関数の定義
int sum_array(const int array[], int size) {
    int i;
    int total = 0;

    for (i = 0; i < size; i++) {
        total += array[i];  // 配列の各要素にアクセス
    }
    return total;
}
実行結果
Sum = 15

プロトタイプ宣言では、array[]と書いても*arrayと書いてもよいですが、「配列を扱っている」ことを強調したい場合は[]の形を使うと読みやすくなります。

構造体を扱う関数のプロトタイプ宣言

構造体を扱う関数のプロトタイプ宣言も、基本は同じです。

ただし、構造体を「値として渡す」のか「ポインタとして渡す」のかによって、意味と効率が変わります。

例として、2次元の点を表すstruct Pointを扱ってみましょう。

C言語
#include <stdio.h>

// 構造体の定義
struct Point {
    int x;
    int y;
};

// プロトタイプ宣言
void print_point(struct Point p);        // 値渡し
void move_point(struct Point *p, int dx, int dy);  // ポインタ渡し

int main(void) {
    struct Point p = {10, 20};

    print_point(p);                    // (10, 20) と表示
    move_point(&p, 5, -3);             // pを書き換える
    print_point(p);                    // (15, 17) と表示

    return 0;
}

// 関数の定義(値渡し)
void print_point(struct Point p) {
    printf("Point(%d, %d)\n", p.x, p.y);
}

// 関数の定義(ポインタ渡し)
void move_point(struct Point *p, int dx, int dy) {
    p->x += dx;   // ポインタなので->でメンバアクセス
    p->y += dy;
}
実行結果
Point(10, 20)
Point(15, 17)

プロトタイプ宣言では、構造体の完全な名前struct Pointを書く必要があります。

typedefで短い別名を付けておくとプロトタイプもすっきりします。

C言語
typedef struct {
    int x;
    int y;
} Point;

void move_point(Point *p, int dx, int dy);  // すっきり書ける

初心者がつまずきやすいポイントと対処法

プロトタイプ宣言の書き忘れで出るコンパイルエラー

プロトタイプ宣言を忘れて関数を呼び出すと、多くのコンパイラはエラーまたは警告を出します。

典型的な例を見てみます。

C言語
#include <stdio.h>

int main(void) {
    int result = add(3, 4);  // addのプロトタイプ宣言がない
    printf("%d\n", result);
    return 0;
}

int add(int a, int b) {
    return a + b;
}

コンパイル時には、例えば次のようなメッセージが出ることがあります。

実行結果
warning: implicit declaration of function 'add' is invalid in C99

あるいは、より厳しい設定ではエラーになります。

対処法はシンプルで、mainより前に正しいプロトタイプ宣言を書くだけです。

C言語
#include <stdio.h>

// プロトタイプ宣言を追加
int add(int a, int b);

int main(void) {
    int result = add(3, 4);
    printf("%d\n", result);
    return 0;
}

int add(int a, int b) {
    return a + b;
}

「未宣言の関数を呼び出していないか」をエラーメッセージから読み取る習慣をつけておくと、トラブルの原因が見つけやすくなります。

プロトタイプ宣言と実装で型が違う時の不具合

プロトタイプ宣言と実際の関数定義のシグネチャが一致していないと、非常に危険な不具合を引き起こします。

次のような例を考えてみます。

C言語
#include <stdio.h>

// 間違ったプロトタイプ宣言
int square(double x);    // ← doubleを受け取ると宣言している

int main(void) {
    int n = 5;
    int result = square(n);  // intからdoubleへの変換を想定してしまう
    printf("%d\n", result);
    return 0;
}

// 実際の定義(シグネチャが不一致)
int square(int x) {      // ← 本当はintを受け取る関数
    return x * x;
}

コンパイラはプロトタイプ宣言をもとに、関数呼び出し時の引数の変換やスタックの積み方を決めます

ところが、実際の定義が異なる型を取っていると、呼び出し側と定義側で前提がずれてしまい、実行時に予測不能な動作を招きます。

多くの場合、コンパイラは次のようなエラーや警告を出します。

実行結果
conflicting types for 'square'
previous declaration of 'square' was here

このようなメッセージが出たら、プロトタイプ宣言と定義の「戻り値の型」「引数の型と数と順番」が完全一致しているかを必ず確認してください。

static関数とプロトタイプ宣言の関係

staticを付けた関数は、その定義されているファイル内からしか呼び出せない「ファイルローカル関数」になります。

これは、外部に公開したくないヘルパー関数を隠すためによく使われます。

C言語
// file.cの中だけで使う関数
static int helper(int x);  // プロトタイプ宣言にもstaticを付ける

int public_interface(int x) {
    return helper(x) * 2;
}

// 定義
static int helper(int x) {
    return x + 1;
}

static関数でも、同じファイル内であればプロトタイプ宣言を行うのが基本です。

特に、mainや他の関数からstatic関数を呼び出す場合には、やはり「先にプロトタイプ宣言、その後に定義」という順序を守る必要があります。

注意点として、static関数のプロトタイプ宣言はヘッダファイルには書かないのが通常です。

ヘッダに書いてしまうと、「他のファイルからも見える宣言」と意味が矛盾してしまうためです。

ヘッダファイルとプロトタイプ宣言のベストプラクティス

最後に、ヘッダファイルとプロトタイプ宣言を扱う際のベストプラクティスをまとめておきます。

まず、ヘッダファイルには次のようなものだけを書くのが基本です。

  • 外部に公開したい関数のプロトタイプ宣言
  • 外部から参照される構造体定義やtypedef
  • 定数マクロ#defineなど

典型的なヘッダファイルの形は次の通りです。

C言語
// mylib.h
#ifndef MYLIB_H
#define MYLIB_H

// 外部公開する関数のプロトタイプ宣言
int add(int a, int b);
double average(double x, double y);

// 公開したい構造体やtypedef
typedef struct {
    int x;
    int y;
} Point;

#endif  // MYLIB_H

インクルードガード#ifndef#endifによって、同じヘッダファイルが複数回インクルードされても問題が起きないようにするのが定石です。

その上で、実装ファイルmylib.cの先頭で自分自身のヘッダを必ずインクルードします。

C言語
// mylib.c
#include "mylib.h"

// 関数定義(実装)
int add(int a, int b) {
    return a + b;
}

double average(double x, double y) {
    return (x + y) / 2.0;
}

「ヘッダに宣言、cファイルに定義」という役割分担を徹底すると、プロトタイプ宣言と定義の不一致も防げますし、他のファイルからの利用も安全になります。

まとめ

プロトタイプ宣言は、C言語における関数の「約束事」をコンパイラと人間に伝える重要な仕組みです。

戻り値の型、引数の型と数、順番を明確に記述することで、コンパイラが引数チェックを行い、バグを未然に防いでくれます。

mainより前に宣言すること、引数なしのときは(void)と書くこと、ヘッダファイルに公開用のプロトタイプをまとめることなどの基本ルールを守れば、大規模なプログラムでも安全かつ読みやすいコードを書けるようになります。

まずは本文のサンプルを写経しながら、プロトタイプ宣言の書き方に慣れていってください。

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

URLをコピーしました!