トラック7・🧩関数とスコープ

値渡しの本質

読む目安 約13分・ゴール:引数はコピーが渡ることを理解し、関数内の変更が呼び出し元に届かない理由を説明できる

アプリでこのトピックを学ぶ(無料・登録不要)

このトピックの要点

このトピックは、第2部のポインタへ続く最重要の伏線です。ここを曖昧にしたままポインタに進むと、必ず迷子になります。

Cの関数に値を渡すと、渡るのは原本ではなくコピーです。書類の原本を手渡すのではなく、コピー機で複製を取って渡すイメージ。これを値渡しと呼びます。

#include <stdio.h>

void add_ten(int n)
{
    n = n + 10;          // 手元のコピーを書き換えただけ
    printf("%d\n", n);   // 15
}

int main(void)
{
    int n = 5;
    add_ten(n);
    printf("%d\n", n);   // 5 のまま!
    return 0;
}

add_ten の中の n と main の n は、名前が同じでも別の箱です。前のトピックのコールスタックで言えば、add_ten のフレームに新しく作られた n に、main の n の値 5 が写し取られただけ。フレームが降ろされれば、書き換えた 15 ごと消えてしまいます。

int も double も char も、引数は例外なくコピーされます。第3部で学ぶ構造体のような大きなデータでも、まるごと写しが取られます。渡す=写す。これがCの基本ルールです。

有名なのが「入れ替え関数が動かない」問題です。

#include <stdio.h>

void swap(int a, int b)   // これは動かない
{
    int t = a;
    a = b;
    b = t;
}

int main(void)
{
    int x = 3, y = 7;
    swap(x, y);
    printf("%d %d\n", x, y);   // 3 7(入れ替わらない)
    return 0;
}

コピーどうしをどれだけ丁寧に入れ替えても、原本は無傷です。

では、どうすれば原本を変えられるのか

方法は2つあります。ひとつは、結果を戻り値で返して呼び出し元が代入し直すこと。もうひとつが、原本の置き場所(番地)を渡すこと。後者がポインタで、第2部の主役になります。

int add_ten(int n)      // 値を返す形なら意図どおり動く
{
    return n + 10;
}
// 呼び出し側: n = add_ten(n);

関数から結果を持ち帰る3つの手

  1. 戻り値で返す(いまできる方法。いちばん安全で分かりやすい)
  2. 原本の番地を渡して直接書き換えてもらう(第2部・ポインタ)
  3. グローバル変数を共有する(追跡しづらいので最後の手段)

初学のうちは1番だけで十分です。「戻り値は1つしか返せないのに、3つの値を更新したい」——そんな場面に出会ったとき、2番が必要になる理由が実感として分かります。

罠・注意

師範関数に渡るのは値の写しじゃ。写しをいくら書き換えても、原本は変わらん

ここまでの話をひとことにすると、「関数の中と外は、値でしかつながっていない」ということです。第2部でポインタを学ぶと、このつながり方に新しい選択肢が加わります。そのとき「なぜわざわざ番地を渡すのか」で悩まないために、いまは値渡しの動きだけをしっかり身体に入れておいてください。

まとめ: 引数はコピー。関数の中の変更は呼び出し元に届きません。届けたいなら「返す」か「番地を渡す」。この一文が、ポインタ理解の土台になります。

アプリでこのトピックを学ぶ(無料・登録不要)

確認問題

確認問題 1

int n = 5; を持つ main から add_ten(n) を呼び、関数の中で仮引数 n に 10 を足しました。main の n はどうなりますか。

  1. 15 になる。関数は呼び出し元の変数を直接書き換えるから
  2. 5 のまま。関数には値のコピーが渡るから
  3. 0 になる。関数を抜けると初期化されるから
  4. コンパイルエラーになる。同じ名前は使えないから
答えを見る

正解:B(5 のまま。関数には値のコピーが渡るから)

Cの引数は値渡しで、渡るのは原本ではなくコピーです。関数のフレーム上に作られた別の箱を書き換えただけなので、呼び出し元の n は 5 のままです。原本を変えたいなら戻り値で返すか、第2部で学ぶポインタで番地を渡します。

確認問題 2

値渡しの動きを追って、空欄を埋めてください(値がない欄は - と書かれています)。

#include <stdio.h>

void add_ten(int n)
{
    n = n + 10;
    printf("%d\n", n);
}

int main(void)
{
    int n = 5;
    add_ten(n);
    printf("%d\n", n);
    return 0;
}
行main:nadd_ten:n出力
115-
125?
55?
651515
135-?
答えを見る

空欄の答え(上から順に):5、15、5

12行目の呼び出しで main の n の値 5 がコピーされ、add_ten の n になります。5行目でコピーが 15 になり表示されますが、13行目で表示される main の n は 5 のままです。名前が同じでも別の箱だからです。

「関数とスコープ」の目次

  1. 関数とは——処理に名前を付ける
  2. 引数と戻り値
  3. プロトタイプ宣言
  4. 変数のスコープ——見える範囲
  5. 変数の寿命とstatic
  6. コールスタック——関数呼び出しの舞台裏
  7. 値渡しの本質
  8. グローバル変数との付き合い方
  9. 関数分割の設計センス
  10. 再帰の入口——自分を呼ぶ関数

もっと先へ:ポインタ・メモリ・ファイル入出力・セキュアコーディングを含む全26トラックと、段位検定・模試のフルセットは完全版に収録しています。

完全版の販売ページは準備中です。

ほかのトラック

📚 姉妹教材:手を動かして覚える Linux 教材 — Linuxとインフラの仕組みを地図で学ぶ

Web版