静岡理工科大学 菅沼ホーム 全体目次 演習解答例 付録 索引

第7章 関数

  1. 7.0 標準関数
    1. 7.0.1 scanf と printf
    2. 7.0.2 文字列操作
      1. (プログラム例 7.0 ) 文字列操作
  2. 7.1 簡単な関数
    1. (プログラム例 7.1 ) 階乗の計算
    2. (プログラム例 7.2 ) 階乗の計算(関数の利用)
    3. (プログラム例 7.3 ) 階乗の計算(再帰呼び出し)
    4. (プログラム例 7.4 ) nrの計算
  3. 7.2 変数の有効範囲(スコープ)
    1. 7.2.1 型宣言
    2. 7.2.2 有効範囲(スコープ)
      1. (プログラム例 7.5 ) 変数の有効範囲(同じファイル)
      2. (プログラム例 7.6 ) 変数の有効範囲(別々のファイル)
      3. (プログラム例 7.7 ) 変数の有効範囲(C++)
    3. 7.2.3 名前空間( namespace )(C++)
      1. (プログラム例 7.56 ) 名前空間
  4. 7.3 データの受け渡し
    1. 7.3.1 データとアドレス
      1. (プログラム例 7.8 ) 複数結果の受け渡し
      2. (プログラム例 7.9 ) デフォルト引数(C++)
    2. 7.3.2 配列
      1. 7.3.2.1 1 次元配列
        1. (プログラム例 7.10 ) 1 次元配列の受け渡し
      2. 7.3.2.2 2 次元以上の配列
        1. (プログラム例 7.11 ) 2 次元配列の受け渡し(方法 1)
        2. (プログラム例 7.12 ) 2 次元配列の受け渡し(方法 2)
        3. (プログラム例 7.13 ) 2 次元配列の受け渡し(方法 3)
    3. 7.3.3 関数名
      1. (プログラム例 7.14 ) 関数名の受け渡し(ニュートン法)
      2. (プログラム例 7.15 ) 関数名の配列
    4. 7.3.4 参照渡し(C++)
      1. (プログラム例 7.16 ) 参照渡し
      2. (プログラム例 7.17 ) 参照渡し(参照型関数)
  5. 7.4 main 関数
    1. (プログラム例 7.18 ) main 関数の引数(数字の和)
    2. (プログラム例 7.19 ) main 関数の引数(環境変数の出力)
  6. 7.5 その他(C++)
    1. 7.5.1 関数名のオーバーロード
      1. (プログラム例 7.20 ) 関数名のオーバーロード
    2. 7.5.2 インライン関数(Javaを除く)
      1. (プログラム例 7.21 ) インライン関数と #define マクロ
    3. 7.5.3 例外処理
      1. (プログラム例 7.22 ) 例外処理
  7. 7.6 様々な例題
    1. 7.6.1 数値計算
    2. (プログラム例 7.23 ) 連立線形方程式,逆行列(ガウス・ジョルダン)
    3. (プログラム例 7.28 ) 非線形方程式(二分法)
    4. (プログラム例 7.29 ) 非線形方程式(セカント法)
    5. (プログラム例 7.63 ) 非線形方程式(ニュートン法)
    6. (プログラム例 7.40 ) 代数方程式(ベアストウ)
    7. (プログラム例 7.43 ) 行列の固有値(フレーム法+ベアストウ法)
    8. (プログラム例 7.44 ) 実対称行列の固有値・固有ベクトル(ヤコビ法)
    9. (プログラム例 7.45 ) 最大固有値と固有ベクトル(べき乗法)
    10. (プログラム例 7.25 ) 数値積分(台形則)
    11. (プログラム例 7.26 ) 数値積分(シンプソン則)
    12. (プログラム例 7.27 ) 微分方程式(ルンゲ・クッタ)
    13. (プログラム例 7.41 ) 補間法(ラグランジュ)
    14. (プログラム例 7.42 ) 補間法(スプライン)
    15. (プログラム例 7.46 ) 補間法(ベジエ曲線)
    16. 7.6.2 最適化
    17. (プログラム例 7.61 ) 最適化(線形計画法)
    18. (プログラム例 7.30 ) 最適化(黄金分割法)
    19. (プログラム例 7.60 ) 最適化(多項式近似法)
    20. (プログラム例 7.31 ) 最適化(最急降下法)
    21. (プログラム例 7.47 ) 最適化(共役勾配法)
    22. (プログラム例 7.48 ) 最適化( Newton 法)
    23. (プログラム例 7.49 ) 最適化(準 Newton 法)
    24. (プログラム例 7.59 ) 最適化(シンプレックス法)
    25. (プログラム例 7.50 ) 最適化(動的計画法)
    26. 7.6.3 確率と統計
    27. (プログラム例 7.32 ) ガンマ関数
    28. (プログラム例 7.64 ) 二項分布
    29. (プログラム例 7.65 ) ポアソン分布
    30. (プログラム例 7.66 ) 一様分布
    31. (プログラム例 7.67 ) 指数分布
    32. (プログラム例 7.33 ) 正規分布
    33. (プログラム例 7.34 ) χ2 分布
    34. (プログラム例 7.35 ) t 分布
    35. (プログラム例 7.36 ) F 分布
    36. (プログラム例 7.57 ) Fisher の直接確率
    37. (プログラム例 7.37 ) 乱数の発生
    38. 7.6.4 多変量解析
    39. (プログラム例 7.24 ) 最小二乗法
    40. (プログラム例 7.51 ) 重回帰分析
    41. (プログラム例 7.52 ) 正準相関分析
    42. (プログラム例 7.53 ) 主成分分析
    43. (プログラム例 7.54 ) 因子分析
    44. (プログラム例 7.55 ) クラスター分析
    45. (プログラム例 7.62 ) 分散分析
    46. 7.6.5 その他
    47. (プログラム例 7.38 ) ソート(並べ替え)
    48. (プログラム例 7.39 ) 基本アルゴリズム(その1)
  8. 演習問題7

7.0 標準関数

  C/C++ のプログラムは関数( function )を基本としています.今まで書いてきたプログラムも main 関数という関数の一種です.ただ,main 関数は,その名前も main と決まっていますし,プログラム内に 1 つだけ必ず存在しなければなりません.しかし,他の関数に対しては,変数名と同じように任意の名前を付けられますし,また,複数存在しても構いません.ただし,同じ名前の関数が複数存在することは許されません( C++ の場合は可能です − 7.5.1 節参照 −).

  関数は,数学の関数と同じように,データを与えると,その結果を返してくれます.しかし,数学の関数の場合は,単に値を返すだけですが,C/C++ の関数の場合は,データを返すことはもちろん,それ以外の複雑な作業の実行も可能です.今までの例で使用した printf,scanf,sqrt 等はシステムが用意している関数(標準関数付録参照−)です.我々は,これらの関数を利用して様々なプログラムを書くことができます.

  たとえば,変数 x に入っている値の平方根を計算したいときは,
	y = sqrt(x);
		
と書くだけで,変数 y に x の平方根が代入されます.付録の中の「sqrt」という関数に関する説明の[形式]の中に,
	#include <math.h>
	double sqrt(double x)
		x : 倍精度浮動小数点数
		
という部分があります.最初の行は,この関数を使用するためには,「math.h」というヘッダファイルをインクルードしなければならないということを意味しています.

  sqrt の前に書かれた「double」は,この関数が結果として double 型の値を返すこと,また,括弧の中の double は,計算に使用するデータ(引数)は double 型でなければならないということを意味しています.

  従って,ある値の平方根を計算するためには,以下のような記述をすれば良いことになります.
	double x = 3.14, y;
	y = sqrt(x);   // y = sqrt(3.14) としても良い
		
  標準関数としては,以下に示すように,様々な種類のものが準備されています.この節では,入出力関数の中の scanf と printf,及び,主な文字列操作関数について簡単に説明しておきます.

  1. 入出力関係  入出力.

  2. 数学関係の関数  三角関数,逆三角関数,対数関数,指数関数,双曲線関数,逆走曲線関数,べき乗等

  3. 乱数  乱数の生成等

  4. 時間関係の関数  現在時刻,経過時間等

  5. 文字・文字列関数 文字列の探索,コピー,結合,変換等

  6. メモリ領域の確保  メモリの動的確保

  7. バッファ関係  バッファ操作

  8. ファイルとディレクトリ操作  ファイルとディレクトリの生成,削除,コピー等

  9. ネットワーク  ネットワーク通信

  10. その他  ソート,探索,プロセスの生成・消滅,コマンドの実行,可変個引数関数の作成等
7.0.1 scanf と printf

  個々の関数に対する説明は付録を参照してもらうとして,ここでは,scanfprintf の説明だけを多少詳しく行っておきます.なお,Java においても,printf 関数(メソッド)が存在し,かつ,似たような機能を有しています(Console クラスPrintStream クラスPrintWriter クラスFormatter クラス参照)が,scanf に対応するメソッドは存在しません.

  標準入出力装置(一般的には,キーボードとディスプレイ)に対して入出力を行う場合,対象とするものは文字列です.キーボードから入力する場合,たとえ数値であっても,それを例えば「123.45」のように,我々が読むことができる文字列として入力します.しかし,数値データを,入力された文字列としてそのまま記憶したならば,演算等を行うことができません.そのため,入力された文字列をコンピュータ内部で使用する表現方法に変換して記憶してやる必要があります.例えば,「123.45」という文字列を数値として扱いたければ,浮動小数点表現に変換して記憶しておく必要があります.もちろん,「123.45」を文字列として内部的に扱いたければ,文字列として記憶しておく必要があります.

  また,ディスプレイに出力する場合も,メモリ(変数)に記憶されているデータを我々が解釈しやすい文字列に変換して出力します.たとえば,整数 3 の内部表現である 00 … 011 がそのまま出力されれば(そのまま出力する方法もある),我々自身がその結果を解釈しなければなりません.

  C++ における std::cin や std::cout を使用する場合は,これらの変換を自動的に行ってくれますが,scnaf や printf を使用する場合,これらの変換方法を指定してやる必要があります.

  例えば,scanf は,以下のようにして使用します.
	double d_data;
	int i_data;
	char c_data[10];
	scanf("%lf %d %s", &d_data, &i_data, c_data);
		
「"%lf %d %s"」の部分が,入力されたデータをどのように変換するかを指定する部分です.「%」に続く文字列が 3 つありますので,3 つのデータが入力されること,また,「%」に続く文字列間のスペースは,各データがスペースで区切られていることを意味します.入力された文字列に対して,「%lf」は double 型( float 型は %f,long double 型は %Lf )の浮動小数点表示に変換して,「%d」は int 型( long int 型は %ld であるが,int 型と long int 型のサイズが等しいときはいずれも %d とする.また,long long int 型は %lld となる.)の整数に変換して,また,「%s」は文字列としてそのまま,記憶することを意味しています.これら 3 つの「%」で始まる文字列は,次に続く 3 つのデータに順番に対応していますので,この scanf 関数に対して,例えば,
	3.141592654 123 abc
		
または,
	3.141592654
	123
	abc
		
