静岡理工科大学 菅沼ホーム 本文目次 演習まとめ 付録 索引

C/C++ 自学・自習(第7章)

第7章 関数

  1. 7.3.1 データとアドレス
      (プログラム例7.8) 複数結果の受け渡し
      (プログラム例7.9) デフォルト引数
  2. (演習問題:アドレス渡し)
  3. 7.3.2 配列
    1. 7.3.2.1 1 次元配列
        (プログラム例7.10) 1 次元配列の受け渡し
    2. (演習問題:1 次元配列)
    3. 7.3.2.2 2 次元以上の配列
        (プログラム例7.11) 2 次元配列の受け渡し(方法1)
        (プログラム例7.12) 2 次元配列の受け渡し(方法2)
        (プログラム例7.13) 2 次元配列の受け渡し(方法3)
    4. (演習問題:2 次元配列)
  4. 7.3.3 関数名
      (プログラム例7.14) 関数名の受け渡し(ニュートン法)
      (プログラム例7.15) 関数名の配列
  5. (演習問題:関数名)
  6. 7.3.4 参照渡し
      (プログラム例7.16) 参照渡し
      (プログラム例7.17) 参照渡し(参照型関数)

7.3 データの受け渡し

7.3.1 データとアドレス

  関数間で情報を受け渡す方法は,大きく分けて,二つあります.一つは,7.1 節で述べたように,引数を利用する方法です.あと一つは,7.2 節で述べた変数の記憶クラスを利用する方法です.しかし,この方法を多用すると,関数の独立性が失われ,後にプログラムの修正をしなければならないようなとき,1 つの関数の修正が他の多くの関数に影響を及ぼすようなことが発生する可能性があります.従って,読み易さの範囲で,できるだけ引数を利用する方法を使った方がよいと思います.

  最も基本的な関数は,7.1 節で述べたように,引数を渡して,1 つの結果を呼び出した側に返します.引数として与えられた変数(定数)は,相手側にそのコピーが受け渡されるだけであり,呼び出された関数内でその値を変更して,その結果を引数を通して呼び出した関数に返すことはできません.つまり,関数の実行結果として得られるのは戻り値として設定された値だけです.しかし,場合によっては,関数から複数の結果を返してもらいたいような場合が起こります.その一つの解決方法は,変数のアドレスを利用する方法です.例えば,

01	{
02		 ・・・
03		int a = 1;
04		int b = 5;
05		n = sub(a, &b);
06		 ・・・
07	}
08	int sub(int a, int *c)   // c には b のアドレスのコピー
09	{
10		 ・・・
11		*c = 10;   // 4 行目の b の値を変更
12		 ・・・
13		a = 100;   // 3 行目の a の値は変化しない
14		 ・・・
15	}
		

のようにすると,5 行目の関数呼び出しによって,変数 b のアドレスが関数 sub の変数 c にコピーされます(右図参照).変数 c の値は b のアドレスと同じですので,その指し示す位置は変数 b そのものになります.従って,11 行目に示すように,間接演算子を使用して,変数 c の指し示す場所の値を変更すれば,変数 b の値が変更されます.この結果,関数 sub を呼び出した後,4 行目の変数 b の値は 10 に変化します.scanf においてアドレス演算子 & を使用する必要があるのも,指定した変数の値を入力値によって変更したいからです.

(プログラム例7.8) 複数結果の受け渡し ( A ~ E の部分を可能な限り一つの変数,定数,演算子等で,埋めてください.その際,文字を削除してから,正しい答えを半角文字で,かつ,余分なスペースを入れないで入力してください.)

  関数 sub ( main と異なるファイル)は,3 つのデータ a,b,及び,n を受け取り,(a + b) / n の計算をしています.関数 sub に必要なデータを extern 宣言と引数を利用して渡しています.n が 0 でない場合は除算を実行し,0 を戻り値として返します.n が 0 の場合は 1 を戻り値として返すと共に,n の値を 100 に設定し,除算を実行しません.また,除算の結果はそのアドレスが渡された変数 res に入れて返しています.

  関数 sub 内の変数 n は,extern 宣言されているので,main 関数の上で宣言されている変数 n と同じものになり,値を変更できます.ただし,あくまで,例として行っているのであり,一般的には引数として渡すべきです.

  なお,変数 ind は,main 及び sub で使用されていますが,名前は同じでも異なる変数であり,関数 sub から return 文で返されない限り main の変数 ind には値が入らないことに注意して下さい.

  このプログラムを実行し,変数 a,b,n の値として,2,4,3,及び,2,4,0 を与えたときの結果は,それぞれ以下のようになります.
