C言語のきほん|値渡しの基本

値渡しのしくみがわかると、関数の中と外の関係がすっきり見えてくる

C言語で関数を学んでいると、引数に数字を渡したり、変数の値を渡したりする場面がたくさん出てきます。
そのとき、「関数に値を渡したあと、元の変数はどうなるのだろう?」と気になることがあります。

たとえば、main 関数にある変数の値を別の関数へ渡したときに、関数の中でその値を変更したら、元の変数まで変わってしまうのでしょうか。
この点を正しく理解するために大切なのが、値渡しの基本です。

C言語では、関数呼び出しのときに、実引数の値がそのまま仮引数へ渡されるのではなく、値がコピーされて渡されるという考え方をします。
このしくみが分かると、

  • 実引数と仮引数の関係
  • なぜ関数の中で値を変えても元の変数が変わらないのか
  • 値を渡す場合とアドレスを渡す場合の違い

といったポイントがとても見えやすくなります。

関数を正しく使うための土台になる大切な内容なので、ここでしっかり整理していきましょう。

値渡しとは

C言語では、関数を呼び出すときに実引数の値がコピーされて、仮引数に代入されます
これを一般に値渡しと呼びます。

たとえば、main 関数にある変数 a の値が 10 だったとします。
その a を関数 func1 に渡すと、func1 側では仮引数 n に 10 が入ります。

ここで大事なのは、a そのものが移動するわけではないという点です。
渡されるのは a という変数そのものではなく、a に入っている値です。

つまり、イメージとしては次のようになります。

  • main 関数の a に 10 が入っている
  • 関数呼び出しで 10 がコピーされる
  • func1 関数の n に 10 が入る

このとき、a と n は同じ値を持っていますが、別々の変数です。

実引数と仮引数を整理しよう

値渡しを理解するには、まず実引数と仮引数の違いをはっきりさせることが大切です。

用語書かれる場所意味
実引数関数を呼び出す側func1(a);実際に渡す値や変数
仮引数関数を定義する側void func1(int n)受け取るための変数

たとえば、

func1(a);

の a は実引数です。

一方で、

void func1(int n)

の n は仮引数です。

このとき、実引数 a の値がコピーされて、仮引数 n に入ります。
この「コピーされる」という感覚がとても大切です。

サンプルプログラム

ここでは、親しみやすい例として、整数値を受け取って表示する関数を作成します。

ファイル名:13_10_1.c

// 値渡しの基本を確認するプログラム
#include <stdio.h>

void show_number(int value);

int main(void)
{
    int score = 80;

    show_number(score);

    printf("main関数のscore=%d\n", score);

    return 0;
}

// 渡された値を表示する関数
void show_number(int value)
{
    printf("show_number関数のvalue=%d\n", value);
}

実行結果例

show_number関数のvalue=80
main関数のscore=80

このプログラムでは、main 関数の変数 score の値 80 を show_number 関数へ渡しています。
その結果、仮引数 value に 80 がコピーされて表示されます。

このプログラムの見方

このプログラムで注目したいのは、次の2行です。

int score = 80;
show_number(score);

ここでは、変数 score を渡しています。
でも、show_number 関数に渡されているのは、score という変数そのものではありません。
score の中に入っている 80 という値です。

そして関数定義側では、

void show_number(int value)

となっているので、value がその値を受け取ります。

流れを表で整理すると、こうなります。

順番内容
1main 関数で score に 80 を代入する
2show_number(score); を実行する
3score の値 80 がコピーされる
4仮引数 value に 80 が代入される
5関数内で value を表示する
6関数が終わり、main 関数へ戻る

値がコピーされるとはどういうことか

値渡しで一番大切なのは、実引数と仮引数は別々の場所に存在するということです。

同じ 80 という値を持っていても、

  • main 関数の score
  • show_number 関数の value

は、メモリ上では別の変数です。

このことを表で整理すると分かりやすいです。

項目main関数側show_number関数側
変数名scorevalue
8080
実体別の変数別の変数

見た目には同じ 80 ですが、片方を変えても、もう片方は自動では変わりません。
なぜなら、同じ箱を共有しているのではなく、別々の箱に同じ値が入っている状態だからです。

関数の中で値を変えても元の変数は変わらない

値渡しの特徴がいちばんよく分かるのは、関数の中で仮引数の値を変えたときです。

次の例を見てみましょう。

ファイル名:13_10_2.c

// 値を変更しても元の変数は変わらないことを確認するプログラム
#include <stdio.h>

void change_value(int number);

int main(void)
{
    int points = 50;

    printf("呼び出し前のpoints=%d\n", points);

    change_value(points);

    printf("呼び出し後のpoints=%d\n", points);

    return 0;
}

// 受け取った値を変更する関数
void change_value(int number)
{
    number = 100;
    printf("change_value関数のnumber=%d\n", number);
}

実行結果例

呼び出し前のpoints=50
change_value関数のnumber=100
呼び出し後のpoints=50

ここでは、main 関数の points は 50 です。
それを change_value に渡しています。

関数の中では、

number = 100;

として値を変更していますが、main 関数に戻ったあとの points は 50 のままです。

これは、number が points そのものではなく、points の値をコピーして作られた別の変数だからです。

なぜ安全な方法といえるのか

値渡しは、元の変数が勝手に変更されないという意味で、安全な方法といえます。

