【初心者向け】C言語の関数の定義と呼び出しを完全解説:引数・返り値・プロトタイプ宣言まで

プログラムを部品ごとに分けて再利用できるのが関数です。

本記事では、C言語の関数を「定義して呼び出す」までを、初心者の方向けに順序立てて解説します。

返り値や引数の型、評価順序の注意、配列やポインタの扱い、プロトタイプ宣言やヘッダ分割、典型的なエラー対策まで一通り学べます。

C言語の関数とは何か:定義と呼び出しの基本

関数は「まとまった処理に名前を付けて再利用できる単位」です。

C言語関数の基本的な構文
戻り値の型 関数名(引数の型 引数名, 引数の型 引数名, ...) {
    // 関数本体
    return 戻り値;
}

C言語では標準ライブラリ関数(printf など)に加え、自分で関数を定義して使います。

関数の役割とメリット

関数を使うと、同じ処理を何度も書く必要がなくなり、コードの見通しと保守性が向上します。

処理を分割することでテストも容易になります。

さらに、引数と返り値で入力と出力を明確にでき、副作用(グローバル変数の書き換えなど)を最小限にできます。

main関数とユーザー定義関数の関係

C言語のプログラムはmain関数から開始します。

mainがユーザー定義関数を呼び出し、ユーザー定義関数がさらに他の関数を呼び出す、といった構造でプログラムは進みます。

mainは「入口」、他の関数は「処理の部品」という関係です。

C言語の関数の構成要素(返り値型・関数名・引数・本体)

関数は次の部品から構成されます。

構成要素書き方例説明
返り値型int / double / void関数が返す値の型。返り値がないときはvoid
関数名sum小文字やアンダースコアを含む識別子(予約語は不可)
引数(仮引数)(int a, double b)関数に渡す値の受け取り口。型は必須
本体(ブロック){ ... }実際の処理。必要に応じてreturnで値を返す

関数を使うには「定義」し、「プロトタイプ宣言(宣言)」してから「呼び出し」ます。

定義の前に呼び出したい場合は宣言が必要です。

C言語の関数の定義方法(構文・記述例)

最小構成は「返り値型+関数名+(引数リスト)+本体」です。

C言語
#include <stdio.h>

// 2つの整数を足して返す関数
int add(int a, int b) {        // 返り値型:int、関数名:add、引数:int a,int b
    return a + b;              // 計算結果を返す
}

// 引数も返り値もない関数
void greet(void) {             // 引数がない場合は(void)と書くのがCの慣習
    printf("Hello, C functions!\n");
}

int main(void) {
    greet();                   // 呼び出し
    int s = add(3, 5);         // 返り値を受け取る
    printf("sum = %d\n", s);
    return 0;
}
実行結果
Hello, C functions!
sum = 8

返り値型とvoidの使い分け

返り値がある処理には具体的な型(intdoubleなど)を使います。

計算や判定結果を返すときに有用です。

一方、副作用(画面表示・ファイル出力など)だけを行う関数はvoidを使います。

引数がないときは()ではなく(void)と書くのがCの正しい宣言です(古いCスタイルとの区別のため)。

明示的に(void)としていると、引数の定義を忘れているわけではないということも一目でわかります。

引数リストの書き方(int・double・構造体)

引数は「型+名前」をカンマで区切って並べます。

複数の型を混在できます。

C言語
#include <stdio.h>
#include <math.h>

// 円の面積(double)を返す
double area_circle(double r) {
    const double PI = 3.141592653589793;
    return PI * r * r;
}

// 構造体を引数に取る(値渡し)
typedef struct {
    double x;
    double y;
} Point;

// 引数が複数ある場合は、カンマ(,)で区切る
double distance2(Point a, Point b) { // 2点間の距離の2乗(sqrtを避けて整数性を保つ用途など)
    double dx = a.x - b.x;
    double dy = a.y - b.y;
    return dx*dx + dy*dy;
}

int main(void) {
    printf("area = %.2f\n", area_circle(2.0));
    Point p = {0.0, 0.0}, q = {3.0, 4.0};
    printf("dist^2 = %.2f\n", distance2(p, q));
    return 0;
}
実行結果
area = 12.57
dist^2 = 25.00

