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

結論

  • 値渡し

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

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

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

背景

プログラムを書いていると「◯◯渡し」という言葉をよく耳にしますが、その定義はよく理解できていませんでした。そこで、ググって解説記事を幾つか読んでみたのですが、記事によって書いてあることが違ったり、言語によっても呼び方が違ったりと、なかなか混沌としていることが判明。
例えば 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 の値をコピーことです。そのままですね。値にアドレスが入っていたとしてもメモリ上の "値" であることには変わりないので、値渡しです。

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

f:id:norikone:20180420201715j:plain

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

参照渡し

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

おわり

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