2017/3/1追記 : Pythonの説明がやや不正確だったため追加の記事を書きました → 参照についてもう少し詳しく ~PythonとJavaを例に~
ありきたりな話題ではあるけど、値渡しとか参照渡しとか、変数を関数に渡した時の挙動について自分もまとめてみることにした。
#はじめに、プリミティブ型について
ほとんどの言語では、プリミティブ型とそれ以外で挙動が違う。
言語によって若干異なるものの、プリミティブ型は概ねこんな感じ。
- 整数
- 浮動小数点数
- 文字
- ブーリアン
- 参照 or ポインタ
それ以外の配列、クラス、構造体などをここではオブジェクトと呼んでいる。
#値渡し
関数に渡した際にその値をコピーした新しい変数が作られ、それが仮引数になる。
関数内で仮引数を変更しても呼び出し元の変数は変化しない。
C++、Java、Pythonの例。
//C++
#include <iostream>
void foo(int x){
x = 42;
}
int main(){
int x = 334;
std::cout << x << "\n";
foo(x);
std::cout << x << "\n";
return 0;
}
//Java
public class Val{
public static void foo(int x){
x = 42;
}
public static void main(String[] args){
int x = 334;
System.out.println(x);
foo(x);
System.out.println(x);
}
}
#Python
def foo(x):
x = 42
x = 334
print(x)
foo(x)
print(x)
結果はいずれも以下の通り。
334
334
こんな感じでプリミティブ型はほとんどの言語で値渡しになる。
また構造体とクラスの区別がある言語だと、C#やSwiftのように構造体を値渡し、クラスを参照渡しや参照の値渡しと規定してたりする。
#参照渡し
関数に渡される実引数と仮引数がメモリ上で同じ場所を指すようになる。いわば同じものに対する「別名」を作る感じ。
関数内で仮引数を変更するとそれが呼び出し元にも反映される。
C++、C#、Swiftの例。
//C++
#include <iostream>
class Box{
public:
int value;
Box(int value) : value(value){}
};
//関数定義時に&をつけると参照渡しになる
void foo(Box& box){
box.value = 42;
}
void bar(Box& box){
box = Box(42);
}
int main(){
Box box1(334);
std::cout << "foo: \n";
std::cout << box1.value << "\n";
foo(box1);
std::cout << box1.value << "\n";
Box box2(334);
std::cout << "bar: \n";
std::cout << box2.value << "\n";
bar(box2);
std::cout << box2.value << "\n";
return 0;
}
//C#
using System;
class Ref{
public class Box{
public int value;
public Box(int value){
this.value = value;
}
}
//関数定義時と呼び出し時にrefをつけると参照渡しになる
public static void Foo(ref Box box){
box.value = 42;
}
public static void Bar(ref Box box){
box = new Box(42);
}
public static void Main(string[] args){
Box box1 = new Box(334);
Console.WriteLine("foo: ");
Console.WriteLine(box1.value);
Foo(ref box1);
Console.WriteLine(box1.value);
Box box2 = new Box(334);
Console.WriteLine("bar: ");
Console.WriteLine(box2.value);
Bar(ref box2);
Console.WriteLine(box2.value);
}
}
//Swift
class Box{
public var value:Int
init(_ value:Int){
self.value = value
}
}
func foo(_ box:Box){
box.value = 42
}
//仮引数に新しく代入する場合関数定義時にinoutを、呼び出し時に&をつける
func bar(_ box:inout Box){
box = Box(42)
}
var box1 = Box(334)
print("foo: ")
print(box1.value)
foo(box1)
print(box1.value)
var box2 = Box(334)
print("bar: ")
print(box2.value)
bar(&box2)
print(box2.value)
結果はこんな感じ。
foo:
334
42
bar:
334
42
#参照の値渡し
共有渡しとも。大抵の言語はオブジェクトを渡すときこの形態になる。
これを参照渡しと呼ぶこともあるけど、以下のように実際には少し違う。
Java、C#、Pythonの例。
//Java
class Ptr{
public static class Box{
public int value;
public Box(int value){
this.value = value;
}
}
public static void foo(Box box){
box.value = 42;
}
public static void bar(Box box){
box = new Box(42);
}
public static void main(String[] args){
Box box1 = new Box(334);
System.out.println("foo: ");
System.out.println(box1.value);
foo(box1);
System.out.println(box1.value);
Box box2 = new Box(334);
System.out.println("bar: ");
System.out.println(box2.value);
bar(box2);
System.out.println(box2.value);
}
}
//C#
using System;
class Ptr{
public class Box{
public int value;
public Box(int value){
this.value = value;
}
}
//refをつけないとクラスは参照の値渡しになる
public static void Foo(Box box){
box.value = 42;
}
public static void Bar(Box box){
box = new Box(42);
}
public static void Main(string[] args){
Box box1 = new Box(334);
Console.WriteLine("foo: ");
Console.WriteLine(box1.value);
Foo(box1);
Console.WriteLine(box1.value);
Box box2 = new Box(334);
Console.WriteLine("bar: ");
Console.WriteLine(box2.value);
Bar(box2);
Console.WriteLine(box2.value);
}
}
#Python
class Box:
def __init__(self, value):
self.value = value
def foo(box):
box.value = 42
def bar(box):
box = Box(42)
box1 = Box(334)
print("foo: ")
print(box1.value)
foo(box1)
print(box1.value)
box2 = Box(334)
print("bar: ")
print(box2.value)
bar(box2)
print(box2.value)
結果はこう。
foo:
334
42
bar:
334
334
このように、仮引数に新しいオブジェクトを割り当てた時の挙動が参照渡しと違っている。
C++では、以下のようにポインタを渡すようにすると上と同じ挙動になる。
//C++
#include <iostream>
class Box{
public:
int value;
Box(int value) : value(value){}
};
void foo(Box *box){
box->value = 42;
}
void bar(Box *box){
box = new Box(42);
delete box;
}
int main(){
Box *box1 = new Box(334);
std::cout << "foo: \n";
std::cout << box1->value << "\n";
foo(box1);
std::cout << box1->value << "\n";
Box *box2 = new Box(334);
std::cout << "bar: \n";
std::cout << box2->value << "\n";
bar(box2);
std::cout << box2->value << "\n";
delete box1;
delete box2;
return 0;
}
実用上は生ポインタじゃなくてstd::shared_ptrとかを使うべきなんだけど、今回は比較のためにあえてこれで。
「参照の値渡し」と言うと混乱しそうだけど、要するにここでいう「参照」とは「ポインタ」のこと。つまりポインタの値渡し。
そう考えると、bar
で行なっている処理は、
・参照渡しの場合
-
bar
に渡された実引数と同じオブジェクトを指す仮引数box
を作る -
box
が指した場所にあるオブジェクトをBox(42)
に置き換える -
box
と呼び出し元のbox2
は同じ場所を指しているので後者も変化する
・参照の値渡しの場合
-
bar
に渡された実引数と同じオブジェクトを指す仮引数box
を作る -
box
が指す場所を新しく作ったオブジェクトBox(42)
の方に変える - 呼び出し元の
box2
が指しているオブジェクトはそのまま
ということになる。図に描くとこんな感じ。
もしポインタを使ったコードで参照渡しの方の挙動を再現するなら、
void bar(Box *box){
box = new Box(42);
delete box;
}
の部分が
void bar(Box *box){
*box = Box(42);
}
になる。
因みにObjective-Cのコードなら、この「オブジェクトがポインタで扱われている」ということがよりはっきりする。
//Objective-C
#import <Foundation/Foundation.h>
@interface Box : NSObject
@property(nonatomic) int value;
- (id)initWithValue:(int)value;
@end
@implementation Box
- (id)initWithValue:(int)value{
if(self = [super init]){
_value = value;
}
return self;
}
@end
void foo(Box *box){
box.value = 42;
}
void bar(Box *box){
box = [[Box alloc] initWithValue:42];
}
int main(){
Box *box1 = [[Box alloc] initWithValue:334];
printf("foo:\n");
printf("%d\n", box1.value);
foo(box1);
printf("%d\n", box1.value);
Box *box2 = [[Box alloc] initWithValue:334];
printf("bar:\n");
printf("%d\n", box2.value);
bar(box2);
printf("%d\n", box2.value);
return 0;
}
#まとめ
・値渡し
実引数の値がコピーされた新しい変数が作られる。プリミティブ型を渡した時はこれ。
仮引数を変更しても呼び出し元には影響なし。
言語によっては構造体なども値渡しになることがある。
・参照渡し
実引数と仮引数がメモリ上の同じ場所を指すようになる。
仮引数を変更するとそれが呼び出し元にも反映される。
この形態はサポートしていない言語も少なくない。
・参照の値渡し
CやC++でいうポインタ渡し。メモリ上の場所を仮引数に与えることで、その場所を通じて実引数と同じオブジェクトを操作できる。
参照渡しと似ているが、仮引数の値自体を書き換えた時の挙動は要注意。
多くの言語はオブジェクトを渡す時この形態になる。