C言語で関数の戻り値は1つですが、実務では複数の値を一度に更新したい場面が多くあります。
そこで役に立つのがアドレス渡し(ポインタ引数)です。
本記事では、関数から呼び出し元の変数を直接変更する基本とコツを、失敗例と成功例の両方を交えて丁寧に解説します。
アドレス渡しの基本
アドレス渡しは、関数に変数のアドレス(場所)を渡し、その場所の中身を関数側で変更する方法です。
はじめに、値渡しとの違いと、ポインタ引数・アドレス演算子・関数プロトタイプの要点を押さえます。
値渡しとの違いを理解する
値渡しでは変数の値のコピーが渡されるため、関数内で変更しても呼び出し元には反映されません。
アドレス渡しでは変数のアドレスを渡すため、呼び出し元の実体そのものを変更できます。
直感的なイメージ
- 値渡し: コピー機で複写した紙に書き込みます。原本(呼び出し元の変数)は汚れません。
- アドレス渡し: 原本そのものを持ち込み、直接書き込みます。原本が変わります。
以下の表は、2つの渡し方のざっくり比較です。
| 方法 | 関数へ渡すもの | 関数が触る対象 | 呼び出し後の元変数 |
|---|---|---|---|
| 値渡し | 値のコピー | ローカルなコピー | 変わらない |
| アドレス渡し | 変数のアドレス | 呼び出し元の実体 | 変更される |
「関数で複数の値を変更したい」なら、値渡しではなくアドレス渡しを選ぶのが基本方針です。
ポインタ引数の意味
ポインタ引数は「アドレスを受け取る」引数です。
例えばint* pxは「int型の場所(アドレス)」を受け取り、pxでその場所にある実際の整数にアクセスできます。
関数がpxへ代入すれば、呼び出し元の変数が更新されます。
入力専用か出力専用かを意識する
- 入力専用(読み取りのみ):
const int* inとすると、「関数内で*inを書き換えない」ことを表明できます。 - 出力専用(書き込み中心):
int* outとし、関数が結果を書き込む位置であることを名前でも明示します。
アドレス演算子の使い方
アドレス演算子&は「変数の住所」を取り出します。
これは「関数に渡す直前」によく登場します。
逆に、デリファレンス演算子*は「住所の先の中身」を読み書きします。
短い動作確認コード
#include <stdio.h>
int main(void) {
int x = 42;
int* px = &x; // xのアドレスを取得してpxに保存
printf("x=%d, &x=%p, px=%p\n", x, (void*)&x, (void*)px);
*px = 99; // pxが指す先(=x)の中身を書き換え
printf("x=%d, *px=%d\n", x, *px);
return 0;
}
x=42, &x=0x7ffeefbff4ac, px=0x7ffeefbff4ac
x=99, *px=99
アドレス&xとポインタ変数pxの値が同じであること、*pxの代入がxを書き換えることが分かります。
関数プロトタイプの書き方
プロトタイプは「関数の使い方」をコンパイラに先に知らせる宣言です。
ポインタ引数も通常の引数と同様に宣言します。
代表例
- 2つの値を入れ替える:
void swap(int* a, int* b);
- 商と余りを返す(戻り値で成否、ポインタで結果):
int divmod(int a, int b, int* q, int* r);
プロトタイプはmainより前、もしくはヘッダファイルに置くのが通例です。
関数で2つの値を変更する例
最も典型的な例として、2つの整数を入れ替えるswapを題材に、値渡しでの失敗例と、アドレス渡しでの成功例を並べて確認します。
C言語の関数宣言と定義
以下のプログラムには、値渡し版とアドレス渡し版の2種類のswap関数を実装しています。
#include <stdio.h>
// 値渡し: 失敗例(呼び出し元は変わらない)
void swap_by_value(int a, int b) {
int tmp = a;
a = b;
b = tmp;
// a, b はコピーなので、ここで頑張っても呼び出し元の変数は変わりません
}
// アドレス渡し: 成功例(呼び出し元の変数が変わる)
void swap_by_address(int* a, int* b) {
int tmp = *a; // ポインタが指す「中身」にアクセス
*a = *b;
*b = tmp;
}
int main(void) {
int x = 10;
int y = 20;
printf("[初期] x=%d, y=%d\n", x, y);
// 値渡し。xとyは変わらない
swap_by_value(x, y);
printf("[値渡し後] x=%d, y=%d (入れ替わっていない)\n", x, y);
// アドレス渡し。xとyが入れ替わる
swap_by_address(&x, &y); // &でアドレスを渡す
printf("[アドレス渡し後] x=%d, y=%d (入れ替わった)\n", x, y);
return 0;
}
[初期] x=10, y=20
[値渡し後] x=10, y=20 (入れ替わっていない)
[アドレス渡し後] x=20, y=10 (入れ替わった)
呼び出し方と実行結果
呼び出し側では&xや&yのようにアドレス演算子&を付けて渡すことが肝心です。
関数側はint* aのようなポインタ引数で受け取り、*aで実体にアクセスします。
ワンポイント
- 呼び出し側:
&変数を渡す - 関数側:
int* 引数で受け取り*引数で読み書き
値渡しで失敗するケース
値渡しは見た目が正しくても、呼び出し元には影響が及ばないため、入れ替えや同時更新のような「副作用」が必要な処理では必ず失敗します。
「関数内の変更を呼び出し元に反映したい」= アドレス渡しと覚えると混乱しにくいです。
複数の結果を返す例
戻り値は主に「成否」や「主要なひとつの値」に使い、複数の詳細な結果はアドレス渡しで返すのがC言語の定番パターンです。
戻り値+アドレス渡しの組み合わせ
次のdivmodは、戻り値で成功(0)/失敗(-1)を返し、商と余りをポインタ経由で返す関数です。
割る数が0のときはエラーにします。
#include <stdio.h>
// 成功なら0、失敗(例えばb==0)なら-1を返す
int divmod(int a, int b, int* out_q, int* out_r) {
if (b == 0 || out_q == NULL || out_r == NULL) {
return -1; // 不正な引数
}
*out_q = a / b; // 商を書き込む
*out_r = a % b; // 余りを書き込む
return 0;
}
int main(void) {
int a = 23;
int b = 5;
int q = -1; // 呼び出し前に初期値を入れておくと変化が分かりやすい
int r = -1;
if (divmod(a, b, &q, &r) == 0) {
printf("divmod(%d, %d): q=%d, r=%d\n", a, b, q, r);
} else {
printf("divmod(%d, %d): error\n", a, b);
}
// エラーの例(0で割る)
q = r = -1;
if (divmod(a, 0, &q, &r) == 0) {
printf("divmod(%d, %d): q=%d, r=%d\n", a, 0, q, r);
} else {
printf("divmod(%d, %d): error (0で割れない)\n", a, 0);
}
return 0;
}
divmod(23, 5): q=4, r=3
divmod(23, 0): error (0で割れない)
関数の引数設計
- 役割を明確にするため、出力先のポインタは
out_*のように命名すると読み手に優しいです。 - 失敗時の挙動(出力先を書き換えない、初期化するなど)をコメントや仕様で明確にします。
- 可能なら
constを使い、読み取り専用の引数と書き込み対象の引数を区別します。
呼び出し時の初期化と確認
呼び出す側では、出力先変数に初期値を入れておくと、関数が本当に書き換えたかが分かりやすいです。
エラー時に初期値のままかどうかで、処理の成否を視覚的に確認できます。
アドレス渡しの注意点とコツ
実装時には、細かいところでつまずきやすいポイントがあります。
以下を押さえておくと安全で読みやすいコードになります。
実体の変数を渡すこと
必ず実体のある変数のアドレスを渡します。
例えば&(x + 1)のような一時式のアドレスは取れませんし、解放済みメモリや寿命が切れた変数のアドレスを渡すのも危険です。
関数呼び出しの瞬間から戻るまで、そのアドレスが指す実体が有効であることが大前提です。
やりがちなNG例
swap_by_address(&(x + 1), &y);は無効(一時値のアドレスは取れません)。- ローカル変数のアドレスを関数の外へ保存して後で使う、といった寿命違反。
*と&の位置に注意する
*は2つの意味があります。
宣言では「ポインタ型」を作り、式では「参照外し(中身アクセス)」です。
読みやすさのために括弧を使って意図を明確にすると安全です。
宣言時の注意点
int* a, b;はaはポインタ、bはintです。混乱を避けるにはint *a, b;のように書くか、1行に1変数の宣言を推奨します。
式の例
*a = *a + 1;や(*a)++;のように括弧で意図を示すと誤読を防げます。- 呼び出し側では
&xを忘れないこと(忘れると値渡しになってしまう)。
読みやすい命名
関数の契約(入力か出力か)が一目で分かるように、ポインタ引数には役割を表す接頭辞や語を使います。
- 入力専用:
const int* in_values、const char* src - 出力専用:
int* out_sum、double* out_avg - 入出力:
int* inout_valueなど
副作用を意図して使う
アドレス渡しは「呼び出し元の変数を書き換える」という強い副作用を伴います。
本当に必要なときだけ使い、関数名・コメント・引数名で副作用の意図を明示しましょう。
計算結果が1つなら戻り値、複数なら戻り値+アドレス渡しのように役割分担すると、APIが分かりやすくテストもしやすいです。
まとめ
C言語で複数の値を関数から返したい場合、アドレス渡し(ポインタ引数)を用いるのが基本です。
値渡しはコピーに対する操作であり、呼び出し元を変えられません。
アドレス渡しでは&で実体のアドレスを渡し、関数側はで参照外しながら書き込みます。
設計面では、戻り値を成否や主要なひとつの値に、ポインタ引数を複数結果の受け口にするのが定石です。
実装では実体の変数を渡す、と&の位置に注意、命名で入出力の役割を明確化、副作用は意図して使うといったポイントを守ることで、安全で読みやすいコードになります。
今回のswapやdivmodの例を土台に、実務でも自信を持ってアドレス渡しを活用していきましょう。