たとえば、main 関数で大事な変数を使っていたとして、その値を別の関数へ渡すたびに元の値まで変わってしまうと、プログラム全体がとても分かりにくくなります。

でも値渡しなら、関数側ではコピーされた値を扱うだけです。
そのため、呼び出し元の変数をうっかり壊してしまう心配が減ります。

値渡しの特徴内容
元の変数が守られる仮引数を変えても実引数は変わらない
関数の影響範囲が分かりやすい関数内の変更が外に漏れにくい
初学者にも理解しやすい渡したのは値そのものだと考えやすい

この考え方は、関数を安心して使ううえでとても大切です。

図で見るとさらに理解しやすい

値渡しは、メモリ上で別々の変数になっていることを図で見ると、とても分かりやすくなります。

この図では、main 関数の score に入っている 80 が、そのまま右側の value に移動しているのではなく、コピーされていることを表しています。

score と value は別々の変数ボックスとして描かれているので、同じ 80 を持っていても別の存在だと分かります。
ここが、値渡しの基本イメージです。

定数を渡す場合も値渡し

これまでの説明では変数を例にしてきましたが、実引数には定数を書くこともできます。
この場合も考え方は同じです。

たとえば、

show_number(25);

と書いた場合、25 という値が仮引数へ入ります。

void show_number(int value)
{
    printf("%d\n", value);
}

このときも、仮引数 value には 25 が入ります。
つまり、実引数が変数でも定数でも、関数が受け取るのは値です。

式の結果を渡すこともできる

実引数には変数や定数だけでなく、式を書くこともできます。

たとえば、

show_number(10 + 5);

のような形です。

この場合、10 + 5 が先に計算され、その結果である 15 が仮引数へ渡されます。
つまり、これもやはり「値がコピーされる」という考え方です。

実引数の書き方仮引数に渡るもの
show_number(25);25
show_number(score);score の値
show_number(10 + 5);15

このように見ると、関数に渡されるのは、最終的に求められた値だと分かります。

値渡しとアドレス渡しの違いに少し触れておこう

今回の中心は値渡しですが、文中にもあるように、C言語では変数や配列のアドレスを渡すこともできます。
その場合、関数側ではポインタで受け取ることになります。

値渡しとアドレスを渡す場合の違いをざっくり整理すると、次のようになります。

方法渡すもの関数内で元の変数を変更できるか
値渡し値そのもの基本的にできない
アドレスを渡す変数の場所できる

今の段階では、まず
値渡しでは元の変数は変わらない
ということをしっかり押さえておけば十分です。

あとでポインタを学ぶと、「なぜアドレスを渡すと元の変数を変更できるのか」がつながって理解しやすくなります。

関数宣言・呼び出し・定義を対応させて見る

値渡しを理解するときも、関数宣言・呼び出し・定義を対応させて見ると分かりやすいです。

たとえば、次の形です。

役割コード意味
関数プロトタイプ宣言void show_number(int value);int 型の値を1つ受け取る
関数呼び出しshow_number(score);score の値を渡す
関数定義void show_number(int value)value で受け取って使う

この3つがつながっていることを意識すると、値がどう流れているかが見やすくなります。

よくある勘違い

値渡しでは、最初のうちにいくつか勘違いしやすいポイントがあります。

よくある勘違い正しい考え方
実引数の変数そのものが関数へ渡る渡るのは変数に入っている値
仮引数を変えたら実引数も変わる値渡しでは変わらない
同じ値なら同じ変数だと思う同じ値でも別々の変数
関数に渡したら元の変数が使えなくなる実引数はそのまま残る

このあたりを意識しておくと、関数の動きがかなり理解しやすくなります。

実践的な見方

値渡しは文法として覚えるだけでなく、プログラムを読むときの見方としても大切です。

関数呼び出しを見たら、次のように考えると整理しやすいです。

  1. 実引数には何が書かれているか
  2. その値はいくつになるか
  3. 仮引数は何という名前で受け取るか
  4. 仮引数を変えても実引数には影響するか

この順番で追うと、関数の中と外の関係が見えやすくなります。

練習用のシンプルな確認例

最後に、値渡しの基本をもう1回確認しやすい短い例を載せておきます。

ファイル名:13_10_3.c

// 値渡しでは元の変数が変わらないことを確認するプログラム
#include <stdio.h>

void add_ten(int number);

int main(void)
{
    int value = 30;

    printf("呼び出し前のvalue=%d\n", value);

    add_ten(value);

    printf("呼び出し後のvalue=%d\n", value);

    return 0;
}

// 受け取った値に10を足して表示する関数
void add_ten(int number)
{
    number = number + 10;
    printf("add_ten関数のnumber=%d\n", number);
}

実行結果例

呼び出し前のvalue=30
add_ten関数のnumber=40
呼び出し後のvalue=30

この結果を見ると、関数の中では 40 になっていても、main 関数の value は 30 のままだと分かります。
これが、値渡しの基本そのものです。

学習のコツ

値渡しをしっかり理解したいときは、同じ値でも別の変数という考え方を何度も意識するのがコツです。

とくに、

  • 実引数の値がコピーされる。
  • 仮引数は別の変数である。
  • 仮引数を変えても実引数は変わらない。

この3点をセットで覚えておくと、今後ポインタやアドレス渡しを学ぶときにも、とても役立ちます。