第三回-02 : 値渡し・アドレス引数・参照引数

本ページでは「関数への object の引渡し」について学ぶ。

本ページで扱う内容は C 言語と C++ の両方に通用するものであり、C/C++ の基礎とも言える内容であるから、しっかりと理解して欲しい。

まず、関数には C 言語で扱うような大域的な関数と C++ で扱うようなクラスのメンバ関数があるが、そのいずれも、引数戻り値を持つ。
例として、第四回の演習の複素数の問題で扱った sqrt() を考えよう。この関数はある数の平方根を求める関数であった。
いま、「x=sqrt(16);」という命令を実行すると、x には 16 に対する正の平方根である 4 が代入される。
このとき、16 のことを引数、4 のことを戻り値と呼ぶ。

引数、戻り値の型は「char, int, double」などのような組み込み型でも構わないし、 自分で作成したクラスであっても構わない。
ここではそのいずれをも考慮して話をすすめる。
戻り値の話は次ページ以降に譲るとして、ここではまず引数について考える。

サンプルコードは以下の通りである。
解説用のコードであるため、C 言語的な関数と、C++ 的なクラスが混在する変則的なコードであるが、注意して読んで欲しい。

#include <iostream>
using namespace std;

class samp{
		int i;     // private なメンバ変数 (private キーワードの省略)
	public:
		// 以下のように簡単なメンバ関数であればクラス定義内で定義できる
		samp(int n){i=n;}       // 引数つきのコンストラクタ
		void set_i(int n){i=n;} // i に値をセット
		int get_i(){return i;}  // i の値を取得
};

// 以下の 3 つの関数は、C 言語的な関数である

int sqr_it1(samp o){   // 資料 3 ページに相当
 	return o.get_i()*o.get_i(); // i の 2 乗を戻す
}

void sqr_it2(samp o){  // 資料 4 ページに相当
 	o.set_i(o.get_i()*o.get_i()); // i の 2 乗を i にセット (?)
	cout << "iのコピーの値は" << o.get_i() << "\n";
}

void sqr_it3(samp *o){  // 資料 7 ページに相当
 	o->set_i(o->get_i()*o->get_i()); // i の 2 乗を i にセット
	cout << "iのコピーの値は" << o->get_i() << "\n";
}

int main(){
 	samp a(10);  // 初期値 10 でクラス samp のオブジェクト a を宣言

	cout << "***sqr_it1 の呼び出し***\n";
	cout << sqr_it1(a) << "\n";
	
	cout << "\n";
	
	cout << "***sqr_it2 の呼び出し***\n";
	sqr_it2(a);
	cout << "関数終了後のiの値は" << a.get_i() << "\n";
	
	cout << "\n";
	
	cout << "***sqr_it3 の呼び出し***\n";
	sqr_it3(&a);
	cout << "関数終了後のiの値は" << a.get_i() << "\n";

}
実行結果:

***sqr_it1 の呼び出し***
100

***sqr_it2 の呼び出し***
iのコピーの値は100
関数終了後のiの値は10

***sqr_it3 の呼び出し***
iのコピーの値は100
関数終了後のiの値は100


内容は、「int i;」をメンバとして持つようなクラス samp を作成し、
それを関数 「sqr_it1(), sqr_it2(), sqr_it3()」から値を呼び出したり、 値を変更しようとしている。

このプログラムにはサンプルとして以上の意味はないが、
「クラスや変数の値を関数から操作する」という処理は C/C++ では基本であり、瀕出することに注意しておく。

main 関数では 3 つの関数 「sqr_it1(), sqr_it2(), sqr_it3()」を一つずつ呼び出しているが、それを以下で順に解説してゆく。


sqr_it1() の呼び出し

まず、main 関数内の「samp a(10);」なる宣言にによって、samp クラスのオブジェクトが値 10 で初期化される。
(もちろん、コンストラクタ samp(int n) が呼び出されている)

その後、関数が sqr_it1(a) のようにオブジェクト a を引数として呼び出される。
sqr_it1() の定義は以下であるから、sqr_it1() はオブジェクト a のメンバ変数の値、すなわち 10 の二乗 = 100 を戻す。

int sqr_it1(samp o){   // 資料 3 ページに相当
 	return o.get_i()*o.get_i(); // i の 2 乗を戻す
}


実行結果は

***sqr_it1 の呼び出し***
100


であるから、確かに sqr_it1() は正しく動作している。

この時のメモリの状態を考えてみよう。

# Java や C# のような新しい言語ではプログラマはメモリの状態について考慮しなくて良いように配慮されているが、
# C 言語や C++ ではプログラマはメモリの状態を常に意識していなければならない。
# そのため、C/C++ ではメモリやポインタについて理解を深めることがレベルアップへの第一歩である。



このように C/C++ で関数に引数を与えた場合、関数にはその引数のコピー (あるいは引数の値) が渡される。
これが「値渡し」の意味である。

面倒に思えるかもしれないが、次の章に進む前にまずこの図をしっかり理解して欲しい。


sqr_it2() の呼び出し

main 関数では次に sqr_it2(a) が呼び出され、さらに a の i の値を a.get_i() で呼び出している。
関数 sqr_it2() の定義は

