関数の参照呼び出しによるデータ共有 (関数同士でのメモリ共有)について理解しよう.
特別な変数「ポインタ(pointer)」が今回のポイント. まぁ,大抵の用途のプログラムは, ポインタなんて使わなくても作成できますが... ポインタを有効に利用すれば, メモリ使用量・実行時間に関して, より効率的なプログラムを実現できるようになる.
プログラムが利用するデータはメモリに格納されている. C言語では,データを操作(読み/書き)するために, 基本的には変数名を利用するが, アドレスも利用できる.
ポインタ変数の宣言時には変数名の前に ポインタ宣言子「*」を指定する. 例:int *p;
変数名の前に参照演算子(アドレス演算子) 「&」を指定する. 例:p = &x;
ポインタ等の前に間接参照演算子「*」を指定する. 例:x = *p;
使用例:(実行は不要.読んで,考えて,理解しよう.)
int *p; // ポインタ変数 p の宣言.どこかの int型データを指すよ. int x = 1, y = 2; p = &x; // xのアドレスを p に代入.p は x を指すことに. y = *p; // p で間接的に xの値 を読み取り,y へ代入.y = x; と同じ. *p = 0; // p で間接的に xの値 を書き換える.x = 0; と同じ. // 結果的に,x = 0, y = 1 となる
この例では,ポインタ p によって変数x の値を間接的に操作している.
変数 y に対しても同様に, ポインタ p にアドレス &y を代入すれば, 間接操作が可能となる.
複数個の変数のどれでも, 1個のポインタ p だけを使って操作できる. これをうまく利用すれば,各々の変数を直接に別々に操作するよりも, ソースコードを短く効率的に記述できるようになる.
ポインタ経由の間接参照について実験してみよう. 実験用プログラム indirect.c:(右クリック でダウンロード)
#include <stdio.h> int main(void) { int x = 1; int y = 2; int *p; // 変数p はポインタだよ // データの確認 printf("&x = %p, x = %d\n", &x, x); // x のアドレスと値を確認 printf("&y = %p, y = %d\n", &y, y); // y のアドレスと値を確認 printf("\n"); // x の間接参照 p = &x; // ポインタp に変数x を参照させたよ printf("p = %p, *p = %d\n", p, *p); // 参照先の x のアドレスと値になるよ *p = 128; // 間接参照で x に128が代入されるよ printf("p = %p, *p = %d\n", p, *p); // 参照先の x のアドレスと値になるよ // データの確認 printf("&x = %p, x = %d\n", &x, x); // x のアドレスと値を確認 printf("&y = %p, y = %d\n", &y, y); // y のアドレスと値を確認 printf("\n"); // y の間接参照 p = &y; // ポインタp に変数y を参照させたよ printf("p = %p, *p = %d\n", p, *p); // 参照先の y のアドレスと値になるよ *p = 256; // 間接参照で y に256が代入されるよ. printf("p = %p, *p = %d\n", p, *p); // 参照先の y のアドレスと値になるよ // データの確認 printf("&x = %p, x = %d\n", &x, x); // x のアドレスと値を確認 printf("&y = %p, y = %d\n", &y, y); // y のアドレスと値を確認 printf("\n"); return (0); }
コンパイルと実行:
$ cc indirect.c -o indirect $ ./indirect &x = 0x7ffde9823624, x = 1 &y = 0x7ffde9823620, y = 2 p = 0x7ffde9823624, *p = 1 # p が x を指している(p は &x,*p は x) p = 0x7ffde9823624, *p = 128 # *p の値を変えると... &x = 0x7ffde9823624, x = 128 # ... x も変わる &y = 0x7ffde9823620, y = 2 p = 0x7ffde9823620, *p = 2 # p が y を指している(p は &y,*p は y) p = 0x7ffde9823620, *p = 256 # *p の値を変えると... &x = 0x7ffde9823624, x = 128 &y = 0x7ffde9823620, y = 256 # ... y も変わる
ポインタ経由で他の変数を操作できることを理解できたかな? ソースコードと実行結果とを徹底的に読み比べよう.
プログラミング言語の一般論として, 関数の呼び出しの際,引数の使用方法には,次の2種類がある:
呼出先の関数内で仮引数の変数値を変化させても, 呼出元の関数内では実引数の変数値は変化しない.
呼出先の関数内で仮引数の変数値を変化させると, 呼出元の関数内でも実引数の変数値が変化する.
C言語の場合,関数呼び出しはすべて値渡しとして実行される. しかし,ポインタを引数にすれば,実質的に参照渡しと同じことになる. また,関数の戻り値は1個だけ(またはゼロ個)に制限されているが, 参照渡しを利用すれば,引数を実質的に第2,第3,...の戻り値とすることも可能となる.
変数値を書き換えるような関数を作成し, 値渡しと参照渡しの動作の違いを確認してみよう.
まず,値渡し版の失敗例を試してみよう. reset.c:
#include <stdio.h> /* 引数 x の値をゼロにする関数 (値渡し版,失敗例) */ void reset(int x) // 仮引数 x は main() 側の x とは別物 { printf("sub():1: x = %d\n", x); x = 0; // 変数x の値をゼロにする printf("sub():2: x = %d\n", x); } int main(void) { int x = 123; printf("main():1: x = %d\n", x); reset(x); // 変数x の値による呼び出し printf("main():2: x = %d\n", x); return (0); }
実行結果:
main():1: x = 123 sub():1: x = 123 sub():2: x = 0 # sub() 側ではリセットできたが... main():2: x = 123 # main() 側に戻るとできてない
次に,関数を参照渡し版に書き換えてみよう.
/* 引数 *p の値をゼロにする関数 (参照渡し版,成功例) */ void reset(int *p) // ポインタp で x のアドレスを受け取る { printf("sub():1: *p = %d\n", *p); *p = 0; // ポインタp の参照先の値をゼロにする printf("sub():2: *p = %d\n", *p); } int main(void) { int x = 123; printf("main():1: x = %d\n", x); reset(&x); // 変数x の参照による呼び出し printf("main():2: x = %d\n", x); return (0); }
実行結果:
main():1: x = 123 sub():1: *p = 123 sub():2: *p = 0 # sub()ではリセットできた main():2: x = 0 # main()でもリセットできてた
ちなみに,これまでによく利用してきた入力関数 scanf() も 実は参照渡しの関数である. たとえば,scanf("%d", &x); として呼び出すと, scanf()内で入力された数値が 呼出元の変数 x のメモリ領域に格納される.
配列データを引数として関数を呼び出すこともできる. ただし,C言語の配列は,次のような性質をもつ:
たとえば,a[0] と a[1], a[1] と a[2], 等は互いにアドレスの隣接したメモリ領域を使う.
たとえば,a は &a[0] と同じ意味をもつ.
要するに配列名はポインタと同じように見えるので, 逆に,アドレス(ポインタ)を配列名としても利用できる. たとえば,int a[10]; int *p; p = a; とすると, p[5] は a[5] と同じ意味をもつ.
したがって,関数の引数が配列の場合には,常に参照渡しとなり, 配列のメモリ領域は呼出元と呼出先の関数の間で共有されることになる.
配列を引数とする関数の定義では, 実引数は配列名(先頭アドレス),仮引数はポインタとすることになる. さらに,配列の要素数も引数として適切に利用し, バッファオーバラン等の重大事故を防止する必要がある.
配列引数の適切な使用例:set.c
#include <stdio.h> /* 配列要素へ安全に数値を代入する関数 引数 p:配列の先頭アドレス n:配列の要素数 i:要素番号 v:数値 戻り値:エラーコード(0:正常,1:エラー発生) */ int set(int *p, int n, int i, int v) { if (i < 0) return (1); // バッファオーバラン防止 if (i >= n) return (1); // 〃 p[i] = v; // p は a なので,a[i] = v; と同じこと return (0); } #define NUM 10 // 配列a の要素数 int main(void) { int a[NUM]; int n = 0; n += set(a, NUM, 8, 1); // これは... // a[8] = 1; n++; // ...これと同じだよね?なぜわざわざ関数化? printf("8: OK\n"); // 動作継続の確認 n += set(a, NUM, -100, 1); // 要素番号を間違えてみよう // a[-100] = 1; n++; // ...これだとオーバランで強制終了かも printf("-100: OK\n"); // 動作継続の確認 n += set(a, NUM, 10000, 1); // さらに盛大に間違えてみよう // a[10000] = 1; n++; // ...これもオーバランかも printf("10000: OK\n"); // 動作継続の確認 printf("オーバラン防止回数:%d\n", n); return (0); }
まずは,このソースをそのままコンパイルし実行しよう. その後,コメント化されている a[-100] = 1; 等の行を 有効化(「//」を削除)し,わざとオーバランを発生させてみよう.
安全対策のためコードは長く,プログラミングは面倒になりますが, これはソフトウェアの品質確保のために必要な投資です. ただし,できるだけ関数化・効率化を図りましょう.
(今回の課題です.)
次のような5個の配列操作関数を定義せよ:
なお,仮引数の p および q は 配列の先頭アドレスのポインタ, n は配列の要素数, v は初期値である.
テスト用ソースファイル断片 array.c:
#include <stdio.h> ... #define NUM 5 int main(void) { int a[NUM]; int b[NUM]; int np, nm, nz; print(a, NUM); // ゴミが表示される nz = check(a, NUM, &np, &nm); // 符号をカウント printf("ゼロ|正|負:%d|%d|%d 個\n", nz, np, nm); reset(a, NUM, 7); // a の全要素を 7 にする print(a, NUM); // 7 7 7 ... が表示される nz = check(a, NUM, &np, &nm); // 符号をカウント printf("ゼロ|正|負:%d|%d|%d 個\n", nz, np, nm); input(a, NUM); // てきとーなデータを a にキーボード入力すると... print(a, NUM); // そいつらが表示される copy(a, b, NUM); // a の全要素を b にコピーする print(b, NUM); // b として,a と同じ内容が表示される return (0); }
余裕ある人は,他の配列操作関数を考案・実装してみては? 合計・平均・最大・最小などの計算とか.
質問 Q1〜Q3 に回答し,電子メールで提出せよ.
メールの送信形式を必ず「テキスト形式 or プレーンテキスト」に変更せよ.