このように、複雑な計算処理があったとしても、一度関数に定義して計算するための値を関数に渡してしまえば、関数を呼び出す1文を書くだけで計算結果を求められます。

戻り値のreturnと到達しないパスへの注意

void関数は必ず全ての実行経路でreturn 値;を実行しなければなりません。

到達しない経路があると未定義動作につながります。

コンパイラ警告も発生します。

C言語
#include <stdio.h>

// NG例:nが負のときにreturnしない可能性
int half(int n) {
    if (n >= 0) {
        return n / 2;
    }
    // ここに到達すると返り値が未定義(警告の対象)
}

int main(void) {
    printf("%d\n", half(4));
    // printf("%d\n", half(-1)); // 未定義動作の可能性
    return 0;
}

改善版では、必ず返すようにします。

C言語
int half_fixed(int n) {
    if (n >= 0) {
        return n / 2;
    } else {
        return -( (-n) / 2 );  // またはエラーコードを返す、あるいは引数の制約を文書化
    }
}

なお、main関数はブロック末尾に明示的なreturn 0;を書かなくても0を返したものとみなされますが、可読性のために書くことをおすすめします。

C言語の関数の呼び出し方法(引数と返り値の受け渡し)

関数呼び出しでは「実引数」を渡して「返り値」を受け取れます。

返り値を無視しても構いません。

関数呼び出しの基本構文と評価順序

呼び出しは関数名(実引数1, 実引数2, ...)の形です。

重要な注意点として、C言語では「実引数の評価順序は未規定(unspecified)」です。

つまり、f(g(), h())ghのどちらが先に呼ばれるかは処理系に依存します。

同じ変数を複数の引数でインクリメントするようなコードは未定義動作になり得ます。

C言語
// 未定義動作の可能性:iを複数の引数で同時に変更
// f(i++, i++); // こうした書き方は避けます

副作用に依存せず、順序に依存しない引数を渡すよう設計してください。

必要なら一時変数で段階を分けます。

C言語
int a = get_a();
int b = get_b();
f(a, b); // 引数の評価順序に依存しない

返り値を変数に受け取る・無視する

返り値は変数で受け取るのが基本ですが、必要なければ捨てても構いません。

明示的に無視することを示すために(void)関数名(...)と書くこともあります。

C言語
#include <stdio.h>

int add(int a, int b) { return a + b; }
int puts_wrapper(const char* s) { return puts(s); } // putsの戻り値を例示

int main(void) {
    int s = add(10, 20);       // 受け取る
    printf("sum=%d\n", s);

    add(1, 2);                 // 無視(OK)
    (void)puts_wrapper("ignored return"); // 明示的に無視

    return 0;
}
実行結果
sum=30
ignored return

標準ライブラリ関数の呼び出し例

標準ライブラリ関数を使うときは対応するヘッダを#includeします。

数学関数は多くの環境で-lmリンクが必要です。

C言語
#include <stdio.h>
#include <math.h>     // sqrt
#include <string.h>   // strlen

int main(void) {
    const char *msg = "Hello";
    size_t len = strlen(msg);          // 文字列の長さ
    double root = sqrt(2.0);           // 平方根(-lmでリンク)

    printf("len=%zu, sqrt(2)=%.6f\n", len, root);
    return 0;
}
実行結果
len=5, sqrt(2)=1.414214

Shellコンパイル例(GCCの場合)
gcc -std=c11 main.c -lm -o app

引数と返り値の実践ポイント(値渡し・ポインタ・配列)

C言語の引数は「すべて値渡し」です。

呼び出し側の変数を関数内で直接書き換えることはできません。

必要ならポインタを使います。

C言語の値渡しと参照の代替としてのポインタ

整数をインクリメントする例で比較します。

C言語
#include <stdio.h>

void add_one_by_value(int x) { // 値渡し:呼び出し側の変数は変化しない
    x = x + 1;
}

void add_one_by_pointer(int *px) { // ポインタ渡し:呼び出し側の変数を更新できる
    if (px) {
        *px = *px + 1;
    }
}

