• 検索結果がありません。

ポインタ

ドキュメント内 note.dvi (ページ 136-141)

第 6 章 C 言語入門

6.14 配列とポインタ(その1)

6.14.2 ポインタ

ポインタ(pointer) とは, 他の変数のアドレスを持つ変数である. ポインタとして定義したオブジェク

トの中身は,記憶領域上のアドレスに他ならないので,ポインタはどのような型のオブジェクトのアドレス も格納できるように思えるのだが, Cでは,どのような型のオブジェクトのアドレスを持つポインタかを明 示的に指定して宣言しなければならない.

例えば,char型の変数のアドレスを持つポインタpを作るには,以下のように行なう.

char *p ;

この定義により,変数pはchar型のオブジェクトを指し示すことが出来る. すなわち,pにはchar型のオ ブジェクトのアドレスを代入することができる. 実際にchar型の変数cのアドレスをpに代入するには, char c ;

char *p ;

p = &c ; p ✲c

とする. 変数に &をつけると, そのアドレスを示す. & はアドレス演算子と呼ばれ, 被演算数は,ビット・

フィールド,registerと宣言されたものを参照する左辺値,関数型であってはならない. また,単項の*は 間接演算子と呼ばれ. 単項の *つけた変数が指し示すアドレスを返す. 単項* をポインタに適用すると, そのポインタの指すオブジェクトがアクセスできる.

Example 6.14.2 極めて人為的だが, この例はポインタの利用法を的確に示している.

int x = 1, y = 2 ; int *ip ;

ip = &x ; /* ip は x を指す. すなわち, ip の中には x のアドレスが入っている. */

y = *ip ; /* y は 1 となる. *ip は ip の指し示す先の値を表す.

この時点では, ip は x を指し示している. */

*ip = 0 ; /* x は 0 となる. *ip は左辺値となりうる.

ip は x を指し示しているので, x の値を変えている. */

この例のように,ポインタを介して,変数の値を受け渡すことができる. ここで,*ipの値はip = &xに よってxを指し示し,*ip = 1でxの値が1 となったことに注意.

Id: C7.tex,v 1.23 2001-03-21 19:23:29+09 naito Exp

Example 6.14.3 この例では,ipの内容(アドレス)が iqにコピーされる73 . int *ip, *iq ;

iq = ip ; この例と

int *ip, *iq ;

*iq = *ip ;

とは全く意味が異なる. こちらの例では, ipが指し示している変数の値がiqが指し示している変数の値 に代入される.

したがって, int *ip, *iq ; int p=1, q=2 ; ip = &p ; iq = &q ; iq = ip ;

とすると,iqは pを指し示めし,qは値2を持つが, int *ip, *iq ;

int p=1, q=2 ; ip = &p ; iq = &q ;

*iq = *ip ;

とすると,iqは qを指し示し, qは値1 を持つことになる.

6.14.2.1 ポインタの演算

まず,次のようなことはできるだろうか.

int *ip ; ip += 1 ;

ipそれ自身は,アドレスを指している. これを行うと,ipの値(指し示すアドレス)が1増えるのではな く,ipの指し示しているアドレス自身がint型の分だけ増加する. もし,intが4バイトを占めていれば, ipは4バイト分増加する74.

また,次のようにすると,xの値を1 だけ増やすことができる.

int *ip ; int x ; ip = &x ;

*ip += 1 ;

これは,ipがxを指し示していることを考えれば,当たり前である.

6.14.2.2 配列とポインタ

「Cでは配列とポインタは強い関係を持ち, ほぼ同様に扱っても良い.」と, C のどのような教科書を見 ても書いてある. この言葉は半分は正しく,半分は間違っている. まず,この言葉の意味を明確にし,配列と ポインタを同様に扱ってもよい文脈を明らかにしよう.

例えば, int a[10] ;

73もともとiqが指し示していたアドレスの中身にはなんら変化はないことに注意.

74ポインタをインクリメントしたとき,どれだけのバイト数移動するかは,そのポインタが指し示す型に依存する.

Id: C7.tex,v 1.23 2001-03-21 19:23:29+09 naito Exp

は, int型の要素数10の配列を定義しているが,識別子aが何を表しているかを考えてみよう. 配列の識 別子は,その配列の先頭のアドレスを表している. すなわち, 次の2つのコードは同じものである75. int *pa ;

pa = &a[0] ;

int *pa ; pa = a ;

左のプログラムでは, &a[0]はオブジェクトa[0] のアドレスを表し76, 右のプログラムでは,aが配列の 先頭要素a[0] のアドレスを表している. したがって,いずれのプログラムでも, paは配列 aの先頭を指 し示すこととなる.

この時,ポインタの演算により,pa+i はa[0]からint型変数i個分先を指し示すことになり,すなわ ち, pa+iはa[i] を指し示していることとなる. したがって,*(pa+i)は a[i]を参照することとなる.

a[0] a[1] a[2] a[3] a[4] a[5]

pa

pa=a pa+1

もっと極端なことを書けば, int a[3] = {1,2,3} ;

printf("%d, %d\n", a[2], 2[a]) ;

はともに正しい構文であり,配列X[Y] という構文はは常に*(X+Y)と変換される.

