2008.12.8 新規作成。
(1999.12.14-16, 1999.12.28, 2001.7.23 の日記を再構成、加筆。)
C言語は、今の感覚だとずいぶん古くさいプログラミング言語ですが、まだまだ幅ひろく使われています。
C言語は、「ポインタ」が難しいという評判があります。優れた解説もあるものの、一方で明らかに妥当でないものも駆逐されずに広く公開されています。
このページでは、C/C++言語でのポインタと配列について改めて整理していこうと思います。
もとの日記の「fjで一時盛り上がっていた」とか「infoseekで・・・を検索」というフレーズが時代を感じさせます。fj.comp.lang.c とかまだあるんでしょうか。
C言語 (plain C) の仕様は次が最新です。JISC 日本工業標準調査会で閲覧できます。
C++は、2009.1現在、改定作業が進められており、次のサイトでドラフトを見ることができます。
N2800がC++0xのドラフトです。
また、C++ Final Draft International Standard でISO/IEC 14882:1998 C++のFDISを参照できます。ただし、最新版はTechnical Corrigendum (正誤表) が適用されたISO/IEC 14882:2003です。
C言語では、オブジェクトは、その「型」に応じた一定の大きさのメモリを占めます。オブジェクトの型は、数値型、ユーザが定義した構造体の型、それから後述するポインタ型 (pointer type) など。
sizeof演算子で、オブジェクトがどのぐらいのメモリを占めるか調べることができます。
![]() | Note. コンパイラのおこなう最適化によって、実際にはオブジェクトが生成されないことがあります。しかし、プログラムを書く場合は、そのことを考える必要はありません。 |
C言語の仕様では、オブジェクトは、次のように定義されます。
3.14 オブジェクト (object) -- その内容によって、値を表現することができる実行環境中の記憶域の部分。参考 オブジェクトを参照する場合、オブジェクトは、特定の型をもっていると解釈してもよい (6.3.2.1参照)。
例えば、次のコード片は、
次のように動作します。
変数もありますが、オブジェクトと言っていることに注意してください。
変数は、何らかのオブジェクトを参照するためのラベルです。すべてのオブジェクトについて対応する変数があるわけではなく、変数によって参照されることのないオブジェクトもあります。
ポインタの「概念」は、プログラミングでは必須だと思います。
C言語のポインタは難しいという伝説が流布していますが、(1) 誤った解説によって学習すると確かに難しい、(2) C言語の文法がそもそも変態、というところから出ているように思います。
ポインタは、別のオブジェクトを指すためのオブジェクトです。
次の例は、よくあるポインタの使用例です。行7でポインタオブジェクトを生成し、変数qと名づけます。
行8で、ポインタが配列の2番目の要素を指すようにし、ポインタに1ずつ足して、なめるようにアクセスします。
あるオブジェクトに & 演算子を作用させると、そのオブジェクトへのポインタが得られます。
また、*演算子でポインタが指すオブジェクトを参照できます。
![]() | Note.
規格では、次のようになっています。それにしても長い。 6.5.3.2 アドレス及び間接演算子 単項&演算子は、そのオペランドのアドレスを返す。オペランドが型“〜型”をもっている場合、結果は、型“〜型へのポインタ”をもつ。 オペランドが、単項*演算子の結果の場合、*演算子も&演算子も評価せず、両演算子とも取り除いた場合と同じ結果となる。ただし、その場合でも演算子に対する制約を適用し、結果は左辺値とならない。 同様に、オペランドが[]演算子の結果の場合、単項&演算子と、[]演算子が暗黙に意味する単項*演算子は評価されず、&演算子を削除し[]演算子を+演算子に変更した場合と同じ結果となる。 これら以外の場合、結果はそのオペランドが指し示すオブジェクト又は関数へのポインタとなる。 単項*演算子は、間接参照を表す。オペランドが関数を指している場合、その結果は関数指示子とする。オペランドがオブジェクトを指している場合、その結果はそのオブジェクトを指し示す左辺値となる。オペランドが型“〜型へのポインタ”をもつ場合、その結果は型“〜型”をもつ。 正しくない値がポインタに代入されている場合、単項*演算子の動作は、未定義とする(注83)。 注(83) &*EはEと等価であり (Eが空ポインタであっても)、&(E1[E2]) は ((E1) + (E2)) と等価である。 Eが単項&演算子の正しいオペランドとなる関数指示子又は左辺値の場合、*&EはEに等しい関数指示子又は左辺値である。*Pが左辺値であり、かつTがオブジェクトポインタ型の名前である場合、キャストを含む式*(T)Pは、Tが指すものと適合する型をもつ左辺値である。 |
C++では、ポインタと同じように操作できるオブジェクトである、イテレータが導入されています。
const_iterator あるいは iterator は、ポインタのように操作できます。
a.begin() は、コンテナの先頭要素を指すイテレータを得ます。
ポインタは、数値を足したり引いたりすることができます。
例えばポインタに1足すと、現在指しているオブジェクトの次のオブジェクトを指す値となります。-1だと一つ手前のオブジェクトを指すポインタになります。
よく、ポインタはメモリのアドレス、という解説がありますが、C言語はアセンブラと違ってポインタが型を持っているので,+1すると次の要素を指す,-1すると前の要素を指す,という意味になります。(アドレスが、指す先のオブジェクトの大きさだけ増減する。)
C++のイテレータも、ポインタに似せて、+1や-1で次のオブジェクト、前のオブジェクトを指すイテレータになります。
C++では、演算子 + や - を上書きすることができ、イテレータはこの機能を利用して実装されています。
次のサンプルの、行14や行23で+や*を定義しています。
TODO: ポインタ同士の引き算
C++では、ポインタ同士は、基底クラスの方向に向かっては、単に代入できます。派生クラスの方向に向かっては、明示的にキャストしなければなりません。
TODO: 別ページにて。
ポインタにも、ほかのオブジェクトと同様に、型があります。整数(オブジェクト)を指すためのポインタ(オブジェクト)の型は「整数へのポインタ」型です。
ポインタ型は、規格ではつぎのようになっています。
6.2.5 型ポインタ型 (pointer type) は、被参照型 (referenced type) と呼ぶ関数型、オブジェクト型又は不完全型から派生することができる。ポインタ型は、被参照型の実体を参照するための値をもつオブジェクトを表す。被参照型Tから派生されるポインタ型は、“Tへのポインタ”と呼ぶ。被参照型からポインタ型を構成することを“ポインタ型派生”と呼ぶ。
ポインタオブジェクトの大きさはintの大きさと同じとは限りません。
現状、32bit環境では、sizeof(int) = 4, sizeof(ポインタ型) = 4の環境なので、うっかりポインタをintにキャストしても動いてしまいます。
64bit環境ではsizeof(int) = 4, sizeof(ポインタ型) = 8 です。
UNIX (POSIX) では、64bit環境はLP64モデルと決められています。一方、WindowsではLLP64モデルが採用されています。
| LP64 | LLP64 | |
|---|---|---|
| char | 1 | |
| short | 2 | |
| int | 4 | |
| long | 8 | 4 |
| long long | 8? | 8 |
| ポインタ型 | 8 | |
UNIXでもWindowsでもない環境では、ほかの組み合わせかもしれません。CrayがILP64を採用していた?(未確認)
次の2行はいずれも変数を宣言し、オブジェクトに紐付けられます。変数bはポインタオブジェクトを表します。
それぞれ、&演算子でオブジェクトのアドレスを取ることができ,そのオブジェクトを指すポインタに設定できます。
変数 d はポインタのポインタです。int* 型のオブジェクトを指すポインタだから,その型は int** になります。
考え方は普通のオブジェクトを指すポインタと変わりません。すなわち,型 Foo のオブジェクトを指すポインタの型は Foo* になるし,変数 p が Foo* 型なら,*p は型 Foo になります。
&でオブジェクトのアドレスを取ることを説明しましたか、まだオブジェクトとして定着していない(実体のない)もののアドレスは取れません。・・・左辺値が要求される。
これはaもbも実体を表すので問題ありませんが,次のはエラーになります。
ポインタは別のオブジェクトを指すためのオブジェクトですから、何も指さない状態でアクセスすると不正です。
誤った例;
これを実行すると、たいていは実行時エラーが生じてプログラムがabortします。
例えば、次のようにして領域を確保し、そこを指すようにします。確保した領域は必ず自分で解放しなければなりません。
配列だとプログラムの実行時に確保する大きさを変えることができないので,ユーザからの入力を格納したい場合など、あらかじめ必要な大きさが不明なときは動的に確保しなければなりません。Cではmalloc() で領域を確保し、free() で解放します。
![]() | Note. C++では、オブジェクトを初期化しなければならないため、newで確保し、deleteで解放します。POD (Cスタイル) ではない構造体ではmallocは使えません。 |
上のサンプルは確保する大きさを決め打ちしている点が手抜きです。
C言語では、不定な (C言語のオブジェクトを指さない) ポインタというものを許可しています。プログラマが明示的に初期化しなければならず、初期化を忘れると正しく動作しません。無用なバグの原因といえます。
C言語は、ポインタと配列という違うものを同じ書き方で操作するようになっているところがあり、混乱があります。
まず配列を用意します。
ここで,単にeと書くと何を表すでしょうか? C言語ではこれが&e[0]の場合とeそのものの場合があります。sizeof(e)と書くと,e自体の大きさを得ることができます。それ以外は&e[0]の意味になります。
そのため、
次の例では、実引数である配列を、f() でポインタ型の仮引数で受けています。
C言語では恐ろしいことに、a[n] という表記は、*(a + n) と同じ意味です。配列でもポインタでも両方の表記が使えます。言語仕様、手を抜きすぎ。
Cでは、主にコールバックのために、関数へのポインタを使います。
行12-14で定義した関数へのポインタを、行19でprint_if へ渡しています。行6でその関数を呼び出しています。
ここでも C の文法の混乱が見えます。配列と同様に、関数名は関数のアドレスにもなります。
TODO:
書いていて飽きてきたので、解説が妥当ではないサイトに突っ込みを入れてみます。性格が悪い気もしますが、間違えやすいところが見えてきます。
a3「ポインタと配列」というページのサンプルプログラム、例2.2を引用。
誤った例;
まず、void main() ではなく int だろ、とか、なんだか空白の入れ方が一貫性がない、というのは置いておいて。
これを動かすと,1 2 3 4 5 まで表示したあと,延々とでたらめな表示が続くか、例外が発生する可能性があります。(正常に終了する可能性もある。)
行6で pa の指すオブジェクトの値が 0 のときに終了としていますが,配列 a の初期化で末尾に0を入れるのを忘れています。
文字列リテラルだと自動的に終端の 0 を補ってくれるので、勘違いしたのかもしれません。
そうとう面白いサイトです。解説がデタラメというか,日本語が訳分かりません。第4話から。
そもそも、コンパイルできないんじゃないかと思いましたが、Cだとコンパイラに通ってビックリ。(C++だともちろんエラー。)
ポインタ云々の前に何がしたいか分かりません。変数 kanji をどうしたいのか。C の char がバイト単位で、文字単位ではないことを示したいなら次のようになるでしょうか。
これもリテラルがロケール依存で、あまりよくないコードですが。
第4章から。
整数の変数aを int a; と宣言することは、CPUのメモリ空間に1つの整数型の変数の領域を確保させるということだった。すると、int *a; と整数型の実体を指すポインタを宣言した場合はメモリ領域を確保してくれるのかな?
とんでもない。かつて北斗の拳が「ポインタには領域などな〜い」と言っていたかどうかは知らないが、ポインタを単に宣言しただけではメモリ領域は確保されない。実体があってのポインタである。
ブッブー。
ポインタの指す先のオブジェクトを自動で確保してくれるか,という話なら確保してくれないというので正しいですが,ポインタオブジェクトの領域は確保されるので、おかしい。
int a;とするとint型の変数aを確保する。int* a;とするとint*型の変数aを確保する。ポインタも使い方が違うだけで普通の変数と変わらない。
なんかポインタの説明も怪しいですが、この辺りがどうかと。
このように、C言語ではポインタと配列は同じもののように扱うことができます。両者の根本的な類似点は、ポインタ変数も配列名もアドレスを表すポインタであるということです(普通の変数は変数名が、その変数の内容を表します)。
ダウト。配列はポインタオブジェクトではありません。
逆に、きちんとした解説をしているサイトも紹介しておきます。
ふぃんろーださんによる、『Cマガジン』に連載されていた内容。私も知らないことが書いてあって面白い。
plain Cの話なので,C++だと微妙に異なるところがあります。例えば次のプログラム,
かなり気合いが入っていて,結構面白い。
Netsphere Laboratories http://www.nslabs.jp/
[PR]