int main(void) {
    int a = 10, b = 10;
    add_one_by_value(a);
    add_one_by_pointer(&b);
    printf("a=%d, b=%d\n", a, b); // aは変わらず、bは11
    return 0;
}
実行結果
a=10, b=11

ポインタはアドレスを渡すため、間接参照*pxで実体を操作します。

ヌルポインタチェックも習慣にしましょう。

配列と文字列を引数で扱う方法

配列は関数引数に渡すと「先頭要素へのポインタ」に暗黙変換されます。

長さは自動では渡らないため、別引数で渡します。

C言語
#include <stdio.h>
#include <stddef.h> // size_t

int sum_array(const int *arr, size_t n) { // 読み取り専用ならconstを付ける
    int sum = 0;
    for (size_t i = 0; i < n; ++i) {
        sum += arr[i];
    }
    return sum;
}

int main(void) {
    int v[] = {1, 2, 3, 4, 5};
    int s = sum_array(v, sizeof v / sizeof v[0]);
    printf("sum=%d\n", s);
    return 0;
}
実行結果
sum=15

Cの文字列はヌル終端のchar配列です。

関数にはconst char *で渡すのが一般的です。

C言語
#include <stdio.h>

void print_upper(const char *s) { // sは読み取り専用
    for (size_t i = 0; s[i] != '\0'; ++i) {
        char c = s[i];
        if ('a' <= c && c <= 'z') c = (char)(c - 'a' + 'A');
        putchar(c);
    }
    putchar('\n');
}

int main(void) {
    print_upper("Hello, world!");
    return 0;
}
実行結果
HELLO, WORLD!

const修飾子で安全に引数を渡す

constは「関数内でそのポインタ経由で値を書き換えない」ことを表明します。

意図せぬ変更を防ぎ、最適化や可読性に役立ちます。

  • const int *p は「読み取り専用のintへのポインタ」
  • int *const p は「変更不可のポインタ(指す先は変更可)」
  • const int *const p は「ポインタも先も変更不可」
C言語
// 読み取り専用にしたいバッファ
size_t count_zeros(const int *arr, size_t n) {
    size_t c = 0;
    for (size_t i = 0; i < n; ++i) {
        if (arr[i] == 0) { ++c; }
        // arr[i] = 1; // コンパイルエラー:const経由の書き換え
    }
    return c;
}

関数のプロトタイプ宣言とヘッダ分割

関数は「使う前に型情報をコンパイラに知らせる」必要があります。

これがプロトタイプ宣言です。

プロトタイプ宣言の書き方と必要性

プロトタイプ宣言は、関数定義の前に「返り値型・関数名・引数型」を宣言します。

これにより、コンパイラは呼び出し時の型チェックを行えます。

C99以降では暗黙の宣言はエラーです。

C言語
#include <stdio.h>

// プロトタイプ宣言(引数名は省略可だが、可読性のため付けるのがおすすめ)
int add(int a, int b);

int main(void) {
    printf("%d\n", add(3, 4)); // ここで型チェックされる
    return 0;
}

// 定義は後ろにあってもOK(宣言済みなので問題ない)
int add(int a, int b) {
    return a + b;
}

宣言がないまま関数を呼ぶと、多くのコンパイラでエラーまたは厳しい警告が出ます。

ヘッダファイル(.h)とソース(.c)の分割手順

複数ファイルに分割するときは、関数の宣言(プロトタイプ)を.hに、実装を.cに置きます。

利用側は.h#includeします。

C言語mathutil.h
/* ヘッダ(宣言) */
#ifndef MATHUTIL_H
#define MATHUTIL_H

int add(int a, int b);
double mean(const int *arr, int n);

#endif
C言語mathutil.c
/* 実装 */
#include "mathutil.h"

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

double mean(const int *arr, int n) {
    if (!arr || n <= 0) return 0.0;
    long long sum = 0;
    for (int i = 0; i < n; ++i) sum += arr[i];
    return (double)sum / n;
}
C言語main.c
/* 利用側 */
#include <stdio.h>
#include "mathutil.h"

int main(void) {
    int v[] = {1, 2, 3, 4};
    printf("add=%d\n", add(2, 5));
    printf("mean=%.2f\n", mean(v, 4));
    return 0;
}