ここまででは,配列とポインタは完全に等価であり, どちらで記述しても,相互に書き換えが可能なよう に思える. しかし,注意すべきことは,ポインタはアドレスを格納する変数であるため, int *paなどとい う(仮)定義において,確保される記憶領域は,オブジェクトのアドレスを格納するために十分な程度の領 域に過ぎない77 . しかし,配列としてint a[10]と定義すると,記憶領域上に連続したint型10個分の 領域が確保され78 ,その領域は定義が実行された時点で確定したアドレスである. したがって,paは左辺値 であるが,aは左辺値にはなり得ない. すなわち, pa = a, pa++は意味のある演算であるが,a = pa, a++

は正しくない. しかし, int *pa, a[10] ; pa = a ;

printf("%d\n",pa[2]) ;

などは意味のある文である. すなわち,pa[2]は*(pa+2)に変換され,pa = aにより,paはa[0] を指し 示すため, *(pa+2)は a[2]に他ならない. ただし,元々ポインタと等価になっている配列の先頭アドレス をポインタに代入して参照することは,配列の要素数を越えてアクセスを行ってしまう元となり,バグにな る危険性を秘めている.

Remark 6.14.3 このように配列とポインタはある意味では似ているのだが,その識別子の持つ意味が異

なる. したがって,次のような2つのファイルによるプログラムはエラーにはならないが正常には実行され ない.

75どのような場合にでも同じオブジェクトコードを生成する.

76演算子&[]の優先順位に注意.

77Solaris 2.6では4バイト(32ビット)である.

78Solaris 2.6gcc 2.95.1では40バイトである.

Id: C7.tex,v 1.23 2001-03-21 19:23:29+09 naito Exp

int a[3] ={1,2,3} ; extern void foo(void) ;

int main(int argc, char **argv) {

foo() ; return ; }

#include <stdio.h>

extern int *a ; void foo(void) {

printf("%d\n", *(a+1)) ; return ;

}

Remark 6.14.4 配列以外を指すポインタに対して加減を行なっても, 意味のある結果が得られるとは限

らない. 例えば int *p ; int a, b ; p = &a;

p++;

としたとき,pがbを指していることは期待できない.

Example 6.14.4 int型の要素数10個の配列aの各要素に値0を代入する4つの方法. ともに int a[10], *p, i ;

と定義されていると仮定する.

方法1

for(i=0;i<10;i++) a[i] = 0 ; 方法2

p = a ;

for(i=0;i<10;i++) p[i] = 0 ; 方法3

p = a ;

for(i=0;i<10;i++) *(p+i) = 0 ; 方法4

p = a ;

for(i=0;i<10;i++) *p++ = 0 ;

さて,これらの4つの方法のうちどれが一番お好みだろうか?これら4つの例はプログラムの書き方は異な るが,生成するコードは全く同一と考えて良い.

Example 6.14.5 double型の要素数3の配列のコピーを行う例.

int main(int argc, char **argv) {

int i ;

double a[3] = {1.0, 2.0, 1.0}, b[3] ; for(i=0;i<3;i++) b[i] = a[i] ;

return 0 ; }

このコピーを b = a ;

で行うことはできない.

Id: C7.tex,v 1.23 2001-03-21 19:23:29+09 naito Exp

Example 6.14.6 double 型の要素数2の配列をR2 上のベクトルと思い,それに直交するベクトルを一 つ求める.

int main(int argc, char **argv) {

int i ;

double a[2] = {1.0, 2.0}, b[2] ; b[0] = -a[1] ; b[1] = a[0] ; return 0 ;

}

6.14.2.3 汎用データ・ポインタ

ここまでは,char,intなどのデータの型が決まったものに対するポインタを考えてきた. しかしながら, どのような型に対しても利用できる汎用データ・ポインタを利用することで,さらに広範囲に利用できる関 数などを作ることができる.

汎用データ・ポインタは void型へのポインタとして定義される.

Example 6.14.7 この例の関数は,どのような型の配列であっても,配列の次の要素をかえす関数である.

void * next_member(void *a, int size, int len, int member) {

if (member >= len) return NULL ; return (a+(member+1)*size) ; }

ここで, ポインタをインクリメントする際に,sizeを掛けていることに注意. ここで,aは対象となる配 列の先頭を指し示すポインタであり,sizeは配列の要素の型のバイト数,lenは配列の要素数,memberは 対象となる配列の要素の添字の番号である. この関数を利用すると,

int a[3] = {0,1,2} ;

double x[3] = {0.0, 1.0, 2.0} ; char c[3] = {’a’, ’b’, ’c’} ; int *pa ;

double *px ; char *pc ;

pa = (int *)next_member(a,sizeof(int), sizeof(a)/sizeof(int), 1) ; printf("%d\n", *pa) ;

px = (double *)next_member(x,sizeof(double), sizeof(x)/sizeof(double), 1) ; printf("%f\n", *px) ;

pc = (char *)next_member(c,sizeof(char), sizeof(c)/sizeof(char), 1) ; printf("%c\n", *pc) ;

として,実際に次の要素を出力することができる. ここで, sizeof演算子を用いて, sizeof(a)/sizeof(int)

によって配列の要素数を得ていることに注意. これと等価な sizeof(a)/sizeof(a[0])

でもよい. 後者の方がaの指し示す型を変えたときの可搬性が高く安全である.

void型へのポインタをインクリメントすると,その変数が指し示すアドレスは1だけ増加する. この部分 だけを見ると,char 型へのポインタと同じであるが, どのような型の変数をも指し示すことができるよう になっているのが汎用データポインタである.

Id: C7.tex,v 1.23 2001-03-21 19:23:29+09 naito Exp

ドキュメント内 note.dvi (ページ 136-141)