値渡し、アドレス渡し、参照渡しの違いをメモリマップ的な視点で考える

結論

  • 値渡し

    • 仮引数が確保したメモリの値に、引数として渡す変数の値部分をコピーする方法
  • アドレス渡し

    • 仮引数が確保したメモリの値に、引数として渡す変数のアドレス部分をコピーする方法
  • 参照渡し

    • 仮引数に引数と同じメモリ領域を指させる方法

背景

値渡しや参照渡しのような「◯◯渡し」という言葉はよく耳にはしますが、その意味はよく理解できていませんでした。ググって解説記事を幾つか読んでみても、記事によって書いてあることが違ったり、言語によっても呼び方が違ったりと、なかなか混沌としている様子。
例えば Java では、メモリ上のインスタンスの場所を指し示す実体(変数が持つ値のこと)を「参照」と呼びます。ですから、この「参照」の値(アドレス番地)を関数に渡すことを「参照渡し」と呼ぶことに違和感はありませんが、C++ などの言語で言うところの参照渡しとは意味が違ったりします。このような状況から、どのような動作を「◯◯渡し」と呼ぶのか問題は、いくつかの派に分かれているという印象です。

とはいえ、一応自分の中での呼び方を定めておきたかったので、少しまとめてみました。汎用のため、言語に依存しない考え方を軸にします。つまり、コードや仕様視点というよりは、どちらかと言うとメモリ上での動作イメージをベースに考えます。

サンプルとして、以下のオブジェクト指向的な擬似コードで考えていきますが、プログラミングパラダイムは今回の定義に影響しません。以下で、func の引数に hoge を指定して仮引数 arg に渡すコードにおいて、メモリ上でどのような動作になるかが主題です。

main() {
    Object hoge = new Object();
    func(hoge);
}

func(Object arg) {
    ...
}
  • new Object(); によって、hoge にはメモリ上のどこかに作成された Object インスタンスのメモリアドレスが値として格納される(Java 的な動作。C++ では Object *hoge = new Object; の動作。)
  • スコープはブロックで区切られている


値渡し

f:id:norikone:20180420200145j:plain

値渡しは、仮引数 arg の値に hoge の値をコピーする動作のことです。そのままですね。図のように hoge 変数の値部分にアドレス値が入っていたとしても、それがメモリ上の "アドレス部分" ではなく "値部分" であることには変わりがないので、値渡しです。

Java で参照型の変数を渡す時にはこの動作になる故、「参照の値渡し」と説明されることもあります。上の図で、値部分にアドレス値ではなく実際のプリミティブ型の値が入っているようなケースでは、より直感的に理解できると思います。



アドレス渡し(ポインタ渡し)

f:id:norikone:20180420201715j:plain

アドレス渡しは、仮引数 arg の値に hoge が指すアドレスの番地そのものをコピーする動作のことです。この渡し方はポインタ渡しと呼ばれているケースが多いですが、"渡す" のは呼び出し側であり、渡しているのはアドレス部分であるので、アドレス渡しと呼びたいところです。

要するに、変数というのは「指している場所」と「指している場所に格納されている値」という2つの情報を持っています。値渡しとアドレス渡しの違いは、コード上で渡す変数のどちらの情報を仮引数にコピーするのか、という違いです。



参照渡し

f:id:norikone:20180420202117j:plain

仮引数 arg に hoge と同じアドレスを指させることです。メモリイメージの視点ではこれを "渡し" 呼ぶのは少し違和感がありますが、コード上では arg に渡しているように見えるので参照渡しと呼びます。


C++で動作確認

冗長な記述がありますが、C++で動作確認した時のコードをそのまま載っけておきます。

#include <iostream>

class MyClass{
};

void val(MyClass* arg) {
  printf("==============begin val=============\n");
  printf("argの確認\n");
  printf("  arg = %p\n", arg);
  printf(" &arg = %p\n", &arg);
  MyClass *piyo = new MyClass;
  printf("piyoの作成\n");
  printf(" piyo = %p\n", piyo);
  arg = piyo;
  printf("argにpiyoを代入\n");
  printf("  arg = %p\n", arg);
  printf(" &arg = %p\n", &arg);
  printf("==============end val=============\n");
}