のように入力すると(改行も,スペースと同じ入力データに対する区切り文字として解釈される),d_data に「3.141592654」が double 型に変換され,i_data に「123」が int 型に変換され,また,c_data に文字列「abc」が,記憶されます.各変数に対して,そのアドレスを指定しなければならない点に注意してください.c_data にはアドレス演算子が付加されていませんが,c_data が配列,つまり,アドレスであるからです.

  printf は,scanf とは逆に,記憶されたデータを文字列に変換する操作を行います.例えば,以下のようにして使用します.
	double d_data;
	int i_data;
	char c_data[10];
	 ・・・・・
	printf("結果は %f %10.3f %d %5d %s %c\n", d_data, d_data, i_data, i_data, c_data, c_data[1]);
		
この結果,まず,「結果は 」という文字列が出力されます.printf においては,「%」で始まる文字列とエスケープシーケンス以外は,記述された内容がそのまま出力されます.次に,以下の順序で 6 つのデータが出力されます( % で始まる文字列が 1 つの半角スペースで区切られているため,各データ間には,1 つのスペースが入る).

  1. 「%f」は double 型( %Lf は long double 型)のデータを固定小数点表現の文字列に変換します.なお,1.23x10-3 のような表現方法に対応する 1.23e-03 という方法−浮動小数点表現−も存在します( %e,%Le).「%」と「f」の間に何も記述しなければ,全体の桁数や小数点以下の桁数はシステムの標準形式に従います.この場合,d_data の内容が,システムの標準形式に従って出力されます.

  2. 最初のデータと変換方法は同じですが,この場合は,出力形式を指定しています.d_data の内容が,小数点以下3桁,全体の桁数 10 桁で出力されます(例: △△△-12.345 ).桁数が 10 桁に満たない場合は,左側にスペースが挿入されます.全体の桁数を指定しない場合は,「10」の部分を省略しても構いません.

  3. 「%d」は int 型( long int 型は %ld であるが,int 型と long int 型のサイズが等しいときはいずれも %d とする.また,long long int 型は %lld となる.)のデータを文字列に変換します.「%」と「d」の間に何も記述しなければ,システムの標準形式に従って出力されます.この場合,i_data の内容が,システムの標準形式に従って出力されます.

  4. 上の i_data と変換方法は同じですが,この場合は,出力形式を指定しています.i_data の内容が,全体の桁数 5 桁で出力されます(例: △△-12 ).桁数が 5 桁に満たない場合は,左側にスペースが挿入されます.

  5. c_data の内容が文字列として出力されます.

  6. c_data の 2 番目の文字 b が出力されます.

  先に述べた scanf によって入力されたデータを,この printf で出力すると,その結果は以下のようになります.
	結果は 3.141593      3.142 123   123 abc b
		
7.0.2 文字列操作

  問題によっては,文字列を扱う場合も多くあります.以下に示す例で使用されている各関数について簡単に説明すると,以下のようになります.

  1. size_t strlen(const char *str)
    str : NULL 文字('\0')で終了する文字列
      文字列のバイト単位の長さを返します.この長さには NULL 文字('\0')は含まれません.

  2. char *strcpy(char *str1, const char *str2)
    str1 : コピー先の文字列
    str2 : コピー元の文字列
      ある文字列を別の文字列に NULL 文字('\0')を含めてコピーし,コピー先の文字列へのポインタを返します.これらの引数の文字列には,NULL 文字が入っているものと想定しています.

  3. char *strcmp(const char *str1, const char *str2)
    str1, str2 : 比較する文字列
      2 つの文字列を比較し,以下の結果を返します.これらの引数の文字列には,NULL 文字が入っているものと想定しています.
    • 負 : 文字列 1 が文字列 2 より小さい
    • 0 : 文字列 1 と文字列 2 は等しい
    • 正 : 文字列 1 が文字列 2 より大きい

  4. char *strcat(char *str1, const char *str2)
    str1 : 結合先の文字列
    str2 : 結合する文字列
      ある文字列に別の文字列を結合します.その結果の文字列の終端に NULL 文字('\0')を付加してから,連結された文字列へのポインタを返します.これらの引数の文字列には,NULL 文字('\0')が入っているものと想定しています.

  5. char *strstr(const char *str1, const char *str2)
    str1 : 文字列
    str2 : 探索する文字列
      文字列の中から指定された文字列を探索します.文字列が見つかった場合は,最初に見つかった位置へのポインタを返し,見つからなかった場合は,NULL を返します.

(プログラム例 7.0 ) 文字列操作 

/****************************/
/* 文字列操作の例           */
/*      coded by Y.Suganuma */
/****************************/
#include <stdio.h>
#include <string.h>

int main()
{
	char abc[] = "abc";
	char *defgh = "defgh";
	char emp[20] = "", *str;
	int i1, c1, c2, len1, len2, len3;
				// 現在の文字列の内容を出力
	printf("-abc- %s\n", abc);   // 文字列として出力
	printf("-abc- ");            // 1文字ずつ出力
	for (i1 = 0; i1 < 3; i1++)
		printf("%c", abc[i1]);
	printf("\n");
				// 文字列の長さ
	len1 = strlen(abc);
	len2 = strlen(defgh);
	len3 = strlen(emp);
 	printf("各文字列の長さ %d %d %d\n", len1, len2, len3);
				// 文字列のコピー
   	str  = strcpy(emp, abc);
   	len1 = strlen(emp);
   	printf("-str- %s -emp- %s 長さ %d\n", str, emp, len1);
				// 文字列の比較
   	c1 = strcmp(emp, abc);
   	c2 = strcmp(emp, defgh);
   	printf("-empとabc- %d -empとdefgh- %d\n", c1, c2);
				// 文字列の結合
   	str  = strcat(emp, " ");
   	str  = strcat(emp, defgh);
   	len1 = strlen(emp);
   	printf("-str- %s -emp- %s 長さ %d\n", str, emp, len1);
				// 文字列の検索
   	str  = strstr(emp, "ef");
   	len1 = strlen(str);
   	printf("-str- %s 長さ %d -emp- %s\n", str, len1, emp);

   	return 0;
}
		

  上のプログラムを実行すると,以下のような出力が得られます.
	-abc- abc
	-abc- abc
	各文字列の長さ 3 5 0
	-str- abc -emp- abc 長さ 3
	-empとabc- 0 -empとdefgh- -1
	-str- abc defgh -emp- abc defgh 長さ 9
	-str- efgh 長さ 4 -emp- abc defgh
		
7.1 簡単な関数

  関数は,自分自身で作成することもできます.関数の一般的な定義方法は以下の通りです.
	関数の型 関数名 ( 引数リスト )
		
ここで,引数とは,関数を呼び出した側と関数との間で,情報の受け渡しに使用される変数や定数のことです.また,関数は計算した結果を何らかの値として戻しますが,関数の型とはその値の型のことです.詳細については,以下の例を参照して下さい.

  プログラムの複数の場所で同じことを繰り返す場合や,プログラムが長くなりすぎる場合は,関数を積極的に利用することによって,わかりやすいプログラムを書くことが可能です.まず,この節では,簡単な例によって,関数の基本的な概念について説明します.

---------------------(C++)関数宣言-------------------------

C++ の関数・関数宣言は,C の場合と以下のような点で多少異なります.

  1. 関数を使用する前に必ず宣言(または,定義)しなければなりません.
  2. 宣言 func() は引数が無いことを意味しています.ただし,func(void) も許されます.
  3. 引数をチェックして欲しくないときは明示的に func(...) と書きます.例えば,
    func(int x, double data ...)
    と記述することにより,引数の内最初の 2 つの引数だけをチェックさせることが可能です.特に,可変個数の引数を持つ関数の定義等に使用されます.
  4. 引数が省略された場合のデフォルト値を設定できます(後述).
  5. 引数の参照渡しが可能です(後述).
  6. 関数名のオーバーロード(多重定義)が可能です(後述).
  7. インライン関数を定義できます(後述).

----------------------(C++)関数宣言終わり--------------------

(プログラム例 7.1 ) 階乗の計算 

  この例は,入力されたデータ n (整数)の階乗を計算するためのプログラムです.今までのプログラムと同じように,標準関数を除いて,main 関数だけを使用して書いています.階乗の値は,通常,非常に大きくなるため,double 型で計算しています.

/****************************/
/* nの階乗の計算           */
/*      coded by Y.Suganuma */
/****************************/
#include <stdio.h>

int main()
{
	double kai;
	int i1, n;
/*
	 データの入力
*/
	printf("nの値を入力して下さい ");
	scanf("%d", &n);
/*
	 階乗の計算
*/
	kai = 1.0;			   /* 初期設定 */
	for (i1 = 1; i1 <= n; i1++)
		kai *= (double)i1;
/*
	 結果の出力
*/
	printf("   %dの階乗は=%f\n", n, kai);

	return 0;
}
		

(プログラム例 7.2 ) 階乗の計算(関数の利用) 

  このプログラムでは,プログラム例 7.1 と同じ内容の計算を,main 関数から,階乗を計算するための関数 kaijo を呼び出して行っています.以下の説明により,関数の基本的事項を理解して下さい.なお,この例においては,main と kaijo を同じファイルに記述されたものとして説明していますが,kaijo( 30 行目以降)を別のファイルに記述することも可能です(以下の例においても同様).

01	/****************************/
02	/* nの階乗の計算           */
03	/*      coded by Y.Suganuma */
04	/****************************/
05	#include <stdio.h>
06
07	double kaijo(int);
08
09	int main()
10	{
11		double kai;
12		int n;
13	/*
14	     データの入力
15	*/
16		printf("nの値を入力して下さい ");
17		scanf("%d", &n);
18	/*
19	     階乗の計算
20	*/
21		kai = kaijo(n);
22	/*
23	     結果の出力
24	*/
25		printf("   %dの階乗は=%f\n",n,kai);
26
27		return 0;
28	}
29
30	/**************************/
31	/* mの階乗               */
32	/*      m : データ        */
33	/*      return : mの階乗 */
34	/**************************/
35	double kaijo(int m)
36	{
37		double s;
38		int i1;
39
40		s = 1.0;
41		for (i1 = 1; i1 <= m; i1++)
42			s *= (double)i1;
43
44		return s;
45	}
		
7 行目

  関数の宣言をしています.この例では,関数 kaijo が double 型の値を返し,また,int 型の値を 1 つ関数に引き渡すことを宣言しています.関数を異なるファイルに記述したり,この例のように,同じファイル内であっても,関数を呼び出している箇所( 21 行目)より前に関数本体の定義が記述されていない場合は,必ずこの行が必要になります.したがって,30 行目以降の関数本体をこの場所に記述すれば,この行は必要なくなります.

  変数と同じように,関数における処理の結果に対しても様々な型が存在します.従って,関数毎にその関数が返す値の型を宣言してやる必要があります.もし,値を返さない場合は void と宣言する必要があります.

  基本的に,各関数は,全く別のプログラムであると考えた方が理解しやすいと思います.従って,関数を呼び出した側の情報を,呼び出された関数に伝えるには何らかの処理が必要です.例えば,階乗を計算する場合,階乗の対象となる n 自身の値が不明であれば,計算不可能です.情報を伝える 1 つの方法が引数です.この宣言では,int 型の引数( n )を 1 つ渡すことを宣言しています.もちろん,引き渡す情報を必要としなければ,引数も必要ありませんし,また,複数の引数を渡すことも可能です.

21 行目

  関数 kaijo にデータ n を渡し,関数を呼び出し,その結果を変数 kai に代入しています.この行は,25 行目と一緒にして,次のようにも書けます.
	printf("   %dの階乗は=%f\n", n, kaijo(n));
			
