C言語の関数は、入力(引数)を受け取り、処理した結果(戻り値)を呼び出し元へ返すのが基本です。
本記事では、初心者がつまずきやすい「引数と戻り値」の仕組みと使い方を、具体例とともに丁寧に解説します。
配列引数やポインタ、const の使い分け、複数の結果を返す設計、プロトタイプ宣言まで順に学びましょう。
引数と戻り値の意味
引数とは何かと関数シグネチャの読み方
シグネチャの基本要素
関数シグネチャ(関数の宣言・定義の見出し)は、戻り値の型・関数名・引数リストからなります。
たとえば次の宣言では、戻り値の型がint
、関数名がadd
、引数がint a, int b
です。
// 戻り値:int, 関数名:add, 引数:int a と int b
int add(int a, int b);
- 引数は「仮引数」と呼ばれ、呼び出し側で渡す値(実引数)を受け取ります。
- 関数は「プロトタイプ宣言」と「定義」を区別します。プロトタイプ宣言は関数の存在と型をコンパイラに知らせ、定義は実際の処理本体です。
プロトタイプと定義の違い
// プロトタイプ宣言(ヘッダなどに書く)
double average(const int *arr, size_t n);
// 定義(ソースファイルに書く)
double average(const int *arr, size_t n) {
// 本体
}
プロトタイプがあることで、コンパイラは引数や戻り値の型チェックを行えます。
C では関数オーバーロードはありません。
同名関数を型違いで複数定義することはできないため、シグネチャは一意である必要があります。
戻り値とは何かとreturn文の役割
return文の基本
return
は関数の実行を終了し、呼び出し元へ値を返します。
void
関数ではreturn;
(値なし)または省略も可能ですが、非void
関数ではすべての経路で値を返す必要があります。
int max2(int a, int b) {
if (a > b) return a;
return b; // すべての経路で値を返す
}
注意点:未定義動作と寿命
- 非
void
関数でreturn
値を返さないと未定義動作になります。 - ローカル変数のアドレスを返してはいけません(関数終了で寿命が切れるため)。
- ポインタを返す設計では、指す先の寿命(静的領域、動的確保済みなど)を意識します。
C言語の引数の使い方と注意点
型と個数と順序の決め方
引数の型・個数・順序は関数の契約そのものです。
呼び出し側はこの契約に従って値を渡します。
順序を入れ替えるだけでロジックが破綻することがあります。
C にはオーバーロードがないため、意味が異なる操作は関数名で区別するか、引数の数や補助的なenum
で切り替えます。
引数の順序は「主対象→条件や閾値→オプション」のように、自然に読める並びにするのが実務上のコツです。
値渡しの仕組みと呼び出し側が変わらない理由
Cの関数引数は「値渡し」です。
呼び出し側の値のコピーが関数に渡されるため、関数内で引数を変更しても呼び出し元の変数は変化しません。
例:インクリメント関数
#include <stdio.h>
// 値渡し(呼び出し側は変わらない)
void inc_wrong(int x) {
x++; // xはコピーなので、呼び出し元の変数は変化しない
}
// アドレスを渡して変更(呼び出し側が変わる)
void inc_right(int *x) {
(*x)++;
}
int main(void) {
int a = 10;
inc_wrong(a);
printf("after inc_wrong: %d\n", a); // 10 のまま
inc_right(&a);
printf("after inc_right: %d\n", a); // 11 に更新
return 0;
}
after inc_wrong: 10
after inc_right: 11
配列引数の扱いとポインタに見える理由
Cでは、配列を関数に渡すと先頭要素へのポインタに自動変換(配列の先頭ポインタへの暗黙変換)されます。
したがって、int arr[]
もint *arr
もシグネチャ上は同じ意味です。
関数内でsizeof(arr)
としても配列のサイズではなくポインタのサイズになります。
#include <stdio.h>
#include <stddef.h>
size_t sum(const int arr[], size_t n) { // const int *arr と同等
size_t s = 0;
for (size_t i = 0; i < n; ++i) {
s += arr[i];
}
printf("sizeof(arr) in function: %zu\n", sizeof(arr)); // ポインタのサイズ
return s;
}
int main(void) {
int a[5] = {1, 2, 3, 4, 5};
printf("sizeof(a) in caller: %zu\n", sizeof(a)); // 配列の総バイト数
size_t s = sum(a, 5);
printf("sum: %zu\n", s);
return 0;
}
sizeof(a) in caller: 20
sizeof(arr) in function: 8
sum: 15
配列全体の長さが必要なら、呼び出し側から要素数(n
)を併せて渡してください。
constを使った引数の意図表現
読み取り専用の意図を明確にするため、const
修飾子を使います。
特に配列・ポインタの引数では有効です。
const int *p
(int const *p
も同じ意味): 参照先の整数は書き換え不可、ポインタ自体の差し替えは可int * const p
: 参照先の整数は書き換え可、ポインタ自体の差し替えは不可const int * const p
: 参照先もポインタ自体も書き換え不可
形式 | 参照先の書き換え | ポインタの差し替え | 説明 |
---|---|---|---|
int *p | 可能 | 可能 | 一般的な可変参照 |
const int *p | 不可 | 可能 | 読み取り専用参照 |
int * const p | 可能 | 不可 | 固定アドレスを扱うとき |
const int * const p | 不可 | 不可 | 完全に不変の参照 |
読み取り専用の関数にconst
を付けると、誤って書き換えるバグをコンパイル時に防ぎやすくなります。
C言語の戻り値の使い方
戻り値の型を選ぶ基準とvoid関数
戻り値の型は、返すべき情報の性質と範囲で選びます。
- 計算結果の数値: オーバーフローや精度を考え、
int
/long long
/double
など適切な型を選びます。 - 要素数・サイズ: 非負であることが明確なので
size_t
が適しています。 - 成功/失敗の有無:
bool
(#include <stdbool.h>
)やエラーコード(int
やenum
)が実用的です。 - 何も返す必要がない:
void
関数にします。
複数の情報を返すときの設計指針
Cは多値返却を直接サポートしないため、次のいずれかを選びます。
- 構造体を戻り値として返す(可読性が高い。小さな構造体なら効率も良い)
- ポインタ引数を使って出力を書き込む(複数値に向く。戻り値はステータス用に使える)
- グローバル変数は基本的に避ける(テストしづらくバグの温床)
一般に「戻り値はステータス(成功/失敗)、結果はポインタ引数」または「結果の構造体を戻す」設計が読みやすく安全です。
エラーコードやboolを戻り値に使う例
成功/失敗をbool
で返し、結果はポインタ引数に書く例です。
#include <stdbool.h>
#include <errno.h>
// 成功: true (結果*outに格納)、失敗: false(ゼロ除算など)
bool safe_divide(int a, int b, int *out) {
if (b == 0) {
errno = EDOM; // ドメインエラー(任意)
return false;
}
if (out) *out = a / b;
return true;
}
このように戻り値を状態に割り当てると、失敗時の分岐が明確になります。
ポインタ引数で値を受け渡す方法
アドレスを渡して関数内で値を更新する
変数を更新したい場合はアドレス(ポインタ)を渡します。
#include <stdio.h>
void swap(int *a, int *b) {
if (!a || !b) return; // 安全性のためのNULLチェック
int tmp = *a;
*a = *b;
*b = tmp;
}
int main(void) {
int x = 3, y = 7;
printf("before swap: x=%d, y=%d\n", x, y);
swap(&x, &y);
printf("after swap: x=%d, y=%d\n", x, y);
return 0;
}
before swap: x=3, y=7
after swap: x=7, y=3
複数の結果を返すためのポインタと構造体の比較
ポインタ出力引数の例
#include <stdbool.h>
#include <stddef.h>
// 成功時にmin_out, max_outへ書き込む。戻り値は成功/失敗
bool minmax2(const int *arr, size_t n, int *min_out, int *max_out) {
if (!arr || n == 0 || !min_out || !max_out) return false;
int mn = arr[0], mx = arr[0];
for (size_t i = 1; i < n; ++i) {
if (arr[i] < mn) mn = arr[i];
if (arr[i] > mx) mx = arr[i];
}
*min_out = mn;
*max_out = mx;
return true;
}
構造体を戻す例
#include <stddef.h>
typedef struct {
int min;
int max;
int ok; // 0:失敗, 1:成功(簡易フラグ)
} MinMax;
MinMax minmax(const int *arr, size_t n) {
MinMax r = {0, 0, 0};
if (!arr || n == 0) return r;
r.min = r.max = arr[0];
for (size_t i = 1; i < n; ++i) {
if (arr[i] < r.min) r.min = arr[i];
if (arr[i] > r.max) r.max = arr[i];
}
r.ok = 1;
return r;
}
方法 | 呼び出しの読みやすさ | 失敗時の扱い | 効率/ABI | 使いどころ |
---|---|---|---|---|
構造体を戻り値 | 直感的(名前付きフィールド) | ok やタグで表現 | 小さな構造体は最適化されやすい | 複数の関連値をひとまとめに返す |
ポインタ出力引数+ステータス戻り値 | 分かりやすい制御フロー(if) | falseで早期return | 参照渡しでオーバーヘッド小 | 成否判定+複数値を返したいとき |
関数プロトタイプ宣言と型チェック
ヘッダに書くプロトタイプと引数・戻り値の宣言
実務では「ヘッダ(.h)にプロトタイプ」「実装(.c)に定義」「利用側(.c)で#include
」の3分割が基本です。
#ifndef MATH_UTILS_H
#define MATH_UTILS_H
#include <stddef.h>
#include <stdbool.h>
int add(int a, int b);
double average(const int *arr, size_t n);
bool safe_divide(int a, int b, int *out);
#endif // MATH_UTILS_H
#include "math_utils.h"
int add(int a, int b) {
return a + b;
}
double average(const int *arr, size_t n) {
if (!arr || n == 0) return 0.0;
long long sum = 0;
for (size_t i = 0; i < n; ++i) sum += arr[i];
return (double)sum / (double)n;
}
bool safe_divide(int a, int b, int *out) {
if (b == 0 || !out) return false;
*out = a / b;
return true;
}
#include <stdio.h>
#include "math_utils.h"
int main(void) {
int s = add(12, 30);
printf("add: %d\n", s);
int v[] = {3, 4, 8, 10};
printf("average: %.2f\n", average(v, 4));
int q;
if (safe_divide(42, 6, &q)) {
printf("quotient: %d\n", q);
} else {
printf("divide error\n");
}
return 0;
}
add: 42
average: 6.25
quotient: 7
型不一致のコンパイル警告と対処
プロトタイプがあれば、引数や戻り値の不一致を検出できます。
たとえば「意図せずdouble
をint
に渡している」などです。
#include <stdio.h>
int add(int a, int b);
int main(void) {
double x = 1.5, y = 2.5;
// 意図しない切捨て。-Wconversion などで警告が出ることがある
int s = add(x, y); // 1 + 2 と解釈される可能性
printf("%d\n", s);
return 0;
}
warning: implicit conversion from 'double' to 'int' changes value from 1.5 to 1 [-Wliteral-conversion]
対処としては、正しい型を渡す、明示的なキャストを見直す、関数の引数型を見直すなどがあります。
また、関数宣言は必ず先に見せること、int f(void)
(引数なし)とint f()
(引数が未指定:古い書き方で避ける)を混同しないことが重要です。
現代のCでは「引数なし」はint f(void)
で表します。
初心者向けサンプルとよくあるミス
例 add関数と平均値関数での引数と戻り値の使い方
#include <stdio.h>
#include <stddef.h>
int add(int a, int b) {
return a + b; // 戻り値で結果を返す
}
double average(const int *arr, size_t n) {
if (!arr || n == 0) return 0.0; // エッジケースに注意
long long sum = 0;
for (size_t i = 0; i < n; ++i) sum += arr[i];
return (double)sum / (double)n;
}
int main(void) {
int s = add(7, 5);
printf("add(7,5) = %d\n", s);
int data[] = {1, 2, 3, 4};
double avg = average(data, 4);
printf("average = %.2f\n", avg);
return 0;
}
add(7,5) = 12
average = 2.50
商と余りを返す関数の設計例(余りはポインタ引数)
戻り値で商を返し、余りはポインタ引数に書き込む設計例です。
ゼロ除算に備えて成功可否をbool
で知らせる案も合わせて示します。
#include <stdio.h>
#include <stdbool.h>
// 戻り値: 商, *rem に余り, *ok に成功可否(NULL可)
int div_with_remainder(int a, int b, int *rem, bool *ok) {
if (b == 0) {
if (rem) *rem = 0;
if (ok) *ok = false;
return 0; // 値は未定義扱いなので使わない
}
if (ok) *ok = true;
int q = a / b;
if (rem) *rem = a % b;
return q;
}
int main(void) {
int r;
bool ok;
int q = div_with_remainder(17, 5, &r, &ok);
if (ok) {
printf("17 / 5 -> quotient=%d, remainder=%d\n", q, r);
}
q = div_with_remainder(10, 0, &r, &ok);
if (!ok) {
printf("division by zero!\n");
}
return 0;
}
17 / 5 -> quotient=3, remainder=2
division by zero!
この設計は、「主たる値(商)」を戻り値で返し、「付随情報(余り)」を出力引数で渡す良い例です。
よくあるエラー 引数の型ミスや戻り値の未使用
- 引数の型ミス
期待型と異なる実引数を渡すと、暗黙変換で意図しない値になったり、警告が出ます。
プロトタイプを正しく書き、
-Wall -Wextra -Wconversion
などの警告オプションを活用しましょう。int f()
vsint f(void)
後者が「引数なし」の正しい宣言です。
前者は古い書き方で、未指定引数を意味し紛らわしいため避けます。
- 戻り値の未使用
scanf
やfgets
などの戻り値を無視すると、失敗を見落とします。戻り値で状態を返す関数は、必ずチェックする習慣をつけてください。
- 非
void
関数でreturn
しない すべての経路で値を返すようにしましょう。
コンパイラの警告を必ず修正します。
- ローカルのアドレスを返す
関数終了後に無効になるため、決して返さないでください。
必要なら動的確保(
malloc
)や静的領域、呼び出し側で用意したバッファを使います。
まとめ
C言語の関数設計では、引数と戻り値が「契約」です。
引数は値渡しである点を踏まえ、呼び出し側に影響を及ぼす必要がある場合はポインタを使います。
配列引数はポインタに見える(先頭要素へのポインタに変換される)ため、要素数は別途渡すのが基本です。
読み取り専用はconst
で意図を明確化し、戻り値は「主結果」か「ステータス」に役割分担すると可読性が上がります。
複数の結果は出力ポインタか構造体で返し、プロトタイプをヘッダで宣言して型チェックを有効にしましょう。
コンパイラ警告は味方です。
常にチェックし、ゼロ除算や未定義動作を避ける堅牢なコードを心がけてください。