void adrs(MyClass** arg) {
  printf("==============begin adrs=============\n");

  printf("argの確認\n");
  printf("  arg = %p\n", arg);
  printf(" &arg = %p\n", &arg);
  printf(" *arg = %p\n", *arg);
  MyClass *piyo = new MyClass;
  printf("piyoの作成\n");
  printf(" piyo = %p\n", piyo);
  *arg = piyo;
  printf("*argにpiyoを代入\n");
  printf("  arg = %p\n", arg);
  printf(" &arg = %p\n", &arg);
  printf(" *arg = %p\n", *arg);

  printf("hugaの作成\n");
  MyClass *huga = new MyClass;
  printf(" huga = %p\n", huga);
  arg = &huga;
  printf("arg に&hugaを代入\n");
  printf("  arg = %p\n", arg);
  printf(" &arg = %p\n", &arg);
  printf(" *arg = %p\n", *arg);

  printf("==============end adrs=============\n");
}

void ref(MyClass*& arg) {
  printf("==============begin ref=============\n");

  printf("argの確認\n");
  printf("  arg = %p\n", arg);
  printf(" &arg = %p\n", &arg);
  MyClass *piyo = new MyClass;
  printf("piyoの作成\n");
  printf(" piyo = %p\n", piyo);
  arg = piyo;
  printf("argにpiyoを代入\n");
  printf("  arg = %p\n", arg);
  printf(" &arg = %p\n", &arg);

  printf("==============end ref=============\n");
}

int main() {
  MyClass *hoge = new MyClass;

  printf("hogeの作成\n");
  printf(" hoge = %p\n", hoge);
  printf("&hoge = %p\n", &hoge);

  val(hoge);

  printf("mainでhogeの確認\n");
  printf(" hoge = %p\n", hoge);
  printf("&hoge = %p\n", &hoge);

  adrs(&hoge);

  printf("mainでhogeの確認\n");
  printf(" hoge = %p\n", hoge);
  printf("&hoge = %p\n", &hoge);

  ref(hoge);

  printf("mainでhogeの確認\n");
  printf(" hoge = %p\n", hoge);
  printf("&hoge = %p\n", &hoge);
}

実行結果は以下

hogeの作成
 hoge = 0x7f9d00402850   # hogeの値
&hoge = 0x7ffee0a24778   # hogeのアドレス
==============begin val=============
argの確認
  arg = 0x7f9d00402850
 &arg = 0x7ffee0a24728   # hogeのアドレスと異なる(新しくメモリが確保され値がコピーされている)
piyoの作成
 piyo = 0x7f9d00402860
argにpiyoを代入
  arg = 0x7f9d00402860   # argの値が書き換えられた
 &arg = 0x7ffee0a24728
==============end val=============
mainでhogeの確認
 hoge = 0x7f9d00402850   # val内でのarg値の書き換えはhogeに影響していない
&hoge = 0x7ffee0a24778
==============begin adrs=============
argの確認
  arg = 0x7ffee0a24778   # hogeのアドレスと等しい
 &arg = 0x7ffee0a24728   # hogeのアドレスと異なる(新しくメモリが確保され値がコピーされている)
 *arg = 0x7f9d00402850   # *argが示す値はhogeの値と等しい(argを1つ辿るとhogeに着く)
piyoの作成
 piyo = 0x7f9d00402870
*argにpiyoを代入
  arg = 0x7ffee0a24778
 &arg = 0x7ffee0a24728
 *arg = 0x7f9d00402870   # *argがpiyoの値を持つ(*argはhogeを示すのでhogeの値が変わる)
hugaの作成
 huga = 0x7f9d00402880
arg に&hugaを代入
  arg = 0x7ffee0a24718   # argの値が新しく作成されたhugaのアドレスに変わる(*argはhugaを示すようになり、hogeとの関係が切れる)
 &arg = 0x7ffee0a24728
 *arg = 0x7f9d00402880   # *argがhugaの値を持つ
==============end adrs=============
mainでhogeの確認
 hoge = 0x7f9d00402870   # adrs内でもpiyoの値に変わっている(hugaの値にはなっていない)
&hoge = 0x7ffee0a24778
==============begin ref=============
argの確認
  arg = 0x7f9d00402870   # hogeの値と等しい
 &arg = 0x7ffee0a24778   # hogeのアドレスとも等しい(arg用のメモリは用意されていない)
piyoの作成
 piyo = 0x7f9d00402890
argにpiyoを代入
  arg = 0x7f9d00402890   # argの値がpiyoの値に書き換えられる
 &arg = 0x7ffee0a24778
==============end ref=============
mainでhogeの確認
 hoge = 0x7f9d00402890   # ref内でのargの値の書き換えが反映されている
&hoge = 0x7ffee0a24778

おわり

ぶっちゃけ具体的なコンパイラの動作とかを詳しく知らないので、その辺を理解していけばまた考えが変わるかもしれませんが、とりあえず今現在の考えを書きました。おわり。