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

ポインタ (: pointer) とは、コンピュータプログラミングにおいて、変数定数サブルーチン(関数)などが置かれたメインメモリ上の番地(メモリアドレス)を格納するための特殊な変数のこと[1][2]。あるオブジェクトがなんらかの論理的位置情報に基づいてアクセスできるとき、それを参照する(指し示す)ためのものである。一般的な意味での「ポインタ」は「何かの位置を指し示すための仕組みや道具」のことであり、転じてコンピュータプログラミングの分野ではメモリアドレス参照の意味で使われるようになった。

概要 編集

コンピュータのメモリ空間memory space)は番地(アドレス)と呼ばれる通し番号で管理される[3]。コンピュータがプログラムを実行する際は、プログラムに含まれる変数や定数、オブジェクトの実体(インスタンス)、サブルーチン[注釈 1]などはそれぞれのデータ型および命令数などに基づいたサイズを持ち、何らかのアドレス値をもってメモリ空間上に配置される。このアドレス値を格納するための変数がポインタである。

アセンブリ言語のような低水準言語ではない)高水準言語において、数値型(例えばCにおけるint型やdouble型)のような、ポインタではない型の変数は、そこに整数や小数といった値を格納するための入れ物であり、その配置場所がメモリ上のどのアドレス(番地)であるかは、取り立てて必要のない限りは意識しなくて済む仕様となっている。一方、この配置場所を値として格納するための変数がポインタである。

厳密にいうと、ポインタではない型の変数の場合も、変数自身が内部的に持つのは整数や小数の値そのものではなくその配置場所のアドレスである。ただしこのアドレス参照は高水準言語のプログラムコード上では隠蔽される。一方のポインタ変数では、値の配置場所のアドレスが格納されていることが高水準言語のコード上でも現れる。なお、レジスタベースのマシンでは、一部のローカル変数はスタックメモリではなくレジスタに直接割り付けられることもある。その場合、メモリ空間上には配置されない[注釈 2]。レジスタはごく少量だが高速な作業領域で、プロセッサの命令やそのオペランド、演算結果といったデータを一時的に格納するなどの目的で使用される。スタックポインタやプログラムカウンタ(インストラクションポインタ)のように、アドレスを指し示す専用レジスタも存在する。

ポインタという仕組みは、操作対象の位置を表すメモリアドレス値を直接参照したり書き換えたりすることができるため、ハードウェア寄りの操作や制御を行うのには都合が良い[1]。その反面、実行時に不正な値を入力されるようなことが起きると、プログラマが予期していなかった位置のデータやコードを読み書きしてしまう場合があり、深刻なバグ、しかもセキュリティの脆弱性となってしまうような深刻なバグがしばしば発生する[1]。→#問題点

多くのプログラミング言語においてポインタあるいはポインタに類する機能を利用できるが、C/C++に言語機能として実装されたポインタが特に有名である。なお、C++には、ポインタとは独立した類似の言語機能として、「参照」がある(#参照の節を参照)。

プログラムコード中では「ptr」と略されることも多い。

C言語のポインタ 編集

最も典型的なポインタの例としては、C言語におけるポインタが挙げられる。C言語のポインタは「特定のメモリ領域を指し示す」ものである。ポインタを経由してメモリ上のデータにアクセスする際、参照するデータの型に応じたポインタ型を用いる。たとえば、int型のデータにアクセスする場合は、int*型を用いる。int*型を持つ変数を「intへのポインタ」(pointer to int[5]) と呼ぶ。

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

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

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

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

実際の例 編集

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

単純なポインタ 編集

  • 宣言例
/* int型変数 n を宣言 */
int n;
/* intへのポインタ型変数 ptr を宣言 */
int *ptr;
/* int型変数 n のアドレスをポインタに代入 */
ptr = &n;

アスタリスク*前後のスペースの有無は任意である。

上記の2つの変数nptrは、以下のようにまとめて宣言することもできるが、混乱を招く恐れがある[6]

/* intへのポインタ型変数 ptr と、int型変数 n を宣言 */
int *ptr, n;

複数のポインタ変数をまとめて宣言する場合は、以下のように書く必要がある。

int *ptr0, *ptr1;

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が定義された。C++11以降でもNULLは引き続き利用可能だが、整数定数のゼロに等しいとは限らず、実装は処理系依存となる[7]

int *ptr = nullptr;
  • 利用例

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

int n = 0;
int *ptr = &n;
*ptr = 10;
printf("%d\n", n); /* 10 */
  • 配列とポインタ

C言語における配列(固定長配列)とポインタはそれぞれ異なるデータ型であるが、配列の添え字演算子は、ポインタの加減算とデリファレンスの糖衣構文である。

double a[10];
int i;
for (i = 0; i < 10; ++i) {
    const double x = i * 0.1;
    /* 以下はともに同じ意味を持つ。 */
#if 1
    a[i] = x;
    printf("%f\n", a[i]);
#else
    *(a + i) = x;
    printf("%f\n", *(a + i));
#endif
}

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

int i;
int *ptr = malloc(sizeof(int) * 10);
for (i = 0; i < 10; ++i) {
  ptr[i] = i;
}
free(ptr); /* 解放により 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にその整数値を出力する例である。scanf関数は書式文字列に応じて、可変長引数に渡された実引数の型が何であるかを判断する。例えば%d書式はintへのポインタが渡されたとみなす。

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

または

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

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

int *ptr;
int **pptr;
pptr = &ptr;
  • ポインタ配列の例
#include <stdio.h>
/* 
argc : コマンドライン引数の数。
argv : コマンドライン引数の文字列群。ゼロ終端文字列先頭要素へのポインタ char* の配列があり、その先頭要素へのポインタであり、char *argv[] と宣言することもできる。配列の要素数は argc + 1 個。
最後の要素 argv[argc] は常に NULL となる(番兵)。
https://ja.cppreference.com/w/cpp/language/main_function
*/
int main(int argc, char **argv) {
  while (*argv != NULL) printf("%s\n", *argv++);
}

ポインタ配列先頭要素へのポインタargvを操作して、それぞれの要素が指すゼロ終端文字列char*)を標準出力に書き出している。しかし、間接演算子"*"とインクリメント演算子"++"のどちらの優先度が高いのかを知らないと、このような記述を理解することはできない。したがって、保守作業の際にバグを誘発しやすいため、以下のように記述したほうがよいとする主張もある。

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