Shellコンパイルとリンク(GCC例)
gcc -std=c11 -c mathutil.c
gcc -std=c11 -c main.c
gcc main.o mathutil.o -o app
./app
実行結果
add=7
mean=2.50

staticとexternで制御するスコープとリンケージ

  • 関数にstaticを付けると「内部リンケージ」になり、その翻訳単位(.cファイル)内からしか参照できません。モジュール内のヘルパー関数を隠すのに有効です。
  • externは「外部リンケージ」を表します。関数はデフォルトで外部リンケージなので、通常は明示不要です。ファイルスコープ変数ではextern宣言と別ファイルでの定義を組み合わせます。
C言語
/* util.c */
static int helper(int x) {     // 他ファイルから見えない
    return x * x;
}

int public_api(int x) {        // 他ファイルから見える(デフォルト外部リンケージ)
    return helper(x) + 1;
}
C言語
/* main.c */
#include <stdio.h>

// externは宣言で用いられる(関数は省略可、変数はよく使う)
extern int public_api(int);

int main(void) {
    printf("%d\n", public_api(3)); // OK
    // helper(3); // リンクエラー:見えない(static)
    return 0;
}

初心者がつまずきやすいエラーと対策

関数周りでは「型」「宣言・定義の順序」「リンク」に関連するエラーが典型です。

型不一致や引数不足のコンパイルエラー

プロトタイプ宣言と異なる呼び出しをするとエラーになります。

C言語
#include <stdio.h>

int add(int a, int b);

int main(void) {
    printf("%d\n", add(1));      // 引数不足
    printf("%f\n", add(1, 2));   // 書式指定子の型不一致(返り値はint)
    return 0;
}

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

コンパイルエラー例(環境により異なります)

実行結果
error: too few arguments to function 'add'
warning: format '%f' expects argument of type 'double', but argument 2 has type 'int'

対策は、プロトタイプと一致する引数を渡し、printfなどの書式指定子も返り値の型に合わせることです。

未定義参照(リンクエラー)の原因と直し方

コンパイルは通るのにリンク時に「未定義参照」となるのは、関数の定義がリンク対象に含まれていない場合が多いです。

C言語
/* main.c */
int add(int, int); // 宣言だけ

int main(void) {
    return add(1, 2);
}

リンクエラー例

undefined reference to `add'

対策は、addを定義したadd.cを同時にリンクする、または静的/動的ライブラリを正しく指定することです。

数学関数sqrt-lmを付け忘れるのもよくある原因です。

再帰関数やグローバル変数の注意点

再帰関数は「終了条件(ベースケース)」が必須です。

深すぎる再帰はスタックオーバーフローの危険があるため、反復(ループ)への置き換えも検討します。

C言語
#include <stdio.h>

// n! を計算(単純再帰)
unsigned long long fact(unsigned int n) {
    if (n <= 1) return 1ULL;         // ベースケース
    return n * fact(n - 1);          // 再帰呼び出し
}

int main(void) {
    printf("10! = %llu\n", fact(10));
    return 0;
}
実行結果
10! = 3628800

グローバル変数は複数の関数から書き換えられるため、バグの温床になりがちです。

できるだけ関数の引数と返り値でデータを受け渡し、どうしても必要な場合はstaticで翻訳単位に閉じる、constで読み取り専用にするなど制御しましょう。

まとめ

関数は「返り値型・関数名・引数・本体」から構成され、プロトタイプ宣言によってコンパイラに型情報を知らせてから呼び出すのが基本です。

C言語の引数はすべて値渡しであり、呼び出し側の値を変更したいときはポインタを用います。

配列・文字列はポインタとして渡されるため、長さやconstの扱いを明確にすると安全です。

呼び出し時の引数評価順序は未規定であり、副作用に依存したコードは避けましょう。

複数ファイルではヘッダに宣言、ソースに定義を分け、static/externで公開範囲を適切に管理します。

最後に、型不一致・リンクエラー・再帰のベースケース漏れといった典型的な落とし穴を意識することで、堅牢で読みやすいCの関数を設計できるようになります。

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

URLをコピーしました!