30 〜 34 行目

  30 から 45 行目までが,関数 kaijo の本体(関数の定義)です.これらの行は,関数の機能についての注釈です.このように,少なくとも,関数の機能,引数の意味,返す値に対する説明は必ず書いておいて下さい.

35 行目

  各関数定義の最初に記述する文です.返す値や引数の型は,必ず,7 行目の宣言や 21 行目の呼び出し時と一致していなければなりません.しかし,この例のように,引数の名前は呼び出し時と異なっても構いません(もちろん,同じでも構いません).main 関数における n の値が関数 kaijo の m にコピーされて関数が実行されます.

  しかし,その値のコピーが渡されるため,関数内で m の値を変更しても,main 関数内の n は全く影響を受けません.また,関数 kaijo で main 関数と同じ変数名 n を使用していても,main の変数 n と kaijo の変数 n とは異なる変数です.従って,もし関数 kaijo 内で変数 n の値を変更しても,main の変数 n の値は変化しません.

  関数内で使用している他の変数についても同様です.ある関数と別の関数において,同じ名前の変数が使用されていても,それらは全く関係ありません.ある関数で,ある変数の値を変化させた場合,たとえそれが別の関数の変数と同じ名前であっても,その情報が何らかの方法で受け渡されない限り,別の関数ではその影響を全く受けません.

44 行目

  関数での処理を終了し,その結果を返すための文です.void 型の関数でない限り,必ずこの文が必要になります.

(プログラム例 7.3 ) 階乗の計算(再帰呼び出し) 

  先の例では,main から kaijo という関数を呼び出しただけでしたが,関数の中で,例えば kaijo の中で別の関数を呼び出すことも可能です.特別の場合として,自分自身を呼び出すことも可能です.それを,再帰呼び出しrecursive call )といいます.再帰呼び出しを頻繁に使用すると,プログラムが分かり難くなる場合が多々あります.やむを得ない場合以外,再帰呼び出しの使用は避けた方がよいと思います.このプログラムは,その概念を理解してもらうために,階乗の計算に再帰呼び出しを利用しています.自分で,実行手順を追ってみて下さい.

/****************************/
/* nの階乗の計算           */
/*      coded by Y.Suganuma */
/****************************/
#include <stdio.h>

double kaijo(int);

int main()
{
	double kai;
	int n;
/*
	 データの入力
*/
	printf("nの値を入力して下さい ");
	scanf("%d", &n);
/*
	 階乗の計算
*/
	kai = kaijo(n);
/*
	 結果の出力
*/
	printf("   %dの階乗は=%f\n",n,kai);

	return 0;
}

/**************************/
/* mの階乗               */
/*      m : データ        */
/*      return : nの階乗 */
/**************************/
double kaijo(int m)
{
	double s;

	if (m > 1)
		s = m * kaijo(m-1);	 /* 自分自身を呼んでいる */
	else
		s = 1;

	return s;
}
		

(プログラム例 7.4 ) nrの計算 

  今までの例では,関数を使用する便利さがあまり明確でなかったかもしれません.そこで,この例では,上で作成した階乗を計算する関数 kaijo を利用して,nr(= n! / (r! (n - r)!) )の計算をしてみます.nrの計算では,階乗の計算を多く行いますので,関数 kaijo を利用すると,下に示すように,非常に簡単に書くことができます.

  また,この例のように,関数を呼び出すときの引数として式を書くこともできます.

/****************************/
/* nCrの計算               */
/*      coded by Y.Suganuma */
/****************************/
#include <stdio.h>

double kaijo(int);

int main()
{
	double sn, sr, snr;
	int n, r;
/*
	 データの入力
*/
	printf("nとrの値を入力して下さい ");
	scanf("%d %d", &n, &r);
/*
	 nCrの計算と出力
*/
	sn  = kaijo(n);
	sr  = kaijo(r);
	snr = kaijo(n-r);
	printf("   %dC%dは=%f\n", n, r, sn/(sr*snr));
		// 上の 4 行の代わりに,下の 1 行でも良い
		// printf("   %dC%dは=%f\n", n, r, kaijo(n)/(kaijo(r)*kaijo(n-r)));

	return 0;
}

/**************************/
/* mの階乗               */
/*      m : データ        */
/*      return : nの階乗 */
/**************************/
double kaijo(int m)
{
	double s;
	int i1;

	s = 1.0;
	for (i1 = 1; i1 <= m; i1++)
		s *= (double)i1;

	return s;
}
		

7.2 変数の有効範囲(スコープ)

  7.1 節において述べたように,基本的に,各関数はそれぞれ独立したプログラムであり,各関数で使用されている変数等は,たとえ同じ名前の変数であっても,全く別のものです.特に,今まで述べてきた例においては,変数の型宣言を関数内部で行ってきました.このような変数をローカル変数と呼び,その有効範囲は関数内またはその一部だけです.そのため,7.1 節では,各関数で必要とする情報の受け渡しの方法として,引数と関数の返す値(戻り値)について説明しました.

  しかし,実際は,変数の型宣言を関数の外部で行い,複数の関数間で情報を共有することも可能です.このような変数をグローバル変数と呼びます.このように,変数や関数の宣言方法やその宣言場所によっては,引数や戻り値以外の方法で情報の伝達が可能になる場合があります.そこで,本節においては,変数や関数の有効範囲(スコープscope )について説明します.

7.2.1 型宣言

  有効範囲の説明に移る前に,3.2.2 節で述べた型宣言についてより詳細に述べておきます.変数の型を宣言する一般形式は以下の通りです.ただし,Java においては,すべての変数や関数(メソッド)がクラスと結びついているため,以下に述べるような意味における記憶クラスや型修飾はないと言っても良いと思います.もちろん,C++ と同様,クラス内に含まれる変数や関数(メソッド)に対する修飾子は存在しますが,詳しくは,第 10 章を参照して下さい.
	[記憶クラス] [型修飾]データ型 変数名, 配列名, 関数名, ・・・
		
  記憶クラスの指定は,以下の 4 つの中から選択されます.
	auto    :内部
	register:内部
	static  :内部,外部
	extern  :内部,外部
		
  記憶クラス auto は,関数の内部で宣言する場合だけに使用可能です.auto として宣言された変数,自動変数は,必要時だけに記憶領域が確保されます.関数が呼び出されると,関数内で宣言された自動変数がスタック上に作成され,関数の実行が終わるとすべて消去されます.自動変数に対し初期設定をする必要がある場合,関数が呼び出される度に行われます.初期設定に対するオーバーヘッドを少なくするためには,static 宣言を利用します.関数内で変数を宣言する場合,記憶クラスを省略すると auto になります.

  記憶クラス register も,関数内部だけで使用可能です.使用頻度の高い変数をレジスタに格納し,実行速度を速めるために使用されます.

  記憶クラス static 及び extern は,関数の内部及び外部の宣言で使用できます.static 宣言された変数,静的変数は,メモリ内に固定的に割り当てられます.関数外部における宣言の場合,static 宣言をしても,また,記憶クラスを省略して宣言しても,静的変数になります.しかし,その内容は少し異なります.省略して宣言した場合,その変数を他の翻訳単位(ソースファイル)や,同じ翻訳単位内のその変数を宣言した箇所より前の部分で参照や変更が可能( extern 宣言が必要)となりますが,static 宣言をした場合は,同じ翻訳単位内のその変数を宣言した箇所より後ろの部分だけで参照や変更が可能となります.

  extern 宣言された変数,外部変数は,他の翻訳単位や同じ翻訳単位内の変数宣言の前の部分で,変数の参照や変更を行うために使用されます.外部変数は,プログラム内のいずれかにおいて,関数の外部で,記憶クラスを省略した宣言が行われていなければなりません.

  型修飾の方法には,
	const
	volatile
		
の 2 種類があります.const は,値の変更できない変数を指定するものです.また,volatile 型を宣言すると,コンパイラは,その変数が,未知の方法によって,また,どの時点においてもアクセスできるような変数であるとみなし,最適化の対象としません.

------------------------(C++)const-------------------------

  C++ における const 指定は,定数を定義するプリプロセッサ #define と同じような役割を果たします.例えば,以下のように,const 指定した変数で配列の定義も可能です.なお,n は定数ですので,プログラム内でその値を変更することはできません.
	const int n = 10;
	double x[n];
		
----------------------(C++)const終了-----------------------

7.2.2 有効範囲(スコープ)

  関数の内部で,記憶クラスが省略されたり,auro,register,static,または,extern 宣言された変数は,その関数の内部だけで参照・変更が可能です.Java の関数(メソッド)内で宣言された変数も同様です.したがって,異なる関数で,上のように宣言された同じ変数名を使用していても,それらの変数間には全く関係がありません.ただし,同じ extern 宣言をした変数は,同じ変数を指すことになりますので,ある関数でその値を変更すれば,その変数を利用している他の関数においてもその影響を受けます.

  関数の外部で,記憶クラスが省略されたり,static,または,extern 宣言された変数は,その変数が宣言された場所以降のすべての関数内で参照・変更が可能になります.ただし,関数内で,外部で宣言された変数と同じ名前の変数名が使用された場合は,内部で宣言された変数が優先されます.

  関数の有効範囲に関しても,基本的に,関数の外部で宣言された変数の場合と同じです.ただし,関数の定義がなされている場合は,それも宣言の一種とみなされます.従って,例えば,プログラム例 7.2 において,関数 kaijo の定義(関数 kaijo の本体)が main 関数の前にあれば,7 行目の宣言は不必要になります.

------------------(C++, Java)Cとの有効範囲の違い------------------

  先に述べましたように,C++ や Java においては,関数(メソッド)内で変数を最初に使用する場所で変数宣言をすることが可能です.その場合,基本的には,その宣言をした場所から関数の最後(ブロック,{ } で囲まれた部分)までが有効範囲になります.特に,if 文,for 文,switch 文等の各ブロック内で宣言をした場合は,宣言された場所から,そのブロックの終わりまでが有効範囲になりますので,注意する必要があります.

  また,より内側のブロックで同じ変数名が宣言された場合は,その変数の方が優先されます.従って,外側のブロックで宣言された変数を見ることができなくなります.ただし,関数の外側で宣言された変数(外部変数)については,変数名の前にスコープ解決演算子 :: を付加することによって,その変数にアクセスできるようになります.例えば,変数 x を外部変数としたとき,その値を変数 y に代入するには,
	y = ::x;
		
のように記述します.

  関数(メソッド)内で変数を宣言する場合,その宣言位置や有効範囲に,C++ と Java では多少の違いがあります.詳細に関しては,プログラム例 7.7 を参照してください.

----------------(C++, Java)Cとの有効範囲の違い終わり--------------

(プログラム例 7.5 ) 変数の有効範囲(同じファイル)

  次の例は,main 関数と 3 つの関数を同じファイルに書いた場合における変数の有効範囲を説明するためのプログラムです.

