2008.12.8 新規作成。
(1999.12.14-16, 1999.12.28, 2001.7.23 の日記を再構成、加筆。)
C言語は、今の感覚だとずいぶん古くさいプログラミング言語ですが、まだまだ幅ひろく使われています。
C言語は、「ポインタ」が難しいという評判があります。優れた解説もあるものの、一方で明らかに妥当でないものも駆逐されずに広く公開されています。
このページでは、C/C++言語でのポインタと配列について改めて整理していこうと思います。
もとの日記の「fjで一時盛り上がっていた」とか「infoseekで・・・を検索」というフレーズが時代を感じさせます。fj.comp.lang.c とかまだあるんでしょうか。
(2020.1) C言語 (plain C) の最新の仕様は, ISO/IEC 9899:2018 です。"C17" と呼ばれます。gcc v9.2 では -std=c17 で対応します。このC17は, 実質的に, 一つ前の C11と変わりません。-std=c11 との違いは __STDC_VERSION__
値ぐらいです。
JIS 化はずいぶん昔の C99,「JIS X 3010:2003 プログラム言語C」で止まっています。JISC 日本工業標準調査会で閲覧できます。
C++は, 2020年1月現在, ISO/IEC 14882:2017 が最新版です。gcc v9.2 は, このC++17をサポートしています。オプションは -std=c++17. 一つ前は C++14で -std=c++14.
Visual C++ 2019は, C++14 (既定), C++17 をサポート。C++11 のスイッチはなし。
名前 | ほぼ規格の文面 | __cplusplus 値
|
---|---|---|
C++17 | N4659 PDF版 HTML版 | 201703L
|
C++14 | N4140 PDF版 HTML版 | 201402L
|
C言語では、オブジェクトは、その「型」に応じた一定の大きさのメモリを占めます。オブジェクトの型は、数値型、ユーザが定義した構造体の型、それから後述するポインタ型 (pointer type) など。
sizeof
演算子で、オブジェクトがどのぐらいのメモリを占めるか調べることができます。
C言語の仕様では、オブジェクトは、次のように定義されます。
3.14 オブジェクト (object) -- その内容によって、値を表現することができる実行環境中の記憶域の部分。参考 オブジェクトを参照する場合、オブジェクトは、特定の型をもっていると解釈してもよい (6.3.2.1参照)。
例えば、次のコード片は、
次のように動作します。
変数もありますが、オブジェクトと言っていることに注意してください。
変数は、何らかのオブジェクトを参照するためのラベルです。すべてのオブジェクトについて対応する変数があるわけではなく、変数によって参照されることのないオブジェクトもあります。
ポインタの「概念」は、プログラミングでは必須だと思います。
C言語のポインタは難しいという伝説が流布していますが、(1) 誤った解説によって学習すると確かに難しい、(2) C言語の文法がそもそも変態、というところから出ているように思います。
ポインタは、別のオブジェクトを指すためのオブジェクトです。
次の例は、よくあるポインタの使用例です。行7でポインタオブジェクトを生成し、変数qと名づけます。
行8で、ポインタが配列の2番目の要素を指すようにし、ポインタに1ずつ足して、なめるようにアクセスします。
あるオブジェクトに & 演算子を作用させると、そのオブジェクトへのポインタが得られます。
また、*演算子でポインタが指すオブジェクトを参照できます。
規格では、次のようになっています。それにしても長い。
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だと一つ手前のオブジェクトを指すポインタになります。
ポインタ同士の引き算は、二つのポインタの間の距離になり、型は組込みの ptrdiff_t
です。
よく、ポインタはメモリのアドレス、という解説がありますが、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(ポインタ型) = 8 です。UNIX (POSIX) では 64bit環境はLP64モデルと決められています。64-Bit Programming Models: Why LP64? 一方, Windowsは LLP64モデルを採用しています。long
の長さが異なります。Windows 環境でポインタを long
にキャストしてしまうと、壊れます。
型 | LP64 | LLP64 | 備考 |
---|---|---|---|
char | 1 | 1 | |
short | 2 | 2 | |
int | 4 | 4 | ILP64モデルでは sizeof(int) = 8. |
long | 8 | 4 | |
long long | 8 | 8 | |
ポインタ型 | 8 | 8 |
UNIXでもWindowsでもない環境では、さらにほかの組み合わせかもしれません。CrayやHAL SPARC64 (1995) の一部実装がILP64を採用していた, ようです。The Long Road To 64 Bits
OSによらずポインタの大きさを格納できる整数型は, intptr_t
(符号あり) または uintptr_t
(符号なし) です。
同様にポインタの引き算の結果も, 64bit UNIX / Windows とも, 64bitです。long
型に格納してはなりません。型は組込みの ptrdiff_t
(符号あり) です。
余談ですが, 64bit Windows は, いろいろな型が64bitになる一方で, off_t
は 32bit のまま、という罠があります。64bit UNIX は off_t
も64bit. 32bit UNIX も現代では off_t
は 64bit. 32bit UNIX で, _POSIX_V6_ILP32_OFFBIG
または _POSIX_V7_ILP32_OFFBIG
が定義されていないようなすごく古い環境では, 32bit の可能性がある. さらにこの場合でも, -D_LARGEFILE_SOURCE -D_FILE_OFFSET_BITS=64 オプションをコンパイル時に指定すれば, 64bit にできる。(Linux / Solaris の場合)
次の2行はいずれも変数を宣言し、オブジェクトに紐付けられます。変数bはポインタオブジェクトを表します。
それぞれ、&演算子でオブジェクトのアドレスを取ることができ,そのオブジェクトを指すポインタに設定できます。
変数 d はポインタのポインタです。int* 型のオブジェクトを指すポインタだから,その型は int** になります。
考え方は普通のオブジェクトを指すポインタと変わりません。すなわち,型 Foo のオブジェクトを指すポインタの型は Foo* になるし,変数 p が Foo* 型なら,*p は型 Foo になります。
&でオブジェクトのアドレスを取ることを説明しましたか、まだオブジェクトとして定着していない(実体のない)もののアドレスは取れません。・・・左辺値が要求される。
これはaもbも実体を表すので問題ありませんが,次のはエラーになります。
ポインタは別のオブジェクトを指すためのオブジェクトですから、何も指さない状態でアクセスすると不正です。
誤った例;
これを実行すると、たいていは実行時エラーが生じてプログラムがabortします。
例えば、次のようにして領域を確保し、そこを指すようにします。確保した領域は必ず自分で解放しなければなりません。
配列だとプログラムの実行時に確保する大きさを変えることができないので,ユーザからの入力を格納したい場合など、あらかじめ必要な大きさが不明なときは動的に確保しなければなりません。Cではmalloc() で領域を確保し、free() で解放します。
上のサンプルは確保する大きさを決め打ちしている点が手抜きです。
C言語では、不定な (C言語のオブジェクトを指さない) ポインタというものを許可しています。プログラマが明示的に初期化しなければならず、初期化を忘れると正しく動作しません。無用なバグの原因といえます。
C言語は、ポインタと配列という違うものを同じ書き方で操作するようになっているところがあり、混乱があります。
まず配列を用意します。
ここで,単にeと書くと何を表すでしょうか? C言語ではこれが&e[0]の場合とeそのものの場合があります。sizeof(e)と書くと,e自体の大きさを得ることができます。それ以外は&e[0]の意味になります。
そのため、
と代入できます。f = &e[0];と書かせるようにすることもできたと思いますが、数文字を節約するために、仕様が混乱してしまいました。次の例では、実引数である配列を、f() でポインタ型の仮引数で受けています。
C言語では恐ろしいことに、a[n] という表記は、*(a + n) と同じ意味です。配列でもポインタでも両方の表記が使えます。言語仕様、手を抜きすぎ。
Cでは、主にコールバックのために、関数へのポインタを使います。
行12-14で定義した関数へのポインタを、行19でprint_if へ渡しています。行6でその関数を呼び出しています。
ここでも C の文法の混乱が見えます。配列と同様に、関数名は関数のアドレスにもなります。
TODO:
淡々とした解説を見ても飽きるので、逆に、誤りを見てみましょう。
誤った例;
余談ですが、void main()
ではなく int main()
が正当。
この例の誤りは, 配列の末尾に 0 を入れていない点です。これを動かすと,1 2 3 4 5 まで表示したあと,延々とでたらめな表示が続くか、例外が発生する可能性があります。(正常に終了する可能性もある。)
行6で pa
の指すオブジェクトの値が 0 のときに終了としていますが,配列 a の初期化で末尾に0を入れるのを忘れています。文字列リテラルだと自動的に終端の '\0'
を補ってくれますが、配列はそうではありません。
C言語のポインタは、単なるアドレスではありません。
次の例は, plain C ならコンパイルできてしまいます。(C++だと, 型チェックがより厳しいので, エラー。)
誤った例;
char*
型から short*
にキャストしてはいけません。異なる型のオブジェクトへのポインタは、互換性がないと考えるべきです。
また、機械に近いレベルでは、メモリに配置される単位 (alignment) がより大きいオブジェクトへのポインタ型にキャストすると、CPUアーキテクチャによっては、境界エラーでいきなりプログラムが異常終了することがあります。
変数 kanji
をどうしたいのか。C の char
がバイト単位で、文字単位ではないことを示したいなら次のようになるでしょうか。文字の境界は mblen()
で進めます。
これもリテラルがロケール依存で、あまりよくないコードですが。
間違い探し。
整数の変数aを int a; と宣言することは、CPUのメモリ空間に1つの整数型の変数の領域を確保させるということだった。すると、int *a; と整数型の実体を指すポインタを宣言した場合はメモリ領域を確保してくれるのか? とんでもない。ポインタを単に宣言しただけではメモリ領域は確保されない。実体があってのポインタである。
どこが違うか分かりますか?
ポインタの指す先のオブジェクトを自動で確保してくれるか,という話なら確保してくれないというので正しいですが, "ポインタ" オブジェクトの領域は確保される。
int a;
とするとint型の変数aを確保する。int* a;
とするとint*型の変数aを確保する。ポインタも, 使い方が違うだけで, 普通の変数と変わらない。
なんかポインタの説明も怪しいですが、この辺りがどうかと。
このように、C言語ではポインタと配列は同じもののように扱うことができます。両者の根本的な類似点は、ポインタ変数も配列名もアドレスを表すポインタであるということです(普通の変数は変数名が、その変数の内容を表します)。
ダウト。配列はポインタオブジェクトではありません。
逆に、きちんとした解説をしているサイトも紹介しておきます。
ふぃんろーださんによる、『Cマガジン』に連載されていた内容。私も知らないことが書いてあって面白い。
plain Cの話なので,C++だと微妙に異なるところがあります。例えば次のプログラム,
plain Cでは文字リテラルはint ですが、C++だとchar (大きさ1) になります。
かなり気合いが入っていて,結構面白い。