コンピュータのメモリを自在にアクセスするために,C言語にはポインタという型がある。 また,C言語を習得しようとしている者にとって,最大の難関が,このポインタを理解して使いこなすことである。
プログラミングにおいてポインタが必要となるのは,次のような場合である。
しかし,そのどれも現段階では残念ながらまだ学習していない。 したがって,「何故ポインタが必要なのか」が分からないかも知れないが, 少なくとも 1. はすぐ後の単元で学習するので, ここでポインタの理解を深めておくこと。
ポインタはプログラムからメモリを自在にアクセスするためのものである。 したがって,ポインタの働きを理解するには,コンピュータのメモリをプログラムが操作しようとしたときに,どのようなもの(メモリモデル)として扱われるか,ということを理解する必要がある。そこで,ここではメモリモデルについて簡単に説明する。
メモリの一番小さい単位は,ビット (bit) である。1ビットは, 0 か 1 かの2通りのうちのどちらであるか,という情報を持つものである。
8ビットをひとまとまりとしたものを,バイト (byte) という。プログラムがコンピュータのメモリにアクセスするときには,メモリは,このバイトが1列に並んだものとして扱われる。(実際は,セグメントと呼ばれるものごとに,1列に並んでいるが,話がややこしくなるので,ここでは解説しない)。
一列に並んだものとされている各バイトには,順に番号づけられたアドレス (address) がついている。プログラムは,このアドレスを指定することで,メモリにアクセスする。バイトはプログラムがメモリにアクセするための最小の単位である。したがって,バイトの中の特定のビットを直接アクセスすることはできない。なぜなら,そのビットを直接指定するようなアドレスがないからである。
次の図は,ビット,バイト,メモリ,アドレスの関係を表している。図にある bffdc20 という文字列は, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, a, b, c, d, f の文字を用いた16進数であり,メモリのアドレスは通常このような16進数で表される。
C言語の char 型は,1バイトのメモリサイズを持つ。その他の全ての型も,バイトの整数倍のメモリサイズを持つ。そこで,変数のメモリサイズは常にバイト単位で表される
昔のパソコン(20年以上前)は,CPU の処理能力が1バイト単位であったが,その後に CPU の処理能力が上がり,一度に数バイト単位で処理が行われるようになった。 そこで,CPU が一度に処理できるバイトを集めたものを,ワード (word) と呼ぶことがある。 今のパソコンの多くは,4バイトが1ワードとなっている (8バイトのパソコンもある)。 ワード単位でメモリアクセスを行うのが,もっとも効率のよい方法と言える。
char 以外の型では,ワード単位のメモリサイズとなっていることが多い。たとえば,今使われているパソコンでCプログラムをコンパイルすると,多くの場合, int や float は1ワード(4バイト), double は2ワード(8バイト)となる。
次のプログラムを実行すると, char 型, int 型, float 型, double 型それぞれの変数のメモリサイズが表示される。
#include <stdio.h> int main() { char c; int i; float f; double d; printf("char c %d byte\n" "int i %d byte\n" "float f %d byte\n" "double d %d byte\n", sizeof(c), sizeof(i), sizeof(f), sizeof(d)); return 0; }
char c 1 byte int i 4 byte float f 4 byte double d 8 byte
文字列リテラル("と"とで括られた文字列)をソースコードに書くとき, 便宜上改行したいことがある。そのようなときには,いったん " で括り終わって, 改行した後, " で括った文字列を書き始めればよい。コンパイル時に自動的に,それらは一つの文字列に結合される。
プログラム中の変数は,それに必要なサイズ分だけのバイト数がメインメモリのどこかの領域に(必要な時に)確保されて,それが変数の実体となる。 一つの変数のアドレスとは,その変数に割り当てられた領域にあるバイトのうち,先頭のもの(アドレスが最初のもの)のアドレスのことである。
変数名にアドレス演算子 & を付けると,その変数に割り当てられた領域がどこであるかを,ポインタという形で得ることができる。
次のプログラムは,char 型, int 型, float 型, double 型それぞれ1個ずつ変数を宣言し,それらのアドレスを表示する。
#include <stdio.h> int main() { char c; int i; float f; double d; printf("char c %p\n" "int i %p\n" "float f %p\n" "double d %p\n", &c, &i, &f, &d); return 0; }
printf の書式文字列にある %p は,ポインタ値(アドレス値)を表示することを指定するものである。
その結果は,システムの状態にもよるので実行のたびに異なるが,おおよそ次のようになる。(0xbfffdc37 の先頭にある 0x は,16進数表示であることを示している。)
char c 0xbfffdc37 int i 0xbfffdc30 float f 0xbfffdc2c double d 0xbfffdc20
変数がメインメモリ上に配置されているこの状態を模式図で表すと,次図のようになる。 宣言した順番とは逆の順にメモリ上に配置されているし,変数間に隙間も空いている。 これは,プログラムの実行に都合が良いように,コンパイラが勝手におこなっていることなので,気にする必要はない。
アドレスとポインタという二つの言葉は,同じものを意味するようであるが,違うものである。
アドレスとはメインメモリの各バイトにつけられた,単なる番地である。
ポインタはそれが指し示す変数の全体(たとえば int なら 4 バイトからなるメモリ領域)の場所を,その先頭バイトのアドレスでもって表している。すなわち,領域の大きさ,という情報も含んでいる。
さらに,ポインタには,それが指し示すものがどういう型のものであるか,という情報もある。
ただ,領域の大きさや型の情報の処理はコンパイル時に行われるので,プログラム実行時には,ポインタはアドレスの情報のみを持つこととなる。
ポインタ値の左に間接参照演算子(間接演算子) * を付けることにより,そのポインタ値が指す変数そのものを参照することができる。 実際の変数名そのものではなく,ポインタ値を介して参照するということで,間接参照と呼ばれる。 *&a は a を指すポインタ値(&a)が指す変数であり,すなわち a そのものである。 したがって,次の2つの代入文は同等なものである。
a = b; *&a = *&b;
ポインタ値をしまっておくポインタ型変数(ポインタ変数)を宣言し使用することができる。ただし,ポインタは単なるアドレスではなく,それが指し示すものがどのような型の変数であるか,という情報も含んでいるので,ポインタ変数を宣言するときにはそのことを明示する必要がある。たとえば, int 型の変数を指し示すポインタ型変数は次のようにして宣言される。
int *p;
これにより, int 型の変数へのポインタ値をしまうことのできるポインタ型変数 p が宣言される。このポインタ変数 p には, int 型の変数を指すポインタ値ならどんなものでも代入することができる。
この宣言の書き方にはとまどうことがあるかと思う。しかし,この宣言「 int *p 」を,「 p の左に * を付けたら int 型になる」と読むと,なるほどと理解できる。実際,int 型のポインタの左に * を付けたら int 型の変数になるからだ。
また,ポインタ変数を同時に複数個宣言するときには,次のように,それぞれの変数の左に * を付ける必要がある。
int *p, *q, *r; /* p, q, r はint型ポインタ変数*/
次のように書いたのでは,int型ポインタ変数 p と int型変数 q, r が宣言されてしまう。
int* p, q, r; /* ポインタ変数なのは p だけ */
ポインタ型変数も,普通の変数と同じように,メモリ上に配置されている。そのメモリサイズはコンピュータによって決まるが,最近のパソコンでは 4 バイトであることが多い。次のプログラムを実行して確認しておこう。
int main() { int *p; printf("%d\n", sizeof(p)); return 0; }
4
上で示した例にある 4 バイトというメモリは,16 進数で8桁の整数値が入るだけのサイズであり,これは一つのバイトに割り振られたアドレスを表すために必要十分なメモリ量である。したがって,ポインタ変数の実体は,アドレスが入るだけのものでしかない。ポインタ変数が持っているはずの他の情報(ポインタが指している変数の型)は,変数に割り振られたメモリ上にはなく,その情報は,コンパイラがコンパイル時に管理するだけのものである。
ポインタ値の左に間接参照演算子(間接演算子) * を付けることにより, そのポインタ値が指すメモリある変数そのものを参照することができる。次のプログラムは, p, q というポインタ型変数に a, b のアドレスを代入したのち, *p, *q を用いることにより,間接的に変数 a, b を扱っている。
int main() { int a, b; /* a, b は整数型変数 */ int *p, *q; /* p, q は整数型を指すポインタ型変数 */ printf("&a == %10p, &b == %10p, &p == %10p, &q == %10p\n", &a, &b, &p, &q); /* 変数のアドレスを表示 */ p = &a; /* a のアドレスを p に代入 */ q = &b; /* b のアドレスを q に代入 */ *p = 3; /* p が指す場所にある変数(すなわち a)に 3 を代入 */ *q = *p + 2; /* q が指す場所にある変数(すなわち b)に, p が指す場所にある変数(すなわち a)の値+2を代入 */ printf(" a == %10d, b == %10d, p == %10p, q == %10p\n", a, b, p, q); /* a, b, p, q の値を表示 */ return 0; }
&a == 0xbfffec34, &b == 0xbfffec30, &p == 0xbfffec2c, &q == 0xbfffec28 a == 3, b == 5, p == 0xbfffec34, q == 0xbfffec30
このプログラムの実行時に,変数の配置やその値がどのようになっているかを,次図で表している。なお,実行時の変数のアドレスはそのときのコンピュータの状態によって変わってくる。
ポインタを用いた簡単なプログラムを作成する。
演習問題 整数型変数 a, b, c と, 整数型ポインタ変数 p, q, r を宣言して, p, q, r に a, b, c のアドレスを代入した後, p, q, r だけを用いて次のことを行え。 「a, b にそれぞれ 3, 5 を代入し,さらに a+b の値を c に代入し, 最後に a, b, c の値を表示する」
おおよそ,次のように書けばよい。
int main() { int a, b, c; 整数型ポインタ変数 p, q, r の宣言; p, q, r に a, b, c のアドレスを代入; p, q, r だけを用いて, 「a, b にそれぞれ 3, 5 を代入し,さらに a+b の値を c に代入し, 最後に a, b, c の値を表示する」 return 0; }
配列とポインタには密接な関係がある。それを説明しよう。
たとえば,次のように宣言された配列があったとする。
int array[5];
普通,配列の要素を扱うときには, array[3] のように 配列名[添字] という使い方をする。この場合, array[3] は整数型変数と同じものである。
ところが,配列名に添字を付けず array とだけ書くと,これは配列の先頭要素 array[0] を指すポインタ,すなわち &array[0] となる。この例の場合は array[0] は整数型であるから, array は整数型を指すポインタ型である。したがって,次のプログラムように, array の値を整数型ポインタ変数 p に代入することができる。
さらに,配列 array の先頭要素のアドレスが入ったポインタ変数 p に, p[n] のように添字を付けると,array[n] と同じものとなる。すなわち p[n] への代入は array[n] への代入と同じこととなる。
int main() { int array[5]; int *p; int i; p = array; /* p = &array[0] と同じ */ for (i = 0; i < 5; i++) { p[i] = i; /* array[i] = i と同じ */ } for (i = 0; i < 5; i++) { printf("%d ", array[i]); } return 0; }
0 1 2 3 4
ポインタ変数 p に配列のある要素 array[n] のアドレスを代入した場合には, p[i] は array[n+i] と同一のものとなる。
int main() { int array[5]; int *p; int i; p = &array[2]; for (i = -2; i < 3; i++) { p[i] = i; /* array[i+2] = i と同じ */ } for (i = 0; i < 5; i++) { printf("%d ", array[i]); } printf("\n"); return 0; }
-2 -1 0 1 2
配列の要素を指すポインタ値には,整数値を足したり引いたりすることができる。その場合,ポインタ値が指す要素から,足された(引かれた)数だけ後ろ(前)の要素を指すポインタ値となる。単に,アドレスに整数値が足される訳ではないことに注意。
たとえば,次のプログラムにおいて,
p は array[2] を指すポインタ値をもっているので,
p-2, p-1, p+0, p+1, p+2 というポインタ値は,それぞれ
&array[0], &array[1], &array[2], &array[3], &array[4] と等しくなる。
すなわち p+i == &array[i+2] であり,
よって, *(p+i) は array[i+2] と同一のものとなる。
int main() { int array[5]; int *p; int i; p = &array[2]; for (i = -2; i < 3; i++) { *(p+i) = i; /* array[i+2] = i と同じ */ } for (i = 0; i < 5; i++) { printf("%d ", array[i]); } printf("\n"); return 0; }
-2 -1 0 1 2
以前に「ポインタはアドレスとは違う」と言った意味は,まさにこのことである。アドレスなら1を足しても,そのアドレス値が1増えるだけだが,ポインタに1を加えると,ポインタが指すものの大きさ(バイト数)を配慮して,その分だけずれるのである。
配列のある要素を指しているポインタ変数 p に対して,インクリメント操作 p++ を行うと, p が指す要素の添字が 1 増え,デクリメント操作 p-- を行うと,添字が 1 減る。
次のプログラムでは,配列 array の先頭のアドレスが代入されたポインタ変数 p に対して, p ++ を次々と行うことにより,*p の参照先を array[0] から array[4] まで変更しつつ,配列 array の各要素に値を代入している。そして,次には, p-- を行いながら, array[4] から array[0] の順で,その値を表示している。
また,一つ目の for ループが終わった時点で,ポインタ p が指すものは,array[4] の次の仮想的な要素 array[5] を指している。
#include <stdio.h> int main() { int array[5]; int *p; int i; p = array; for (i = 0; i < 5; i++) { *p = i; /* p が指す配列要素に i を代入 */ p++; /* p が指す配列要素の添字が1増える */ } for (i = 0; i < 5; i++) { p--; /* p が指す配列要素の添字が1減る */ printf("%d ", *p); /* p が指す配列要素の値を表示 */ } printf("\n"); return 0; }
4 3 2 1 0
ポインタは,宣言された配列の範囲から一つだけ後方(添字が増える方向)へ範囲を逸脱することが許されている(前方はだめ)。 しかし,その場所を配列要素としてアクセスしてはならない。なぜなら,そこはあくまでも配列の範囲外であるから。 (他の変数がそこにあるかも知れない。)
+, -, ++, -- などと,参照演算子 * とを一つの式で同時に使うときには, 演算子の結合の順序に注意し,必要に応じて括弧を用いる必要がある。 また, / の直後に * を用いるときには間に空白を入れないと, コメントの始まりと解釈される。
int main() { int a[10]; int *p, *q, *r; p = &a[1]; q = &a[2]; r = &a[3]; *r = *p+1; /* a[3] = a[1] + 1; と同等 */ *r = *(p+1); /* a[3] = a[2]; と同等 */ *r++; /* r = r+1; と同等(先頭の * は無意味) この場合 r == &a[4] となる */ (*r)++; /* a[r]++; と同等 */ *r = *p / *q; /* a[4] = a[1] / a[2]; と同等 */ *r = *p /*q; 誤り(コンパイルエラー) /*q; 以後はコメントとなってしまう return 0; }
ある配列内の要素を指している2つのポインタ値 p, q の差 p - q をとると,それは, p が指す要素の添字と q が指す要素の添字の差となる。たとえば,
&array[4] - &array[1] == 3
である。これは,ポインタ値に整数値を加えると,その整数値の分だけ添字が大きい添字の要素を指すようになる,ということの逆を考えればよく分かる。すなわち,
&array[1] + 3 == &array[4]
という等式の &array[2] を移項したものだと思えばよいのである。
また,次のように,引く方の添字が大きいときは負の値となる。
&array[1] - &array[4] == -3
ある配列内の要素を指している2つのポインタ値 p, q のうち,p が指す要素の添字の方が大きいかどうかは
p - q > 0
を判断すればよい。また,これは次のように書いてもよい。
p > q
次のプログラムは,先ほどのプログラムと同様の動きをするが,for 文の代わりに
while 文を用いている。そして,
p++ を繰り返す条件として
p < array + 5
を指定し, p-- を繰り返す条件として
array <= p
を指定している。
#include <stdio.h> int main() { int array[5]; int *p; int i; array[0] = 1; p = array + 1; /* p には &array[1] が入る */ while (p < array + 5) { *p = *(p-1) + 1; /* p が指す配列要素にその一つ前の配列要素の値+1を代入 */ p++; /* p が指す配列要素の添字が1増える */ } while (p > array) { p--; /* p が指す配列要素の添字が1減る */ printf("%d ", *p); /* p が指す配列要素の値を表示 */ } printf("\n"); return 0; }
5 4 3 2 1
ポインタを用いて配列を操作するプログラムを作ってみる。
int a[10] の内容をすべて int b[10] に複写し, b の内容を表示することを,ポインタを用いて行え.
int main() { int a[10] = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10}; int b[10]; int *p, *q; p = a; q = b; while (ここには p と a を使う) { .... .... while 文の中では p, q のみを用いる } for (q = ???; q < ???; ???) { ??? の部分を書く printf("%4d", *q); } printf("\n"); return 0; }
次のように宣言された変数 p は,整数型を指すポインタ変数である。そして,それには整数型変数 a を指すポインタ値が代入されている。
int a; int *p; p = &a;
このポインタ変数 p も変数であるから,もちろんそれ自身のアドレスがあり, &p は p を指すポインタ値となる。このポインタ値の型は,整数型を指すポインタ型を指すポインタ型である。短く言うと,整数型ポインタ型ポインタ型,もっと短く言うと,整数ポインタポインタ型である。
*&p は p 自身であり, **&p は p が指す整数型変数 a である。
整数型ポインタ型ポインタ型の値をしまうための変数 pp を宣言するには,次のようにする。
int *pp;
このように宣言された変数には,整数を指すポインタ型変数 p を指すポインタ値を代入することができる。
int **pp; pp = &p;
このとき, *pp は p であり, **pp は a である。
ポインタへのポインタへのポインタというものも作ることができる。たとえば,整数型ポインタ型ポインタ型ポインタ型の変数 ppp は次のように宣言される。
int ***ppp;
この場合, p, *ppp, **ppp, ***ppp の値は, それぞれ int***, int**, int*, int 型である
int ***ppp; int **pp; int *p; int a; pp = *ppp; p = **ppp; a = ***ppp;
#includeint main() { int a[10] = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10}; int b[10]; int *p, *q; int i; p = ???; q = ???; while( ??? ここでは p と a を使う) { ???; ???; ここでは p と q を使う ???; } for (i = 0; i < 10; i++) { printf("%d ", b[i]); } return 0; }
このプログラムを完成させて,結果が次のようになるようにせよ.
ただし, while 文の本体では p, q 以外は使わないように.
10 9 8 7 6 5 4 3 2 1