void sqr_it2(samp o){  // 資料 3 ページに相当
 	o.set_i(o.get_i()*o.get_i()); // i の 2 乗を i にセット (?)
	cout << "iのコピーの値は" << o.get_i() << "\n";
}


であるから、これはオブジェクト a のメンバ変数 i の値を、i の二乗に上書きすることを意図したプログラムである。
2 行目の cout はテスト用である。
その後、 main 関数で a.get_i() で呼び出すことで、i が変更されたかどうかを確認している。

実行結果は

***sqr_it2 の呼び出し***
iのコピーの値は100
関数終了後のiの値は10


である。

ここでおかしな事が起こっている事に気がつくだろうか。
sqr_it2(a) の呼び出し中は「iのコピーの値は100」と表示され、確かに i の値は変更されていることがわかる。
しかし、sqr_it2(a) が終了し main 関数に戻った後の a.get_i() では i の値は 10 に戻ってしまっている

このように、関数内で引数の内容を変更する場合 C/C++ では注意が必要なのである。
この現象は、以下のようにメモリの状態を考えることで理解できる。



基本的には sqr_it1() の場合と大きく変わらない。

ここで、sqr_it2() 呼び出し中 (上図 (2)) を考えよう。
i の値 (10) の二乗 (100) が新たな i の値としてセットされているのだが、
この操作がオブジェクト a ではなく a のコピー o に対して施されていることに注意しよう。

もちろん、オブジェクト a のコピー o は sqr_it2(a) の終了と同時に解放されてしまう (上図 (3)) のであるから、
a.get_i() は 10 を戻すのである。

このように、引数の値渡し (コピー渡し) では、引数の内容を (そのままでは) 変更できない。
この状況を改善するためには、引数としてポインタを渡す必要がある。それが sqr_it3() である。


sqr_it3() の呼び出し

main 関数では次に sqr_it3(&a) が呼び出され、さらに a の i の値を a.get_i() で呼び出している。
&a によって、a のアドレスが取り出され、その内容が sqr_it3() に渡されていることに注意しよう。
関数 sqr_it3() の定義は

void sqr_it3(samp *o){  // 資料 7 ページに相当
 	o->set_i(o->get_i()*o->get_i()); // i の 2 乗を i にセット
	cout << "iのコピーの値は" << o->get_i() << "\n";
}


である。sqr_it2() と同様、これはオブジェクト o のメンバ変数 i の値を、i の二乗に上書きすることを意図したプログラムである。
ただし、オブジェクト o はポインタで渡されているので、メンバ関数の呼び出しが ドット演算子「.」ではなくアロー演算子「->」になっていることに注意しよう。
ポインタの取扱いが苦手な人のため、第三回-01にてポインタの基礎を解説したので参照して欲しい。
2 行目の cout はテスト用である。
その後、 main 関数で a.get_i() で呼び出すことで、i が変更されたかどうかを確認している。

実行結果は

***sqr_it3 の呼び出し***
iのコピーの値は100
関数終了後のiの値は100


であり、今度は期待した結果になっていることがわかる。

以下、メモリの模式図を使用して sqr_it3() の働きを理解しよう。



このように、引数として与えた変数やオブジェクトの内容を変更するには、ポインタを引数として関数に与えなければならない
これを第四回資料ではアドレス渡しと呼んでいる。


# (この注釈は混乱を招くかもしれないので自信のある人向け)
# ここで「アドレス渡し」と書いたが、細かく言うとこれは「ポインタの値」を関数に渡しているのであるから、
# 厳密には値渡しの一つの形態である。


参照引数

ここで資料を少し離れて「参照」と呼ばれるものについて触れよう。

上の例における、ポインタを引数として与える関数 sqr_it3() を思い出そう。 定義は次のようであった。


void sqr_it3(samp *o){  // 資料 7 ページに相当
 	o->set_i(o->get_i()*o->get_i()); // i の 2 乗を i にセット
	cout << "iのコピーの値は" << o->get_i() << "\n";
}


このとき、関数内部でオブジェクト o のメンバ関数の呼び出しにアロー演算子「->」を用いなければならないため、
関数内部がアロー演算子でゴチャゴチャしてしまうのがわかるだろう。


ポインタを渡した時のような効果があり、なおかつメンバ関数の呼び出しがドット演算子で良いような仕組みがあると便利である。
それが参照である。

参照の使用例は以下のようになる。

void sqr_it4(samp &o){ // 参照を引数として渡す
 	o.set_i(o.get_i()*o.get_i()); // i の 2 乗を i にセット
	cout << "iのコピーの値は" << o.get_i() << "\n";
}


さらに、参照を引数として渡す関数を呼び出す際、

sqr_it4(a);


のように、(&a ではなく) 値渡しのように a で渡せるというメリットがある。

この関数 sqr_it4() を本ページ最上部のプログラムに組み込み、 main 関数から呼び出すことで、動作を確認してみよ。

sqr_it2() のような記述でありながら、sqr_it3() のように正しく動作することがわかるであろう。
(ただし、sqr_it3() 終了の時点で a のメンバ変数 i の値は既に 100 に変化していることに注意せよ)

なお、参照は C 言語には存在せず、C++ で新たに加わった機能である。

←第三回-01 : ポインタの使い方第三回-03 : 関数の戻り値→

第三回トップページへ

クラスから入る C++ へ戻る