ポインタ (プログラミング)

出典: フリー百科事典『ウィキペディア(Wikipedia)』
移動先: 案内検索

ポインタ (pointer) とは、あるオブジェクトがなんらかの論理的位置情報でアクセスできるとき、それを参照する(指し示す)ものである。有名な例としてはC/C++でのメモリアドレスを表すポインタが挙げられる。

なお、C++では、さらに独立した「参照」という機能がある(#参照の節を参照)。

C言語のポインタ[編集]

最も典型的なポインタの例としては、C言語による「特定のメモリ領域を表現する」ポインタが挙げられる。

C言語にポインタが存在する理由は、効率上の問題である。C言語は、元々UNIXを記述するシステム用言語として開発されたものである。したがって、アセンブラが実行できる操作のほぼ全てを行える必要があった。そのため、特定のメモリ領域への値の直接代入能力を持つなど、他のプログラミング言語と比較して、むしろ異例とも言える強力なポインタ機能を備えている。

C言語の実行モデルでは、実行プログラム上の関数コード、データが全て1次元のアドレスに直列配置される。そのため、データはおろか、関数のアドレスを取得し、他の関数にエントリーポイント情報として渡すこともできる。

また、C言語の関数では、引数は、値渡しだけをサポートし、参照渡しをサポートしない。これは、アドレスの数値を取得すれば、参照に可能な全てを行えるため、実質的に参照を数値と同一視できるからである。実際、初期のC言語では、アドレス値は、整数型互換するものとして扱われていた。これは、値と参照を明確に区別するPascalなどとは対照的である。現在[いつ?]でもC言語は、void*により任意のメモリ領域にアクセスできる。なお後発のC++では参照渡しもサポートするようになった。

しかし、コード領域も含むメモリを直接扱えるということは、言語レベルでは(意図的でないとしても)不正なメモリアクセスを事実上保護できないということを示しており、C言語のプログラムにおけるポインタ関連のバグの多さがそれを証明している。

実際の例[編集]

一般的なC言語のソースコードでは、ポインタが指している領域の値を参照する間接演算子(indirection operator)"*"と、アドレス演算子"&"を用いて記述される。未初期化のポインタ変数は、不定の領域を指している。しかし、その場合、「未初期化状態」と「有効な領域を指している状態」の区別がつかない。そのため、Null(ヌル)値を代入することによって、ポインタが無効な領域を指していることを明示する必要がある。

単純なポインタ[編集]

  • 宣言例
int n;
/* int型変数へのポインタである、ポインタ型変数"ptr"を宣言 */
int *ptr;
/* int型変数"n"へのポインタを代入 */
ptr = &n;

C言語の処理系では通例、無効なポインタを示す値として下記のようなNULLマクロが定義されている。

#define NULL ((void*)0)

C言語では、voidへのポインタは任意の型へのポインタに自由に代入することができる。ポインタに無効値を代入する場合、通例このNULLマクロを使う。

int *ptr = NULL;

C++では、NULLは整数定数のゼロに等しい。

#define NULL 0

そのため、C++では下記のように書くこともできる。

int *ptr = 0;

C++ではNULLが整数定数のゼロに等しいことから起こる関数オーバーロードのルックアップに関する問題を解決するため、C++11以降では、std::nullptr_t型として評価されるキーワードnullptrが定義された。

int *ptr = nullptr;
  • 利用例

下記はポインタ"ptr"の参照先である変数"n"に整数値10を代入することになる。

int n;
int *ptr = &n;
*ptr = 10;

実行時に要素数の決まる配列を作成する際など、動的にメモリ領域を確保するときは結果をポインタで受け取る。確保したメモリを解放するときもポインタを利用する。メモリ解放直後のポインタは無効な領域を指しており、これを「ダングリングポインタ」と呼ぶ。ダングリングポインタが指している領域を誤って使用することのないように、セキュリティ対策として明示的にNULLを代入しておく手法が推奨されている[1]

int i;
int *ptr = malloc(sizeof(int) * 10);
for (i = 0; i < 10; ++i) {
  ptr[i] = i;
}
free(ptr);
ptr = NULL;

C言語の関数は前述のように参照渡しをサポートせず、値渡しのみをサポートするため、出力は戻り値(返り値)による1つのみを持つことしかできないが、ポインタを利用することで疑似的に複数の出力を持つ関数を定義することが可能となる。

/* xはdouble型配列へのポインタであり、const double *xと宣言することもできる。 */
double func(const double x[], int num, double *minVal, double *maxVal) {
  int i;
  double sum = 0.0;
  assert(num > 0);
  *minVal = +DBL_MAX;
  *maxVal = -DBL_MAX;
  for (i = 0; i < num; ++i) {
    *minVal = MIN(x[i], *minVal);
    *maxVal = MAX(x[i], *maxVal);
    sum += x[i];
  }
  return sum;
}
/* 関数の呼び出し例。 */
double x[] = { 3, 19, 1, -3, -8, 0, 4 };
double minVal, maxVal, sum;
sum = func(x, 7, &minVal, &maxVal);

下記は標準入力を整数値に変換し、scanf関数の第2引数の参照先である"n"にその整数値を出力する例である。

int n;
int *ptr = &n;
scanf("%d", ptr);

または

int n;
scanf("%d", &n);
  • ポインタへのポインタ

「ポインタへのポインタ」(多重間接参照、ダブルポインタ)を定義することも可能である。動的に確保したメモリへのポインタを関数引数で返却するときや、ポインタ配列を扱うときなどに利用される。

int *ptr;
int **pptr;
pptr = &ptr;
  • トリッキーな例
#include <stdio.h>
/* argvはコマンドライン引数の文字列群へのポインタ配列であり、char *argv[]と宣言することもできる。 */
int main(int argc, char** argv) {
  while (*argv != NULL) printf("%s\n", *argv++);
}

配列argvがポインタとしても扱えることを利用している。しかし、間接演算子"*"とインクリメント演算子"++"のどちらの優先度が高いのかを知らないと、このような記述を理解することはできない。したがって、保守作業の際にバグを誘発しやすいため、以下のように記述したほうがよいとする主張もある。

while (*argv != NULL) {
  printf("%s\n", *argv);
  argv++;
}

関数ポインタ[編集]

上述の通り、C言語では関数を指すポインタを作成することができる。

  • 宣言例 (引数double・返値doubleの関数を受け取るポインタ "f" を定義)
double (*f)(double);
  • 呼び出し例
f = sqrt;
printf("f(16) = %g\n", f(16));
printf("f(25) = %g\n", (*f)(25)); /* 明示のためかこちらの方が使われやすい */

関数ポインタを引数にとって使うことで、処理を外部から組み込む(カスタマイズする)ことが可能である。引数として渡される関数は、コールバック関数と呼ばれることもある。主にイベント処理や、判断を外部に任せて処理を行うときなどに用いられる。C言語には無名関数の仕組みがないため、後者の用途では、少し記述が冗長になるという嫌いがある。しかし、C++ではBoostという外部ライブラリを使うことで、これを補完できる。C++11ではラムダ式が標準化された。

上の例では、直接変数を定義した。しかし、実際には、可読性を上げるために、typedefして使うことが多い。

typedef double (*mathfunc)(double);
mathfunc f;
f = sqrt;
/* ... */

C++11ではusingを使うこともできる。

using mathfunc = double (*)(double);

(ポインタと実体が混乱する例)[要説明]

ポインタ演算[編集]

記憶 (メモリ) 空間は、1次元空間である。ポインタが示すアドレスは、この1次元記憶空間の座標 (Point) であり、その他の1次元空間における座標と同じ演算が可能となっている。逆に言えば、整数のような自由な四則演算は制限されており、座標に対する演算と同じ演算しかできないということである。ポインタという名称も、この座標の概念に由来する。なお、1次元空間であるため、座標に対する加減算は、ベクトルではなくスカラー値が用いられる。

char *point1, *point2, *point3;

/* 任意の座標(アドレス)を代入できる */
point1 = (char*)0x4;

/* 座標にスカラー値を加算できるように、スカラー値を加算できる */
point2 = point1 + 1;

/* スカラー値で減算できる */
point1 -= 1;

/* 座標に乗算除算できないのと同じく、下記のような乗算除算はできない */
point1 *= 2;
point1 /= 2;

/*
座標同士の減算では、座標間の向きと距離をベクトルで得られるのと同じく、向きと距離をスカラー値で得られる
*/
ptrdiff_t difference;
difference = point2 - point1;

/*
座標同士では引き算以外できないのと同じく、下記のような加算・乗算・除算・剰余演算はできない
*/
point3 = point1 + point2;
point3 = point1 * point2;
point3 = point1 / point2;
point3 = point1 % point2;

問題点[編集]

ポインタには不正な領域を示しうるという問題がある。たとえば、近年[いつ?]セキュリティ上で問題となっているバッファオーバーランの原因の多くは、ポインタ演算のエラーで起こる不正領域の書き換えによるものである。また、「オブジェクトそのものに対する操作」と「オブジェクトの位置に対する操作」が混在することは、プログラマの混乱を招きやすい。このような問題もあって、JavaC#など、C言語よりも新しい後発のプログラミング言語では、言語レベルでのポインタ機能は、排除されるか制限される方向にある。

しかし、プログラマーに直接ポインタ操作を許可していない言語でも、ポインタ概念は存在する。たとえば、配列中にオブジェクトを格納し、それを要素のインデックスで参照すれば、これは「ポインタ概念」を活用していることになる。したがって、配列の要素数を超えた領域をアクセスすれば、エラーが発生する。しかし、配列へのインデックスアクセスを完全に排除してしまうと、その言語の制限が厳しくなり、単純な動作を簡易に記述できる領域を狭めてしまう(言語の表現力が低下する)。このように、ポインタには危険性があるが、プログラミングをするうえでは、非常に強力なテクニックである。また、C言語でマイコンの周辺デバイスを制御する場合、メモリバス上の特定のアドレスにあるレジスタに値を読み書きする必要があるため、必須のテクニックとなる。

一方、関数型言語などの発展により、ポインタの必要性は、今後減少する可能性が考えられる[独自研究?]。また、データベース領域では、SQLのように関係式からデータを導き出し情報の位置を抽象化する概念が古くからあり、こちらもプログラミングパラダイムに影響を与えることが考えられる[独自研究?]

参照[編集]

C++における参照は、ポインタと同様「変数がメモリ上に置かれている場所」と解される場合もあるが、それよりもむしろ「その変数を参照する(=変数の値を操作したり出来る)権限」と解されることが多い。参照の概念そのものはメモリの概念と切り離して考えることが可能である(実装上はポインタと同じであることも多い)。

C++における参照の例[編集]

int n = 5;
int& n2 = n; // n2をnへの参照で初期化する

この場合変数n2はnを参照している。n2とnはオブジェクトを共有しているのでn2と呼んでもnと呼んでも同じものを表す。すなわち変数nにエイリアス(別名)n2が付いたことになる。

脚注[編集]

[ヘルプ]

関連項目[編集]