180 7. 関数(その2)
9 #define SIZE 100 10 #define WIDTH 10 11 #define TRUE 1
12 void set_an_array_random(int x[], int size);
13 void pretty_print(int x[], int size);
14 void quicksort(int x[], int from, int to);
15 int partition(int x[], int from, int to);
16 int main(void) 17 {
18 int a[SIZE], seed; /* a[SIZE] を外部配列にはしない */
19 printf("Input a random seed (0 - %d): ", RAND_MAX);
20 scanf("%d", &seed);
21 srand(seed);
22 set_an_array_random(a, SIZE);
23 printf("\nbefore sorting:\n");
24 pretty_print(a, SIZE);
25 quicksort(a, 0, SIZE-1);
26 printf("\nafter sorting:\n");
27 pretty_print(a, SIZE);
28 return 0;
29 }
30 /*---*/
31 /* 引数で与えられた配列の各要素をランダムに設定する */
32 /*--- */
33 /* (仮引数) x : int型配列 */
34 /* size : int型配列 x の大きさ */
35 /* (機能) : 配列要素 x[0]〜x[size-1] に 0〜999 の間の乱数を */
36 /* 設定する。 */
37 /*---*/
38 void set_an_array_random(int x[], int size) 39 {
40 int i;
41 for (i=0; i<size; ++i) 42 x[i] = rand() % 1000;
43 }
7.4. 自習 一次元配列を関数パラメータとして受渡しする方法 181
44 /*---*/
45 /* 引数で与えられた配列の要素を順番に全て出力する */
46 /*--- */
47 /* (仮引数) x : int型配列 */
48 /* size : int型配列 x の大きさ */
49 /* (機能) : 配列要素 x[0]〜x[size-1] の値を順番に全て出力 */
50 /* する。但し、各々の値は横幅7カラムのフィールド */
51 /* に出力することにし、また、1行にWIDTH個の要素 */
52 /* を出力する。 */
53 /*---*/
54 void pretty_print(int x[], int size) 55 {
56 int i, count=1;
57 for (i=0; i<size; ++i, ++count) { 58 printf("%7d", x[i]);
59 if (count >= WIDTH) { 60 printf("\n");
61 count = 0;
62 }
63 }
64 if (count > 1) 65 printf("\n");
66 }
67 /*---*/
68 /* 引数で与えられた配列要素を小さい順に並べ替える */
69 /*--- */
70 /* (仮引数) x : int型配列 */
71 /* from : int型配列 x の添字 */
72 /* to : int型配列 x の添字 */
73 /* (関数値) : なし */
74 /* (機能) : quicksortアルゴリズムを使って、配列要素 */
75 /* x[from],x[from+1],x[from+2], ..., x[to] */
76 /* を値の小さい順に並べ替える。 */
77 /*---*/
78 void quicksort(int x[], int from, int to) 79 {
80 int pivot_sub; /* pivot subscript の意 */
81 if (from < to) {
82 pivot_sub = partition(x, from, to); /* 分割操作 */
83 quicksort(x, from, pivot_sub - 1);
182 7. 関数(その2)
84 quicksort(x, pivot_sub + 1, to);
85 }
86 }
87 /*---*/
88 /* 引数で与えられた配列の部分列にquicksortの分割操作を施す */
89 /* (quicksortの関数) */
90 /*--- */
91 /* (仮引数) x : int型配列 */
92 /* from : int型配列 x の添字 */
93 /* to : int型配列 x の添字 */
94 /* (関数値) : 分割操作によって得られた枢軸要素の添字番号 */
95 /* (以下の「(機能)」の項で出て来る pivot_sub) */
96 /* (機能) : x[from]〜x[to]を並べ替えて */
97 /* max{x[from],...,x[pivot_sub-1]} <= x[pivot_sub]*/
98 /* x[pivot_sub] < min{x[pivot_sub+1],...,x[to]} */
99 /* となるようにする。 */
100 /*---*/
101 int partition(int x[], int from, int to) 102 {
103 int pivot;
104 pivot = x[from]; /* 最初の要素を枢軸要素に選ぶ。 */
105 while (TRUE) { /* 工夫の余地あり。 */
106 for ( ; from<to && x[to]>pivot; --to)
107 ;
108 if (from == to) { 109 x[from] = pivot;
110 return from;
111 }
112 x[from++] = x[to];
113 for ( ; from<to && x[from]<=pivot; ++from)
114 ;
115 if (from == to) { 116 x[to] = pivot;
117 return to;
118 }
119 x[to--] = x[from];
120 }
121 } ここで、
• プログラムの22行目,24∼25行目,27行目,82∼84行目が、配列を引数にして関数を呼
7.4. 自習 一次元配列を関数パラメータとして受渡しする方法 183
んでいる部分である。この様に、配列を関数に引き渡したい時には、単に配列名を実 引数として与えればよい。 [一次元配列の場合、配列名は計算機内部では先頭要素を 指す定数ポインタとして扱われるから、これで、配列の先頭番地が関数に引き渡され ることになります。]
• プログラムの12∼15行目,38行目,54行目,78行目,101行目に現われる int x[] の部 分は、仮引数 x がint型一次元配列の先頭番地と結合することを宣言している。 [但 し、プログラマ側が「int型一次元配列の先頭番地」という意図で宣言したとしても、
コンパイラ側はこれを「int型領域へのポインタ」と理解するだけである。] この部 分は、
int *x あるいは
int x[ 配列の大きさ ] と書くことも出来る。
• プログラム39∼43行目,55∼66行目,79∼86行目,102∼121行目の関数本体の中では、仮 引数の配列はこれまでと全く同じ様に使うことが出来ます。
一次元配列 a を関数の引数として受渡しする方法:
• 仮引数側では、
データ型 配列名[] または データ型 *配列名 または データ型 配列名[ 大きさ] という書き方をする。
補足:配列の大きさを明示する必要はない。 明示したとしても捨てられる。
• 関数本体の中では、仮引数の配列要素の参照はこれまでと全く同じ様な書き方をする。
• 実引数側では、
a または &a[0]
という書き方をする。
配列名は、
計算機内部では先頭要素を指す定数ポインタとして扱われている。
例題 7.4 (一次元配列の一部を関数パラメータとして受け渡す) double型一次元配
列の部分要素列についての情報を引数として受け取り、その部分配列内の要素の総和 を計算して返す関数 sum() を定義せよ。 そして、
v[k]= 249−k (k = 0∼49)
という風に値の設定された大きさ50のdouble型一次元配列について、この関数を用 いて、
v[0]+v[1]+v[2]+· · ·+v[49], v[40]+v[41]+v[42]+· · ·+v[49], v[20]+v[21]+v[22]+· · ·+v[39]
の値を計算して出力するCプログラムを作成せよ。
(考え方) 素直に考えるなら、部分配列内の要素の総和を計算する関数sum() には
184 7. 関数(その2)
1 配列の名前(i.e.先頭要素の番地), 2 総和の始めとなる配列要素の添字番号, 3 総和を締めくくる配列要素の添字番号
の3つを引数として引き渡すことが頭に浮かぶ。もちろん、これは妥当な考えで、関数
sum() も使い易くなる。(=⇒ 具体的なプログラミングは演習問題として残しておく。)
しかし、配列を関数パラメータとして受け渡しする場合、実際に受け渡されるのは配列要 素へのポインタ(i.e.番地)に他ならない。 呼ばれる関数側としては、そのポインタが実 際に関数を呼んだ側で確保された配列の先頭番地を指しているかどうかは重要ではなく、
そのポインタが指す領域以降に然るべき型のデータ領域が十分に長く(i.e.関数実行の間 に配列アクセス違反を起こさない程度に)確保されていれば良いだけである。 従って、逆 に、これさえ守れば良い訳で、もし
double型配列の名前 a と大きさ sizeを引数として受け取り、
a[0]+a[1]+· · ·+a[size-1] を計算して返す関数 double sum(double a[], int size)
が定義できているなら、この関数をsum(&v[from], size)という風に使うことも許される はずで、この呼び出しによって部分配列の総和v[from]+v[from+1]+· · ·+v[from+size-1]
が計算されることになる。 '
&
$
% 確認:
配列要素v[from]∼v[from+size-1]がsum()を呼び出す側で確保さ れていれば、確かに、
3 &v[from]は配列要素を指すポインタで、
3 &v[from]番地以降にも同じ型のデータが十分長く続いている。
呼び出された側の関数が実際にメモリ確保された領域だけを使うよう にするのはプログラマの責任である。
(プログラミング) (部分)配列の要素の総和を計算する関数 sum() は、引数として配列
の名前(先頭要素の番地)a と大きさ sizeを受け取り、a[0]+· · ·+a[size-1] を計算して 返すものとする。この関数sum() の中では、途中までの総和a[0]+· · ·+a[i] (0≤i <size) を保持するために関数名と同じ sum という名前のdouble型局所変数を用意する。 ま た、main()関数の中では、配列要素v[0]∼v[49]に対する値の設定は数学関数pow()を 使うのではなく
v[49] ←− 1,
v[48] ←− v[49]×2, v[47] ←− v[48]×2,
...
という風に行うことにして、プログラムを構成した。このCプログラムと、これをコン パイル/実行している様子を次に示す。(下線部はキーボードからの入力を表す。)
[motoki@x205a]$ nl func-bind-part-of-array-Kelley.c Enter 1 #include <stdio.h>
2 double sum(double a[], int size);
3 int main(void) 4 {
7.4. 自習 一次元配列を関数パラメータとして受渡しする方法 185
5 int i;
6 double v[50];
7 v[49] = 1.0;
8 for (i=48; i>=0; --i) 9 v[i] = v[i+1] * 2.0;
10 printf("v[0] +v[1] + ... +v[49] = %16.0f\n", sum(v, 50));
11 printf("v[40]+v[41]+ ... +v[49] = %16.0f\n", sum(&v[40], 10));
12 printf("v[40]+v[41]+ ... +v[49] = %16.0f\n", sum(v+40, 10));
13 printf("v[20]+v[21]+ ... +v[39] = %16.0f\n", sum(v+20, 20));
14 return 0;
15 }
16 /*---*/
17 /* double型配列(もしくは配列の断片)の要素の総和を計算して返す */
18 /*---*/
19 /* (仮引数) a : double型配列 */
20 /* size : double型配列 a の大きさ */
21 /* (関数値) : a[0]+a[1]+a[2]+...+a[size-1] */
22 /*---*/
23 double sum(double a[], int size) 24 {
25 int i;
26 double sum=0.0;
27 for (i=0; i < size; ++i) 28 sum += a[i];
29 return sum;
30 }
[motoki@x205a]$ gcc func-bind-part-of-array-Kelley.c Enter [motoki@x205a]$ ./a.out Enter
v[0] +v[1] + ... +v[49] = 1125899906842623 v[40]+v[41]+ ... +v[49] = 1023 v[40]+v[41]+ ... +v[49] = 1023 v[20]+v[21]+ ... +v[39] = 1073740800 [motoki@x205a]$
ここで、
• 4.5 節で説明した名前の有効範囲の規則により、sum という名前 は、プログラムの 24∼30行目のブロックの中では26行目で宣言された局所変数として解釈され、その ブロックの外では2行目と23行目で宣言された関数名として解釈される。
• プログラムの11行目 は、一次元配列の一部v[40]∼v[49]の先頭番地とその大きさ
を関数sum()に引き渡している。
186 7. 関数(その2)
'
&
$
% 補足:
関数側では受け渡されるものが元々(呼出し側で)配列として確保さ れたものかどうかのチェックはしない。仮引数として与えられるも のを配列の先頭番地と見なして処理を進めるだけである。
• プログラムの12行目 も11行目と同じ処理をしている。一次元配列の場合、配列名は 計算機内部では先頭要素を指す定数ポインタとして扱われているので、v+40 は先頭 から40個後の要素を指すポインタ値である。
'
&
$
% 補足:ポインタに関する算術は普通の算術演算とは異る。v+40の番地は実際には
&v[0]+40×sizeof(double)番地、すなわち &v[40]番地 である。