アドレスとポインタ

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関数に引数として渡しているのは実はアドレスだったのだ。

scanf関数を使う例

int x;
scanf("%d", &x); /* xのアドレスを引数として渡している */

そのように、アドレスを引数にとる関数を作るには、これから紹介するポインタを理解する必要がある。アドレスは整数値であるが、それを格納するための型はintではなく、ポインタだからだ。

ポインタ型

ポインタ型

ある型 T の変数のアドレスを格納するための型

宣言の一例

int *foo;       /* intへのポインタfooを宣言 */
double *bar;    /* doubleへのポインタbarを宣言 */

概説

宣言

文法

型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);

ポインタを引数、戻り値に取る関数

ポインタを引数、戻り値に取る関数

通常の変数と同じようにポインタを引数、戻り値に取れる。

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問目を解いてから次の内容に進もう。

ポインタ演算

ポインタ演算

ある型 T へのポインタ型変数 に x 足すと xsizeof(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)になる

例(コード片)

ポインタだけを使って、配列4番目の要素と2番目の要素の間にいくつ要素があるか計算する

int arr[] = {10, 20, 30, 40, 50};
int* a = &arr[3];
int* b = &arr[1];
printf("%d\n", a - b);
出力結果
2

NULLポインタ

値 0 もしくは (void *) にキャストした 0 を持つポインタ

ポインタ p をNULLポインタにする

p = 0;

もしくは

p = NULL;

配列とポインタ

配列のアドレス

配列のアドレスとは、配列の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 func(int arr[]);
int func(int arr[10]);
int func(int *arr);

前のページの例の関数 func のプロトタイプはこう書き換えることが出来る。

int func(int *arr, int n);

次回

次回予告