C言語のきほん|アドレス渡し(ポインタ渡し)の仕組み

アドレスを渡す仕組みがわかると、関数の外の変数も自在に扱えるようになる

C言語で関数を学んでいくと、これまでは「値を渡す」方法を中心に見てきました。
値渡しでは、実引数の値がコピーされて仮引数へ渡されるため、関数の中で仮引数を変更しても、呼び出し元の変数そのものは変わりません。

この仕組みはとても安全で分かりやすいのですが、場合によっては呼び出し元の変数そのものを関数の中で変更したいことがあります。
たとえば、

  • 2つの変数の値を入れ替えたい
  • 関数の中で計算した結果を呼び出し元へ直接反映したい
  • 配列の中身を関数で更新したい

といった場面です。

そんなときに使うのが、アドレス渡し ポインタ渡し の考え方です。
これは、変数の値そのものを渡すのではなく、その変数が置かれている場所の情報 アドレス を関数へ渡す方法です。

関数側では、そのアドレスをポインタで受け取り、間接参照を使って呼び出し元の変数にアクセスします。
このしくみが分かると、なぜ関数の中から main 関数の変数を書き換えられるのかが、すっきり見えてきます。

ここでは、アドレス渡しの基本的な流れをやさしく整理しながら、値渡しとの違い、ポインタの役割、const を使った安全な書き方まで、順番に確認していきましょう。

アドレス渡しとは

アドレス渡しとは、変数そのものの値ではなく、その変数が置かれている場所 アドレス を関数へ渡す方法です。
関数側では、そのアドレスをポインタで受け取ります。

たとえば、main 関数に変数 a があり、そのアドレスを渡したいときは、次のように書きます。

func2(&a);
func2(&a);

この &a は、「変数 a のアドレス」を表しています。
つまり、a の中身ではなく、a がメモリ上のどこにあるかという情報を渡しているわけです。

関数側では、そのアドレスを受け取るために、ポインタを使います。

void func2(int *p)

この p は、int 型の変数のアドレスを受け取るポインタです。

まず大事な注意点

ここでとても大事なことがあります。
C言語には、本当の意味での「参照渡し」という仕組みはありません。
C言語で行われているのは、あくまで値渡しです。

ただし、ここで渡している値が「普通の整数値」ではなく、アドレスという値だという点がポイントです。

つまり、

  • 値渡しではある
  • でも渡している値がアドレスなので
  • 関数側から元の変数にたどり着ける

という仕組みです。

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

呼び方実際に渡しているもの
値渡し普通の値
アドレス渡しアドレスという値

なので、「アドレス渡し」という言葉は便利な呼び方ですが、厳密にはポインタという値を渡していると考えるのが正確です。

値渡しとの違い

前に学んだ値渡しと比べると、アドレス渡しの特徴がよく見えてきます。

項目値渡しアドレス渡し
渡すもの値そのものアドレス
関数側の受け取り方普通の変数ポインタ
関数内で元の変数を変更できるかできないできる
安全性比較的高い書き換えの影響に注意が必要

値渡しでは、実引数の値がコピーされて仮引数に入るだけなので、関数の中で仮引数を変更しても元の変数は変わりません。
一方、アドレス渡しでは、元の変数の場所が分かるため、その場所にある値を直接変更できます。

サンプルプログラム

ここではシンプルな例として、点数に 5 点を加算する関数を作成します。

ファイル名:13_11_1.c

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

void add_bonus(int *p_score);

int main(void)
{
    int score = 70;

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

    add_bonus(&score);

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

    return 0;
}

// 点数にボーナスを加える関数
void add_bonus(int *p_score)
{
    printf("関数内で見た元の値=%d\n", *p_score);
    *p_score += 5;
}

実行結果例

関数呼び出し前のscore=70
関数内で見た元の値=70
関数呼び出し後のscore=75

このプログラムの流れ

この例では、main 関数の変数 score のアドレスを add_bonus 関数へ渡しています。

add_bonus(&score);

