アドレスとポインタ
C言語勉強会 第十回
kumar
July 3, 2013
引用 : Programming Place Plus
C言語勉強会 第十回
kumar
July 3, 2013
引用 : Programming Place Plus
※(ような動きをする)
このように、他の言語では当たり前にできることを、C言語ではポインタを介して行う。
メモリ上の位置の一意な識別子
つまり、アドレスは下記の値を取りうる。
変数はメモリのどこかに保存されていて、その座標を得ることが出来る。
&変数名
変数 x が宣言されているとすると
&x
と書くことで、変数xのアドレスを得ることが出来る。
& を、アドレス演算子と呼ぶ。変数のためのメモリ領域の確保は、そのときメモリが空いているところのどこかになる。
変数 x のアドレスを出力する
#include<stdio.h>
int main(void) {
int x;
printf("%p\n", &x); /* %pはポインタのフォーマット指定子 */
return 0;
}
0x7fff59161b48
この出力結果は実行するごとに違う。なぜなら、先程述べたように
変数のためのメモリ領域の確保は、そのときメモリが空いているところのどこかになる
からだ。
アドレスが指し示す変数にアクセスする
*アドレス
*は乗算演算子と同じ記号だが、全く違う意味を持つ。* をこの場合、間接演算子と呼ぶ。メモリの1000番地に10を代入する。
*(1000) = 10;
メモリの1000番地の値を式で使う。
int x = 20 + *(1000);
※なので、実際には動作しない
xのアドレスが示す番地に10を代入したり、値を式で使う。
*(&x) = 10;
int y = 20 + *(&x);
これは結局以下のように書くのと同じ動きをする。
x = 10;
int y = 20 + x;
&変数名と聞いて、scanf関数で使ったなぁと思った人はカンがいい。scanf関数に引数として渡しているのは実はアドレスだったのだ。
int x;
scanf("%d", &x); /* xのアドレスを引数として渡している */
そのように、アドレスを引数にとる関数を作るには、これから紹介するポインタを理解する必要がある。アドレスは整数値であるが、それを格納するための型はintではなく、ポインタだからだ。
ある型 T の変数のアドレスを格納するための型
int *foo; /* intへのポインタfooを宣言 */
double *bar; /* doubleへのポインタbarを宣言 */
(int *)(float *)(int *) → intへのポインタへのポインタ (int **) → intへのポインタへのポインタへのポインタ (int ***) → …ポインタ変数の実体は (Windows、UNIXの32bit、64bit環境において)
ポインタに対しての演算は通常の演算と異なる。(後述)
型T *変数名
または
型T* 変数名
*演算子は乗算の演算子と同じ記号だが、全く違う意味を持つ。
intへのポインタ p を宣言する
int *p;
x をint型変数とし、p に x のアドレスを代入する
p = &x;
p を出力する
printf("%p\n", p); /* %pはポインタのフォーマット指定子 */;
アドレスが指し示す変数にアクセスする
*アドレス
すなわち
*ポインタ
*は宣言時の*とは同じ記号だが別物である。int型変数 x 、intへのポインタ p が宣言されていて、p には &x (xのアドレス)が代入されているとする。
ポインタ p から間接参照で x へ 10を代入する
*p = 10;
ポインタ p を間接参照して、式で使う
int y = 20 + *p;
ポインタ p から間接参照で x を出力する
printf("%d\n", *p);
*p と書くと x と書くのは結局同じ動きである。通常の変数と同じようにポインタを引数、戻り値に取れる。
int型へのポインタを引数にとり、アドレスと値を出力する関数。nをそのまま戻り値として返す。
int *myPrint(int *n) {
printf("アドレス: %p\n", n);
printf("値: %d\n", *n);
return n;
}
渡されたポインタが指し示す変数を、前述の間接参照で書き換えることが出来る。
引数に渡されたポインタ変数 n が示す先を 1 に書き換える関数
void assignOne(int *n) {
*n = 1;
}
この関数を呼び出す例(コード片)
int x = 100;
assignOne(&x);
printf("%d\n", x);
1 と出力されるポインタ渡しと逆の、今までどおりの普通な引数の渡し方
理解を整理すべく、演習問題1問目を解いてから次の内容に進もう。
ある型 T へのポインタ型変数 に x 足すと x の sizeof(T) 倍 足される
int x;
int *p = &x;
printf("%p\n", p);
p++;
printf("%p\n", p);
printf("%p\n", p + 2);
0x7fff5a396b48
0x7fff5a396b4c // インクリメントしたら 4 増えた
0x7fff5a396b54 // 更に 2 足したら 8 増えた
xのアドレスは毎回違うのだが、三つの出力の差を見て欲しい。まず、pをインクリメントした結果、4増えている。次に p + 2 を出力した場合も、8増えている。
この特徴は、配列のポインタへの演算に便利である。
int arr[] = {10, 20, 30, 40};
int* ap = &arr[0];
printf("%d\n", *ap);
ap++;
printf("%d\n", *ap);
配列の要素はメモリ上に隙間なく順番に並ぶことが保証されているため、配列のi番目の要素へのポインタである ap をインクリメントするだけで、apは 次の要素を指すようになる。
a,bをT型変数へのポインタとし、a - b すると、その答えは (a - b) / sizeof(T)になる
ptrdiff_tである。
ポインタだけを使って、配列4番目の要素と2番目の要素の間にいくつ要素があるか計算する
int arr[] = {10, 20, 30, 40, 50};
int* a = &arr[3];
int* b = &arr[1];
printf("%d\n", a - b);
2
値 0 もしくは (void *) にキャストした 0 を持つポインタ
ポインタ p をNULLポインタにする
p = 0;
もしくは
p = NULL;
NULL は 0 もしくは (void *)0 を示すマクロである。
配列のアドレスとは、配列の0番目の要素のアドレスのことである。
配列名
だけ書くと、配列の先頭のアドレスを示す。それは&配列名[0]と同じアドレスである。
int arr[10];
printf("%p\n", arr);
printf("%p\n", &arr[0]);
0x7fff5ed1db10
0x7fff5ed1db10
当然アドレスなので毎回違う結果が出力されるが、arrと&arr[0]は同じなので、二行とも同じになる。
今まで配列 arr の i 番目の要素にアクセスするときは
arr[i]
と書いてきたが、実はこれは
*(arr + i)
の糖衣構文である。
糖衣構文(とういこうぶん)は、プログラミング言語において、読み書きのしやすさのために導入される構文
定義上、糖衣構文はプログラムの意味としては同じものを、よりわかりやすい構文で書けるものを指す。
糖衣構文 - Wikipedia
また、この添字は、配列以外の物にも使える。例えば、intへのポインタ p がある場合、
*p
と
p[0]
は同じである。(p[0] は、*(p + 0) つまり *p に展開されるから)
ただ、このような表現は遠回りで誤解を招くのでやめよう
前述のとおり添字は単なるポインタ演算の糖衣構文であるから、配列のアドレスを代入したポインタで同じように扱える。
int arr[] = {10, 20, 30, 40, 50};
int *p = arr; /* 配列のアドレスを代入 */
int i;
for(i = 0; i < 5; i++)
printf("%d\n", p[i]); /* 配列と同じようにアクセス */
だが、完全に配列の機能を代替出来るわけではない。(次項)
sizeof演算子で大きさを得たとき、配列は配列の大きさが得られるが、ポインタだとポインタの大きさしか得られない。
int arr[] = {0, 1, 2, 3, 4};
printf("%d\n", sizeof arr);
int *p = arr;
printf("%d\n", sizeof p);
20 // 配列arrの大きさ(sizeof(int) * 要素数 = 4 * 5 = 20)
8 // ポインタの大きさ(sizeof(int *))
ポインタへポインタを代入することは出来ても、配列へ配列を代入できない。
int arr1[] = {10, 20, 30, 40, 50};
int arr2[] = {60, 70, 80, 90, 100};
int *p = arr1; /* 配列のアドレスを代入 */
int *p = arr2; /* アドレスを代入し直すことも可能 */
arr2 = arr1; /* 不可能。コンパイルエラーとなる */
関数の引数として配列をとることも出来る。
配列arrのn番目までの要素をすべて出力する関数
int func(int arr[], int n) {
int i;
for(i = 0; i < n; i++)
printf("%d\n", arr[i]);
}
実は、配列を関数の引数としてとったように見えるが、実体はただの配列の最初の要素へのポインタである。
int arr[] は int *arr の糖衣構文である。以下のプロトタイプはすべて正しく、同じ意味である
int func(int arr[]);
int func(int arr[10]);
int func(int *arr);
前のページの例の関数 func のプロトタイプはこう書き換えることが出来る。
int func(int *arr, int n);