01	/**********************************/
02	/* 変数の有効範囲(同じファイル) */
03	/*      coded by Y.Suganuma       */
04	/**********************************/
05	#include <stdio.h>
06
07	void sub3(void);
08
09	extern int j;   // 26 行目で宣言された j
10
11	void sub1(void)
12	{
13		extern int i;   // 25 行目で宣言された i
14		int k = 3;
15		printf("%d %d %d\n", i, j, k);
16	}
17
18	void sub2(void)
19	{
20		int i = 5;
21		int k = 6;
22		printf("%d %d %d\n", i, j, k);
23	}
24
25	int i = 1;
26	int j = 2;
27
28	int main()
29	{
30		sub2();
31		sub1();
32		sub3();
33
34		return 0;
35	}
36
37	void sub3(void)
38	{
39		int k = 7;
40		printf("%d %d %d\n", i, j, k);
41	}
		
7 行目

  main 関数において,関数 sub1,sub2,及び,sub3 を参照していますが,関数 sub3 の記述は main 関数の後になっています.従って,この関数に対する型宣言が必要になります.他の関数に関しては,その記述以降にその関数を参照していますので必要ありません.

9 行目

  関数 sub1 及び sub2 において変数 j を参照( 15,22 行目)していますが,その宣言は,26 行目でなされています.この宣言により,sub1,及び,sub2 で参照している変数 j は,26 行目で宣言されている変数 j と同じものになります.もちろん,関数 sub3 の変数 j は,変数 j の関数外部における宣言の後ろにありますので,同じものになります.従って,新しい宣言を加えずに,いずれかの関数において変数 j の値を修正すれば,その影響は他のすべての関数に及びます.ただし,ある関数内部において「 int j; 」のような宣言がなされていると,その関数内ではこの宣言が優先され,この変数 j の有効範囲はその関数内だけになります.

13 行目

  関数 sub1 の内部において 25 行目で宣言されている変数 i と同じものを参照するため,この宣言を行っています.9 行目と同じ extern 宣言ですが,関数内部で行われているため,その有効範囲は関数 sub1 の内部に限られます.もちろん,関数 sub3 で使用している変数 i とは同じものですので,関数 sub1 において変数 i の値を変更すれば,関数 sub3 における出力結果も異なってきます.しかし,13 行目の宣言は関数 sub2 には影響を与えませんので,関数 sub2 で宣言されている変数 i は,25 行目の i とは全く別の変数になります.

  また,各関数において,変数 k が宣言されています.その有効範囲は各関数内部に限られ,同じ変数名ではありますが全く関係ありません.

(プログラム例 7.6 ) 変数の有効範囲(別々のファイル)

  次のプログラムは,プログラム例 7.5 の各関数を別々の 4 つのファイルに分け,同じ結果を得るように書き直したものです.先の例では必要なかった宣言が必要になることに注意して下さい.この例により,変数の有効範囲の概念を十分理解して下さい.なお,Java においては,先に述べましたように,すべての関数はいずれかのクラスに含まれています.したがって,クラスまたはそのオブジェクトを介して参照することになり,この例のような場合は存在しません.

------------------------file 1----------------------------
/************************************/
/* 変数の有効範囲(別々のファイル) */
/*      coded by Y.Suganuma         */
/************************************/
void sub1(void);
void sub2(void);
void sub3(void);

int i = 1;
int j = 2;

int main()
{
	sub2();
	sub1();
	sub3();
	return 0;
}
------------------------file 2----------------------------
#include <stdio.h>

void sub1(void)
{
	extern int i;   // file 1 で宣言された i
	extern int j;   // file 1 で宣言された j
	int k = 3;
	printf("%d %d %d\n", i, j, k);
}
------------------------file 3----------------------------
#include <stdio.h>

void sub2(void)
{
	extern int j;   // file 1 で宣言された j
	int i = 5;
	int k = 6;
	printf("%d %d %d\n", i, j, k);
}
------------------------file 4----------------------------
#include <stdio.h>

void sub3(void)
{
	extern int i;   // file 1 で宣言された i
	extern int j;   // file 1 で宣言された j
	int k = 7;
	printf("%d %d %d\n", i, j, k);
}
		

(プログラム例 7.7 ) 変数の有効範囲(C++) 

  次のプログラムは,C と C++ との有効範囲の違いを示すためのものです.その違いは,特に,C++ において,任意の場所で型宣言や初期化を行えることから来ています.

01	/*****************************/
02	/* C++における変数の有効範囲 */
03	/*      coded by Y.Suganuma  */
04	/*****************************/
05	#include <stdio.h>
06
07	int x = 10;   // 以下に記述されたすべての関数で有効
08
09	int main()
10	{
11		int x = 20;   // main 関数内で有効
12		int y;        // main 関数内で有効
13
14		y = ::x;   // 7 行目の x を参照
15		printf("x %d y %d\n", x, y);
16
17		if (x > 5) {
18			int y = 30;   // 18,19 行目だけで有効
19			printf("x %d y %d\n", x, y);
20		}
21
22		printf("x %d y %d\n", x, y);
23
24		for (int x = 1; x <= 3; x++) {   // x は 24 〜 27 行目で有効
25			int y = x + 1;   // 25,26 行目だけで有効
26			printf("x %d y %d\n", x, y);
27		}
28
29		printf("x %d y %d\n", x, y);
30
31		return 0;
32	}
		
  このプログラムを実行すると以下のような出力が得られます.
	x 20 y 10
	x 20 y 30
	x 20 y 10
	x 1 y 2
	x 2 y 3
	x 3 y 4
	x 20 y 10
			
  この結果からも明らかなように,15 行目の段階では,変数 x には 11 行目で宣言したときの初期値,また,変数 y には 7 行目で宣言された x の値が,スコープ解決演算子による 14 行目の代入文によって入っています.また,19 行目の段階では,18 行目で宣言された変数 y が優先されています.しかし,この変数の有効範囲は 20 行目までですので,22 行目では再び 15 行目の状態に戻っています.同様に,24 から 27 行目においても,ブロック内で宣言された x および y が優先されます.

7.2.3 名前空間( namespace )(C++)

  たとえば,大きなプログラムを複数の人で作成するような場合,全く同じ名前で,かつ,同じ引数を持つ関数を,異なる人が異なる目的で作成してしまうような場合が考えられます.関数の有効範囲は基本的にグローバル(大域的なスコープを持っている)ですから,そのまま放置すればとんでもないことになってしまいます.そこで,グローバル変数,関数,後に説明するクラス名などに対しては,名前空間を利用してその有効範囲を制御することができます.

  名前空間の定義は,
	namespace 名前空間名 { リスト };
		
のように行います.たとえば,
	namespace name1
	{
		int func1( ・・・ ) { ・・・ }
			・・・・・
	};
		
のようにして,名前空間 name1 を定義し,その中に関数 func1 を定義しておけば,関数 func1 は,この名前空間内だけで有効な関数となります.グローバルに定義された同じ名前の関数が存在したとしても,
	x = name1::func1( ・・・ );
		
のように,名前空間名とスコープ解決演算子を利用して,名前空間 name1 内に定義された関数を指定することができます.

(プログラム例 7.56 ) 名前空間

  この例では,名前も引数も同じ 2 つの関数が定義されていますが,名前空間を利用することによって,適切な関数を利用することができます.

#include <stdio.h>

namespace Test1 {
	void print(int k1, int k2)
	{
		printf("%d %d\n", k1, k2);
	}
}

namespace Test2 {
	void print(int k1, int k2)
	{
		printf("%d / %d", k1, k2);
	}
}

/****************/
/* main program */
/****************/
int main()
{
	Test1::print(10, 20);
	Test2::print(10, 20);

	return 0;
}
		

  このプログラムを実行すると,以下に示すような出力が得られます.
10 20
10 / 20
		
  この例における main 以下の部分は,using を利用して,以下に示すように,名前空間 Test1 に対するスコープ解決演算子を省略することも可能です.同様な方法で,名前空間 Test2 に対するスコープ解決演算子を省略することも可能ですが,2 つとも省略することはできません.また,名前空間内に定義されている変数,関数,クラスなどを,スコープ解決演算子や using を使用せずに参照することは不可能です.

/****************/
/* main program */
/****************/
using namespace Test1;

int main()
{
	print(10, 20);
	Test2::print(10, 20);

	return 0;
}
		

7.3 データの受け渡し

7.3.1 データとアドレス

  関数間で情報を受け渡す方法は,大きく分けて,2 つあります.1 つは,7.1 節で述べたように,引数を利用する方法です.あと 1 つは,7.2 節で述べた変数の記憶クラスを利用する方法(関数外部で変数を定義し,extern 宣言などを利用する)です.しかし,この方法を多用すると,関数の独立性が失われ,後にプログラムの修正をしなければならないようなとき,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 に変化します.

(プログラム例 7.8 ) 複数結果の受け渡し 

  関数 sub ( main と異なるファイル)は,3 つのデータ a,b,及び,n を受け取り,(a + b) / n の計算をしています.n が 0 でない場合は除算を実行し,0 を戻り値として返します.n が 0 の場合は 1 を戻り値として返すと共に,n の値を 100 に設定し,除算を実行しません.

  関数 sub の実行に必要なデータのうち,変数 n は関数外部で定義し,関数 sub 内で extern 宣言を行うという方法を利用して関数 sub に渡しています.変数 n は,関数 sub 内で extern 宣言されていますので,main 関数の上で宣言されている変数 n と同じものになり,値を変更できます.あくまで,例として行っているのであり,一般的には引数として渡すべきです.また,他のデータは引数を利用して渡しています.ただし,除算の結果を受け取る変数 res は,関数 sub 内でその値を変更したいため,そのアドレスを渡しています.

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

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

int sub(int, int, int *);

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

