[program 1]
ポインタ変数に加減算を行うと, 加わえたり減らしたりした要素の数だけ離れた要素を指す.
#include <stdio.h> #include <stdlib.h> // ポインタ変数に整数 n を加えると, // そのポインタの型の n 要素分加えたアドレスを表わす. // ここで, n は負の数でも構わない. // また, 配列要素が存在しない領域を指すことも可能で注意が必要である. int main(void) { double x[5] = {0.0, 1.0, 2.0, 3.0, 4.0}; double *ptr; ptr = x; // [1] 配列名x は &x[0] はの意味. printf("[1] x = %p\n", x); printf("[1] &x[0] = %p\n", &x[0]); printf("[1] sizeof(x) = %u\n", sizeof(x)); printf("[1] sizeof(x[0]) = %u\n", sizeof(x[0])); printf("[1] ptr = %p\n", ptr); // ptr には x[0] のアドレスが代入されている. printf("[1] *ptr = %e\n", *ptr); // *ptr は ptrが指す変数の値. ここでは x[0] // * は ポインタが指すアドレス上の値を得る演算子であることに注意すること. // [2] ptr はdouble 型で宣言したので, ptr + 1 は ptrからdouble 型のサイズ分移動したアドレス. printf("[2] ptr = %p\n", ptr+1); // ptr + 1 は double 型の次の要素を指す. printf("[2] *ptr = %e\n", *(ptr+1)); // ptr+1 は次の要素のアドレスなので, *(ptr+1) は x[1]の値. printf("[3] ptr = %p\n", ptr+2); // [3] ptr に 2を加えると double 型で2要素分離れたアドレスを指す. printf("[3] *ptr = %e\n", *(ptr+2)); // ptr +2 は, 2要素分後ろのアドレスなので, *(ptr+2) は x[2]の値. printf("[4] ptr = %p\n", ptr-1); // [4] ptr-1は, double 型で1要素分戻ったアドレスになる. printf("[4] *ptr = %e\n", *(ptr-1)); // 一般に, ポインタの減算も可能であるが, // ここでは, ptr=xとしたので x[-1]を指すことになってしまい適切ではない. // double 型のポインタを宣言すると double 型の1要素を単位としたアドレスの増減ができるようになっている. return EXIT_SUCCESS; }
[program 2]
次のプログラムでは, 加減算をした結果を再び同じポインタ変数に代入している.
#include <stdio.h> #include <stdlib.h> // ポインタ変数に整数 n を加えると, // そのポインタの型の n 要素分加えたアドレスを表わす. // ここで, n は負の数でも構わない. // また, 配列要素が存在しない領域を指すことも可能で注意が必要である. int main(void) { double x[5] = {0.0, 1.0, 2.0, 3.0, 4.0}; double *ptr; ptr = x; // [1] x は &x[0] printf("[1] ptr = %p\n", ptr); // ptr には x[0] のアドレスが代入される. printf("[1] x = %p\n", x); printf("[1] x[0] = %p\n", &x[0]); printf("[1] *ptr = %e\n", *ptr); // *ptr は ptrが指す変数の値. すなわち x[0] ptr = ptr + 1; // [2] ptr はdouble 型で宣言したので printf("[2] ptr = %p\n", ptr); // ptr+1 は ptrからdouble 型のサイズ分移動したアドレス. printf("[2] *ptr = %e\n", *ptr); // ptr の値は x[1] のアドレスなので, *ptr は x[1]の値. ptr = ptr + 2; // [3] さらに ptr に 2を加えてみる. printf("[3] ptr = %p\n", ptr); // さらに double 型で2要素分移動したアドレスになる. printf("[3] *ptr = %e\n", *ptr); // ptr の値は x[3] のアドレスなので, *ptr は x[3]の値. ptr = ptr - 1; // [4] さらに ptr から 1を引いてみる. printf("[4] ptr = %p\n", ptr); // double 型で1要素分戻ったアドレスになる. printf("[4] *ptr = %e\n", *ptr); // ptr の値は x[2] を指し, *ptr は x[2]の値. return EXIT_SUCCESS; }
添字演算子を用いると配列と同じ表記ができる. [program 3]
#include <stdio.h> #include <stdlib.h> // ポインタ変数に整数 n を加えると, // そのポインタの型の n 要素分加えたアドレスを表わす. // ここで, n は負の数でも構わない. // また, 配列要素が存在しない領域を指すことも可能で注意が必要である. int main(void) { double x[5] = {0.0, 1.0, 2.0, 3.0, 4.0}; double *ptr; ptr = x; // [1] 配列名x は &x[0] はの意味. printf("[1] x = %p\n", x); printf("[1] &x[0] = %p\n", &x[0]); printf("[1] sizeof(x) = %u\n", sizeof(x)); printf("[1] sizeof(x[0]) = %u\n", sizeof(x[0])); printf("[1] ptr = %p\n", ptr); // ptr には x[0] のアドレスが代入されている. printf("[1] *ptr = %e\n", *ptr); // *ptr は ptrが指す変数の値. ここでは x[0] // ポインタ変数の後ろに[]を付けると, ポインタが指すアドレスから, 添字の数だけ移動したアドレスにある値を得る. // [2] ptr はdouble 型で宣言したので, ptr[1] は ptrからdouble 型のサイズ分移動したアドレスにある値. printf("[2] ptr = %p\n", &ptr[1]); // ptr[1] は double 型の次の要素を指す. printf("[2] *ptr = %e\n", ptr[1]); printf("[3] ptr = %p\n", &ptr[2]); // [3] ptr に 2を加えると double 型で2要素分離れたアドレスを指す. printf("[3] *ptr = %e\n", ptr[2]); // ptr[2] は, 2要素分後ろのアドレスにある値. printf("[4] ptr = %p\n", &ptr[-1]); // [4] ptr[-1]は, double 型で1要素分戻ったアドレスになる. printf("[4] *ptr = %e\n", ptr[-1]); // 一般に, ポインタの減算も可能であるが, // ここでは, ptr=xとしたので x[-1]を指すことになってしまい適切ではない. // double 型のポインタを宣言すると double 型の1要素を単位としたアドレスの増減ができるようになっている. return EXIT_SUCCESS; }
[program 4]
#include <stdio.h> int main(void) { int i; double x[5] = {0.0, 1.0, 2.0, 3.0, 4.0}; double *ptr; ptr = x; printf("x = %p\n", x); printf("&x[0] = %p\n", &x[0]); printf("ptr = %p\n", ptr); for(i=0; i<5; i++) printf("ptr+%d = %p\n", i, ptr+i); for(i=0; i<5; i++) printf("&ptr[%d] = %p\n", i, &ptr[i]); for(i=0; i<5; i++) printf("ptr[%d]=%g\n", i, ptr[i]); return 0; }
以下に, 関数呼び出しを行う際の配列やポインタ変数の記憶域上でのイメージについて調べるプログラムの例を挙げる.
[program 5]
#include <stdio.h> #include <stdlib.h> void func(double *x) { printf("(2) &x = %p\n", &x); // 変数 x が存在するアドレス. printf("(2) x = %p\n", x); // xの内容. ポインタであるので値はアドレス. printf("(2) sizeof(x) = %u\n", sizeof(x)); // サイズ x[2] = 200.0; // 呼び出し側の配列要素の値が変更になる. // x は &x[0] (すなわち 配列xの先頭のアドレス) の意味. // x[2] は 「x + 2*sizeof(double)」 のアドレスに格納されている値. } int main(void) { double a[5] = {0.0, 1.0, 2.0, 3.0, 4.0}; printf("(1) a=%p\n", a); // アドレス printf("(1) a[2] = %g\n", a[2]); // 値 printf("(1) sizeof(a[2]) = %u\n", sizeof(a[2])); //サイズ printf("(1) sizeof(a) = %u\n", sizeof(a)); //サイズ func(a); // 関数funcを呼び出す際に, 引数x に対して, x=aという代入がおきる. printf("(3) a[2] = %g\n", a[2]); // 値 return EXIT_SUCCESS; }
関数の引数として使う範囲では, double *x double x[]は同じ意味. ともにポインタ変数として使われている.
[program 6]
#include <stdio.h> #include <stdlib.h> void func(double x[]) // <<== ここに注目. { printf("(2) &x = %p\n", &x); printf("(2) x = %p\n", x); printf("(2) sizeof(x) = %u\n", sizeof(x)); x[2] = 200.0; } int main(void) { double a[5] = {0.0, 1.0, 2.0, 3.0, 4.0}; printf("(1) a=%p\n", a); printf("(1) a[2] = %g\n", a[2]); printf("(1) sizeof(a[2]) = %u\n", sizeof(a[2])); printf("(1) sizeof(a) = %u\n", sizeof(a)); func(a); printf("(3) a[2] = %g\n", a[2]); return EXIT_SUCCESS; }
実行例
(1) a=0xbfc9c318 (1) a[2] = 2 (1) sizeof(a[2]) = 8 (1) sizeof(a) = 40 (2) &x = 0xbfc9c300 (2) x = 0xbfc9c318 (2) sizeof(x) = 4 (3) a[2] = 100
[自由課題]
以下に, 配列とポインタの関係を調べるプログラムの例を挙げる. 自分で作成したプログラムについて, 各変数のアドレス, サイズ, 値を調べよ. これにより, プログラム実行時に各変数の領域が記憶域のどこに確保され, どのように使用されているか答えよ.
#include <stdio.h> #include <stdlib.h> int main(void) { char str[] = "ABC"; char *ptrA = "123"; char *ptrB = str; // [a] str, ptrA, ptrB の記憶域上での配置を答えよ. printf("%p\n", str); printf("%p\n", &ptrA); printf("%p\n", &ptrB); printf("%u", sizeof(ptrA)); printf("%u", sizeof(ptrB)); // [b] ptrA や ptrB の値が何を表しているか答えよ. printf("%p\n", ptrA); printf("%p\n", ptrB); // 文字列リテラルは, それ自身のアドレスを保持する. // [c] ここで出力される結果と, str, ptrA, ptrBとの関係を答えよ. printf("%p\n", "ABC"); printf("%p\n", "123"); // C 言語における通常の用法. printf("str = %s\n", str); printf("ptrA = %s\n", ptrA); printf("ptrB = %s\n", ptrB); return EXIT_SUCCESS; }
[自由課題]
以下に, 関数に配列を渡すときの変数について調べるプログラムの例を挙げる. 上と同様に, 各配列変数についてプログラム実行時に各変数の領域が記憶域のどこに確保され, どのように使用されているか答えよ.
#include <stdio.h> #include <stdlib.h> // main 関数内の 配列aとfunc関数の仮引数x の // アドレス, サイズ, 値について答えよ. // さらに, このプログラムを実行した時の a と x の関係について答えよ. void func(double x[]) { double b[5] = {0.0, 10.0, 20.0, 30.0, 40.0}; printf("&x = %p\n", &x); // 変数 x が存在するアドレス. printf("x = %p\n", x); // xの内容. ポインタであるので値はアドレス. printf("sizeof(x) = %u\n", sizeof(x)); // サイズ x = b; // x はポインタであるのでアドレスを代入できる. printf("x = %p\n", x); printf("b = %p\n", b); } int main(void) { double a[5] = {0.0, 1.0, 2.0, 3.0, 4.0}; printf("a=%p\n", a); // アドレス printf("a[0] = %e\n", a[0]); // 値 printf("sizeof(a[0]) = %u\n", sizeof(a[0])); //サイズ printf("sizeof(a) = %u\n", sizeof(a)); //サイズ func(a); printf("a=%p\n", a); // アドレス return EXIT_SUCCESS; }
関数を呼び出す際, 値を渡す場合は呼び出し側の変数の値は変わらないが, ポインタを渡す場合は呼び出し側の変数の値は変わる. この観点から, 関数と変数をここでもう一度整理してみる.
次の例では, 予め aに3が代入した後, 関数funcを呼び出している. このとき, 関数にはa の値 (すなわち3) が渡され, 仮引数の double x に代入され, 関数内で利用される. 仮引数は, 呼び出し側の変数とは別の変数で, 関数内で値を変更しても, 呼び出し側には反映されない.
#include <stdio.h> #include <stdlib.h> // 仮引数は, 宣言された関数の中のみで有効. void func(double x) { x = 5.0; } int main(void) { double a = 3.0; printf("a=%g\n", a); func(a); // a の値は3. 呼出し時に仮引数xに3が代入される. printf("a=%g\n", a); return EXIT_SUCCESS; }
仮引数や関数内で宣言された変数は, 呼び出し側からは遮蔽されており, これらの値を変更しても呼び出し側の変数の値は変更されない. したがって, 関数を呼び出すときは関数の入力と出力だけを考えればよく, 内部で使用する変数が呼び出し側の変数の値を変えてしまうことがない. これは, 大きなプログラムを作成する上で重要な「構造化」の機能である.
#include <stdio.h> #include <stdlib.h> void func(double *x) // x はアドレスを代入するための変数 (ポインタ変数). { *x = 5.0; // xが指す領域に値を代入する時は, * を用いる. } int main(void) { double a = 3.0; printf("a=%g\n", a); func(&a); // a のアドレスを渡す. x にアドレスが代入される. printf("a=%g\n", a); return EXIT_SUCCESS; }
C言語では, 関数は1つのオブジェクトしか返せないので, ここで挙げた機能は, 関数から複数の値を返したいときに利用できる. ただし, 1つの構造体の中に複数の変数を宣言し, その構造体を返す関数とすれば, 複数の値を返すことができる.
配列名は, その配列へのポインタで, 配列の先頭の要素のアドレスを保持している. 関数の仮引数として, double x[]のように[ ] を用いて宣言するとその変数は, ポインタになり, アドレスを代入できる変数になる. 関数の仮引数として宣言する場合には, double x[] と double *x は等価で ともにポインタ変数である.
次の例では, main 関数内でaは, 5つの要素からなるdouble 型の配列 double a[5]の先頭のアドレスを表す. main 関数内からfunc関数を呼び出す際, 仮引数の x にそのアドレスが代入される.
x[2] は x が保持するアドレスから double 型で2要素分先のアドレス上の値を意味し, x には main 関数中の a が代入されているので, a[2] の値が変更されることになる.
#include <stdio.h> #include <stdlib.h> // 仮引数 double x[] の x はポインタ変数. // 関数の仮引数として用いる範囲では, double *x と等価. void func(double x[]) { x[2] = 500.0; } int main(void) { double a[5] = {0.0, 1.0, 2.0, 3.0, 4.0}; printf("a=%g\n", a[2]); func(a); // a は double a[5] の先頭のアドレス. printf("a=%g\n", a[2]); return EXIT_SUCCESS; }
このような, 配列へのポインタを関数に渡し, 関数内で演算や, 変更を行う機能は, 多くの要素を持つ配列を扱うプログラムでは重要である. ポインタを用いると, 先頭アドレスを関数に渡すだけで, 配列要素の代入が起きない. これにより, 実行速度が速くなり, より小さい記憶域で計算が可能となる.
C言語では, 配列の各要素の値を関数に渡すことはできない. 必要であれば, 構造体の中に配列を宣言することで, 実現できる.
例えば, 次のようなプログラムを考えてみる.
#include <stdio.h> #include <stdlib.h> void data_print(int x0, int x1, int x2, int x3) // [2] x0, x1, ... が記憶域に確保される. { printf("[0] %d\n", x0); printf("[1] %d\n", x1); printf("[2] %d\n", x2); printf("[3] %d\n", x3); } int main(void) { int a0, a1, a2, a3; // [1] a0, a1,... の領域が記憶域に確保される. a0 = 0, a1 = 1, a2 = 2, a3 = 3; data_print(a0, a1, a2, a3); // [2] 実引数 a0, a1,... が 仮引数 x0, x1,... に代入される. return EXIT_SUCCESS; }このプログラムでは, main 関数内の変数の値 a0, a1, ... を実引数として関数 data_print を呼び出し, 次のように実行される.
同じことを配列へのポインタを用いると次のようになる. int a[4]やint x[]と定義したときの aやx はアドレスを表すことに注意.
#include <stdio.h> #include <stdlib.h> void data_print(int x[]) // [2] ポインタ変数 xが記憶域に確保される. { printf("&x=%p\n", &x); // x のアドレス. printf(" x=%p\n", x); // x の値. printf("[0] %d\n", x[0]); // x[0] は xが指すアドレスの 整数の値. printf("[1] %d\n", x[1]); // x[1] は xが指すアドレスの1つ先の整数の値. printf("[2] %d\n", x[2]); // x[2] は xが指すアドレスの2つ先の整数の値. printf("[3] %d\n", x[3]); // x[3] は xが指すアドレスの3つ先の整数の値. } int main(void) { int a[4]; // [1] a[0], a[1],... の領域が記憶域に確保される. a[0] = 0, a[1] = 1, a[2] = 2, a[3] = 3; printf(" a=%p\n", a); data_print(a); // [2] 実引数 a の値が 仮引数 x に代入される. return EXIT_SUCCESS; }このプログラムを実行すると例えば次のように標準出力に出力される.
a=0xbf9ed894 &x=0xbf9ed880 x=0xbf9ed894 [0] 0 [1] 1 [2] 2 [3] 3この場合, このプログラムは次のように実行されている.
大きな配列を用いたプログラムを実行する上では, 以下の点が重要である.
C言語では配列を関数に渡すとき, アドレスを渡すことはできるが, 各要素の値を渡すことはできない. 構造体の中に配列を宣言すると, 実質的にアドレスを渡すことと, 値を渡すことができる. また, 構造体を用いると表記上は配列でない変数と同じ表記法で, 関数宣言や関数呼出しをできる.
ここでは, 構造体を用いて, 関数にアドレスを渡すことと, 各要素を渡すことをより直接比較してみよう. ただし, 構造体の知識が必要となるので, 構造体を学んだ後に再度復習するとよい.
まず, 各配列要素を関数に渡す場合についてプログラムの例を挙げる.
#include <stdio.h> #include <stdlib.h> #define N 10000 struct DATA { double val[N]; }; void data_print(struct DATA x) // x.val[N] が記憶域に確保される. { int i; for(i=0; i<N; i++) printf("[%d]%e\n", i, x.val[i]); } int main(void) { struct DATA a = {{0}}; // a.val[N] が記憶域に確保される. data_print(a); // 実引数の a.val[N] の各要素が 仮引数の x.val[N]に代入される. return EXIT_SUCCESS; }この場合, data_print関数を呼び出すときに, 仮引数 struct DATA x の中の double val[N]; の N個の要素が記憶域に確保される. さらに, 実引数 a.val[N]の各要素が仮引数x.val[N] に代入される.
次に, 配列のアドレスを渡す場合の例を挙げる.
#include <stdio.h> #include <stdlib.h> #define N 10000 struct DATA { double val[N]; }; void data_print(struct DATA *x) // アドレスを記憶するための x が記憶域に確保される. { int i; for(i=0; i<N; i++) printf("[%d]%e\n", i, x->val[i]); // x.val[i] でなく x->val[i] であることに注意. } int main(void) { struct DATA a = {{0}}; // a.val[N] が記憶域に確保される. data_print(&a); // 実引数の a のアドレス &a が 仮引数の x に代入される. return EXIT_SUCCESS; }
関数へのポインタ[20.6.4]を用いることにより, 仮引数として関数を受け取る関数や, 操作(関数)をデータとして処理できる. これにより汎用性の高いプログラムを作成ことができる. たとえば, 関数を受け取る関数を用いることで, 受け取った関数を積分する関数や 受け取った関数を微分する関数を作成できる.
C言語では, 変数として確保した領域以外の任意の記憶域の領域を読み書きできる. 次の例では, 0x12345678 番地から始まる領域を整数とみなし, 書き込みと読み出しを行っている.
int main(void) { int a; *(int *)0x12345678 = 1; // 書き込み a = *(int *)0x12345678; // 読み出し return 0; }ただし, OSなどにより制限されている場合もあるし, 他のプログラムがその領域を使用している可能性もある.
指定したアドレスへの読み書きが必要な重要な用途にメモリーマップドI/Oがある. CPUには, 外部の周辺装置から信号を入力する(Input)場合や 信号を出力する(Output)場合に, メモリーマップドI/Oを用いるCPUと, ポートマップドI/Oを用いるCPUがある. このうち, メモリーマップドI/Oを用いる場合, メモリ上のあるアドレスの読み出しや書き込みがある周辺装置からの入力や出力になるように作られている.
#include <stdio.h> int main(void) { int a; int x, *ptr; a = 3; x = a; // x にはこの行を評価するときのa の値が代入される. *ptr = &a; // ptr にはaの値は代入されない. アドレスが代入される. printf("x=%d\n", x); printf("*ptr=%d\n", *ptr); a = 30; // ここで aの値を変更すると xと*ptrの相違が明確になる. printf("x=%d\n", x); // xに代入を行ったときのaの値. printf("*ptr=%d\n", *ptr); // この行を評価するときのaの値. return 0; }
(c)1999-2013 Tetsuya Makimura