ここで &score は、score の値ではなく、score のアドレスです。

そして関数定義側では、

void add_bonus(int *p_score)

のように、ポインタ p_score で受け取っています。

そのあと、

*p_score += 5;

としているので、p_score が指している先の値、つまり main 関数の score の値に 5 を加えています。

流れを整理すると、次のようになります。

順番内容
1main 関数で score に 70 を入れる
2add_bonus(&score); で score のアドレスを渡す
3仮引数 p_score がそのアドレスを受け取る
4*p_score で score を間接参照する
5*p_score += 5; により score の値が 75 になる
6関数終了後、main 関数でも score が 75 になっている

& と * の役割

アドレス渡しでは、& と * の意味をしっかり区別することが大切です。

記号役割意味
&アドレスを取り出す&scorescore のアドレス
*ポインタ宣言子 / 間接参照int *pp はポインタ
*間接参照*pp が指す先の値

最初は少しややこしく感じるかもしれませんが、使われる場所ごとに見れば整理しやすいです。

宣言での *

int *p_score;

ここでの * は、「p_score は int 型の値を指すポインタです」という意味です。

式の中での *

*p_score += 5;

ここでの * は、「p_score が指している先の値」を取り出しています。

つまり、同じ * でも、
宣言ではポインタであることを示し
式では指している先の値を使う
という違いがあります。

間接参照とは何か

アドレス渡しの中心にあるのが、間接参照です。

ポインタ変数そのものには、アドレスが入っています。
でも、私たちが本当に操作したいのは、そのアドレスが指している先の変数です。

そこで使うのが * です。

たとえば、

*p_score

と書くと、p_score が指している先にある int 型の値を取り出せます。
今回の例では、その先にあるのは main 関数の score です。

つまり、

*p_score += 5;

は、実質的には

score += 5;

のような効果を持ちます。

このように、ポインタを通して元の変数を操作することを、間接参照して操作するといいます。

図で見るとかなりわかりやすい

アドレス渡しは、文章だけよりも図で見ると一気に理解しやすくなります。
特に、「ポインタがどこを指しているのか」が見えると整理しやすいです。

この図では、main 関数の score がメモリ上のある場所 AAAA に置かれているイメージを表しています。
add_bonus 関数に渡しているのは 70 という値ではなく、その場所 AAAA です。

関数側の p_score には AAAA が入り、p_score は score を指します。
そのため、*p_score と書くと score の値そのものを操作できます。

ここが、値渡しとの大きな違いです。

どうして呼び出し元の変数を書き換えられるのか

値渡しでは、仮引数は実引数のコピーなので、関数の中で変更しても元の変数は変わりませんでした。
でも、アドレス渡しでは元の変数の場所が分かります。

たとえば、main 関数側にある score のアドレスを関数側が知っていれば、その場所に対して直接

  • 値を読む
  • 値を書き換える

ことができます。

だから、

*p_score += 5;

と書くと、関数のローカル変数を変えているのではなく、呼び出し元の score が置かれている場所の中身を変えていることになります。

const を使って誤った書き換えを防ぐ

アドレス渡しはとても便利ですが、元の変数を変更できるぶん、うっかり書き換えてしまう危険もあります。
そこで役立つのが const 型修飾子です。

たとえば、次のように書きます。

void show_value(const int *p)

この const int *p は、「p が指している先の int 値は変更しません」という意味です。

この形にしておくと、関数の中で

*p = 100;

のような書き換えをしようとしたとき、コンパイルエラーになります。

つまり、const を使うと、呼び出し元の変数を誤って変更しないことを保証しやすくなるわけです。

例:

ファイル名:13_11_2.c

// 参照先を読み取るだけの関数
#include <stdio.h>

void print_score(const int *p_score);

int main(void)
{
    int score = 88;

    print_score(&score);

    return 0;
}

// 点数を表示するだけの関数
void print_score(const int *p_score)
{
    printf("点数は%dです。\n", *p_score);
}

この関数は表示だけを目的としているので、書き換えを禁止しておくと安心です。

