概要
最近Javaを触り始めて、参照値渡しで混乱してしまいました。そこでポインタや参照について自分なりに調べ、考えをまとめてみました。
自分の考察もだいぶ入ってるので間違ってる部分もあると思います。
準備
JavaではアドレスがみれないのでC++でやっていきます。(参照値渡しはJavaで書いてます)
Personというクラスを作ってそのオブジェクトを引数に渡して、アドレスなどを調べていきたいと思います。(これからPersonを省いて書いていきます)
class Person
{
private:
string name;
public:
Person(string n){name=n;}
~Person(){}
string getName(){return name;}
void setName(string n){name = n;}
};
ポインタ渡し
まずアドレスがどうなってるかみてみます。
アドレス確認
void pointer(Person* pp)
{
cout << "ppの中身: " << pp << endl;
cout << "ppのアドレス(&pp): " << &pp << endl;
}
int main()
{
Person *p = new Person("init");
cout << "--------init--------" << endl;
cout << "初期pアドレス: " << p << endl;
cout << "--------pointer-------" << endl;
pointer(p);
}
この結果をみると*pのアドレスpと引数ppの値が一緒になっています。ただもちろんppのアドレス(下図で言う&pp)はアドレスp(下図で言う&p)と違うことがわかります。このことからメモリは
のような形になっていると考えられます。このメモリを実感するために中身を書き換えてどうなってるか確認します。
書き換え確認
void setNewName(Person* pp)
{
(*pp).setName("setNewName");
}
void setNewPerson(Person* pp)
{
Person *newPerson = new Person("setNewPerson");
// newPerson("setNewPerson");
*pp = *newPerson;
}
void setNewPersonAddress(Person* pp)
{
Person *newPerson = new Person("setNewPerson");
pp = newPerson;
}
int main()
{
Person *p = new Person("init");
cout << "--------init--------" << endl;
cout << "初期ネーム: " << (*p).getName() << endl;
cout << "--------pointer-------" << endl;
setNewName(p);
cout << "新しいネーム: " << (*p).getName() << endl;
setNewPerson(p);
cout << "新しいPerson: " << (*p).getName() << endl;
setNewPersonAddress(p);
cout << "新しいPersonアドレス: " << (*p).getName() << endl;
}
メモリの配列の想像通り引数のアドレスppと*pのアドレスpが一緒なため、*pp(=*p)からsetName()をよぶと元も変わりますし、*pp(=*p)にあたらしいPersonを代入しても元のデータが変わっています。
そしてアドレスppを変えてしまうと元のデータは変わりません。なぜならアドレスppの値を元のデータ(p)はさしているからです。
参照渡し
こちらもまず先にアドレスをみていきたいと思います。
アドレス確認
void reference(Person& rp)
{
cout << "&rpの中身: " << &rp << endl;
}
int main()
{
Person *p = new Person("init");
cout << "--------init--------" << endl;
cout << "初期pアドレス: " << p << endl;
cout << "--------referece--------" << endl;
reference(*p);
}
この結果を見るとpのアドレスとrpのアドレスが一緒なのでpとrpが同じものを参照しているということがわかります。つまりメモリは
というふうにrpはp(上図でいう&p)をさす新しいもう一つの変数だということがわかります。ここでもこのメモリ配列が本当にそうなのか確認してみます。
書き換え確認
void setNewName(Person& rp)
{
rp.setName("setNewName");
}
void setNewPerson(Person& rp)
{
Person newPerson("setNewPerson");
rp = newPerson;
}
/*
void setNewPersonAddress(Person& rp) {
Person newPerson("setNewPerson");
&rp = &newPerson;
}
*/
int main()
{
Person *p = new Person("init");
cout << "--------init--------" << endl;
cout << "初期ネーム: " << (*p).getName() << endl;
cout << "--------referece--------" << endl;
setNewName(*p);
cout << "新しいネーム: " << (*p).getName() << endl;
setNewPerson(*p);
cout << "新しいPerson: " << (*p).getName() << endl;
//setNewPersonAddress(*p);
//cout << "新しいPersonアドレス: " << (*p).getName() << endl;
}
結果
イメージしたメモリの配列通り引数rpのアドレスとpは一緒なものをさしてる同じものなので、rp.setName()(= (*p).setName())でも変わりますし、
rp=newPerson(= (*p)=newPerson)なので新しいオブジェクトをいれても元のデータは変わります。
ポインタと違っていいところはアドレスの値を直接いじれてないところだと思います。
ポインタだと
void setNewPersonAddress(Person* pp)
{
Person newPerson("setNewPersonAddress");
pp = &newPerson;
}
のように直接アドレスを代入できてしまいます。一応参照のコードのところにアドレスを入れるコードをコメントアウトして書きましたが、試して見るとエラーが出ると思います。
参照値渡し
参照値渡しにかんしてはアドレスのコードからの確認ができていません。ただどのような書き換え結果になるかはJavaからみれるので、その結果から逆にメモリがどんな感じか予想してみようと思います。
書き換え確認
public static void setNewName(Person rvf) {
rvf.setName("setNewName");
}
public static void setNewPerson(Person rvf) {
Person newSB = new Person("setNewPerson");
rvf = newSB;
}
public static void main(String[] args) {
Person p = new Person("init");
System.out.println("-------init-------");
System.out.println(p.getName());
System.out.println("--------setNewName-------");
setNewName(p);
System.out.println(p.getName());
System.out.println("--------setNewPerson-------");
setNewPerson(p);
System.out.println(p.getName());
}
この結果を見るとポインタや参照と違って新しいオブジェクトを代入した時に、元のデータに変更がみられません。
また、調べてみると、参照値渡しとは「参照の値」を「値渡し」していると出てきます。
参考リンク: もう参照渡しとは言わせない
そこでこのようなメモリをしているのではないかと考えました。
アドレス予想
参照との違いは&pを直接さしているわけではないこと。
ポインタとの違いは引数rvfがアドレス&pをさしているわけではないということです。
こう考えると、上の結果がすんなり受け入れるかと思います。
rvf自体は&P実体をさしているので rvf.setName() をするとPerson実体自体が書き換わり、もとのデータpも変わります。
ただ、新しいobjectをrvfに入れた場合、rvfのアドレスは&pとは違うため、もとのpデータを書き換えることができません。
またこれをみると参照値の値渡しというのも理解しやすいかと思います。
まとめ
ポインタは新しいアドレスに元のデータのアドレス情報を
参照は元のデータとおなじアドレスをさす変数を
参照値渡しは新しいアドレスに参照しているデータのアドレスを
こんな感じで動いてると思うと違いがわかりやすくなるかな?と思います。
ちょっとした話。
ちなみにjavaでStringやIntegerはオブジェクトなのに、上の書き換え結果のように、元のデータが書き変わらないと思ってる方がいるかと思います。
javaのStringやIntegerは書き換えができないようになっているため、たとえば
Integer i = 1
とした時は新しいIntegerオブジェクトが代入されていることになってます。またIntegerにintを代入したとしてもautoboxing機能で自動的にIntegerを新しく作って代入します。