n 3 result 2
n = 0 (n 100)
		
  上のプログラムにおいては,除算の結果に対してだけ,アドレスを使って受け渡しをしましたが,変数 ind に対してもアドレスを使用することができます.

/****************************/
/* 複数結果の受け渡し       */
/*      coded by Y.Suganuma */
/****************************/
#include <stdio.h>

void sub(int, int, int *, int *);

int n;	/* 以下の関数にすべて有効 */

int main()
{
	int a, b, res, ind;
/*
	 データの入力
*/
	printf("a,b,及び,nの値を入力して下さい ");
	scanf("%d %d %d", &a, &b, &n);
/*
	 関数の呼び出し
*/
	sub(a, b, &res, &ind);
/*
	 結果の出力
*/
	if (ind == 0)
		printf("n %d result %d", n, res);
	else
		printf("n = 0 (n %d)\n", n);

	return 0;
}
/**************************/
/* (a+b)/nの計算          */
/*      a,b : データ      */
/*      res : 計算結果    */
/*      ind : =0 : normal */
/*            =1 : n = 0  */
/**************************/
void sub(int a, int b, int *res, int *ind)
{
	extern int n;

	if (n == 0) {
		*ind = 1;
		n    = 100;
	}
	else {
		*ind = 0;
		*res = (a + b) / n;
	}
}
		

  また,複数結果を受け取る方法として,6.4.2 節で述べた new 演算子を使用して結果を保存する領域を確保し,その領域に対するポインタを返す方法があります.以下に示すプログラムは,上記と同じ内容をこの方法で書いたものです.

/****************************/
/* 複数結果の受け渡し       */
/*      coded by Y.Suganuma */
/****************************/
#include <stdio.h>

int *sub(int, int);

int n;	/* 以下の関数にすべて有効 */

int main()
{
/*
	 データの入力
*/
	int a, b;
	printf("a,b,及び,nの値を入力して下さい ");
	scanf("%d %d %d", &a, &b, &n);
/*
	 関数の呼び出し
*/
	int *res = sub(a, b);   /* resに結果が入る */
/*
	 結果の出力
*/
	if (res[0] == 0)
		printf("n %d result %d", n, res[1]);
	else
		printf("n = 0 (n %d)\n", n);

	delete [] res;

	return 0;
}
/**************************************/
/* (a+b)/nの計算                      */
/*      a,b : データ                  */
/*      return : res[0] : =0 : normal */
/*                        =1 : n = 0  */
/*               res[1] : 計算結果    */
/**************************************/
int *sub(int a, int b)
{
	extern int n;
	int *res = new int [2];

	if (n == 0) {
		res[0] = 1;
		n      = 100;
	}
	else {
		res[0] = 0;
		res[1] = (a + b) / n;
	}

	return res;
}
		
  さらに,次節で述べるように,2 つの要素を持つ 1 次元配列 res[2] を定義し,この配列を介してデータを受け渡すことも可能です.勿論,new 演算子を使用して配列を定義しても構いません.

/****************************/
/* 複数結果の受け渡し       */
/*      coded by Y.Suganuma */
/****************************/
#include <stdio.h>

void sub(int, int, int *);

int n;	/* 以下の関数にすべて有効 */

int main()
{
/*
	 データの入力
*/
	int a, b, res[2];
	printf("a,b,及び,nの値を入力して下さい ");
	scanf("%d %d %d", &a, &b, &n);
/*
	 関数の呼び出し
*/
	sub(a, b, res);
/*
	 結果の出力
*/
	if (res[0] == 0)
		printf("n %d result %d", n, res[1]);
	else
		printf("n = 0 (n %d)\n", n);

	return 0;
}
/***********************************/
/* (a+b)/nの計算                   */
/*      a,b : データ               */
/*      res : res[0] : =0 : normal */
/*                     =1 : n = 0  */
/*            res[1] : 計算結果    */
/***********************************/
void sub(int a, int b, int *res)   // void sub(int a, int b, int res[]) でも可
{
	extern int n;

	if (n == 0) {
		res[0] = 1;
		n      = 100;
	}
	else {
		res[0] = 0;
		res[1] = (a + b) / n;
	}
}
		

  C++ の場合,関数の宣言または定義中に,関数の呼び出しで引数が省略されたときのデフォルト値default value )を設定しておくことができます.例えば,