関数ポインタ 編集

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

ポインタ演算 編集

記憶域 (メモリ) のアドレス空間は、1次元空間である。たとえば32ビットシステムのアドレス空間は、16進数表記で 0x00000000 から 0xFFFFFFFF (4GiB-1) までの整数値が有効な範囲である。ポインタはこのアドレス空間を抽象化し、メモリ上の任意位置のデータ(オブジェクト)にアクセスするためのデータ型である。なお、プロセスのアドレス空間は物理メモリ上のアドレス(物理アドレス)に直接対応するとは限らない。通例、オペレーティングシステムによって物理メモリは抽象化され、プロセスごとに仮想アドレス空間が割り当てられ、プロセスごとの仮想アドレス空間におけるメモリの読み書き処理はOSによって物理アドレスに対する処理に変換される(メモリマッピング)。

ポインタに対する算術演算は、加減算のみが許可される。ポインタに加減算すると、そのポインタが指すデータ型のサイズに比例したオフセットがアドレスに加減算されることになる。これは配列の添え字演算子が、ポインタの加減算とデリファレンスの糖衣構文であることからも自明である。つまり、ある型Tへのポインタの加減算は、メモリ全体をTの配列とみなして、インデックスを増減していることに他ならない。

int* p = NULL;
printf("Size of int = %d\n", (int)sizeof(int));
printf("%p\n", p);
p++; /* sizeof(int) * 1 だけアドレスが加算される。 */
printf("%p\n", p);
p += 10; /* sizeof(int) * 10 だけアドレスが加算される。 */
printf("%p\n", p);
p--; /* sizeof(int) * 1 だけアドレスが減算される。 */
printf("%p\n", p);

バイト単位のアドレッシングが必要な場合、char / signed char / unsigned charへのポインタを利用する。これらの型はサイズが1であることが規格で保証されているため、これらの型へのポインタに対する加減算操作はバイト単位のアドレッシングとなる。

なお、汎用ポインタvoid*は型が不明のため、アドレスの加減算を行なうことができないが、GCC拡張の非標準動作ではvoid型のサイズに1を割り当てるため、加減算が可能となっている[9][10]

void* pv = NULL;
pv++; /* C/C++標準規格に準拠した環境ではコンパイルエラーとなる。 */
printf("%p\n", pv);

ポインタ型のサイズは処理系依存であり、通例ターゲットとするプロセッサアーキテクチャのレジスタ幅と同じサイズを持つ。

オフセット計算などでポインタ間の差を得る必要がある場合、<stddef.h>で定義されているptrdiff_t型を用いる。ポインタ(アドレス値)を整数型に代入する必要がある場合、<stdint.h>で定義されているポインタ互換の整数型intptr_tuintptr_tを用いる(C99およびC++11で標準化されている)。なお、C/C++ではintlongなどの組み込み整数型のサイズもまた処理系依存である。移植性を損なうため、ポインタを扱う目的で組み込み整数型を直接使用してはならない。

