はじめに
昨日ちょっと気になること[^1]があったので備忘録として、僕の理解を置いときます。
そのため、ブログ的に僕なりの理解を書いているのであって、間違っている箇所は多いと思います。 間違っているところがあればコメントでも直接にでもバンバン言ってください。議論してお互いに理解を深めていきたいです。 読者層としては、これからC/C++使っていくよ!って人をターゲットにしているので、void *だ使い分けだ配列だなんだかんだは触れないです。あと、就活するときにQiitaとかに記事とか投稿していないのですか?とか聞かれるらしいので、身内向けだけに作ってた資料をうpするためのはじめてのQiita記事。
ホントはブログの方に書いてたんだけど、鯖のデータが吹っ飛んじゃったからね。
途中からはただ書いただけで校正してない。読み直してない。これから加筆修正するかどうかは知らん。やる気次第
「〇〇渡し」
さて本題。
いろんな言語でのプログラミングにおいて、「値渡し」「参照渡し」の話は切り離せない問題だと思う。
僕の理解において、この2つの言葉の定義としては
- 「値そのものを直接渡す」方法を「値渡し」
- 「参照を渡して、間接的に値を利用する」方法を「参照渡し」
だと思っている。
もう少し言い換えるなら「変数の中に入っている値」を関数の中から「直接的に扱う」か「間接的に扱う」か。類語ではなくて対義語なので、その2つ。
そして、「参照渡し」する場合、つまり値を間接的に扱う場合は外からでも中身をいじることができる。
本で例えるならば
値渡しは、
本に落書きをしても(織田信長の額に「肉」を書いても)、誰もが同じ本の内容(肉とは書かれていない織田信長の顔)を見ることができる。
落書きをしたAくんの本では落書きされているけど、Bさんの本は落書きされてない。(図1の上図)
対して、参照渡しは、
原本を直接扱えるロボットのコントローラをAくんに渡すようなもので、Aくんが落書き操作をロボットに指示すると、原本が落書きされるので、Bさんが読むと落書きされたままになる。そして、Bさんは『織田信長とは額に「肉」と書かれた人』と認識してしまう状態になる。(図1の下図)
図1. 値渡しと参照渡しの僕なりの本で例えたイメージ図
このときのロボットとかコントローラの仕組みがアドレスとかポインタとか、原本を実態だとかインスタンスだとかなんだとか、一見わけわからん用語で言うのである。
また、値渡しはAくんもBさんも全員分の本を用意する手間が必要になるので、ルーズリーフ程度ならまだしも、教科書みたいな本だと印刷代も印刷時間も手間になる。
対して、一人変更したら全員分変わっちゃう参照渡しも、全員分用意しなくて良かったり、場所も取らないのでよく使う。1家に1つモナリザは必要ないのだ。
そして、このコントローラをC++ではもうちょっとわかりやすく書きやすくしたよ!ってのが、今回話になった「参照」というものである。
そう、**参照渡しされた変数をC++で扱いやすくしたよ!**ってだけの機能を勘違いしているように見える解説記事が検索するとぼろぼろ出てくる。
僕の理解では「値渡し」と「参照渡し」は類義語ではなくて対義語。2つだけのはずである。
なんなら、英語で言えば「値渡し」はdirect、「参照渡し」はindirect。
excelとかでもindirect関数によってセルの値を参照する機能があることからそれで正しいと思っている。(英語に関してはよく調べていないけども…)
ちょっとだけ調べたけど、call by reference/valueの直訳で「値/参照渡し」になるらしい。日本語ではreferenceで参照としている場合が多いが、IBMとか英語圏だとちょっと調べもの~の範囲でもうめちゃくちゃだった。1
対義語ではある。が、やっぱわからん。
そして、この対になっている*[direct/indirect]*という話に対して、とある解説記事を中心として「ポインタ渡し」という第三の勢力が出てきて、わからなくなった。2 3
とはいえ、コードを憎んで人を憎まず。僕もこういう間違いは良くするので、これを機に僕も復習兼ねてこの記事を書きました。
説明する前によく使う図の説明をば
僕はプログラムの話をするときはいつも、以下の図2の様なイメージ図を多用する。
こんな感じで、変数という名前の箱と、その箱がメモリのどこにあるかという場所としてアドレスを表記して表現しています。
図2. int x;
としたときのメモリ的なイメージ図。(x=1;
やprint
したときの出力例)4
こんな感じで、xという名前のついたの箱があって、そこにx = 1;
を実行して1が入れられる。
そして、その箱の場所は、どこか見知らぬメモリ空間の場所は0x4f6aという感じ。
この変数xをprintf("%d", x);
とかで出力すると1が出力される。同じようにprintf("%x", &x);
とかで出力すると4f6aが出力される。という感じ。
では本題
「値渡し」
まずC/C++の値渡しの疑似コードは以下のようになる。
void f1(int x) {
x = 2;
}
...
int a = 1;
f1( a );
これは変数aという名前の箱に入っている「1」という値を、関数f1に渡し、
関数f1の中では変数xという名前の箱に「1」という値が入るわけである。
「1」という直接的な値が、変数xという名前の箱に入るわけで、
x = 2;
としても、変数xの値が変わるだけなので、外においてある変数aという箱の中身は変わらないのである。
図でいうと下図の図3の感じになる。
図3. 値渡しされている関数のイメージ図
関数f1のメモリ空間の中にある変数xに変数aに入っていた1が代入され、x = 2;
が実行され、xは2になる。という感じ。
「参照渡し」
次にC/C++の参照渡しをの疑似コードは以下のようになる。
void f2(int *x) {
*x = 2;
}
...
int a = 1;
f2( &a );
これは変数aという名前の箱が置いてある場所「&a」、通称「アドレス」を、関数f2に渡し、
関数f2の中では変数xという「箱がどこにあるか」ということを記した「アドレス__x__」を参照して、その場所にある箱「*x」に「2」という値を入れるわけである。
イメージ図としては以下の図4のようになる。
図4. 参照渡しのイメージ図
もう少し詳しく見ていく
void f2(int *x)
というのは、
「int *x」によって、変数xという名前の箱には、「int型の箱の場所が入るよ」ということであり、
関数f2に、箱の場所を渡すことができるのである。
この「__int *__x」を「xはint型のポインタ(変数)**」と呼ぶのである。
そして、f2( &a );
というのは、
「&」をつけることで「a」という箱の場所を使うことができ、aの箱の場所「&a」を関数f2に渡している。
この「&」を「参照演算子」と呼ぶのだ。
変数xという名前の箱には「&a」というaの箱の場所が入ってくるが、そのままだと箱の場所がわかるだけである。使いたいのは箱の中身。
そこで、アドレスに「*」をつけることで箱の中身を参照してきて、間接的に箱の中身を触るよ!というのが「*x」である。
つまり、*x = 2;
というのは、
変数xの箱に入っている「箱の場所」を参照して、その中に「2」を入れるよ!という操作であり、
変数xの箱には、変数aの箱の場所が入っているので、変数aの箱に「2」が入るのである。
そうして、関数f2から抜けたあとの変数aの値は2と変えられているのである。
また、この「*」を「間接参照演算子」とか呼ぶのである。
「int」と「int *」は「*」がついてるかどうかだけで、一見すごく似てるように見えるが、「int型変数」に「"hogehoge"」という文字列が入らないのと同じように、「値を入れる箱」か「箱の場所を入れる箱」かとまるっきり中身は別物である。
問題にしていた「参照」渡し
これがなかなか曲者である。C++特有のものなのでそういうとこが厄介なのだとは思う。
少なくとも、「参照渡し」というのはポインタを渡していない。ポインタに渡しているのだ。
「ポインタにアドレスを渡して参照させている」のだ。
そして、今から述べるのは「参照渡し」をしているのではない、「参照」を渡しているのだ。
何を言っているのか分からないと思うが俺もわからん。
C++で追加された参照について
C++では参照演算子&を使って直接変数を定義し、参照を利用することができる。
サンプル的なコードで言えば以下の通り
int a = 1;
int &x = a;
a = 3;
std::cout << "x is " << x << std::endl;
//> x is 3
x = 4;
std::cout << "a is " << a << std::endl;
//> a is 4
これは変数xに変数aの箱の位置を入れるのではなく、
「変数xの箱の位置として、変数aの箱の位置を設定する」
という意味に近いのである。
もう少し言えば、「xのアドレスとしてaのアドレスを代入する」という感じ?
なので、変数aとxは全く同じ箱を指しているので、aに値を代入してもxの値は変わる。逆も同じ。
イメージ図としては下の感じ
自分が一番信用できる初級者用参考書の一つとして使っているアンク社の「C++の絵本」5では、「変数a」にxというあだ名としてつけることができる。と表現している。
この使い方の場合、確かにその通りだ…
こんなことはできないけども、イメージとしてint &x = a;
というのは、
int x;
でxの箱ができ、
&x = &a;
によって「変数xの箱の位置」に変数aの箱の位置が入る様な感じ。
この参照の使い方は自分の知っている限りだと、基本的には以下のように使用する。ポインタを用いた参照渡しをする関数も同時に書いておく。
void f2(int *x) {
*x = 2;
}
void f3(int &x) {
x = 3;
}
int a = 1;
std::cout << "a is " << a << std::endl;
//> a is 1
f2(&a);
std::cout << "a is " << a << std::endl;
//> a is 2
f3(a);
std::cout << "a is " << a << std::endl;
//> a is 3
関数f2も関数f3もほぼ同じ動作をする関数である。
どちらも変数aの箱の場所を関数に渡し、それぞれ変数aを参照して値を変えているのである。
この参照を仮引数にしたときの関数のイメージ図を下の図5に示す。
図5. 参照を関数の仮引数に指定したときの値の渡し方や動くイメージ図
動作は同じではあるが、この関数f3のように参照を仮引数として使用することで、
関数f3の中も、f3に渡す変数もポインタを意識せず、まるで普通の変数のように扱えるのである。
結論
参照を仮引数に指定することで、ポインタとか意識しなくても参照できる!外の変数を弄れる!
めっちゃ便利!アロー演算子とかでポインタ表現もない!データが大きくても図1の本の例みたいに本を印刷せずに読み出せるから高速だ!すごい!
というだけの話である。結局ただの参照渡しなのだ。
ただ、ちょっとxに入れる値の入れ方が違うだけ。
っと僕は理解しているのである。
そもそも、この参照を仮引数に利用する方法をなんというか、手元のポケットリファレンス6等で調べても出てこない。
「参照を引数に」とか「参照による受け渡し」とかとか…
「参照渡し」みたいな技名がないのである。
技名がないテクニックの一つだからこんな混乱が起きているのだと思う。(少なくとも俺の中では)
そして、技名がないからこそ、関数の仮引数(int *x)
と(int &x)
に注目してみると、なんとなく原因がわかる。
-
*x
とポインタ変数を利用しているからポインタ渡しではない -
&x
と参照を利用しているから参照渡しではない
どちらも変数xを宣言しているが、変数xの箱には箱の位置が入るようにしているのか、変数xの箱の位置として代入されてきた変数の箱を設定するのかである。
めでたしめでたし…
以下、構成前に悩んでできた文章を置いてます。
つまり書きかけの下書き。この説明を書く上でポインタとかの話をすべきかしまいか
説明はいらないから図だけあればわかるという人には、今回の内容は全てここだけでわかるかも。
![C++2019-04-25.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/394370/83c729e6-c195-02c2-a210-254342ab8288.png) ![C++2019-04-25-2.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/394370/5e70edc8-324c-53d8-23de-097ec3d9a9e0.png)そもそも参照やポインタとは
僕はいつも変数という名前の箱(入れ物)と、その箱がメモリのどこにあるかというアドレスとして、以下の図2を多用する。 ![C++2019-04-26-1.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/394370/d03897ff-51fb-2144-8733-7a74bcd11d43.png) 図2.`int x;`としたときのメモリ的なイメージ図。(`x=1;`や`print`したときの出力例)[^3] `int x;`と宣言すると、とあるメモリ空間(ここでは0x4f6a)に`int`分の大きさの箱が作られる。 この**x**の箱に1を代入する`x = 1;`で、箱の中身が「1」になる。 なので、**x**を`printf("%d", x);`とかで出力すると**1**が出力される。 また、この箱がある場所、メモリ空間の中の位置**0x4f6a**という値は**&x**を使うことで利用することができる。 なので、**x**を`printf("%x", &x);`とかで出力すると**4f6a**が出力される。 とはいえ、このメモリの位置というは出力こそできるが、この値をみて一つ増やしてみよう!とかどうこうというのは普通はしない。普通はね 基本的にこれを使うのはパソコン側である。 すこし話は変わって、**int**という型は整数が入るので、**float**とかの小数点のある実数は普通は入らない。(小数点以下切り捨てて整数にして**int**に入れるとか、この**x**箱の大きさが違ったりとかそういう話もあるが) 基本的には、箱にはどういう数値が入るかという**型**があるのである。 *0(0x00)~255(0xff)*の数値と文字を1対1で対応させることで文字が表現できるので、**char**という型があるのだ。 そう、**0x4f6a**みたいな箱の位置を表すアドレスの型もまたあるのだ。 それがみんなを苦しめる要因の一つである**ポインタ**である。-
基本的なとこは間違ってはいないと思うが、Qiitaに関しては2019/04/25現時点での検索上位[C++ 値渡し、ポインタ渡し、参照渡しを使い分けよう]とか[値渡し、ポインタ渡し、参照渡しの違い]あたり。誤解を生みやすいような書き方だったり、表現方法がちょっと違うんでないか?と思い復習としてこの記事を書きました。 ↩
-
どうも調べてるとJAVAあたりから「ポインタ渡しと参照渡しの違い!」とかそのへんの第三勢力から来ているっぽい。なぜ参照渡ししかほぼないJAVAとかから来てるのかわからない。でも、なんかちょっと大きい?会社の解説記事なのでここにはリンクさせません。就活中だし、怖い。 ↩
-
いつもprintfでアドレス出力してたときは%x使ってたんだけど、VS2019にしたときに%p使えよ!って注意されて正直びっくりした。それで資料は混在しています。VS2012~2017は注意してくれなかったじゃん! ↩
-
株式会社アンク の C++の絵本 第2版 C++が好きになる新しい9つの扉 https://amzn.to/2XJcpL0 ↩
-
高橋 晶 の [改訂第3版]C++ポケットリファレンス (POCKET REFERENCE) https://amzn.to/2XNN0jr ↩