int func(int, int = 5, char * = "test");
		
と宣言しておくことにより,
func(x)
func(x, 10);
		
は,それぞれ,
func(x, 5, "test")
func(x, 10, "test");
		
と解釈されます.

  デフォルト引数は引数の後ろから順に設定でき,中間の引数をデフォルト引数に設定したり,また,関数を呼び出すとき,中間の引数を省略することはできません.

(プログラム例7.9) デフォルト引数

  この例では,加算する一つの値と出力先にデフォルト値( 5 と標準出力)を設定しています.関数本体を 07 行目の位置に記述する場合は,26 行目に示すように記述する必要があります.

01	/****************************/
02	/* デフォルト引数           */
03	/*      coded by Y.Suganuma */
04	/****************************/
05	#include <stdio.h>
06
07	void sub(int, int = 5, FILE * = stdout);
08
09	int main()
10	{
11		sub(10);   // sub(10, 5, stdout)と解釈,標準出力
12		sub(10, 10);   // sub(10, 10, stdout)と解釈,標準出力
13		sub(10, 20, stdout);   // 標準出力
14		FILE *out = fopen("test.txt", "w");
15		sub(10, 30, out);   // ファイル test.txt へ出力
16		fclose(out);
17		return 0;
18	}
19	
20	/*********************************/
21	/* 整数の足し算                  */
22	/*      x,y : データ             */
23	/*      fp : 出力先              */
24	/*********************************/
25	void sub(int x, int y, FILE *fp)
26	//void sub(int x, int y = 5, FILE *fp = stdout)
27	{
28		fprintf(fp, "結果は=%d\n", x+y);
29	}
		

-------------------------演習問題開始-------------------------

  次の演習問題は,return 文,及び,アドレスを渡すことによって複数結果を受け取る例です.

-------------------------演習問題終了-------------------------

7.3.2 配列

7.3.2.1 1 次元配列

  多量のデータを引数として渡したい場合,最も簡単な方法はそれらのデータを配列に入れて引き渡すことです.先の節で簡単に述べましたが,この節では,配列を引数とする場合についてより詳細に考えてみます.

  1 次元配列の場合は簡単です.例えば,以下のような方法によって行われます.
{
	int b[5];
	  ・・・
	n = sub(・・・, b, ・・・);
	  ・・・
}
int sub(・・・, int c[], ・・・)   // int sub(・・・, int *c, ・・・) でも可
{
	  ・・・
}
		
このように記述することにより,呼び出した側の配列 b と関数 sub 内の配列 c は同じものになり,どちらでも,配列データの参照・修正が可能になります.また,関数側で,配列の添え字を書く必要はありません.先に述べましたように,変数 c は実質上ポインタですので,関数 sub の定義を
int sub(・・・, int *c, ・・・)
		
と書いても構いません.

(プログラム例7.10) 1 次元配列の受け渡し ( A ~ D の部分を可能な限り一つの変数,定数,演算子等で,埋めてください.その際,文字を削除してから,正しい答えを半角文字で,かつ,余分なスペースを入れないで入力してください.)

  関数 wa は,n 次元ベクトルの和を計算するためのものです.この例では,行列( 2 次元配列 a )の各行を 1 つのベクトルとみなし,それらの和を計算しています.&(a[0][0]),及び,&(a[1][0]) ( a[0],及び,a[1] でも可)という記述は,配列変数 a の 1 行目,及び,2 行目の先頭アドレスを示しています.このような方法により,配列の一部を参照することも可能です.

  このプログラムを実行すると,以下に示すような結果が得られます.
5 7 9
		

-------------------------演習問題開始-------------------------

  次の演習問題は,1 次元配列を引数と渡し,また,場合によっては,結果も配列として受け取る必要があるような場合に対応しています.

-------------------------演習問題終了-------------------------

7.3.2.2 2 次元以上の配列

  2 次元の配列を受け渡す場合も,基本的には,1 次元の場合と同様です.しかし,受け渡される関数の側で,列の数を必ず記述しなければなりません.3 次元以上の場合も同様に,最初の添え字(次元)だけは省略できますが,後の次元はすべて記述してやる必要があります.例えば,配列 c が
