ポインタ

ポインタとは,メモリ上のあるアドレスを「指し示す(ポイント)」ための仕組みである.指し示すアドレスは有効なアドレスでなければならないので,必然的に既に宣言された変数(配列)の存在が前提となる.ポインタもまた変数であるので,ポインタ変数などとも呼ばれる.ここではポインタの基本について学習する.

宣言方法

ポインタの宣言にはアスタリスク*を用いて,

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

以上のように,一度宣言されたポインタ変数の中身はアスタリスク*で取得する.

初学者にとってポインタがわかりにくいと言われる理由の一つが,このように同じアスタリスク記号を違う目的で使う文法にあると言われる.

指し示すアドレスの中身(データ)へのアクセス: 配列の場合

配列を指し示すポインタの場合,さらに以下のような使い方ができる.

リスト 1. ポインタを用いた配列の各要素へのアクセス
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はあくまでポインタである.配列を指し示すポインタはこのように指し示す配列の要素を大かっこを用いた添字で参照できる.このとき,指し示すアドレスの中身(データ)へのアクセス: 配列でない場合で用いたようなアスタリスクは不要である.

配列とポインタのパターン

配列へのポインタの典型的な使い方として,配列の初期化や,コピーがある.

ポインタを用いた配列の初期化

05 pointer move
図 1. ポインタによる配列へのアクセス例(初期化)
リスト 2. ポインタを用いた配列の初期化
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++ は値を参照してからポインタをインクリメントすることになる.

したがって,図 1に示すように,ループの終わりには,pの指すアドレスは配列dataの最後の要素のアドレスとなっている.すなわち,ループ終了後には,pは,配列dataの範囲外のアドレスを指し示すことになる.ここで,仮に配列の先頭の要素data[0]に対してポインタpを使ってアクセスしようとして,p[0]を用いようとすると,これは配列の範囲外のアドレスのため実行時エラーの原因となる.これを回避するために,配列data[]の先頭アドレスを別のポインタp_dataに保存しておき,後に利用するときには,再びp = p_dataとする.図 1からも,p_dataは動いていないことが分かる.

ポインタを用いた配列のコピー

リスト 3. ポインタを用いた配列のコピー
int src_data[100], dst_data[100];
int i;
int *sp, *dp;

sp = &src_data[0];
dp = &dst_data[0];

/* src_data[]の中身の定義 */

for (i = 0; i < 100; ++i) {
  *dp = *sp;
  sp++;
  dp++;
}
11-13行目はまとめて*dp++ = *sp++;と書くことが可能.
05 pointer copy
図 2. ポインタによる配列へのアクセス例(コピー)

関数の引数としてのポインタ

例として,2つのint型変数の値を入れ替える関数swapを考える.

これまでの関数における引数(値渡し)

C言語では,関数の引数として与えられた値は,そのコピーが関数の内部に渡される値渡しという方法であった.

リスト 4. 2つの変数の値を交換しようとしたコード
#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関数での変数の値は変化していないことが分かる.

このように,これまでの引数の使い方(値渡し)は,関数の処理の局所性を向上させるには有効な方法であるが,入力される引数そのものを書き換える必要がある処理には使えないことが分かる.

引数としてポインタを使った関数(参照渡し)

変数の値を入れ替えるために,関数の引数としてポインタを使用するコードの例を以下に示す.

リスト 5. 2つの変数の値を交換するコード
#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型のポインタになっていることである.関数の引数としてポインタを使うということは,アドレスを受け渡しするということである.値渡しが,呼び出し元で与えられた引数の値のコピーを受け渡しするのに対して,ポインタを使った変数のアドレスの受け渡し方法を参照渡しと言う.

したがって,リスト 5swap関数で受け取るのは,呼び出し元のmain関数で宣言された変数aおよびbアドレスである.15行目のように,関数における仮引数名を*aおよび*bというように,アスタリスクを付けることによってこれらの仮引数がポインタであることを宣言している.17-20行目のように,ポインタとして宣言された仮引数名は,関数内でポインタとして振る舞うので,その中身にアクセス(値を読み取る,値を代入する)するには*を付けている.

参照渡しの応用

配列の参照渡し

配列を引数にする場合,ポインタを引数とし,その先頭アドレスを参照渡しすることが多い.

リスト 6. 参照渡しによる配列へのアクセス
#include <stdio.h>

double get_average(int *, int);

int main() {
  int data[5] = {1, 3, 2, 4, 8};
  int num_data = 5;
  double average = get_average(&data[0], num_data);
  printf("平均値は%fです\n", average);
}

double get_average(int *in, int N) {
  double sum = 0.0;
  int i;
  for (i = 0; i < N; ++i) {
    sum += (double)in[i];
  }
  return sum / N;
}

リスト 6の実行結果は,

平均値は3.600000です

となる.

配列をポインタで受け渡しするときの注意は,配列の範囲外のアドレスにアクセスしないようにすることである.ここでは,配列の要素数を保持する変数Nを宣言し,それをget_average関数に渡すことによって,範囲外アクセスを回避している.また,16行目の(double)キャストと呼ばれる,変数の型変換のための演算子である.このキャスト演算子によって,int型のin[i]は,double型に変換され,sumへと加算される.

複数の戻り値の代用としての参照渡し

C言語の戻り値の数の最大値は1である.ポインタを使った参照渡しを使って,2つ以上の戻り値と等価な動作を行う関数を作ることができる.以下のリスト 7に,配列の要素の平均と標準偏差を求め,その結果をポインタを介して呼び出し元へ返す関数の例を示す.リスト 6と比較してみてほしい.このような関数の場合,本来の意味の戻り値としては0個で良いため,戻り値の方はvoidとしている.get_average_stdev関数の最後の2つの引数はdouble型のポインタである.呼び出し元から与えるのは,get_average_stdev関数で求めた平均と標準偏差という2つの結果を格納すべきアドレスである.関数内で,求めた平均および標準偏差の値を代入・読み取りするときに,アスタリスクを付けてそれぞれのアドレスの中身にアクセスしていることが分かる.

リスト 7. 平均と分散を求めてポインタを介して結果を返す関数
#include <math.h>  // sqrt()を使うため
#include <stdio.h>

void get_average_stdev(int *, int, double *, double *);

int main() {
  int data[5] = {1, 3, 2, 4, 8};
  int num_data = 5;
  double average, stdev;
  get_average_stdev(&data[0], num_data, &average, &stdev);
  printf("平均値は%f, 分散は%fです\n", average, stdev);
}

void get_average_stdev(int *in, int N, double *average, double *stdev) {
  double sum = 0.0;
  int i;
  for (i = 0; i < N; ++i) {
    sum += (double)in[i];
  }
  *average = sum / N;

  double tmp;
  double var = 0.0;  // 分散
  for (sum = 0.0, i = 0; i < N; ++i) {
    tmp = (double)in[i] - *average;
    tmp *= tmp;
    sum += tmp;
  }
  var = sum / N;
  *stdev = sqrt(var);  // 標準偏差 = 分散の非負の平方根
}

参考までに,リスト 7の実行結果は,

平均値は3.600000, 分散は2.416609です

となる.

ちなみに,標準偏差 \(S\)は,平均を\(\mu\)として

\[S = \sqrt{\frac{1}{N}\sum_{i=0}^{N-1}|x_i - \mu|^2}\]
\[\mu = \frac{1}{N}\sum_{i=0}^{N-1}x_i\]

で定義される.