ポインタ
ポインタとは,メモリ上のあるアドレスを「指し示す(ポイント)」ための仕組みである.指し示すアドレスは有効なアドレスでなければならないので,必然的に既に宣言された変数(配列)の存在が前提となる.ポインタもまた変数であるので,ポインタ変数などとも呼ばれる.ここではポインタの基本について学習する.
宣言方法
ポインタの宣言にはアスタリスク*を用いて,
int *a;
double *x;
のように宣言する.変数の直前の*がその変数がポインタであることを意味する.このようにして宣言されたポインタは,メモリ上のどこも指していないため,使用することができない.
使用すると,おそらく実行時エラー(例えばSegmentation faultなど)となる.
|
以下のようにポインタが指し示すアドレスを代入することで,初めて使用可能になる.
int *a; // int型の変数を指し示すためのポインタ
int b = 10; // int型の通常の変数
a = &b; // ポインタaはbのアドレス(&はアドレスを求める演算子)を指す
もちろん,宣言と代入を同時に行うことも可能である.
int b = 10;
int *a = &b;
指し示すメモリが配列の場合は,以下のように書く.
char *c;
char name[20];
c = &name[0]; // または c = name;
以上の例のように,ポインタは,その宣言時にはアスタリスクを*伴うものの,アドレスの代入時には*が付かない.
使用方法
指し示すアドレスが代入されたポインタは,以下のように使用できる.
指し示すアドレスの中身(データ)へのアクセス: 配列でない場合
ポインタが指し示すアドレスの中身,すなわちデータを取得するには,以下のように書く.
int a = 10;
int *b = &a; // bは変数aのアドレスを指し示す
printf("Data is d\n", *b); // bが指し示すアドレスの中身を取得
実行結果:
Data is 10
また,ポインタが指し示すアドレスの中身を書き換えるには,
int a = 10;
int *b = &a; // bは変数aのアドレスを指し示す
*b += 100; // bが指し示すアドレスの中身に100を足す
printf("Data is d\n", *b); // bが指し示すアドレスの中身を取得
実行結果:
Data is 110
以上のように,一度宣言されたポインタ変数の中身はアスタリスク*で取得する.
|
初学者にとってポインタがわかりにくいと言われる理由の一つが,このように同じアスタリスク記号を違う目的で使う文法にあると言われる. |
指し示すアドレスの中身(データ)へのアクセス: 配列の場合
配列を指し示すポインタの場合,さらに以下のような使い方ができる.
int i;
int data[5] = {0, 1, 2, 3, 4};
int *p_data = &data[0];
printf("data[2] = %d\n", p_data[2]);
p_data[2] = 10;
printf("data[2] = %d\n", p_data[2]);
このリストの実行結果は,
data[2] = 2 data[2] = 10
となる.あたかもp_dataという配列があるように見えるが,p_dataはあくまでポインタである.配列を指し示すポインタはこのように指し示す配列の要素を大かっこを用いた添字で参照できる.このとき,指し示すアドレスの中身(データ)へのアクセス: 配列でない場合で用いたようなアスタリスクは不要である.
配列とポインタのパターン
配列へのポインタの典型的な使い方として,配列の初期化や,コピーがある.
ポインタを用いた配列の初期化
int data[100];
int i;
int *p_data, *p;
p_data = &data[0];
p = p_data; (1)
for (i = 0; i < 100; ++i) {
*p++ = 0;
}
| 1 | この行がなぜ必要なのか考えてみよう |
ここで,リスト 2の6行目が必要な理由を考えよう.for文の中にある,*p++ = 0の意味は,「pの指し示すアドレスの中身をゼロにして,その後,pが指し示すアドレスそのものをインクリメントせよ」である.
ポインタの中身を取り出す*は,インクリメント演算子よりも優先順位が低い.したがって,*++pはポインタをインクリメントしてから値を参照することになり,*p++ は値を参照してからポインタをインクリメントすることになる.
|
関数の引数としてのポインタ
例として,2つのint型変数の値を入れ替える関数swapを考える.
これまでの関数における引数(値渡し)
C言語では,関数の引数として与えられた値は,そのコピーが関数の内部に渡される値渡しという方法であった.
#include <stdio.h>
void swap(int, int);
int main() {
int a = 1, b = 2;
printf("before: a = %d, b = %d\n", a, b);
swap(a, b);
printf("after : a = %d, b = %d\n", a, b);
}
void swap(int a, int b) {
int c;
c = a;
a = b;
b = c;
printf("swap : a = %d, b = %d\n", a, b);
}
このコードの実行結果は,
before: a = 1, b = 2 swap : a = 2, b = 1 after : a = 1, b = 2
となる.明らかに,値の入れ替えに失敗していることがわかる.この理由は,関数swapには値渡し,すなわち引数のコピーが渡されるからである.したがって,入れ替えが行われるのはコピーされた変数に対してであり,呼び出し元の変数は影響を受けない.
関数swap内のprintf文では,1と2が入れ替わって表示されているのはそのためである.入れ替えられたコピーは,swap関数の終わりで破棄されるので,呼び出し元のmain関数での変数の値は変化していないことが分かる.
このように,これまでの引数の使い方(値渡し)は,関数の処理の局所性を向上させるには有効な方法であるが,入力される引数そのものを書き換える必要がある処理には使えないことが分かる.
引数としてポインタを使った関数(参照渡し)
変数の値を入れ替えるために,関数の引数としてポインタを使用するコードの例を以下に示す.
#include <stdio.h>
void swap(int *, int *);
int main() {
int a = 1, b = 2;
int *pa, *pb;
pa = &a;
pb = &b;
printf("before: a = %d, b = %d\n", a, b);
swap(pa, pb);
printf("after : a = %d, b = %d\n", a, b);
}
void swap(int *a, int *b) {
int c;
c = *a;
*a = *b;
*b = c;
printf("swap : a = %d, b = %d\n", *a, *b);
}
このコードの実行結果は,
before: a = 1, b = 2 swap : a = 2, b = 1 after : a = 2, b = 1
となる.変数の入れ替えに成功していることが分かる.
リスト 4との違いは,関数swapの引数がint型のポインタになっていることである.関数の引数としてポインタを使うということは,アドレスを受け渡しするということである.値渡しが,呼び出し元で与えられた引数の値のコピーを受け渡しするのに対して,ポインタを使った変数のアドレスの受け渡し方法を参照渡しと言う.
したがって,リスト 5のswap関数で受け取るのは,呼び出し元のmain関数で宣言された変数aおよびbのアドレスである.15行目のように,関数における仮引数名を*aおよび*bというように,アスタリスクを付けることによってこれらの仮引数がポインタであることを宣言している.17-20行目のように,ポインタとして宣言された仮引数名は,関数内でポインタとして振る舞うので,その中身にアクセス(値を読み取る,値を代入する)するには*を付けている.