int c[4][10];
		
のように 2 次元配列として記述してあったとします.このとき,関数 sub 側では,
int sub(・・・, int c[][10], ・・・)
		
または,
int sub(・・・, int (*c)[10], ・・・)
		
と書く必要があります.2 番目の方法において,(*c)[10] を *c[10] と書かないように注意して下さい.後者は,10 個のポインタの配列を意味します.

  このように,多次元配列の場合,添え字の一部を必ず記述しなければならないため,もし,呼び出す側で配列のサイズを変更すると,呼び出される側の関数においても修正が必要になります.このようなことを避ける方法については,プログラム例7.12 以降を参照して下さい.

(プログラム例7.11) 2 次元配列の受け渡し(方法 1 ) ( A ~ G の部分を可能な限り一つの変数,定数,演算子等で,埋めてください.その際,文字を削除してから,正しい答えを半角文字で,かつ,余分なスペースを入れないで入力してください.)

  関数 seki は,n 行 m 列の行列と m 行 l 列の行列の積を計算するためのものです.なお,行列 A ( n 行 m 列)と B ( m 行 l 列)の行列のかけ算の定義は以下の通りです.
C = AB, cij = Σkaikbkj  k = 1, ・・・, m
		
ただし,cij 等は,行列 C 等の i 行 j 列要素とします.

  このプログラムを実行すると,以下に示すような結果が得られます.
1.000000 2.000000
4.000000 5.000000
		
(プログラム例7.12) 2 次元配列の受け渡し(方法 2 ) ( A ~ I の部分を可能な限り一つの変数,定数,演算子等で,埋めてください.その際,文字を削除してから,正しい答えを半角文字で,かつ,余分なスペースを入れないで入力してください.)

  new 演算子( malloc 等でも構いません)を使用して配列を定義し,先の例の関数をポインタを使用した記述になおしたものです.このようにすれば,配列の大きさが変化しても,次の例と同様,関数自身を修正する必要がありません.

(プログラム例7.13) 2 次元配列の受け渡し(方法 3 ) ( A ~ C の部分を可能な限り一つの変数,定数,演算子等で,埋めてください.その際,文字を削除してから,正しい答えを半角文字で,かつ,余分なスペースを入れないで入力してください.)

  プログラム例 7.11 の関数 seki は,各行列の列の数が,プログラム内に記述してある値より小さいときは,main 関数を書き直すだけで使用できますが,それ以上の値の場合は,main 関数だけでなく,関数 seki も書き換える必要があります.

  しかし,プログラム例 7.10 の関数 wa は,1 次元配列を使用しているが故に,ベクトルの次元がどのように変化しても修正すること無しに使用可能です.そこで,6.3 節で説明したように,2 次元配列であっても連続した領域に確保されていますので,その並び方に注意しさえすれば,1 次元配列として取り扱うことができるという性質を利用して書き直したのが下のプログラムです.このプログラムの関数 seki は,任意の n,m,及び,l の値に対して修正無しで使用可能となっています.

-------------------------演習問題開始-------------------------

  次の演習問題は,2 次元配列を引数と渡し,また,場合によっては,結果も 2 次元配列として受け取る必要があるような場合に対応しています.

-------------------------演習問題終了-------------------------

7.3.3 関数名

  場合によっては,関数名を引数としたい場合があります.例えば,非線形方程式の根を求める関数を作成したいとします.また,根を求めるアルゴリズム自体は,方程式が異なっても変わらないとします.このようなとき,方程式の計算を別の関数で行い,その関数名を根を求める関数に受け渡すように作成すれば,方程式が変わっても,同じ根を求める関数を利用できます.

  関数名を引数とするには,基本的に,関数のアドレスを使用すれば可能です.例えば,
int (*sub)(double, char *)
		
という記述は,sub が,double 及び char に対するポインタという 2 つの引数をもち,int を返す関数へのアドレスであることを表しています.

(プログラム例7.14) 関数名の受け渡し(ニュートン法) ( A ~ D の部分を可能な限り一つの変数,定数,演算子等で,埋めてください.その際,文字を削除してから,正しい答えを半角文字で,かつ,余分なスペースを入れないで入力してください.)

  次のプログラムの関数 newton は,ニュートン法により非線形方程式 f(x) = 0 (この例では,ex - 3x = 0 )の解を求めるためのものです.f(x) 及び f(x) の微分を計算する関数名( snx と dsnx )を引数としています.ただし,ニュートン法とは,関数 f(x) が単調連続で変曲点が無く,かつ,微分可能であるとき利用できる方法であり,根の適当な初期値 x0 から始めて,反復公式