int main()
{
	int a, b, res, ind;
/*
	 データの入力
*/
	printf("a,b,及び,nの値を入力して下さい ");
	scanf("%d %d %d", &a, &b, &n);
/*
	 関数の呼び出し
*/
	ind = sub(a, b, &res);   /* 変数resはアドレス渡し */
/*
	 結果の出力
*/
	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 : 計算結果       */
/*      return : =0 : normal */
/*               =1 : n = 0  */
/*****************************/
int sub(int a, int b, int *res)
{
	extern int n;
	int ind;

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

	return ind;
}
		

  このプログラムを実行し,変数 a,b,n の値として,2,4,3,及び,2,4,0 を与えたときの結果は,それぞれ以下のようになります.
	n 3 result 2
	n = 0 (n 100)
		
  上のプログラムにおいては,除算の結果に対してだけ,アドレスを使って受け渡しをしましたが,
	sub(a, b, &res, &ind);
		
のように,変数 ind に対してもアドレスを使用することができます.その場合,関数 sub の宣言は以下のようになります.
	void sub(int a, int b, int *res, int *ind)
		
  また,複数結果を受け取る方法として,6.4.2 節で述べた new 演算子を使用して結果を保存する領域を確保し,その領域に対するポインタを返す方法があります.以下に示すプログラムは,上記と同じ内容をこの方法で書いたものです.

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

int *sub(int, int);
//void sub(int, 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に結果が入る */
//	int res[2];
//	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)
//void sub(int a, int b, int *res)
{
	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 次元配列
	int res[2]
		
を定義し,この配列を介してデータを受け渡すことも可能です.この場合における関数呼び出しは,
	sub(a, b, res);
		
のようになり,関数 sub の宣言は,
	void sub(int a, int b, int *res)   // void sub(int a, int b, int res[]) でも良い
		
のようになります.関数の内容は,「new 演算子を使用しない」,「 return 文を使用しない」点を除き,上述のプログラムとほとんど同じです(上のプログラムにおけるコメント // の部分参照).

----------------------(C++)デフォルト引数-------------------

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

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

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

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		return 0;
15	}
16	/*********************************/
17	/* 整数の足し算                  */
18	/*      x,y : データ             */
19	/*      fp : 出力先              */
20	/*           coded by Y.Suganuma */
21	/*********************************/
22	void sub(int x, int y, FILE *fp)
23	//void sub(int x, int y = 5, FILE *fp = stdout)
24	{
25		fprintf(fp, "結果は=%d\n", x+y);
26	}
		

------------------(C++)デフォルト引数終わり------------------

7.3.2 配列

7.3.2.1 1 次元配列

  多量のデータを関数間でやりとりしたい場合,最も簡単なのは配列を利用する方法です.配列を関数外部で宣言し,extern 宣言等を利用することも可能ですが,この節では,配列を引数とする場合について考えます.

  1 次元配列の場合は簡単です.例えば,以下のような方法によって行われます.
	{
		int b[5];
		  ・・・
		n = sub(・・・, b, ・・・);
		  ・・・
	}
	int sub(・・・, int c[], ・・・)   // int sub(・・・, int *c, ・・・) でも可
	{
		  ・・・
	}
		
このように記述することにより,呼び出した側の配列 b と関数 sub 内の配列 c は同じものになり,どちらでも,配列データの参照・修正が可能になります.このことは,6.2 節で述べましたように,変数 b が配列宣言で確保された領域の先頭アドレスを指していることを考えれば当然です.つまり,前節で述べた変数のアドレスを引数として与える場合と同じことになります.なお,関数側の宣言において,配列の添え字を書く必要はありませんし,また,そのコメントに示されているように記述しても構いません.

(プログラム例 7.10 ) 1 次元配列の受け渡し 

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

/****************************/
/* 1次元配列の受け渡し     */
/*      coded by Y.Suganuma */
/****************************/
#include <stdio.h>

void wa(int, int *, int *, int *);

int main()
{
	int a[2][3] = {{1, 2, 3}, {4, 5, 6}};
	int i1, b[3];
/*
	 関数の呼び出し
*/
	wa(3, &(a[0][0]), &(a[1][0]), b);   /* 1行目と2行目のベクトル和の計算 */
/*
	 結果の出力
*/
	for (i1 = 0; i1 < 3; i1++)
		printf("%d ", b[i1]);
	printf("\n");

	return 0;
}
/***********************************/
/* n次元ベクトルの和              */
/*      n : 次元数                 */
/*      a,b : 和を計算するベクトル */
/*      c : 計算結果が入るベクトル */
/***********************************/
void wa(int n, int *a, int *b, int *c)
{
	int i1;

	for (i1 = 0; i1 < n; i1++)
		c[i1] = a[i1] + b[i1];
}
		

7.3.2.2 2 次元以上の配列

  2 次元の配列を受け渡す場合も,基本的には,1 次元の場合と同様です.しかし,受け渡される関数の側で,列の数を必ず記述しなければなりません.3 次元以上の場合も同様に,最初の添え字(次元)だけは省略できますが,後の次元はすべて記述してやる必要があります.例えば,配列 b が
	int b[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)

  関数 seki は,n 行 m 列の行列と m 行 l 列の行列の積を計算するためのものです.上で述べたように,関数 seki は,配列 a[2][3] の[3] の部分,または,配列 b[3][2] の [2] の部分が変更になると,使用できなくなります.つまり,関数 seki は,その内容を修正しない限り,任意のサイズの行列の乗算に対応していないことになります.なお,行列 A ( n 行 m 列)と行列 B ( m 行 l 列)の乗算の定義は以下の通りです.

  C = AB, cij = Σkaikbkj  k = 1, ・・・, m

ただし,cij 等は,行列 C 等の i 行 j 列要素とします.

/**********************************/
/* 2次元配列の受け渡し(方法1) */
/*      coded by Y.Suganuma       */
/**********************************/
#include <stdio.h>

void seki(int, int, int, int [][3], int [][2], int [][2]);

int main()
{
	int a[2][3] = {{1, 2, 3}, {4, 5, 6}};
	int b[3][2] = {{1, 0}, {0, 1}, {0, 0}};
	int i1, i2, c[2][2];
/*
	 関数の呼び出し
*/
	seki(2, 3, 2, a, b, c);
/*
	 結果の出力
*/
	for (i1 = 0; i1 < 2; i1++) {
		for (i2 = 0; i2 < 2; i2++)
			printf("%d ", c[i1][i2]);
		printf("\n");
	}

	return 0;
}
/**************************************/
/* 行列の積                           */
/*      n,m,l : 次元数                */
/*      a : n x m                     */
/*      b : m x l                     */
/*      c : 計算結果が入る行列,n x l */
/**************************************/
void seki(int n, int m, int l, int a[][3], int b[][2], int c[][2])
{
	int i1, i2, i3;

	for (i1 = 0; i1 < n; i1++) {
		for (i2 = 0; i2 < l; i2++) {
			c[i1][i2] = 0;
			for (i3 = 0; i3 < m; i3++)
				c[i1][i2] += a[i1][i3] * b[i3][i2];
		}
	}
}
		

(プログラム例 7.12 ) 2 次元配列の受け渡し(方法 2) 

  new 演算子( malloc 等でも構いません)を使用して配列を定義し,先の例の関数をポインタを使用した記述になおしたものです.このようにすれば,関数を呼び出す側で配列の大きさが変化しても,次の例と同様,呼び出される側の関数 seki を修正する必要がありません.つまり,関数 seki は,任意のサイズの行列の乗算に対応していることになります.

/**********************************/
/* 2次元配列の受け渡し(方法2) */
/*      coded by Y.Suganuma       */
/**********************************/
#include <stdio.h>

void seki(int, int, int, int **, int **, int **);

int main()
{
	int i1, i2, **a, **b, **c;
/*
	 領域の確保と値の設定
*/
	a = new int * [2];
	b = new int * [3];
	c = new int * [2];

	for (i1 = 0; i1 < 2; i1++) {
		a[i1] = new int [3];
		c[i1] = new int [2];
		if (i1 == 0) {
			a[i1][0] = 1;
			a[i1][1] = 2;
			a[i1][2] = 3;
		}
		else {
			a[i1][0] = 4;
			a[i1][1] = 5;
			a[i1][2] = 6;
		}
	}

	for (i1 = 0; i1 < 3; i1++) {
		b[i1] = new int [2];
		if (i1 == 0) {
			b[i1][0] = 1;
			b[i1][1] = 0;
		}
		else {
			if (i1 == 1) {
				b[i1][0] = 0;
				b[i1][1] = 1;
			}
			else {
				b[i1][0] = 0;
				b[i1][1] = 0;
			}
		}
	}
/*
	 関数の呼び出し
*/
	seki(2, 3, 2, a, b, c);
/*
	 結果の出力
*/
	for (i1 = 0; i1 < 2; i1++) {
		for (i2 = 0; i2 < 2; i2++)
			printf("%d ", c[i1][i2]);
		printf("\n");
	}

	return 0;
}
/**************************************/
/* 行列の積                           */
/*      n,m,l : 次元数                */
/*      a : n x m                     */
/*      b : m x l                     */
/*      c : 計算結果が入る行列,n x l */
/**************************************/
void seki(int n, int m, int l, int **a, int **b, int **c)
{
	int i1, i2, i3;

	for (i1 = 0; i1 < n; i1++) {
		for (i2 = 0; i2 < l; i2++) {
			c[i1][i2] = 0;
			for (i3 = 0; i3 < m; i3++)
				c[i1][i2] += a[i1][i3] * b[i3][i2];
		}
	}
}
		

(プログラム例 7.13 ) 2 次元配列の受け渡し(方法 3)

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

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

/**********************************/
/* 2次元配列の受け渡し(方法3) */
/*      coded by Y.Suganuma       */
/**********************************/
#include <stdio.h>

void seki(int, int, int, int *, int *, int *);

int main()
{
	int a[2][3] = {{1, 2, 3}, {4, 5, 6}};
	int b[3][2] = {{1, 0}, {0, 1}, {0, 0}};
	int i1, i2, c[2][2];
/*
	 関数の呼び出し
*/
	seki(2, 3, 2, &a[0][0], &b[0][0], &c[0][0]);
/*
	 結果の出力
*/
	for (i1 = 0; i1 < 2; i1++) {
		for (i2 = 0; i2 < 2; i2++)
			printf("%d ", c[i1][i2]);
		printf("\n");
	}

	return 0;
}
/**************************************/
/* 行列の積                           */
/*      n,m,l : 次元数                */
/*      a : n x m                     */
/*      b : m x l                     */
/*      c : 計算結果が入る行列,n x l */
/**************************************/
void seki(int n, int m, int l, int *a, int *b, int *c)
{
	int i1, i2, i3;

	for (i1 = 0; i1 < n; i1++) {
		for (i2 = 0; i2 < l; i2++) {
			c[l*i1+i2] = 0;
			for (i3 = 0; i3 < m; i3++)
				c[l*i1+i2] += a[m*i1+i3] * b[l*i3+i2];   /* 添え字に注意 */
		}
	}
}
		

7.3.3 関数名

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

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

(プログラム例 7.14 ) 関数名の受け渡し(ニュートン法) 

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

/****************************/
/* 関数名の受け渡し         */
/*      coded by Y.Suganuma */
/****************************/
#include <stdio.h>

double newton(double(*)(double), double(*)(double), double, double,
			  double, int, int *);
double snx(double);
double dsnx(double);

int main()
{
	double eps1, eps2, x, x0;
	int max, ind;

	eps1 = 1.0e-7;
	eps2 = 1.0e-10;
	max  = 20;
	x0   = 0.0;

	x = newton(snx, dsnx, x0, eps1, eps2, max, &ind);

	printf("ind=%d  x=%f  f=%f  df=%f\n",ind,x,snx(x),dsnx(x));

	return 0;
}

/*****************************************************/
/* Newton法による非線形方程式(f(x)=0)の解            */
/*      fn : f(x)を計算する関数名                    */
/*      dfn : f(x)の微分を計算する関数名             */
/*      x0 : 初期値                                  */
/*      eps1 : 終了条件1(|x(k+1)-x(k)|<eps1)   */
/*      eps2 : 終了条件2(|f(x(k))|<eps2)       */
/*      max : 最大試行回数                           */
/*      ind : 実際の試行回数                         */
/*            (負の時は解を得ることができなかった) */
/*      return : 解                                  */
/*****************************************************/
#include <math.h>

double newton(double(*f)(double), double(*df)(double), double x0,
			  double eps1, double eps2, int max, int *ind)
{
	double g, dg, x, x1;
	int sw;

	x1   = x0;
	x    = x1;
	*ind = 0;
	sw   = 0;

	while (sw == 0 && *ind >= 0) {

		sw    = 1;
		*ind += 1;
		g     = (*f)(x1);

		if (fabs(g) > eps2) {
			if (*ind <= max) {
				dg = (*df)(x1);
				if (fabs(dg) > eps2) {
					x = x1 - g / dg;
					if (fabs(x-x1) > eps1 && fabs(x-x1) > eps1*fabs(x)) {
						x1 = x;
						sw = 0;
					}
				}
				else
					*ind = -1;
			}
			else
				*ind = -1;
		}
	}

	return x;
}

/************************/
/* 関数値(f(x))の計算 */
/************************/
double snx(double x)
{
	double y;
	y = exp(x) - 3.0 * x;
	return y;
}

/********************/
/* 関数の微分の計算 */
/********************/
double dsnx(double x)
{
	double y;
	y = exp(x) - 3.0;
	return y;
}
		

(プログラム例 7.15 ) 関数名の配列 

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

/****************************/
/* 関数名の配列             */
/*      coded by y.suganuma */
/****************************/
#include <stdio.h>
/*
	 FP は 2 つの int の引数を受け取り,int を返す関数へのポインタであると宣言
*/
typedef int (*FP) (int, int);
/*
	 4つの関数を宣言
*/
int add(int, int);
int sub(int, int);
int mul(int, int);
int div(int, int);

int main()
{
	int i1, x = 12;
	FP f_tb[] = {&add, &sub, &mul, &div};   /* 関数へのポインタ配列 */

	for (i1 = 0; i1 < 4; i1++)
		printf("result %d\n", f_tb[i1](x, i1+1));

	return 0;
}

/********/
/* 加算 */
/********/
int add(int x, int y)
{
	return (x + y);
}

/********/
/* 減算 */
/********/
int sub(int x, int y)
{
	return (x - y);
}

/********/
/* 乗算 */
/********/
int mul(int x, int y)
{
	return (x * y);
}

/********/
/* 除算 */
/********/
int div(int x, int y)
{
	return (x / y);
}
		

  上のプログラムを実行すると下のような結果が得られます.
	result 13
	result 10
	result 36
	result 3
		

7.3.4 参照渡し(C++)

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

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

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

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

  Java においては,参照型変数のような宣言方法はありません.しかし,関数の引数において,int や double のような基本的なデータ型以外はすべてそのアドレスが渡されていると考えた方が良いと思います.ただし,関数内でそれらの変数等に対する参照方法に差が無いため,参照渡しのようにも見えます.いずれにしろ,関数内でその値を変更することが可能です.

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

/****************************/
/* 参照渡し                 */
/*      coded by Y.Suganuma */
/****************************/
#include <stdio.h>

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

int main()
{
	int x = 10, y = 20, z = 0;
	char *data1 = "data1", *data2 = "data2";

	printf("x %d y %d z %d data1 %s data2 %s\n", x, y, z, data1, data2);

	sub(x, y, &z, data1, data2);

	printf("x %d y %d z %d data1 %s data2 %s\n", x, y, z, data1, data2);

	return 0;
}

/****************************/
/* 参照渡しの例             */
/*      a : 参照渡し        */
/*      b : データ渡し      */
/*      c : アドレス渡し    */
/*      c1 : 配列の参照渡し */
/*      c2 : 通常の配列渡し */
/****************************/
void sub(int &a, int b, int *c, char *&c1, char *c2)
{
	a += 5;
	b += 5;
	c1++;
	c2++;
	*c = a + b;

	printf("a %d b %d c %d c1 %s c2 %s\n", a, b, *c, c1, c2);
}
		

  上のプログラムを実行すると下のような結果が得られます.この結果からも明らかなように,通常の引き渡し( y と data2 )では,値がコピーされて渡されるだけ(配列の場合は,アドレスのコピー)ですので,呼び出した側の対応する変数の内容を,呼び出された関数側で変えることができません.しかし,参照渡しですと,別名ですが,変数それ自身と同じものです.従って,関数を呼んだ側と同じ処理で,その内容も変更されます.データでなくアドレスで渡した変数 z (関数側では c )との記述方法の違いにも注意して下さい.
	x 10 y 20 z 0 data1 data1 data2 data2
	a 15 b 25 c 40 c1 ata1 c2 ata2
	x 15 y 20 z 40 data1 ata1 data2 data2
		
(プログラム例 7.17 ) 参照渡し(参照型関数)

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

/****************************/
/* 参照渡し(参照型関数)   */
/*      coded by Y.Suganuma */
/****************************/
#include <stdio.h>

int x = 10;

int &sub(int);

int main()
{
	int y = 20;

	printf("x %d y %d\n", x, y);
/*
	 現在の変数xの値の参照(関数を右辺)
*/
	y = sub(5);
	printf("x %d y %d\n", x, y);
/*
	 変数xに結果を代入(関数を左辺)
*/
	sub(100) += 3;
	printf("x %d y %d\n", x, y);

	return 0;
}

/*********************/
/* 関数が変数xの別名 */
/*********************/
int &sub(int a)
{
	x += a;

	return x;
}
		

  このプログラムを実行した結果は,以下のようになります.
    x 10 y 20
    x 15 y 15
    x 118 y 15
		

7.4 main 関数

  main も一種の関数です.今までのプログラムでは,
	int main()
		
と記述していたため,引数を持ちませんでした.しかし,例えば,
	add 2 3
		
とキーボードから入力すると,2 つの値 2 と 3 の和を計算し,その結果をディスプレイに出力したいような場合が存在します.このような場合,2 や 3 が main 関数(メインプログラム主プログラムmain program )の引数とみなされます.ただし,main 関数の場合は,引数の受け渡し方が以下のように決まっています.
	int main ( int argc, char *argv[], char *envp[] )
		
argc : コマンドラインからプログラムへ渡された引数の数を指定する整数.プログラム名も引数と見なされるので, argc は 1 以上の値となります.「 add 2 3 」のような場合は,3 となります.

argv : null 文字で終わる文字列の配列.char へのポインタの配列 ( char *argv[] ),または,char へのポインタへのポインタ ( char **argv ) として宣言できます. 最初の文字列 ( argv[0] ) はプログラム名で,その後に続く各文字列はコマンドラインからプログラムへ渡される引数(文字列)です. 最後のポインタ( argv[argc] )は NULL です.

envp : 環境文字列の配列へのポインタ.char へのポインタの配列 ( char *envp[] ),または,char へのポインタへのポインタ ( char **envp ) として宣言できます. 配列の最後は NULL ポインタで示します.

(プログラム例 7.18 ) main 関数の引数(数字の和) 

  次の例は,複数個の整数を加えるプログラムです.なお,関数 atoi は,文字列を整数に変換する関数です.

/******************************/
/* main関数の引数(数字の和) */
/*      coded by Y.Suganuma   */
/******************************/
#include <stdio.h>
#include <stdlib.h>

int main(int argc, char *argv[], char *envp[])
{
	int i1, k, sum = 0;
/*
	 引数の内容の出力
*/
	printf("	 引数の数 %d\n",argc);
	printf("	 プログラム名 %s\n",argv[0]);

	for (i1 = 1; i1 < argc; i1++) {
		k = atoi(argv[i1]);				  /* 文字を整数に変換 */
		printf("     %d 番目の引数 %d\n", i1+1, k);
		sum += k;
	}
/*
	 結果の表示
*/
	printf("結果=%d\n", sum);

	return 0;
}
		

  例えば,「add 2 3」と入力すると,以下のような出力が得られます.
	     引数の数 3
	     プログラム名 b:/temp/add.exe
	     1番目の引数 2
	     2番目の引数 3
	結果=5
		

(プログラム例 7.19 ) main 関数の引数(環境変数の出力)

  次は,定義されている環境変数を出力するプログラムです.

/*******************************/
/* main関数(環境変数の出力)  */
/*      coded by Y.Suganuma    */
/*******************************/
#include <stdio.h>

int main(int argc, char *argv[], char *envp[])
{
	int i1 = 0;
	while (envp[i1] != NULL) {
		printf("%d %s\n", i1+1, envp[i1]);
		i1++;
	}

	return 0;
}
		

7.5 その他(C++)

7.5.1 関数名のオーバーロード

  C++ においては,引数の型や数が異なれば,2 つ以上の関数に同じ名前を付けることができます.それを,関数名のオーバーロード多重定義function name overloaded )と呼びます.この機能はどのようなことに利用できるでしょうか.

  例えば,与えられたデータを出力する関数を printf 関数を使用して作成したいとします.printf 関数は,出力するデータの型が異なると,それに対応して % に続く文字も変えてやらなければなりませんので,データ毎に呼び出す関数名を変更しなければなりません.しかし,関数名のオーバーロード機能を使用すれば,同じ関数を呼び出すことによって処理できます.コンパイラが,与えられたデータの型によって適当な関数を選んでくれるからです.

  次のプログラム例は,関数名のオーバーロード機能を使用して,上のことを実現しています.

(プログラム例 7.20 ) 関数名のオーバーロード 

/****************************/
/* 関数名のオーバーロード   */
/*      coded by Y.Suganuma */
/****************************/
#include <stdio.h>

/****************/
/* 文字列の出力 */
/****************/
void print(char *moji)
{
	printf("%s\n", moji);
}

/*****************/
/* double の出力 */
/*****************/
void print(double dbd)
{
	printf("%f\n", dbd);
}

/**************/
/* int の出力 */
/**************/
void print(int ind)
{
	printf("%d\n", ind);
}

/********/
/* main */
/********/
int main()
{
	print("moji-retu");   // 以下,データの型に対応した関数が選択される
	print(3.14);
	print(100);

	return 0;
}
		

7.5.2 インライン関数(Javaを除く)

  インライン関数( inline function )とは,関数の本体をコンパイル時にプログラム内にインライン展開してしまう関数です.インライン関数を定義するには,関数定義の際に,例えば,
	inline int sub(int x, ・・・)
		
のように記述すれば可能です.インライン展開されるので,関数を呼び出すオーバーヘッドが無くなり,実行速度が速くなります.ただし,プログラムサイズは,当然,大きくなります.

  機能としては,プリプロセッサの #define 疑似命令でマクロを定義するのとほとんど同じです.#define マクロの場合は,型のチェックや副作用等についてユーザーが注意しなければなりませんが,インライン関数の場合は,普通の関数と同じ感覚で書き,また,使用することが可能です.

(プログラム例 7.21 ) インライン関数と #define マクロ

  次のプログラムは,「 sqrt( x * x + y * y ) 」の計算を,#define マクロ,インライン関数,及び,普通の関数で 1000000 回計算し CPU 時間を比較しています.ただし,コンパイラ等の処理の方法によって CPU 時間は異なると思いますので,結果の値にはあまりこだわらないで下さい.

/*********************************/
/* インライン関数と#defineマクロ */
/*      coded by Y.Suganuma      */
/*********************************/
#include <stdio.h>
#include <math.h>
#include <time.h>

#define ookisa(x, y) sqrt(x * x + y * y)   /* マクロによる計算 */

inline double length1(double, double);
double length2(double, double);

int main()
{
	double a = 3.0;
	double b = 4.0;
	double x;
	long i1;
	clock_t c1, c2;
/*
	 マクロによる計算
*/
	c1 = clock();
	for (i1 = 0; i1 < 1000000; i1++)
		x = ookisa(a, b);
	c2 = clock();

	printf("計算時間は %f 秒です(マクロ)\n", (double)(c2-c1)/CLOCKS_PER_SEC);
/*
	 インライン関数
*/
	c1 = c2;
	for (i1 = 0; i1 < 1000000; i1++)
		x = length1(a, b);
	c2 = clock();
	printf("計算時間は %f 秒です(インライン関数)\n", (double)(c2-c1)/CLOCKS_PER_SEC);
/*
	 普通の関数
*/
	c1 = c2;
	for (i1 = 0; i1 < 1000000; i1++)
		x = length2(a, b);
	c2 = clock();
	printf("計算時間は %f 秒です(普通の関数)\n", (double)(c2-c1)/CLOCKS_PER_SEC);

	return 0;
}
/***************************/
/* sqrt(x*x+y*y)(inline) */
/*     x,y : 2つのデータ  */
/*     return : 結果       */
/***************************/
inline double length1(double x, double y)
{
	return sqrt(x * x + y * y);
}
/*******************************/
/* sqrt(x*x+y*y)(普通の関数) */
/*     x,y : 2つのデータ      */
/*     return : 結果           */
/*******************************/
double length2(double x, double y)
{
	return sqrt(x * x + y * y);
}
		

  上のプログラムを実行すると下のような結果が得られます.
	計算時間は 35 秒です(マクロ)
	計算時間は 39 秒です(インライン関数)
	計算時間は 40 秒です(普通の関数)
		

7.5.3 例外処理

  C++ には,プログラム実行中に起こったエラーを見知して,ユーザ固有の処理を行うための方法があります.それが,例外処理(exception)です.

  まず,try ブロックはブロック内で発生する例外を捕まえます.つまり,try ブロックには,例外が発生する可能性のある処理を書きます.例外が発生すると,throw 文によって例外が try ブロックに渡され,try ブロックの後ろにある throw された型と同じ型を受け取る catch ブロックで処理されます( catch ブロックは複数書くことができます).詳しくは,次のプログラム例を見て下さい.

(プログラム例 7.22 ) 例外処理

/****************************/
/* 例外処理                 */
/*      coded by Y.Suganuma */
/****************************/
#include <iostream>
#include <math.h>
using namespace std;

void sq(double x, double y)
{
	if (x < 0.0 && y < 0.0) {
		char *str = "両方とも負\n";
		throw str;
	}
	else if (x < 0.0 || y < 0.0) {
		char *str = "片方が負\n";
		throw str;
	}

	double z = sqrt(x+y);
	cout << z << endl;
}

int main()
{
	try {
		double x, y;
		cout << "1 つ目のデータは? ";
		cin >> x;
		cout << "2 つ目のデータは? ";
		cin >> y;
		sq(x, y);
	}

	catch (char *str)
	{
		cout << str;
	}

	return 0;
}
		

  このプログラムに,例えば,1 と 2 を入力し実行すると上の行が,また,-1 と -2 を入力し実行すると下の行に示すような結果が出力されます.
	1.73205
	両方とも負
		
  上の例では,char * 型を送出しましたが,次の例のように,クラス(クラスに関しては,第 10 章以降を参照してください)のオブジェクトを送出することも可能です.なお,あまり意味はありませんが,1 番目の引数だけが負の場合は,その値を正に修正して再実行しています.

/****************************/
/* 例外処理                 */
/*      coded by Y.Suganuma */
/****************************/
#include <iostream>
#include <math.h>
#include <string.h>
using namespace std;

class Negative {
	public:
		char *str;
		double x, y;
		Negative(char *str, double x, double y) {
			this->str = new char [100];
			strcpy(this->str, str);
			this->x = x;
			this->y = y;
		}
		void message(int sw) {
			if (sw == 0)
				cout << "    1 番目の値を正にして再実行しました\n";
			else
				cout << "    データを修正してください\n";
		}
};

void sq(double x, double y)
{
	if (x < 0.0 && y < 0.0)
		throw Negative("両方とも負\n", x, y);
	else if (x < 0.0 || y < 0.0)
		throw Negative("片方が負\n", x, y);

	double z = sqrt(x+y);
	cout << z << endl;
}

int main()
{
	try {
		double x, y;
		cout << "1 つ目のデータは? ";
		cin >> x;
		cout << "2 つ目のデータは? ";
		cin >> y;
		sq(x, y);
	}

	catch (Negative &ng)
	{
		cout << ng.str;
		if (ng.y > 0.0) {
			ng.message(0);
			sq(-ng.x, ng.y);
		}
		else
			ng.message(1);
	}

	return 0;
}
		

  このプログラムに,例えば,-1 と 2 を入力し実行すると,以下のような結果が出力されます.
	片方が負
	    1 番目の値を正にして再実行しました
	1.73205
		

7.6 様々な例題

  この節では,主として科学技術計算に使用される様々なプログラム例を与えます.可能な限り,各関数(クラス)は一般的に使用できるように書いたつもりです.基本的に,C/C++ によるプログラムは C の範囲で記述しますが(一部,クラスを使用したものを含みます),先に述べたように,メモリの動的確保が必要な場合 malloc 関数の替わりに new 演算子を使用したり,注釈に「//」を使用して書きます.なお,プログラムの具体的使用法に関しては,各手法の説明の箇所も参照してください.

7.6.1 数値計算

(プログラム例 7.23 ) 連立線形方程式,逆行列(ガウス・ジョルダン) 

  添付したプログラムは,連立線形方程式の解(逆行列)をガウスの消去法によって求めた例です.アプレット版及び JavaScript 版では,任意のデータに対して,連立方程式の解,逆行列,行列の乗算,及び,行列式の値を画面上で計算することが可能になっています.

(プログラム例 7.28 ) 非線形方程式(二分法) 

  添付したプログラムは,f(x) = exp(x) - 3x = 0 の根を二分法で求めた例です.JavaScript 版では,JavaScript の仕様に適合した形で解を求めたい式を入力することによって,任意の非線形方程式の解を画面上で求めることができます.

(プログラム例 7.29 ) 非線形方程式(セカント法) 

  添付したプログラムは,f(x) = exp(x) - 3x = 0 の根をセカント法で求めた例です.JavaScript 版では,JavaScript の仕様に適合した形で解を求めたい式を入力することによって,任意の非線形方程式の解を画面上で求めることができます.

(プログラム例 7.63 ) 非線形方程式(ニュートン法) ,(多次元:

  添付したプログラムは,f(x) = exp(x) - 3x = 0 の根をニュートン法で求めた例です.多次元の場合に対するプログラムは,3 点 ( 0.5, 1.0 ),( 0.0, 1.5 ),( 0.5, 2.0 ) を通る円の中心座標と半径を多次元のニュートン法で求めた例です.JavaScript 版(多次元の場合に対する JavaScript 版)では,JavaScript の仕様に適合した形で解を求めたい式を入力することによって,任意の非線形方程式の解を画面上で求めることができます.

(プログラム例 7.40 ) 代数方程式(ベアストウ) 

  添付したプログラムは,実係数代数方程式 (x + 1)(x - 2)(x - 3)(x2 + x + 1) = 0 の解を,ベアストウ法で求めた例です.アプレット版及び JavaScript 版では,任意のデータに対して画面上で解を得ることができます.

(プログラム例 7.43 ) 行列の固有値(フレーム法+ベアストウ法) 

  添付したプログラムは,行列の固有値をフレーム法とベアストウ法によって求めるためのものです.アプレット版及び JavaScript 版では,任意のデータに対して画面上で解を得ることができます.

(プログラム例 7.44 ) 実対称行列の固有値・固有ベクトル(ヤコビ法) 

  添付したプログラムは,実対称行列の固有値及び固有ベクトルを,ヤコビ法で求めるためのものです.アプレット版及び JavaScript 版では,任意のデータに対して画面上で解を得ることができます.

(プログラム例 7.45 ) 最大固有値と固有ベクトル(べき乗法) 

  添付したプログラムは,行列の固有値と固有ベクトルを,固有値の絶対値が最大のものから順に求めていく方法(べき乗法)です.アプレット版及び JavaScript 版では,任意のデータに対して画面上で解を得ることができます.

(プログラム例 7.25 ) 数値積分(台形則) 

  添付したプログラムは,台形則により sin(x) を 0 から π/2 までの積分するプログラム例です.シンプソン則による方法と比較してみてください.JavaScript 版では,JavaScript の仕様に適合した形で積分したい式を入力することによって,任意の関数の積分を画面上で求めることができます.

(プログラム例 7.26 ) 数値積分(シンプソン則) 

  添付したプログラムは,シンプソン則により sin(x) を 0 から π/2 までの積分するプログラム例です.JavaScript 版では,JavaScript の仕様に適合した形で積分したい式を入力することによって,任意の関数の積分を画面上で求めることができます.

(プログラム例 7.27 ) 微分方程式(ルンゲ・クッタ) 

  添付したプログラムは,以下の微分方程式をルンゲ・クッタ法によって,0 から 1 秒まで解いた例です.JavaScript 版では,JavaScript の仕様に適合した形で微分方程式を入力することによって,任意の微分方程式の解を画面上で求めることができます.

  d2y/dt2 + 3dy/dt + 2y = 1  初期条件はすべて0
  (x[0] = y, x[1] = dy/dt)

(プログラム例 7.41 ) 補間法(ラグランジュ) 

  添付したプログラムは,ラグランジュ補間法のプログラムです.アプレット版及び JavaScript 版では,n 次補間多項式による計算結果を画面上で求めることができます.

(プログラム例 7.42 ) 補間法(スプライン) 

  添付したプログラム(クラスを使用したプログラム例も含みます)は,3次スプライン関数によってスプライン補間するためのものです.アプレット版及び JavaScript 版では,任意のデータに対して,スプライン補間法による計算結果を画面上で求めることができます.

(プログラム例 7.46 ) 補間法(ベジエ曲線) 

  添付したプログラムは,ベジエ多角形を B0 = (1 1),B1 = (2 3),B2 = (4 3),B3 = (3 1) としたとき,対応するベジエ曲線を描くためのものです.アプレット版及び JavaScript 版では,任意のデータに対して,ベジエ曲線上の座標を画面上に出力することができます.

7.6.2 最適化

(プログラム例 7.61 ) 最適化(線形計画法) 

  添付したプログラム(クラスを使用して記述してあります)は,線形計画法に対するプログラム例です.実行に関しては,使用方法を参考にしてください.

(プログラム例 7.30 ) 最適化(黄金分割法) 

  添付したプログラムは,f(x) = x4 + 3x3 + 2x2 + 1 の最小値を黄金分割法で求めた例です.JavaScript 版では,JavaScript の仕様に適合した形で最小値を求めたい式を入力することによって,任意の関数の最小値を画面上で求めることができます.

(プログラム例 7.60 ) 最適化(多項式近似法) 

  添付したプログラムは,f(x) = x4 + 3x3 + 2x2 + 1 の最小値を多項式近似法で求めた例です.JavaScript 版では,JavaScript の仕様に適合した形で最小値を求めたい式を入力することによって,任意の関数の最小値を画面上で求めることができます.

(プログラム例 7.31 ) 最適化(最急降下法) 

  添付したプログラムは,最急降下法を使用して,非線形関数の最小値を求めるためのものです( C/C++ 及び Java によるプログラムの使用方法).JavaScript 版では,JavaScript の仕様に適合した形で最小値を求めたい式を入力することによって,任意の関数の最小値を画面上で求めることができます.

(プログラム例 7.47 ) 最適化(共役勾配法) 

  添付したプログラムは,共役勾配法を使用して,非線形関数の最小値を求めるためのものです( C/C++ 及び Java によるプログラムの使用方法).JavaScript 版では,JavaScript の仕様に適合した形で最小値を求めたい関数を入力することによって,任意の関数の最小値を画面上で求めることができます.

(プログラム例 7.48 ) 最適化(Newton 法) 

  添付したプログラムは,Newton 法を使用して,非線形関数の最小値を求めるためのものです( C/C++ 及び Java によるプログラムの使用方法).JavaScript 版では,JavaScript の仕様に適合した形で最小値を求めたい関数を入力することによって,任意の関数の最小値を画面上で求めることができます.

(プログラム例 7.49 ) 最適化(準 Newton 法) 

  添付したプログラムは,準 Newton 法を使用して,非線形関数の最小値を求めるためのものです( C/C++ 及び Java によるプログラムの使用方法).JavaScript 版では,JavaScript の仕様に適合した形で最小値を求めたい関数を入力することによって,任意の関数の最小値を画面上で求めることができます.

(プログラム例 7.59 ) 最適化(シンプレックス法) 

  添付したプログラムは,シンプレックス法を使用して,非線形関数の最小値を求めるためのものです( C/C++ 及び Java によるプログラムの使用方法).JavaScript 版では,JavaScript の仕様に適合した形で最小値を求めたい関数を入力することによって,任意の関数の最小値を画面上で求めることができます.

(プログラム例 7.50 ) 最適化(動的計画法) 

  添付したプログラムは,動的計画法を使用して,資源配分問題を解くためのものです.

7.6.3 確率と統計

(プログラム例 7.32 ) ガンマ関数 

  添付したプログラムは,ガンマ関数の値を計算するプログラムです.アプレット版及び JavaScript 版では,任意のデータに対するガンマ関数の値を画面上で求めることができます.

(プログラム例 7.64 ) 二項分布 

  添付したプログラムは,グラフ出力を指定すると,ベルヌーイ試行を n 回行い,0 〜 n 回成功する場合に対する二項分布の密度関数および分布関数の値をファイルに出力します.また,グラフ出力を指定しないと,指定された値における密度関数および分布関数の値を出力します.Java 版ではグラフも表示されます.アプレット版及び JavaScript 版では,同様の処理を画面上で実行することが可能であり,結果はテキストエリアに出力されると共に,「確率(複数点)」を選択すればグラフも表示されます.なお,グラフに関しては,20.5.2 グラフの表示を参照して下さい.

(プログラム例 7.65 ) ポアソン分布 

  添付したプログラムは,グラフ出力を指定するとポアソン分布の密度関数および分布関数の値を指定した範囲だけファイルに出力します.また,グラフ出力を指定しないと,指定された値における密度関数および分布関数の値を出力します.Java 版ではグラフも表示されます.アプレット版及び JavaScript 版では,同様の処理を画面上で実行することが可能であり,結果はテキストエリアに出力されると共に,「確率(複数点)」を選択すればグラフも表示されます.なお,グラフに関しては,20.5.2 グラフの表示を参照して下さい.

(プログラム例 7.66 ) 一様分布 

  添付したプログラムは,グラフ出力を指定すると一様分布の密度関数および分布関数の値を指定した範囲だけファイルに出力します.また,グラフ出力を指定しないと,指定された値における密度関数および分布関数の値,または,%値を出力します.Java 版ではグラフも表示されます.アプレット版及び JavaScript 版では,同様の処理を画面上で実行することが可能であり,結果はテキストエリアに出力されると共に,「確率(複数点)」を選択すればグラフも表示されます.なお,グラフに関しては,20.5.2 グラフの表示を参照して下さい.

(プログラム例 7.67 ) 指数分布 

  添付したプログラムは,グラフ出力を指定すると指数分布の密度関数および分布関数の値を指定した範囲だけファイルに出力します.また,グラフ出力を指定しないと,指定された値における密度関数および分布関数の値,または,%値を出力します.Java 版ではグラフも表示されます.アプレット版及び JavaScript 版では,同様の処理を画面上で実行することが可能であり,結果はテキストエリアに出力されると共に,「確率(複数点)」を選択すればグラフも表示されます.なお,グラフに関しては,20.5.2 グラフの表示を参照して下さい.

(プログラム例 7.33 ) 正規分布 

  添付したプログラムは,グラフ出力を指定すると正規分布の密度関数および分布関数の値を指定した範囲だけファイルに出力します.また,グラフ出力を指定しないと,指定された値における密度関数および分布関数の値,または,%値を出力します.Java 版ではグラフも表示されます.アプレット版及び JavaScript 版では,同様の処理を画面上で実行することが可能であり,結果はテキストエリアに出力されると共に,「確率(複数点)」を選択すればグラフも表示されます.なお,グラフに関しては,20.5.2 グラフの表示を参照して下さい.

(プログラム例 7.34 ) χ2 分布 

  添付したプログラムは,グラフ出力を指定すると χ2 分布の密度関数および分布関数の値を指定した範囲だけファイルに出力します.また,グラフ出力を指定しないと,指定された値における密度関数および分布関数の値,または,%値を出力します.Java 版ではグラフも表示されます.アプレット版及び JavaScript 版では,同様の処理を画面上で実行することが可能であり,結果はテキストエリアに出力されると共に,「確率(複数点)」を選択すればグラフも表示されます.なお,グラフに関しては,20.5.2 グラフの表示を参照して下さい.

(プログラム例 7.35 ) t 分布 

  添付したプログラムは,グラフ出力を指定すると t 分布の密度関数および分布関数の値を指定した範囲だけファイルに出力します.また,グラフ出力を指定しないと,指定された値における密度関数および分布関数の値,または,%値を出力します.Java 版ではグラフも表示されます.アプレット版及び JavaScript 版では,同様の処理を画面上で実行することが可能であり,結果はテキストエリアに出力されると共に,「確率(複数点)」を選択すればグラフも表示されます.なお,グラフに関しては,20.5.2 グラフの表示を参照して下さい.

(プログラム例 7.36 ) F 分布 

  添付したプログラムは,グラフ出力を指定すると F 分布の密度関数および分布関数の値を指定した範囲だけファイルに出力します.また,グラフ出力を指定しないと,指定された値における密度関数および分布関数の値,または,%値を出力します.Java 版ではグラフも表示されます.アプレット版及び JavaScript 版では,同様の処理を画面上で実行することが可能であり,結果はテキストエリアに出力されると共に,「確率(複数点)」を選択すればグラフも表示されます.なお,グラフに関しては,20.5.2 グラフの表示を参照して下さい.

(プログラム例7.57) Fisher の直接確率 

  添付したプログラムは,Fisher の直接確率を計算する例です.なお,Java に対してはアプレットを使って計算していますので,表示して実行可能です.

(プログラム例7.37) 乱数の発生 

  添付したプログラムは,一様乱数,指数乱数,および,正規乱数の発生方法の例です.このプログラムにおいては,標準関数の rand を使用していますが,rand は,16 ビットを使用しているためその周期が短く(最大でも,32767 ),あまり質が良い乱数を生成してくれません.そこで,メルセンヌ・ツイスタを使用した例( MT.h を含む)も添付しておきます.なお,メルセンヌ・ツイスタでは,以下に示すような乱数を生成できます.

genrand_int32() //符号なし32ビット長整数
genrand_int31() //符号なし31ビット長整数
genrand_real1() //一様実乱数[0,1] (32ビット精度)
genrand_real2() //一様実乱数[0,1) (32ビット精度)
genrand_real3() //一様実乱数(0,1) (32ビット精度)
genrand_res53() //一様実乱数[0,1) (53ビット精度)

7.6.4 多変量解析

(プログラム例 7.24 ) 最小二乗法(多項式近似) 

  添付したプログラムは,最小二乗法(多項式近似)を実行するためのものです.データの入力方法に関しては,プログラムの最後に提示してある入力例に対する説明を参考にして下さい.アプレット版及び JavaScript 版では,任意のデータに対して画面上で実行することができます.

(プログラム例 7.51 ) 重回帰分析 

  添付したプログラムは,重回帰分析を行うためのものです.データの入力方法に関しては,プログラムの最後に提示してある入力例に対する説明を参考にして下さい.アプレット版及び JavaScript 版では,任意のデータに対して画面上で実行することができます.

(プログラム例 7.52 ) 正準相関分析 

  添付したプログラムは,正準相関分析を行うためのものです.データの入力方法に関しては,プログラムの最後に提示してある入力例に対する説明を参考にして下さい.アプレット版及び JavaScript 版では,任意のデータに対して画面上で実行することができます.

(プログラム例 7.53 ) 主成分分析 

  添付したプログラムは,主成分分析を行うためのものです.データの入力方法に関しては,プログラムの最後に提示してある入力例に対する説明を参考にして下さい.アプレット版及び JavaScript 版では,任意のデータに対して画面上で実行することができます.

(プログラム例 7.54 ) 因子分析 

  添付したプログラムは,因子分析を行うためのものです.データの入力方法に関しては,プログラムの最後に提示してある入力例に対する説明を参考にして下さい.アプレット版及び JavaScript 版では,任意のデータに対して画面上で実行することができます.

(プログラム例 7.55 ) クラスター分析 

  添付したプログラムは,クラスター分析を行うためのものです.データの入力方法に関しては,プログラムの最後に提示してある入力例に対する説明を参考にして下さい.アプレット版及び JavaScript 版では,任意のデータに対して画面上で実行することができます.

(プログラム例 7.62 ) 分散分析 

  添付したプログラムは,分散分析を行うためのものです.データの入力方法に関しては,プログラムの最後に提示してある入力例に対する説明を参考にして下さい.アプレット版及び JavaScript 版では,任意のデータに対して画面上で実行することができます.

7.6.5 その他

(プログラム例 7.38 ) ソート(並べ替え) 

  添付したプログラムは,関数名の引き渡し(比較する関数名を渡している),および,再帰呼び出しを使用して,4 種類の方法(バブルソート,選択ソート,クイックソート,および,バケツソート)のいずれかでソートを行う例を書いたものです.

(プログラム例 7.39 ) 基本アルゴリズム(その1) 

  添付したプログラムは,特に数学的な基本アルゴリズム(角度の和,行列式,最大公約数,三角形の面積,三点を通る平面,素数,点と直線の距離,点と平面の距離,二直線の交点,二直線間の最短距離,座標軸の回転,入出力)に関するプログラムです.探索手法に関しては,基本アルゴリズム(その2)を参照して下さい.

演習問題7

:以下の問題において,main 関数以外では基本的に入出力を行わないこと.なお,配列を使用する場合は,可能ならば,任意の大きさの配列に対応できるように作成すること.

[問1]演習問題 5 の問 1 から問 18 までを(問 6,7,11,12,13,及び,16 を除く),関数を利用して書け.

[問2]演習問題 6 の問 1 から問 13 まで(ただし,問 8,9,及び,12 を除く)を,関数を利用して書け.

[問3]演習問題 5 の問 22 と問 23 を,関数を利用して書け.ただし,f(x) の計算を別の関数で行い,二分法及び台形則による積分を実行する関数にその名前を引数として受け渡せ.

[問4]演習問題 6 の問 16 を,関数を利用して書け.

[問5]n 行 m 列の行列の足し算を行うプログラムを,関数を利用して書け.なお,関数は,任意の n 及び m に対応できるようにせよ.

[問6]キーボードから,「 English This is a pen 」 のように English の後に英文を入力すると,英文を構成する単語数及びすべての文字数を出力するプログラムを main 関数の引数を利用して書け.

静岡理工科大学 菅沼ホーム 全体目次 演習解答例 付録 索引