C言語で関数を正しく使いこなすには、引数と戻り値の仕組みを深く理解することが欠かせません。
本記事では、仮引数と実引数の違い、C言語は値渡しであること、配列や文字列の扱い、そして複数の値を返すための実践的なパターンまで、サンプルコードとともに丁寧に解説します。
引数と戻り値
仮引数と実引数の違い
関数の定義側で受け取る変数を仮引数、呼び出し側から渡す値を実引数と呼びます。
例えば int add(int a, int b)
と定義した場合、a
と b
は仮引数です。
呼び出し側で add(2, 3)
と書くと、2 と 3 が実引数です。
仮引数は関数の内部でのみ有効なローカル変数であり、スコープの外(呼び出し元)には影響しません。
returnと戻り値の型
関数には戻り値の型があり、return
文で返す値の型が宣言と一致している必要があります。
例えば戻り値の型が int
の関数で double
を返すと暗黙の型変換が発生し、意図しない丸めが起きる可能性があります。
戻り値がない場合は void
を使います。
コンパイラ警告を無視せず、型を合わせる設計が重要です。
サンプルコード add(int a, int b)の例
加算関数の基本形を確認します。
#include <stdio.h>
// 2つの整数を受け取り、その合計を返す関数
int add(int a, int b) {
// a と b は仮引数(この関数の中だけで有効)
return a + b; // 戻り値の型(int)と一致
}
int main(void) {
int x = 2;
int y = 3;
int sum = add(x, y); // x, y は実引数
printf("add(%d, %d) = %d\n", x, y, sum);
return 0;
}
add(2, 3) = 5
C言語は値渡し: 引数の挙動を理解
呼び出し時に値がコピーされる
C言語ではすべての引数は「値渡し」です。
つまり、呼び出し側の実引数の値が関数の仮引数へコピーされます。
仮引数を関数内で変更しても、呼び出し元の変数は変わりません。
「参照渡し」はC言語にはありません。
参照のような挙動が必要なときは、ポインタを使います(後述)。
サンプルコード 値を変えても元は変わらない
次の例では、関数内で引数を変更しても呼び出し元の値は変わらないことを確認します。
#include <stdio.h>
void increment(int n) {
// n は呼び出し時にコピーされたローカル変数
n = n + 1;
printf("increment内 n = %d\n", n);
}
int main(void) {
int a = 10;
increment(a); // a の値(10)がコピーされる
printf("main内 a = %d\n", a); // a は変わらない
return 0;
}
increment内 n = 11
main内 a = 10
配列の引数はポインタに退化する
関数に配列を渡すとき、配列は先頭要素へのポインタに「退化」します。
関数定義の int arr[]
と int *arr
は等価です。
したがって、関数内の sizeof(arr)
は配列全体のサイズではなくポインタのサイズになります。
そのため、要素数は別引数で渡すのが定石です。
#include <stdio.h>
void wrong_sizeof(int arr[]) {
// arr は int* に退化しているため、sizeof(arr) はポインタのサイズ
printf("wrong_sizeof: sizeof(arr) = %zu (配列のサイズではない)\n", sizeof(arr));
}
void fill_seq(int *arr, size_t n) {
// n を必ず受け取り、境界内でアクセスする
for (size_t i = 0; i < n; ++i) {
arr[i] = (int)i;
}
}
int main(void) {
int a[5] = {0}; // 要素数は 5
wrong_sizeof(a);
fill_seq(a, 5); // a は先頭要素へのポインタとして渡される
for (size_t i = 0; i < 5; ++i) {
printf("%d ", a[i]); // 要素の変更は呼び出し元にも反映される
}
printf("\n");
return 0;
}
wrong_sizeof: sizeof(arr) = 8 (配列のサイズではない) ← 64bit環境の例
0 1 2 3 4
ここでのポイントは、「値渡し」なのはポインタ値(アドレス)であり、その先にある実データは同じ領域を参照していることです。
結果として要素の変更は呼び出し元にも見えます。
文字列(char*)の引数の扱い
Cの文字列はヌル終端('\0'
)された char
の並びへのポインタです。
読み取り専用の文字列リテラルは変更してはいけません。
読み取り専用でよい関数は const char
を引数に取り、書き換える関数は書き込み可能なバッファ(char
)を受け取ります。
#include <stdio.h>
#include <string.h>
size_t str_len_readonly(const char *s) {
// s は読み取り専用として扱う
return strlen(s);
}
void to_upper_inplace(char *s) {
// s は書き込み可能なバッファであることが前提
for (size_t i = 0; s[i] != '#include <stdio.h>
#include <string.h>
size_t str_len_readonly(const char *s) {
// s は読み取り専用として扱う
return strlen(s);
}
void to_upper_inplace(char *s) {
// s は書き込み可能なバッファであることが前提
for (size_t i = 0; s[i] != '\0'; ++i) {
if ('a' <= s[i] && s[i] <= 'z') {
s[i] = (char)(s[i] - 'a' + 'A');
}
}
}
int main(void) {
const char *lit = "hello"; // 文字列リテラル(変更不可)
char buf[] = "Hello, C"; // 配列(変更可)
printf("len(lit) = %zu\n", str_len_readonly(lit));
printf("before: %s\n", buf);
to_upper_inplace(buf);
printf("after : %s\n", buf);
// 以下は未定義動作になるため絶対にしないこと:
// to_upper_inplace((char*)lit); // <mark style="background-color:rgba(0, 0, 0, 0);color:#cf2e2e" class="has-inline-color"><strong>リテラルを書き換えるのは危険</strong></mark>
return 0;
}
'; ++i) {
if ('a' <= s[i] && s[i] <= 'z') {
s[i] = (char)(s[i] - 'a' + 'A');
}
}
}
int main(void) {
const char *lit = "hello"; // 文字列リテラル(変更不可)
char buf[] = "Hello, C"; // 配列(変更可)
printf("len(lit) = %zu\n", str_len_readonly(lit));
printf("before: %s\n", buf);
to_upper_inplace(buf);
printf("after : %s\n", buf);
// 以下は未定義動作になるため絶対にしないこと:
// to_upper_inplace((char*)lit); // <mark style="background-color:rgba(0, 0, 0, 0);color:#cf2e2e" class="has-inline-color"><strong>リテラルを書き換えるのは危険</strong></mark>
return 0;
}
len(lit) = 5
before: Hello, C
after : HELLO, C
書き込みの可能性がない引数には積極的に const
を付けると、意図が明確になりバグを防げます。
複数の値を返す方法
戻り値は1つだけ
C言語の関数は戻り値を1つしか返せません。
複数の情報を返したい場合は、ポインタ引数や構造体を使います。
用途に応じて設計を選びます。
ポインタ引数で出力を受け取る
最も一般的な方法は、出力用の引数をポインタとして受け取り、関数内で書き込むやり方です。
戻り値はエラーコードや成否に使うと扱いやすくなります。
サンプルコード swapの成功例
値渡しでは入れ替えは失敗しますが、ポインタを使えば成功します。
#include <stdio.h>
void swap_fail(int a, int b) {
// a, b はコピーなので呼び出し元の変数は入れ替わらない
int tmp = a;
a = b;
b = tmp;
}
void swap_ok(int *a, int *b) {
// a, b はポインタ(アドレスのコピー)。指し先の実体を書き換える
int tmp = *a;
*a = *b;
*b = tmp;
}
int main(void) {
int x = 1, y = 2;
swap_fail(x, y);
printf("swap_fail後: x=%d, y=%d\n", x, y); // 変化なし
swap_ok(&x, &y);
printf("swap_ok後 : x=%d, y=%d\n", x, y); // 入れ替わる
return 0;
}
swap_fail後: x=1, y=2
swap_ok後 : x=2, y=1
構造体を戻り値にしてまとめて返す
複数の値をひとまとまりの概念として返したい場合、構造体を戻り値にするのが読みやすく、拡張にも強い方法です。
近年のコンパイラは構造体の戻り値最適化に優れており、効率面でも実用的です。
#include <stdio.h>
#include <stdbool.h>
typedef struct {
bool ok; // 成否
int quotient; // 商
int remainder; // 余り
} DivResult;
DivResult divide_int(int a, int b) {
DivResult r;
if (b == 0) {
r.ok = false;
r.quotient = 0;
r.remainder = 0;
return r;
}
r.ok = true;
r.quotient = a / b;
r.remainder = a % b;
return r; // 構造体ごと返す
}
int main(void) {
DivResult r1 = divide_int(10, 3);
DivResult r2 = divide_int(10, 0);
if (r1.ok) {
printf("10 / 3 = quotient:%d, remainder:%d\n", r1.quotient, r1.remainder);
}
if (!r2.ok) {
printf("10 / 0 はエラー\n");
}
return 0;
}
10 / 3 = quotient:3, remainder:1
10 / 0 はエラー
エラーは戻り値 結果は引数で返す
I/Oやパース関数では、戻り値でエラー(0/非0や列挙値)を示し、結果はポインタ引数で返すのが定番です。
標準ライブラリの多くもこの設計に倣います。
#include <stdio.h>
#include <stdlib.h>
#include <errno.h>
#include <limits.h>
#include <stdbool.h>
// 成功: 0、失敗: 非0を返す。結果は *out に格納する
int parse_int(const char *s, int *out) {
if (s == NULL || out == NULL) return EINVAL;
char *end = NULL;
errno = 0;
long v = strtol(s, &end, 10);
if (errno == ERANGE || v < INT_MIN || v > INT_MAX) return ERANGE;
if (end == s || *end != '#include <stdio.h>
#include <stdlib.h>
#include <errno.h>
#include <limits.h>
#include <stdbool.h>
// 成功: 0、失敗: 非0を返す。結果は *out に格納する
int parse_int(const char *s, int *out) {
if (s == NULL || out == NULL) return EINVAL;
char *end = NULL;
errno = 0;
long v = strtol(s, &end, 10);
if (errno == ERANGE || v < INT_MIN || v > INT_MAX) return ERANGE;
if (end == s || *end != '\0') return EINVAL; // 数字以外が含まれる
*out = (int)v;
return 0;
}
int main(void) {
int value;
int rc = parse_int("1234", &value);
if (rc == 0) {
printf("parse成功: %d\n", value);
} else {
printf("parse失敗: errno=%d\n", rc);
}
return 0;
}
') return EINVAL; // 数字以外が含まれる
*out = (int)v;
return 0;
}
int main(void) {
int value;
int rc = parse_int("1234", &value);
if (rc == 0) {
printf("parse成功: %d\n", value);
} else {
printf("parse失敗: errno=%d\n", rc);
}
return 0;
}
parse成功: 1234
用途に応じて選びやすいよう、方法を比較します。
方法 | 特徴 | 向き不向き |
---|---|---|
ポインタ引数 | メモリを共有して直接書き込む。戻り値を成否に使える | 複数の独立値を返す、I/Oやパース |
構造体の戻り値 | 意味のまとまりを表現しやすい。読みやすい | 設計を明確にしたいAPI、拡張性が重要 |
戻り値と引数の設計のコツ
void関数と戻り値の使い分け
「結果を計算して返す」関数は戻り値を使い、「手続きを実行するだけ」なら void
にすると意図が伝わります。
副作用のある関数に戻り値で情報を詰め込みすぎると、使い手が見落としがちです。
成否は戻り値で、詳細は引数やログで伝えるなど、役割を分離します。
constを付けて意図を明確にする
読み取り専用の引数には const
を付けます。
特に配列や構造体へのポインタは積極的に const
を付けると安全です。
ポインタに関する const
の位置で意味が変わる点も押さえておきましょう。
表では、宣言と意味を整理します。
宣言 | 意味 |
---|---|
const char *p | 指し先の文字は変更不可、ポインタの付け替えは可能 |
char * const p | ポインタ自体は変更不可、指し先の文字は変更可能 |
const char * const p | ポインタも指し先も変更不可 |
NULLや異常値の扱い
引数としてポインタを受け取る関数では、NULLチェックを忘れないことが大切です。
特にsize_t n
の長さ引数とポインタの組み合わせでは、n == 0
のときに p == NULL
を許す設計かを明文化します。
また、戻り値でエラー種別を返すときは、errno
や独自の列挙型を使って異常値を区別可能にしましょう。
サンプルコード 文字列を返す関数の注意(静的領域と動的確保)
文字列を戻り値として返す場合、静的領域のポインタを返す方法と、動的確保して呼び出し側に所有権を渡す方法があります。
それぞれ利点と注意点が異なります。
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
// (1) 静的バッファを返す: 再入不可/スレッド非安全、後の呼び出しで上書きされる
const char* greet_static(const char *name) {
static char buf[64]; // 静的領域に確保(プログラム終了まで有効)
snprintf(buf, sizeof(buf), "Hello, %s!", name ? name : "world");
return buf; // 呼び出し側は解放不要だが、内容は次回呼び出しで上書きされる
}
// (2) 動的確保して返す: 呼び出し側が free で解放する責任を負う
char* greet_alloc(const char *name) {
const char *who = name ? name : "world";
size_t len = strlen(who) + strlen("Hello, ") + 1; // "Hello, " + name + '#include <stdio.h>
#include <stdlib.h>
#include <string.h>
// (1) 静的バッファを返す: 再入不可/スレッド非安全、後の呼び出しで上書きされる
const char* greet_static(const char *name) {
static char buf[64]; // 静的領域に確保(プログラム終了まで有効)
snprintf(buf, sizeof(buf), "Hello, %s!", name ? name : "world");
return buf; // 呼び出し側は解放不要だが、内容は次回呼び出しで上書きされる
}
// (2) 動的確保して返す: 呼び出し側が free で解放する責任を負う
char* greet_alloc(const char *name) {
const char *who = name ? name : "world";
size_t len = strlen(who) + strlen("Hello, ") + 1; // "Hello, " + name + '\0'
char *p = (char*)malloc(len);
if (!p) return NULL;
snprintf(p, len, "Hello, %s", who);
return p; // 所有権を移譲
}
int main(void) {
// 静的版の例
const char *s1 = greet_static("Alice");
const char *s2 = greet_static("Bob"); // s1 の内容は上書きされる
printf("s1: %s\n", s1);
printf("s2: %s\n", s2);
// 動的版の例
char *d1 = greet_alloc("Carol");
if (d1) {
printf("d1: %s\n", d1);
free(d1); // 呼び出し側で必ず解放
} else {
printf("メモリ確保に失敗しました\n");
}
return 0;
}
'
char *p = (char*)malloc(len);
if (!p) return NULL;
snprintf(p, len, "Hello, %s", who);
return p; // 所有権を移譲
}
int main(void) {
// 静的版の例
const char *s1 = greet_static("Alice");
const char *s2 = greet_static("Bob"); // s1 の内容は上書きされる
printf("s1: %s\n", s1);
printf("s2: %s\n", s2);
// 動的版の例
char *d1 = greet_alloc("Carol");
if (d1) {
printf("d1: %s\n", d1);
free(d1); // 呼び出し側で必ず解放
} else {
printf("メモリ確保に失敗しました\n");
}
return 0;
}
s1: Hello, Bob!
s2: Hello, Bob!
d1: Hello, Carol
静的バッファを返す関数は、連続呼び出しで内容が上書きされるため注意が必要です。
スレッド安全性もありません。
一方、動的確保は解放責任が呼び出し側に発生します。
関数名に _alloc
を含めるなど、所有権のルールを伝わるように設計しましょう。
まとめ
本記事では、C言語の引数と戻り値の本質として、値渡しの性質、配列とポインタの退化、文字列の扱い、そして複数値返却の実用的パターン(ポインタ引数/構造体戻り値/エラーは戻り値)を、サンプルコードとともに解説しました。
読み取り専用にはconst、配列は要素数を別引数で渡す、戻り値は役割を明確にといった設計指針を守ることで、初学者でも安全で読みやすい関数が書けます。
今日のポイントを意識して、日々のコーディングに取り入れてみてください。