// C99 の例。
#include <stdio.h>
#include <stddef.h>
#include <stdint.h>
...
int a[10] = { 0, 1, 2, 3, 4, 5, 6, 7, 8, 9 };
int* p1 = &a[0]; // または (a + 0)
int* p2 = &a[9]; // または (a + 9)
printf("p1 = %p, p2 = %p\n", p1, p2);
ptrdiff_t difference = p2 - p1;
printf("Difference as pointer = %td\n", difference); // 9
intptr_t i1 = (intptr_t)p1;
intptr_t i2 = (intptr_t)p2;
printf("Difference in bytes = %jd\n", (intmax_t)(i2 - i1)); // sizeof(int) * 9

問題点 編集

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

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

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

参照 編集

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

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

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

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

ポインタは未初期化の状態や、何も参照していない状態(ヌルポインタ)が許可されるが、C++における参照は必ず初期化が必要となり、何も参照していない状態は許可されない。ただし、ダングリングポインタと同様、破棄された領域への参照は不正となる。

int& foo() {
    int a = 0;
    return a; // 不正。制御が関数呼び出し元に戻った時点で a は破棄されている。
}

ダングリングポインタ 編集

ダングリングポインタ英語版: dangling pointer)は解放後のメモリ領域を参照するポインタである[11]

メモリ領域は変数のために確保され利用後に解放される。ゆえにその領域が正しい意味を持つのは確保から解放までの間のみであり、解放後にその領域へアクセスすること (Use-After-Free) は未定義の動作を引き起こす。ダングリングポインタは解放後のメモリ領域を参照しているため、このポインタの利用は Use-After-Free であり予測できない結果を生み出してしまう。

防止策が無い場合、ダングリングポインタは容易に生成されうる。例えば、参照を生成し元の変数を free するだけで参照先が解放された状態になってしまう(参照先と参照での生存期間ズレ)。可変長配列のスライス参照ではより複雑で、配列へのpushに伴うメモリ再割り当てで変数の生存期間中なのに最初の領域が解放されるケースがある[12]

このためダングリングポインタはバグの大きな原因であり、それを悪用したハッキングにしばしば利用される。これを事前に防ぐためにモダンなプログラミング言語では言語仕様レベルでのダングリングポインタ防止策が取り入れられている(例: Rust の借用チェッカー)。

スマートポインタ 編集

動的確保したメモリのアドレスをポインタにより管理するとき、注意深くプログラミングしないと解放忘れやダングリングポインタといった問題が発生しやすい。C++ではポインタをクラスでラップし、デストラクタの機構を利用して解放処理を自動化した「スマートポインタ」が利用されることが多い。またオブジェクトの所有権の移動や共有といった、生のポインタでは扱いが難しい概念をスマートポインタの機能として簡潔に実現するライブラリも存在する。

脚注 編集

注釈 編集

  1. ^ サブルーチンも命令情報の集合であり、メモリ上でサイズを持つデータであることには変わりない。
  2. ^ Cにはコンパイラ最適化のヒントとして、registerキーワードがある。レジスタを使用する最適化が実際に行なわれるか否かにかかわらず、register指定された変数のアドレスを取得することはできなくなる[4]

出典 編集

  1. ^ a b c ポインタとは - IT用語辞典 e-Words
  2. ^ POINTER | Definition of POINTER by Oxford Dictionary on Lexico.com also meaning of POINTER | §1.5
  3. ^ 「第3回:アドレスとポインタ変数」
  4. ^ 記憶域クラス指定子 - cppreference.com
  5. ^ "pointer of" ではない。
  6. ^ DCL04-C. ひとつの宣言で2つ以上の変数を宣言しない | JPCERT/CC
  7. ^ NULL - cppreference.com
  8. ^ MEM01-C. free() した直後のポインタには新しい値を代入する
  9. ^ Using and Porting the GNU Compiler Collection (GCC) - C 言語ファミリに対する拡張機能
  10. ^ Using the GNU Compiler Collection (GCC): Warning Options
  11. ^ "解放後のメモリ領域を参照するダングリングポインタ" 池上. (2014). メモリ再利用を禁止するライブラリにより Use-After-Free 脆弱性攻撃を防止する手法の提案. Computer Security Symposium 2014.
  12. ^ "`push` によって `data` の格納先が再割り当てされてしまった ... 単純なスコープ解析では、このバグは防げません。 ... 問題は、その参照を保持している間に、参照先が変わってしまったことです。" rust-lang. 所有権とライフタイム. The Rustonomicon. 2022-12-22閲覧.

関連項目 編集