ポインタによるデータ共有について理解しよう. (まぁ,大抵の用途のプログラムは, ポインタなんて使わなくても作成できますが... ポインタを有効に利用すれば, メモリ使用量・実行時間が節約された より効率的なプログラムを実現できるようになる.)
プログラムが利用するデータはメモリに格納されている. C言語では,データを操作(読み/書き)するために, 基本的には変数名を利用するが, アドレスも利用できる.
ポインタ変数の宣言時には変数名の前に ポインタ宣言子「*」を指定する. 例:int *p;
変数名の前に参照演算子(アドレス演算子) 「&」を指定する. 例:p = &x;
ポインタ等の前に間接参照演算子 「*」を指定する. 例:x = *p;
使用例:
int *p; // ポインタ変数 p の宣言.int型データの位置を指すよ int x = 1, y = 2; p = &x; // x のアドレスを p に代入 y = *p; // 間接的に x を読み取る.y = x; と同じ *p = 0; // 間接的に x へ書き込む.x = 0; と同じ // 結果的に,x = 0, y = 1 となったハズ
ポインタ経由の間接参照について実験してみよう. 実験用プログラム indirect.c:
#include <stdio.h> int main(void) { int x = 1; int y = 2; int *p; // 変数p はポインタだよ // データの確認 printf("&x = %lx, x = %d\n", &x, x); // x のアドレスと値を確認 printf("&y = %lx, y = %d\n", &y, y); // y のアドレスと値を確認 printf("\n"); // x の間接参照 p = &x; // ポインタp に変数x を参照させたよ printf("p = %lx, *p = %d\n", p, *p); // 参照先の x のアドレスと値になるよ *p = 128; // 間接参照で x に128が代入されるよ printf("p = %lx, *p = %d\n", p, *p); // 参照先の x のアドレスと値になるよ // データの確認 printf("&x = %lx, x = %d\n", &x, x); // x のアドレスと値を確認 printf("&y = %lx, y = %d\n", &y, y); // y のアドレスと値を確認 printf("\n"); // y の間接参照 p = &y; // ポインタp に変数y を参照させたよ printf("p = %lx, *p = %d\n", p, *p); // 参照先の y のアドレスと値になるよ *p = 256; // 間接参照で y に256が代入されるよ. printf("p = %lx, *p = %d\n", p, *p); // 参照先の y のアドレスと値になるよ // データの確認 printf("&x = %lx, x = %d\n", &x, x); // x のアドレスと値を確認 printf("&y = %lx, y = %d\n", &y, y); // y のアドレスと値を確認 printf("\n"); return (0); }
ポインタ経由で他の変数を操作できることを理解できたかな?
プログラミング言語の一般論として, 関数の呼び出しの際,引数の使用方法には,次の2種類がある:
呼出先の関数内で仮引数の変数値を変化させても, 呼出元の関数内では実引数の変数値は変化しない.
呼出先の関数内で仮引数の変数値を変化させると, 呼出元の関数内でも実引数の変数値が変化する.
C言語の場合,関数呼び出しはすべて値渡しとして実行される. しかし,ポインタを引数にすれば,実質的に参照渡しと同じことになる. また,関数の戻り値は1個以内であるが, 参照渡しによって,引数を第2,第3の戻り値としても利用可能となる.
変数値を書き換えるような関数を作成し, 値渡しと参照渡しの動作の違いを確認してみよう.
まず,値渡し版の失敗例を試してみよう. reset.c:
#include <stdio.h> /* 引数 x の値をゼロにする関数 (値渡し版,失敗例) */ void reset(int 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 でアドレスを受け取る { 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; return (0); } int main(void) { int a[10]; printf("%d\n", a[8]); set(a, 10, 8, 888); // a[8] = 888; と同じですが... set(a, 10, -100, 0); // a[-100] = 0; だとオーバランで強制終了かも set(a, 10, 10000, 0); // a[10000] = 0; でもオーバランかも printf("%d\n", a[8]); // オーバランを発生させず,無事に終了 return (0); }
次のような配列操作関数を定義せよ:
なお,引数の p および q は配列の先頭アドレスのポインタ, n は配列の要素数, v は初期値である.
テスト用ソースファイル断片 array.c:
#include <stdio.h> #define NUM 5 ... int main(void) { int a[NUM], b[NUM]; print(a, NUM); // ゴミが表示される reset(a, NUM, 7); // a の全要素を 7 にする print(a, NUM); // 7 7 7 ... が表示される input(a, NUM); // てきとーなデータを a にキーボード入力すると... print(a, NUM); // そいつらが表示される copy(a, b, NUM); // a の全要素を b にコピーする print(b, NUM); // a と同じ内容が表示される return (0); }
余裕ある人は,他の配列操作関数を考案・実装してみては? 合計・平均・最大・最小などの計算とか.
提出: