ポインタあるいはポインター(pointer)は、 コンピュータで処理するデータやプログラムの場所を記憶して指示するレジスタや変数の分類や呼称 。
概要
コンピュータは処理対象のデータと処理内容であるプログラムとを、一定の規則で記憶装置に並べて処理を行う。 記憶装置の特定の場所を指示すため、(現在のコンピュータの多くは)連続した整数のアドレス(Address)、番地を1バイト単位で割当てている。またプロセッサの内部にもレジスタと呼ばれる記憶場所を、数個から数10個程度配置している。レジスタや記憶装置の中身は全て有限桁ビット列のデジタル数値であるが、特にアドレス指定のための数値を記憶するレジスタや、記憶装置での特定アドレス(高水準言語での特定変数)を、ポインタと呼んで区別している 。
命令セットでのポインタ
プログラムはInstruction(命令・指示)と呼ばれる、個々のプロセッサ(CPU)に対応した命令を、処理順序に従って並べることで実現されている(命令の集まりのことは命令セットと呼ばれる)。 並べた命令を自動的に実行するためには、プログラムカウンタ(Program Counter、PC)と呼ばれる、CPUに内蔵された記憶回路(レジスタ)が用いられる。 プログラムカウンタには、現在実行中(あるいは次に実行予定)の命令のアドレスが記憶され、CPUで現在の命令が処理されるとともに、プログラムカウンタには次に実行予定の命令の所在地(アドレス)が収納される。 プログラムによっては、条件分岐やループといった処理も必要になる。 これらはジャンプ等の命令を実行することで、間接的にプログラムカウンタの値をジャンプ先のアドレスに書換えることで実施される。プログラムカウンタは命令ポインタ(Instruction Pointer、IP)と呼ぶ場合もあり、場所を指示するポインタの1つである。
高級言語が利用されプログラムが大規模で複雑になると、プログラムを分割して作成管理利用するようになる。 分割されたプログラムは、言語によってサブルーチン(Subroutine)やプロシージャ(Procedure)、関数(Function)やメソッド(Method)などと呼ばれる。 これらの実現には、引数の受渡し、戻り値の受渡し、呼出し先へのジャンプ、呼出し元へのジャンプなどが必要になるが、多くの場合スタックと呼ばれるLIFO(Last In, First Out)のデータ構造を利用して実現している。スタックの先頭を示すレジスタが用意される場合もあり、Stack Pointer(SP)などと呼ばれ、これもポインタの1つである。
データ構造でのポインタ
コンピュータで扱うデータ形式は有限桁の整数か浮動小数点数を基本とし、それらを規格や処理内容に従い、適切にエンコード(符号化)して記憶装置に配置する必要がある。 多く用いられるソート整列や探索、文字列処理や数値計算などは、コンピュータの命令セットやプログラミング言語の種類に依存しない、一般化された解法アルゴリズムとして類型化されている。 また各種アルゴリズムに適したデータ構造も類型化されている 。
例えば学生のデータを作成し処理する場合、学生毎に記憶する内容は氏名や住所や生年月日、各授業の出欠状況や試験結果など多岐にわたる。 こうした学生1人分のデータは、構造体やクラスを用いて実現される。 複数の学生データの全体について、氏名の辞書順やある試験結果の順に並べるソート処理や、ある年に生まれた学生や及第点の学生の存在有無を調べる探索処理、転入した学生の追加処理などを考慮してデータ構造が設計採用される。 データ構造の類型として、配列や構造体に加えて連結リストが挙げられる。 連結リストは、学生の氏名や住所などデータそのものに加えて、別の学生への連結(リンク)を1つ以上持つデータ構造であり、データの追加削除や整列に適している 。
連結リストは、LISPやJavaやC では言語仕様やクラスライブラリで用意されている。一方でC言語ではポインタをメンバーとする構造体として、利用者が設計し実現する必要がある。 連結リストにより制限をかけた利用方法が後入先出のスタックと先入先出のキューである。 また上下(親子)関係を明確にしたものが木構造である。 グラフ構造は連結リストをより一般化したものである 。
下記は前後用にリンクを2つ持つリストのための、構造体の例である。
『プログラミング言語C』では上記のような構造体を、自己参照的構造体として紹介し、木構造が具体例で用いられている 。
C言語のポインタ
最も典型的なポインタの例としては、C言語におけるポインタが挙げられる。C言語のポインタは「特定のメモリ領域を指し示す」ものである。ポインタを経由してメモリ上のデータにアクセスする際、参照するデータの型に応じたポインタ型を用いる。たとえば、int型のデータにアクセスする場合は、int*型を用いる。int*型を持つ変数を「intへのポインタ」(pointer to int) と呼ぶ。
C言語にポインタが存在する理由は、効率上の問題である。C言語は、元々UNIXを記述するシステム用言語として開発されたものである。したがって、アセンブリ言語で実行できる操作のほぼ全てを行える必要があった。そのため、特定のメモリ領域への値の直接代入能力を持つなど、他のプログラミング言語と比較すると異色とも言える強力なポインタ機能を備えている。
C言語の実行モデルでは、実行プログラム上の関数コード、データが全て1次元のアドレスに直列配置される。そのため、データはおろか、関数のアドレスを取得し、他の関数にエントリーポイント情報として渡すこともできる。
また、C言語の関数では、引数は、値渡しだけをサポートし、参照渡しをサポートしない。これは、アドレスの数値を取得すれば、参照に可能な全てを行えるため、実質的に参照を数値と同一視できるからである。実際、初期のC言語では、アドレス値は、整数型互換するものとして扱われていた。これは、値と参照を明確に区別するPascalなどとは対照的である。現在でもC言語は、void*により任意のメモリ領域にアクセスできる。なお後発のC では参照渡しもサポートするようになった。
しかし、コード領域も含むメモリを直接扱えるということは、言語レベルでは(意図的でないとしても)不正なメモリアクセスを事実上保護できないということを示しており、C言語のプログラムにおけるポインタ関連のバグの多さがそれを証明している。
実際の例
一般的なC言語のソースコードでは、ポインタが指している領域の値を参照する間接演算子 (indirection operator) "*"と、アドレス演算子 (address operator) "&"を用いて記述される。未初期化のポインタ変数は、不定の領域を指している。しかし、その場合、「未初期化状態」と「有効な領域を指している状態」の区別がつかない。そのため、Null(ヌル)値を代入することによって、ポインタが無効な領域を指していることを明示する必要がある。
単純なポインタ
- 宣言例
アスタリスク*前後のスペースの有無は任意である。
上記の2つの変数nとptrは、以下のようにまとめて宣言することもできるが、混乱を招く恐れがある。
複数のポインタ変数をまとめて宣言する場合は、以下のように書く必要がある。
- 何も指していないポインタ(ヌルポインタ)
C言語の処理系では通例、無効なポインタを示す値として下記のようなNULLマクロが定義されている。
C言語では、voidへのポインタは任意の型へのポインタに自由に代入することができる。ポインタに無効値を代入する場合、通例このNULLマクロを使う。
一方、C では、NULLは整数定数のゼロに等しい。
そのため、C では下記のように書くこともできる。
C ではNULLが整数定数のゼロに等しいことから起こる関数オーバーロードのルックアップに関する問題を解決するため、C 11以降では、std::nullptr_t型として評価されるキーワードnullptrが定義された。C 11以降でもNULLは引き続き利用可能だが、整数定数のゼロに等しいとは限らず、実装は処理系依存となる。
- 利用例
下記はポインタptrの参照先である変数nに整数値10を代入することになる。
- 配列とポインタ
C言語における配列(固定長配列)とポインタはそれぞれ異なるデータ型であるが、配列の添え字演算子は、ポインタの加減算とデリファレンスの糖衣構文である。
実行時に要素数の決まる配列を作成する際など、動的にメモリ領域を確保するときは結果をポインタで受け取る。確保したメモリを解放するときもポインタを利用する。メモリ解放直後のポインタは無効な領域を指しており、これを「ダングリングポインタ」(dangling pointer) と呼ぶ。ダングリングポインタが指している領域を誤って使用することのないように、セキュリティ対策として明示的にNULLを代入しておく手法が推奨されている。
- 関数の引数
C言語の関数は前述のように参照渡しをサポートせず、値渡しのみをサポートするため、出力は戻り値(返り値)による1つのみを持つことしかできないが、ポインタを利用することで疑似的に複数の出力を持つ関数を定義することが可能となる。
下記は標準入力を整数値に変換し、scanf関数の第2引数の参照先であるnにその整数値を出力する例である。scanf関数は書式文字列に応じて、可変長引数に渡された実引数の型が何であるかを判断する。例えば%d書式はintへのポインタが渡されたとみなす。
または
- ポインタへのポインタ
「ポインタへのポインタ」(多重間接参照、ダブルポインタ)を定義することも可能である。動的に確保したメモリへのポインタを関数引数で返却するときや、ポインタ配列を扱うときなどに利用される。
- ポインタ配列の例
ポインタ配列先頭要素へのポインタargvを操作して、それぞれの要素が指すゼロ終端文字列(char*)を標準出力に書き出している。しかし、間接演算子"*"とインクリメント演算子" "のどちらの優先度が高いのかを知らないと、このような記述を理解することはできない。したがって、保守作業の際にバグを誘発しやすいため、以下のように記述したほうがよいとする主張もある。
関数ポインタ
上述の通り、C言語では関数を指すポインタ (pointer to function / function pointer) を作成することができる。
ポインタ演算
記憶域 (メモリ) のアドレス空間は、1次元空間である。たとえば32ビットシステムのアドレス空間は、16進数表記で 0x00000000 から 0xFFFFFFFF (4GiB-1) までの整数値が有効な範囲である。ポインタはこのアドレス空間を抽象化し、メモリ上の任意位置のデータ(オブジェクト)にアクセスするためのデータ型である。なお、プロセスのアドレス空間は物理メモリ上のアドレス(物理アドレス)に直接対応するとは限らない。通例、オペレーティングシステムによって物理メモリは抽象化され、プロセスごとに仮想アドレス空間が割り当てられ、プロセスごとの仮想アドレス空間におけるメモリの読み書き処理はOSによって物理アドレスに対する処理に変換される(メモリマッピング)。
ポインタに対する算術演算は、加減算のみが許可される。ポインタに加減算すると、そのポインタが指すデータ型のサイズに比例したオフセットがアドレスに加減算されることになる。これは配列の添え字演算子が、ポインタの加減算とデリファレンスの糖衣構文であることからも自明である。つまり、ある型Tへのポインタの加減算は、メモリ全体をTの配列とみなして、インデックスを増減していることに他ならない。
バイト単位のアドレッシングが必要な場合、char / signed char / unsigned charへのポインタを利用する。これらの型はサイズが1であることが規格で保証されているため、これらの型へのポインタに対する加減算操作はバイト単位のアドレッシングとなる。
なお、汎用ポインタvoid*は型が不明のため、アドレスの加減算を行なうことができないが、GCC拡張の非標準動作ではvoid型のサイズに1を割り当てるため、加減算が可能となっている。
ポインタ型のサイズは処理系依存であり、通例ターゲットとするプロセッサアーキテクチャのレジスタ幅と同じサイズを持つ。
オフセット計算などでポインタ間の差を得る必要がある場合、で定義されているptrdiff_t型を用いる。ポインタ(アドレス値)を整数型に代入する必要がある場合、で定義されているポインタ互換の整数型intptr_tやuintptr_tを用いる(C99およびC 11で標準化されている)。なお、C/C ではintやlongなどの組み込み整数型のサイズもまた処理系依存である。移植性を損なうため、ポインタを扱う目的で組み込み整数型を直接使用してはならない。
問題点
ポインタには不正な領域を示しうるという問題がある。たとえば、近年セキュリティ上で問題となっているバッファオーバーランの原因の多くは、ポインタ演算のエラーで起こる不正領域の書き換えによるものである。また、「オブジェクトそのものに対する操作」と「オブジェクトの位置に対する操作」が混在することは、プログラマの混乱を招きやすい。このような問題もあって、JavaやC#など、C言語よりも新しい後発のプログラミング言語では、言語レベルでのポインタ機能は、排除されるか制限される方向にある。
しかし、プログラマーに直接ポインタ操作を許可していない言語でも、ポインタ概念は存在する。たとえば、配列中にオブジェクトを格納し、それを要素のインデックスで参照すれば、これは「ポインタ概念」を活用していることになる。したがって、配列の要素数を超えた領域をアクセスすれば、エラーが発生する。しかし、配列へのインデックスアクセスを完全に排除してしまうと、その言語の制限が厳しくなり、単純な動作を簡易に記述できる領域を狭めてしまう(言語の表現力が低下する)。このように、ポインタには危険性があるが、プログラミングをするうえでは、非常に強力なテクニックである。また、C言語でマイコンの周辺デバイスを制御する場合、メモリバス上の特定のアドレスにあるレジスタに値を読み書きする必要があるため、必須のテクニックとなる。
一方、関数型言語などの発展により、ポインタの必要性は、今後減少する可能性が考えられる。また、データベース領域では、SQLのように関係式からデータを導き出し情報の位置を抽象化する概念が古くからあり、こちらもプログラミングパラダイムに影響を与えることが考えられる。
参照
C における参照は、ポインタと同様「変数がメモリ上に置かれている場所」と解される場合もあるが、それよりもむしろ「その変数を参照する(=変数の値を操作したり出来る)権限」と解されることが多い。参照の概念そのものはメモリの概念と切り離して考えることが可能である(実装上はポインタと同じであることも多い)。
C における参照の例
この場合変数n2はnを参照している。n2とnはオブジェクトを共有しているのでn2と呼んでもnと呼んでも同じものを表す。すなわち変数nにエイリアス(別名)n2が付いたことになる。
ポインタは未初期化の状態や、何も参照していない状態(ヌルポインタ)が許可されるが、C における参照は必ず初期化が必要となり、何も参照していない状態は許可されない。ただし、ダングリングポインタと同様、破棄された領域への参照は不正となる。
ダングリングポインタ
ダングリングポインタ(英: dangling pointer)は解放後のメモリ領域を参照するポインタである。
メモリ領域は変数のために確保され利用後に解放される。ゆえにその領域が正しい意味を持つのは確保から解放までの間のみであり、解放後にその領域へアクセスすること (Use-After-Free) は未定義の動作を引き起こす。ダングリングポインタは解放後のメモリ領域を参照しているため、このポインタの利用は Use-After-Free であり予測できない結果を生み出してしまう。
防止策が無い場合、ダングリングポインタは容易に生成されうる。例えば、参照を生成し元の変数を free するだけで参照先が解放された状態になってしまう(参照先と参照での生存期間ズレ)。可変長配列のスライス参照ではより複雑で、配列へのpushに伴うメモリ再割り当てで変数の生存期間中なのに最初の領域が解放されるケースがある。
このためダングリングポインタはバグの大きな原因であり、それを悪用したハッキングにしばしば利用される。これを事前に防ぐためにモダンなプログラミング言語では言語仕様レベルでのダングリングポインタ防止策が取り入れられている(例: Rust の借用チェッカー)。
スマートポインタ
動的確保したメモリのアドレスをポインタにより管理するとき、注意深くプログラミングしないと解放忘れやダングリングポインタといった問題が発生しやすい。C ではポインタをクラスでラップし、デストラクタの機構を利用して解放処理を自動化した「スマートポインタ」が利用されることが多い。またオブジェクトの所有権の移動や共有といった、生のポインタでは扱いが難しい概念をスマートポインタの機能として簡潔に実現するライブラリも存在する。
脚注
注釈
出典
関連項目
- 間接参照
- 変数 (プログラミング)
- 主記憶装置
- メモリアドレス
- 引数#変数渡し
- 参照 (計算機科学)

+これならば入れ替えができる+void+swap(int+*x%2C+int+*y)+{+int+tmp%3B+tmp+%3D+*x%3B+*x+%3D+*y%3B+*y+%3D+tmp%3B+return%3B+}.jpg)


