計算機言語
I
第11
回 関数(
その3)
この講義資料は
,
次の場所にあります.
http://www.math.u-ryukyu.ac.jp/~suga/gengo/2018-1/11.pdf
1
関数での配列引数の扱い前回
, C
言語で配列変数以外の引数での関数呼び出しでの動作のモデルを述べました. 1.
関数に引数のための場所をメモリに確保する.
2.
上の場所に引数の値を代入する.
3.
返り値を代入する場所と,
上で確保した引数の場所の情報を保持して,
関数に飛ぶ.
すなわち,
配列変数以外では,
関数の引数には,
元の値のコピーが渡されます.
配列を関数と引数にする場合は
,
元の値のコピーを渡すことはしません.
関数に渡る値は,
その配列の先頭要 素が保存されている場所の情報です(
この場所情報を,
ポインター(pointer)
と呼ぶ).
配列のコピーの時に述べましたが
,
配列をコピーするのは多くの動作を必要とするので,
変数のコピーを渡 すと,
関数呼び出しのオーバーヘッドが大きくなりすぎるからです.
配列を引数とする関数も
,
その記述は,
他の型を引数にするときと同じです.
例えば, int
型配列とdouble
型配列を引数に取り, int
型を返す関数は,
下のように記述します.
ただし,
上で述べたように,
配列の先頭要素 の位置情報が関数に渡されるので, 1
次元配列の場合は配列のサイズは意味を持たず,
不要です.
int hogehoge(int number[], double value[]) {
...
}
このとき
,
関数hogehoge
は次のように使います.
int time[] = {1, 1, 2};
double score[] = { 2, 3.5, 4};
int result;
result = hogehoge(time, score);
1
つまり
,
関数には,
配列の名前を引数として渡します.
関数の動作の記述では
,
仮引数を配列として,
元の配列にアクセスします.
上のhogehoge
だと, number[0], number[1],...
value[0], value[1],...
が
,
元の配列要素の値です.
この形で
,
配列を関数に渡すと,
関数の中で元の値が書き換えることができます.
上の例だと, number[0]
と いうのは,
引数として受け取った配列の先頭要素そのもので,
そこに別の値を代入することができます.
すなわ ち,
もとの配列要素そのものが,
関数hogehoge
に渡っているのと同じです.
これは
,
前回述べた「変数値を変更できる範囲を狭くする」という考え方には,
反しています.
しかし,
最初 に述べた関数呼び出しの高速化を理由に, C
では配列要素だけは,
関数に対して参照渡しになっています.
*1なお
,
配列に対する参照渡しが,
完全にデメリットかというとそうでもありません.
配列は,
同じ型が並んだ 変数ですが,
その値を大きい順や小さい順に並び替えるという操作は,
よく行われる行為です.
そのような操作 は,
次の形の関数として実装されます.
void sort(int array[]);
配列を受け取る関数は
,
配列の大きさを知るすべがないC
言語の配列引数を持つ関数は,
引数名から配列の大きさを知ることができません.
したがって,
関数内で,
配列の範囲外をアクセスしても,
コンパイルエラーになりません. C
言語の欠点で,
実行時エラーやSecurity hole
の原因になります.
配列の扱いに対する
C
言語の特徴は, C
がOS
の開発用言語として作られたことが理由です.
配列の主な利 用目的は,
文字を並べた「文字列」の操作を想定しており,
これらの操作に時間がかかると, OS
の動作が遅く なり,
コンピュータの使い勝手が悪くなるからだと思われます.
多次元配列
2
次元以上の配列を引数とする関数や,
それに対する値の受け渡し方も, 1
次元配列と同様です.
ただし,
多 次元の配列も,
その値が保持されるメモリの中では1
次元的に整列され,
どの順に値が配置されるかが,
決まっています
(
教科書p. 125
を見よ).
したがって,
目的の要素に正確にアクセスするには,
先頭以外の後ろ側の配列要素数を指定する必要があります
.
前の要素の添え字が1
進むには,
後ろがいくつ進むかを知る必要があるのです
.
これが,
教科書p. 159, Column 6-2
に書かれている内容です.
const
型修飾子上で述べたように
,
素朴に配列を引数にして関数を記述すると,
引数として受け取る関数で,
元の配列要素 の値が変更できます.
これがプログラムの読みづらさに繋がることもあるので,
これを抑止する方法として,
*1配列と同じような派生型の変数として,構造体というのもあって,これも値をコピーするのが面倒なのですが,こちらはなぜか関数 にコピーが渡るという仕様になっています.
2
const
型修飾子があります.
修飾子というのは
,
型を修飾するためのもので, const(constant
の略)
は,
これを用いて修飾すると,
その変 数の値の変更ができなくなる(
定数になる)
ものです.
引数として受け取る配列の値の変更を禁止するには
,
次のように記述します. int hogehoge(const int number[],...)
{ ...
}
2
本日の実習1
教科書を読みつつ
, p. 150, List 6-11
からp. 159, List 6-C1
までを実行せよ.
教科書
, p. 154
からの線形探索での「番兵法」は,
教科書を良く読んでその仕組みを理解してください.
計算量を減らすための非常に「うまい」方法です
.
番兵法は
,
大きな配列であれば,
線形探索に対して計算量を劇的に減らすことがわかります.
そのためには,
配列の大きさを番兵のために1
つ増やす(
よりメモリを消費する)
必要があります.
このような現象は,
よく起 こることです.
つまり,
•
計算量を少なくする.
•
使用するメモリの量を少なくする.
を両立するのは
,
難しい問題なのです.
番兵法は,
上の2
つがうまく両立する簡単でわかりやすい素晴らしい 方法なのです.
3
変数の存在時間(
記憶域クラス)
何度も述べていますが
,
変数はブロックの中だけでアクセスでき,
ブロックが違えば,
同じ変数名でも異なる オブジェクトになります.
また
,
ブロックの中にさらにブロックを作りその中に同じ変数名を持つ変数を宣言すると,
より内側のブ ロックの変数名の値が参照されます.
現実的には
,
同じ変数名を多用すると,
読みづらいプログラムになるので,
このようなことはしません.
教科書
p. 160 List 6-17
は解説のためにわざわざこのようなプログラムを書いた例で,
マネをしてはいけません.
for loop
のカウンタにi
を使うとかいう,
誰もが行うイディオムは例外ですが.
関数内の変数は
,
通常は,
関数の実行中だけ値を保持する場所が存在しており,
関数の実行が終わると,
その 変数の値を参照することはできません.
このような通常の変数を「自動変数」と言います.
自動変数は,
関数呼 び出しでその関数に実行が移った際に,
その変数を保持する場所が確保され,
関数の処理が終わると,
その変数 を保持する場所(
メモリの領域)
は,
他の変数のために解放されます.
関数の処理が終わった後も
,
値を保持し続けられるようにすることもできます.
すなわち,
プログラムの実行 中,
常にある特定の場所を確保して,
その値を保持し続けるようにするのです.
このような変数を宣言するには
, static
を型の前に書きます.
このような変数を,
「静的変数」と言います.
3
static int status;
関数内に静的変数を宣言すると
,
1.
関数の最初の処理に入った時にその領域が確保され, 0
で初期化される.
2.
関数の処理が終わって次に移る時にも,
その関数の処理で用いられた値がそのまま保持され,
次にその 関数の処理に再び入った時は,
その保持された値がそのまま残っている.
といいう動作が
,
プログラムの実行中に起こります.
4
実習2
教科書
p. 160
からp. 165
まで,
教科書を読みながらプログラムを実行せよ.
レポート問題
締め切りは