参照渡しと値渡し [ひとこと言わねば]
参照渡しと値渡し
プログラムの世界には、他のプログラムに変数を引き渡す場合、参照渡しと値渡しの2つの手法がある。
これは言葉で説明してもわからないと思うので、アセンブラ的な処理が可能なC言語で簡単なプログラムを作り、参照渡しと値渡しについて説明することにする。
たとえば、a=1とb=2があり、aとbの値を入れ替える副プログラムを作ることにする。
#include <stdio.h>
void Swap1(int a, int b) { // 値渡しでaとbの値を交換させる
int w;
// aとbの値を交換する
w=a; // aの値をwに退避
a=b; // bの値をaにセット
b=w; // wに退避させたaの値をbにセット
printf("副プログラム内でのaとbの値\n");
printf("%s%d %s%d\n","a=", a, "b=", b);
}
void Swap2(int *a, int *b) { // 参照渡しで交換させる
int w;
// aとbの値を交換する
w=*a; // aの値をwに退避
*a=*b; // bの値をaにセット
*b=w; // wに退避させたaの値をbにセット
}
main() {
int a=1 , b = 2;
printf("交換前\n");
printf("%s%d %s%d\n","a=", a, "b=", b);
// 値渡しのSwap1で交換させる
Swap1(a,b);
// 交換出来ているかのチェック
printf("Swap1から戻ってきた\n");
printf("%s%d %s%d\n","a=", a, "b=", b);
// 参照渡しで交換
Swap2(&a,&b);
printf("Swap2から戻ってきた\n");
printf("%s%d %s%d\n","a=", a, "b=", b);
}
このプログラムをコンパイルし、実行させると、次のようになる。
値渡しで変数aとbを交換する副プログラムSwap1に引き渡すと、Swap1内ではaとbの値は交換されているけれど、メインプログラムに戻ってくると交換されていないかのように見える。
これは、メインプログラム内で変数a、bに割り当てられているコンピュータ内のアドレスと、交換プログラムSwap1の変数a、bに割り当てられているコンピュータ内にアドレスが違っているからなんだケロ。
値渡しは、mainプログラムのaとbの値を、Swap1のaとbにコピーするだけだから、Swap1の変数a、bを交換しても、mainプログラムのaとbは変化しないといわけ。
対して、参照渡しのSwap2はどうかというと、mainプログラムからSwap2にデータを引き渡すとき、Swap2(&a,&b)となっているでしょう。
この&マークは、コンピュータ内に割り当てられたmainプログラムの変数a、bのアドレスを渡すというオマジナイなんだにゃ。
で、Swap2内では、例えば、
w=*a
となっているでしょう。
aの前に付いている*は、「*以降に続いているアドレスに保存されている(整数型の)データ」を意味しているんだケロ。
そして、これが、C言語で、悪名高き、C言語を勉強しようという多くのヒトを奈落の底に突き落とす、ポインタ操作というやつだにゃ。
――C言語を勉強しようというヒトの圧倒的大多数は、このポインタで討ち死にする(^^ゞ――
さらに、
*a=*b; // bの値をaにセット
この文は「(メインプログラムの)変数bに割り当てられたアドレスのデータを(メインプログラムの)変数aに割り当てられたアドレスに書き込む」ということを意味している。
こういう操作は、アセンブラや機械語――奇怪語とも言われる(^^ゞ――レベルの操作なので、「C言語はFortranやBasic、CobolやJavaといった高級言語ではない」と言われる所以。
本来、コンピュータの安全性のために、コンピュータのユーザーがすべきではない、メインプログラムの変数a、bに割り当てられたアドレスに直接値を書き込むということをやっているので、Swap2から戻ってきても、aとbの値が交換されているというわけ。
こういう違いが、値渡しと参照渡しには違いがある。
ddt³さんがコメントに書いているように、Fortranはすべて参照渡し。だから、Fortranの場合は、
a=1; b=2;
call Swap(a,b)
・・・
subroutine Swap(a,b)
w=a; a=b; b=a;
end
で、aとbのデータが交換される。
Basic系の言語は、配列変数は参照渡しで、スカラー変数は値渡しを採用している。
ddt^3です。この話はネコ先生しかわからないかも知れない(^^;)。
じつは.NET以降のVisual Basicでは、C言語のポインターより奇怪な事が起こるのです。まず.NET以降のVisual Basicは完全なオブジェクト指向言語になったので、全ての変数は少なくともオブジェクト型になります。整数も浮動小数点実数も文字列も。別にこの仕様に文句がある訳ではありません。問題はそこからのローカル仕様です。
いわゆる自分で作ったクラスCのインスタンスであるオブジェクトaを、サブルーティンS(b)に渡す事を考えます。VBの引数渡しは、値渡しが基本です。引数にbyRef修飾子を付けた時だけ、参照渡しになります。Sの定義は、
Private Sub S(b as C)
'bに対する作業
End Sub
くらいだとしときます。値渡しです。「bに対する作業」の中でbのメンバのいくつかを書きかえるとします。S(a)を呼んでメインに復帰した時、値渡しだから元のaは安全なのさなどと思っていると、メインのaのメンバも書き換わっているのです(^^;)。
ここでポインターとオブジェクト型の動作仕様に慣れた人なら、「値渡しされたのは、aのアドレスなのね!」と気づきます。つまりメインとサブで同じアドレスをポイントしてるので、サブで値を書き換えたら、メインでもそうなります。同じ実体にアクセスしてたからです。よってオブジェクト型の値渡しは、参照渡しのようにふるまいます。ここまでは、自分もけっこうすんなり行けました(^^)。
VBでは、整数も浮動小数点実数も文字列も結局はオブジェクト型でした。そうするとユーザー(プログラマー)にとっては単なる数値でしかない整数型とかにも同じ事が起こります。この余りにも便利すぎる(?)仕様は危険でもあるので、VBでは全てのオブジェクトを参照型と値型に大別し、総称してプリミティブ型と呼びます(なんてわかりにくい用語だろう)。
参照型の代表は、自作クラスのインスタンスオブジェクトなどです。値型の代表は、整数,浮動小数点実数,文字列などです。整数型aに対してS(a)として、次を呼びます。
Private Sub S(b as Integer)
'bの値を変える
End Sub
そうすると今度は本当に値渡しとして機能します。でも「値渡しされた」のは今回も「aのアドレス」なんです。以下のコードは動きませんが、マシン語レベルでは、以下のようなコードになります。VBではConstractorをNewと書き、指定されたアドレスに値のみを書き込む組み込みメソッド、Cloneがあります。
Private Sub S(b as Integer)
Dim a as Integer = New Integer '新しいアドレス指定
a = b.Clone 'aのアドレスに値コピー
'aの値を変える
End Sub
・・・わかる、わかるよ。舞黒素腐党さんも色々言われながらも、色々と考えたのね(^^;)。という訳で、参照型は参照渡し、値型は値渡しと同じという頭が出来上がります。
で値型の配列、
Dim a(2) as Integer
なんかは当然「値型だよね」と思って、
Dim a as Integer() = {0, 1, 2}
'a(0)=0,a(1)=0,a(2)=2と初期化したのと同じ
S(a) 'aの内容を書きかえる
'a(1)の値を取り出す
※ Private Sub S(a() as Integer)
みたいなコードを書くと、今度は参照型として動作するのです。全くもぉ~!、舞黒素腐党は不親切だな。ちゃんとヘルプに書いとけよ!。
それで「参照型は参照渡し、値型は値渡しだが、値型の配列やリストは参照渡し」は仕様だ!(バグであっても仕様だ!)と自分を納得させ、次のコードを書きます。今度はSの中でaの内容をセットさせるとします。
Dim a(2) as Integer
'aは初期化されていない。この時点でaのアドレスはNull
S(a) 'aの内容をセットする。要するにSでaを初期化したい
'a(1)の値を取り出す
なんと今度は、「a(1)の値を取り出す」ところで「Null参照エラー」が出やがるのです(^^;)。「値型の配列やリストは参照渡し!」じゃなかったのかよ!・・・?。
目を点にしたまま30秒ほど地団駄踏んだ末、はたと気づきます。「アドレスを値渡しだった・・・俺が悪かったぁ~あ!」(涙目)。つまり、
Private Sub S(byRef a() as Integer)
'aのアドレスを参照渡しする
が、この場合の正解なのです(^^;)。
こういう事は、あまり意識されてないようなのです。自分はSEに転職した口なので(今は古巣の業界に戻ってますが)ほとんど独学のため、いたるところで痛い目にあっておぼえたのですが、IT業界生え抜きのその時の隣の同僚は、いつも先輩などが近くにいたせいか、けっこう経験と勘で作業しており、こういう事を知りませんでした。デバックで悩んでました(^^;)。
by ddtddtddt (2017-10-25 21:00)
こんばんはです。
「最近、Javaが人気だそうだから・・・」ということで、Javaを勉強したとき、私は苦労しました。
私の場合、Basic⇒Fortran⇒Cという手続き型の言語から、完全Object形の、しかも、クラスのJavaでしたから、何かと、大変でしたよ。
そして、
Javaでもddt³さんが書いているようなことが起きますから、ホント、苦労しましたよ。
混乱するだけだから、いっそのこと、プリミティヴ型は全廃し、すべてオブジェクトやクラスにしてくれたほうが、首尾一貫していて、学習は容易だったと容易だったと思いますね。
by nemurineko (2017-10-25 21:43)