C言語ポインタ談義
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 日本工業標準調査会で閲覧できます。
- JIS X 3010:2003 プログラム言語C (ISO/IEC 9899:1999 Programming Languages C, Technical Corrigendum 1:2001)
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参照)。
例えば、次のコード片は、
- int x;
- x = y + 1;
次のように動作します。
- int x で整数型のオブジェクトが生成され、プログラム上ではxという変数で参照できるようになる。
- 変数yが参照するオブジェクトに 1 を加えた新しいオブジェクトが(一時的に)生成される。
- 変数xが参照するオブジェクトが2.で生成されたオブジェクトに上書きされる。
変数もありますが、オブジェクトと言っていることに注意してください。
変数は、何らかのオブジェクトを参照するためのラベルです。すべてのオブジェクトについて対応する変数があるわけではなく、変数によって参照されることのないオブジェクトもあります。
ポインタとポインタ型
ポインタの「概念」は、プログラミングでは必須だと思います。
C言語のポインタは難しいという伝説が流布していますが、(1) 誤った解説によって学習すると確かに難しい、(2) C言語の文法がそもそも変態、というところから出ているように思います。
ポインタは、別のオブジェクトを指すためのオブジェクトです。
次の例は、よくあるポインタの使用例です。行7でポインタオブジェクトを生成し、変数qと名づけます。
行8で、ポインタが配列の2番目の要素を指すようにし、ポインタに1ずつ足して、なめるようにアクセスします。
- #include <stdio.h>
- int main() {
- int a[] = {1, 2, 4, 8, -1};
- printf("%d, %d; size = %d\n", a[0], a[1], sizeof a);
- int* q; // ポインタ(オブジェクト)を生成
- for (q = &a[1]; *q != -1; q++)
- *q += 5;
- printf("%d, %d, %d\n", a[0], a[1], a[2]);
- return 0;
- }
あるオブジェクトに & 演算子を作用させると、そのオブジェクトへのポインタが得られます。
また、*演算子でポインタが指すオブジェクトを参照できます。
Note.
|
C++では、ポインタと同じように操作できるオブジェクトである、イテレータが導入されています。
- #include <vector>
- #include <cstdio>
- int main() {
- typedef std::vector<int> A;
- A a;
- a.push_back(5);
- a.push_back(10);
- A::const_iterator p = a.begin();
- printf("%d %d\n", *p, *(p + 1));
- return 0;
- }
const_iterator あるいは iterator は、ポインタのように操作できます。
a.begin() は、コンテナの先頭要素を指すイテレータを得ます。
ポインタの操作
ポインタは、数値を足したり引いたりすることができます。
例えばポインタに1足すと、現在指しているオブジェクトの次のオブジェクトを指す値となります。-1だと一つ手前のオブジェクトを指すポインタになります。
よく、ポインタはメモリのアドレス、という解説がありますが、C言語はアセンブラと違ってポインタが型を持っているので,+1すると次の要素を指す,-1すると前の要素を指す,という意味になります。(アドレスが、指す先のオブジェクトの大きさだけ増減する。)
C++のイテレータも、ポインタに似せて、+1や-1で次のオブジェクト、前のオブジェクトを指すイテレータになります。
C++では、演算子 + や - を上書きすることができ、イテレータはこの機能を利用して実装されています。
次のサンプルの、行14や行23で+や*を定義しています。
- #include <stdio.h>
- #include <stdlib.h>
- class A {
- static int x[];
- class AIter {
- int idx;
- int size;
- public:
- AIter(int idx_, int size_): idx(idx_), size(size_) {}
- AIter operator + (int diff) const {
- if (idx + diff < size)
- return AIter(idx + diff, size);
- else {
- printf("error.\n");
- abort();
- }
- }
- int operator * () const { return A::x[idx]; }
- };
- public:
- typedef AIter iterator;
- iterator begin() const { return AIter(0, 3); }
- };
- int A::x[] = {1, 2, 3};
- int main() {
- A a;
- A::iterator p = a.begin();
- printf("%d\n", *(p + 2)); // => 3
- A::iterator q = p + 3; // abort.
- return 0;
- }
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を採用していた?(未確認)
- The UNIX System -- 64bit and Data Size Neutrality
- Abstract Data Models (Windows)
- Designing 64-bit-Compatible Interfaces (Windows)
ポインタのポインタ
次の2行はいずれも変数を宣言し、オブジェクトに紐付けられます。変数bはポインタオブジェクトを表します。
- int a;
- int* b;
それぞれ、&演算子でオブジェクトのアドレスを取ることができ,そのオブジェクトを指すポインタに設定できます。
- int* c = &a;
- int** d = &b;
変数 d はポインタのポインタです。int* 型のオブジェクトを指すポインタだから,その型は int** になります。
考え方は普通のオブジェクトを指すポインタと変わりません。すなわち,型 Foo のオブジェクトを指すポインタの型は Foo* になるし,変数 p が Foo* 型なら,*p は型 Foo になります。
&でオブジェクトのアドレスを取ることを説明しましたか、まだオブジェクトとして定着していない(実体のない)もののアドレスは取れません。・・・左辺値が要求される。
- int a;
- int* b = &a;
- int** c = &b;
これはaもbも実体を表すので問題ありませんが,次のはエラーになります。
- int a;
- int** b = &(&a);
領域の動的な確保
ポインタは別のオブジェクトを指すためのオブジェクトですから、何も指さない状態でアクセスすると不正です。
誤った例;
- #include <string.h>
- int main() {
- char* buf;
- strcpy(buf, "It's too bad!"); // 不正
- return 0;
- }
これを実行すると、たいていは実行時エラーが生じてプログラムがabortします。
例えば、次のようにして領域を確保し、そこを指すようにします。確保した領域は必ず自分で解放しなければなりません。
- #include <stdio.h>
- #include <stdlib.h>
- #include <string.h>
- int main() {
- char* const buf = (char*) malloc(1000);
- strcpy(buf, "abcdefg 12345");
- printf("%s\n", buf);
- free(buf);
- return 0;
- }
配列だとプログラムの実行時に確保する大きさを変えることができないので,ユーザからの入力を格納したい場合など、あらかじめ必要な大きさが不明なときは動的に確保しなければなりません。Cではmalloc() で領域を確保し、free() で解放します。
Note.
|
上のサンプルは確保する大きさを決め打ちしている点が手抜きです。
C言語では、不定な (C言語のオブジェクトを指さない) ポインタというものを許可しています。プログラマが明示的に初期化しなければならず、初期化を忘れると正しく動作しません。無用なバグの原因といえます。
ポインタと配列の混同
C言語は、ポインタと配列という違うものを同じ書き方で操作するようになっているところがあり、混乱があります。
まず配列を用意します。
- int e[] = { 1, 2, 3, 4, 5 };
ここで,単にeと書くと何を表すでしょうか? C言語ではこれが&e[0]の場合とeそのものの場合があります。sizeof(e)と書くと,e自体の大きさを得ることができます。それ以外は&e[0]の意味になります。
そのため、
- int* f = e;
次の例では、実引数である配列を、f() でポインタ型の仮引数で受けています。
- #include <stdio.h>
- int f(int* x) { printf("%d\n", *x); }
- int main() {
- int a[] = {1, 2, 3, -1};
- f(a);
- return 0;
- }
C言語では恐ろしいことに、a[n] という表記は、*(a + n) と同じ意味です。配列でもポインタでも両方の表記が使えます。言語仕様、手を抜きすぎ。
関数へのポインタ
Cでは、主にコールバックのために、関数へのポインタを使います。
- #include <stdio.h>
- void print_if(const int* begin, const int* end, int(*func)(int)) {
- const int* p;
- for (p = begin; p != end; p++) {
- if (func(*p))
- printf("%d ", *p);
- }
- printf("\n");
- }
- int less_than_10(int x) {
- return x < 10;
- }
- int main()
- {
- int a[] = {1, 5, 10, 20, 50, 3};
- print_if(a, a + 6, less_than_10);
- return 0;
- }
行12-14で定義した関数へのポインタを、行19でprint_if へ渡しています。行6でその関数を呼び出しています。
ここでも C の文法の混乱が見えます。配列と同様に、関数名は関数のアドレスにもなります。
オブジェクトが「同じ」とは
TODO:
ダメなサイト
書いていて飽きてきたので、解説が妥当ではないサイトに突っ込みを入れてみます。性格が悪い気もしますが、間違えやすいところが見えてきます。
a3「ポインタと配列」というページのサンプルプログラム、例2.2を引用。
誤った例;
- void main(void)
- {
- static int a[ ] = {1,2,3,4,5};
- int *pa ;
- pa = a ;
- while(*pa!=0)
- {
- printf("%d \n", *pa) ;
- pa++ ;
- }
- }
まず、void main() ではなく int だろ、とか、なんだか空白の入れ方が一貫性がない、というのは置いておいて。
これを動かすと,1 2 3 4 5 まで表示したあと,延々とでたらめな表示が続くか、例外が発生する可能性があります。(正常に終了する可能性もある。)
行6で pa の指すオブジェクトの値が 0 のときに終了としていますが,配列 a の初期化で末尾に0を入れるのを忘れています。
文字列リテラルだと自動的に終端の 0 を補ってくれるので、勘違いしたのかもしれません。
- なまおのパソコン講座 内の「C言語会話講座」
そうとう面白いサイトです。解説がデタラメというか,日本語が訳分かりません。第4話から。
- #include <stdio.h>
- int main(void)
- {
- char strings[] = "mother";
- char *roman;
- short *kanji;
- kanji = roman = strings; /* 右から左へ順に代入されます */
- roman++; /* romanの値を一つ増やすという意味です */
- kanji++;
- printf("Address:%X, Strings:%s\n", strings, strings);
- printf("Address:%X, Strings:%s\n", roman, roman);
- printf("Address:%X, Strings:%s\n", kanji, kanji);
- return 0;
- }
そもそも、コンパイルできないんじゃないかと思いましたが、Cだとコンパイラに通ってビックリ。(C++だともちろんエラー。)
ポインタ云々の前に何がしたいか分かりません。変数 kanji をどうしたいのか。C の char がバイト単位で、文字単位ではないことを示したいなら次のようになるでしょうか。
- #include <stdio.h>
- #include <stdlib.h>
- #include <locale.h>
- int main()
- {
- setlocale(LC_ALL, "");
- const char s[] = "日本語のテキスト";
- const char* p = s;
- const char* q = s;
- p += mblen(s, MB_CUR_MAX);
- q++; // 不正な位置
- printf("addr = %p, str = %s\n", s, s);
- printf("addr = %p, str = %s\n", p, p);
- printf("addr = %p\n", q);
- return 0;
- }
これもリテラルがロケール依存で、あまりよくないコードですが。
第4章から。
整数の変数aを int a; と宣言することは、CPUのメモリ空間に1つの整数型の変数の領域を確保させるということだった。すると、int *a; と整数型の実体を指すポインタを宣言した場合はメモリ領域を確保してくれるのかな?
とんでもない。かつて北斗の拳が「ポインタには領域などな〜い」と言っていたかどうかは知らないが、ポインタを単に宣言しただけではメモリ領域は確保されない。実体があってのポインタである。
ブッブー。
ポインタの指す先のオブジェクトを自動で確保してくれるか,という話なら確保してくれないというので正しいですが,ポインタオブジェクトの領域は確保されるので、おかしい。
int a;とするとint型の変数aを確保する。int* a;とするとint*型の変数aを確保する。ポインタも使い方が違うだけで普通の変数と変わらない。
なんかポインタの説明も怪しいですが、この辺りがどうかと。
このように、C言語ではポインタと配列は同じもののように扱うことができます。両者の根本的な類似点は、ポインタ変数も配列名もアドレスを表すポインタであるということです(普通の変数は変数名が、その変数の内容を表します)。
ダウト。配列はポインタオブジェクトではありません。
薦められるサイト
逆に、きちんとした解説をしているサイトも紹介しておきます。
ふぃんろーださんによる、『Cマガジン』に連載されていた内容。私も知らないことが書いてあって面白い。
plain Cの話なので,C++だと微妙に異なるところがあります。例えば次のプログラム,
- #include <stdio.h>
- int main() {
- printf("size = %d\n", sizeof('a'));
- return 0;
- }
かなり気合いが入っていて,結構面白い。
Note.