アドレス渡しが有効な場面

アドレス渡しは、呼び出し元の変数を変更したいときに特に有効です。
よくある場面を整理すると、次のようになります。

場面アドレス渡しが向いている理由
変数の値を関数内で更新したい呼び出し元の値を直接変更できる
2つの値を入れ替えたい両方の変数にアクセスできる
配列の中身を処理したい要素そのものを更新できる
複数の結果を返したい複数の変数へ直接書き込める

特に有名なのが、2つの変数の入れ替えです。

2つの変数を入れ替える例を別の内容で見てみよう

ここではシンプルな例として、2人の得点を入れ替える関数を作成します。

ファイル名:13_11_3.c

// 2つの点数を入れ替える関数の例
#include <stdio.h>

void swap_scores(int *p1, int *p2);

int main(void)
{
    int math = 65;
    int english = 90;

    printf("関数呼び出し前: math = %d, english = %d\n", math, english);

    swap_scores(&math, &english);

    printf("関数呼び出し後: math = %d, english = %d\n", math, english);

    return 0;
}

// 2つの点数を入れ替える関数
void swap_scores(int *p1, int *p2)
{
    int temp = *p1;
    *p1 = *p2;
    *p2 = temp;
}

実行結果例

関数呼び出し前: math = 65, english = 90
関数呼び出し後: math = 90, english = 65

この入れ替えの仕組み

この関数では、math と english のアドレスをそれぞれ渡しています。

swap_scores(&math, &english);

関数側では、

  • p1 が math のアドレスを受け取る
  • p2 が english のアドレスを受け取る

という形になります。

そのあと、

int temp = *p1;
*p1 = *p2;
*p2 = temp;

という手順で入れ替えています。

流れを表にすると次のようになります。

手順内容
1*p1 の値を temp に保存する
2*p2 の値を *p1 に代入する
3temp の値を *p2 に代入する

このように、間接参照を使うことで、呼び出し元の 2 つの変数をそのまま入れ替えられます。

図で入れ替えの流れを表すと理解しやすい

2つの変数の入れ替えも、図で見るとかなり分かりやすくなります。

この図では、p1 と p2 がそれぞれ math と english を指していることがポイントです。
関数の中では、math や english という名前を直接使っていませんが、ポインタがその場所を知っているので、結果として呼び出し元の変数そのものを入れ替えられます。

値渡しでは入れ替えできない理由

ここはとても大事なポイントです。
もし swap_scores を値渡しで書いたら、受け取るのは math と english のコピーです。
すると、関数の中で入れ替わるのはコピーされた値だけで、main 関数の変数は変わりません。

だからこそ、呼び出し元の変数そのものを操作したいときにアドレス渡しが必要になります。

方法入れ替え後に main 側が変わるか
値渡し変わらない
アドレス渡し変わる

この違いは、値渡しとアドレス渡しを見分けるうえでとても大切です。

よくある混乱ポイント

アドレス渡しを学び始めたときに、つまずきやすい点を整理しておきます。

よくある混乱正しい考え方
p に値そのものが入っていると思うp にはアドレスが入っている
*p は p と同じだと思う*p は p が指す先の値
& と * が逆になる& はアドレス取得、* は間接参照
C言語には参照渡しがあると思う厳密にはアドレスという値を渡している

このあたりを何度か確認すると、かなり整理しやすくなります。

学習のコツ

アドレス渡しを理解するときは、次の3つをセットで考えるのがコツです。

何を渡しているのか

渡しているのは変数そのものではなく、変数のアドレスです。

関数側でどう受け取るのか

関数側ではポインタで受け取ります。

どうやって元の変数を操作するのか

  • * を使って間接参照することで、元の変数の値を読み書きします。

この3点をつなげて考えると、アドレス渡しの仕組みがかなり分かりやすくなります。

また、最初は

  • & でアドレスを渡す。
  • * で中身を操作する。

という2つの役割を、図とコードを行き来しながら確認すると理解が深まりやすいです。