xn+1 = xn - f(xn) / f'(xn)
		
を繰り返すことによって,非線形方程式の解を求める方法です.

  このプログラムを実行すると,以下に示すような結果が得られます.
ind=5  x=0.619061  f=0.000000  df=-1.142816
		
(プログラム例7.15) 関数名の配列

  次のプログラムでは,関数名(関数に対するアドレス)を配列に入れ,条件によって異なる関数を呼び出しています.

  上のプログラムを実行すると下のような結果が得られます.A ~ D の空白部分を埋めてください.その際,文字を削除してから,正しい答えを半角文字で,かつ,余分なスペースを入れないで入力してください.

-------------------------演習問題開始-------------------------

  最初の演習問題は,様々なデータを引数として渡す場合の例です.送る側,及び,受け取る側のプログラムを十分理解して空白を埋めてください.また,2 番目の問題は,関数名を引数として送る場合の例です.このようにしておけば,ascend,または,descend の内容を変えるだけで,同じバブルソートの関数を使用し,様々な順番で並べ替えを実行することができます.

-------------------------演習問題終了-------------------------

7.3.4 参照渡し

  参照型変数の宣言方法は以下の通りです.
データ型 &別名 = 式;
		
例えば,
int &y = x;
		
と宣言することにより,参照型変数 y は変数 x の別名としてふるまい,
x = 10;
y = 10;
		
の 2 つの文は全く同じ意味になります.

  関数への引数を参照型(参照渡し,call by reference )にすることが可能です.この機能を利用することにより,呼ばれた側の関数で,呼んだ側と同様の記述が可能になります.また,定数や式を参照することも可能です.C++ コンパイラは,一時変数に定数や式の値をおさめ,この一時変数を参照の実体とします.この機能により,参照型の引数の位置に定数や式を書いても,関数側で特別な処理をしなくても良いわけです.さらに,参照を返す関数を作ることも可能です.詳しくは,以下に述べるプログラム例を見て下さい.

  単純変数( int や double 等)に対して,値渡しの代わりに参照渡しを利用することにはそれほどのメリットを感じませんが,後の述べる構造体やクラスのオブジェクトを引数として渡したいときには意味を持ってきます.非常に大きな構造体やクラスオブジェクトに対して値渡しをすれば,そのコピーを作成するために大きな領域や時間を必要とすると共に,クラスオブジェクトの場合にはコンストラクタやデストラクタが呼ばれます.参照渡しによって,これらのオーバーヘッドを避けることが可能になります.もちろん,ポインタで渡すことも可能ですが,変数や関数を参照するための記述が多少見にくくなります.

  参照渡しをすれば,関数内で値の修正が可能となりますが,もし修正を許さないならば,その引数に対して,
const
		
の指定をしておくべきです.

(プログラム例7.16) 参照渡し

  上のプログラムを実行すると下のような結果が得られます.A ~ O の空白部分を埋めてください.その際,文字を削除してから,正しい答えを半角文字で,かつ,余分なスペースを入れないで入力してください.

  この結果からも明らかなように,通常の引き渡し( y と data2 )では,値がコピーされて渡されるだけ(配列の場合は,アドレスのコピー)ですので渡された内容自身を呼ばれた関数側で変えることができません.しかし,参照渡しですと,別名ですが,変数それ自身と同じものです.従って,関数を呼んだ側と同じ処理で,その内容も変更されます.データでなくアドレスで渡した変数 z(関数側では c )との記述方法の違いにも注意して下さい.

(プログラム例7.17) 参照渡し(参照型関数)

  以下では,参照を返す関数(参照型関数)を使用しています.この場合,関数自身が return される変数の別名になっています.従って,関数を代入演算子の右辺にも左辺にも書くことができます.

  このプログラムを実行した結果は,以下のようになります.A ~ F の空白部分を埋めてください.その際,文字を削除してから,正しい答えを半角文字で,かつ,余分なスペースを入れないで入力してください.

静岡理工科大学 菅沼ホーム 本文目次 演習まとめ 付録 索引