その2 「ポインタ」

ポインタはアドレスなのか?

以前、fj.comp.lang.cあたりを読んでいたら、 「C言語を勉強しているのですが、ポインタがよくわかりません。 教えてください」 みたいなポストがあって、 ま、fjみたいな場所に、 こんな書き込みをする方もする方だと思いますけれども それは取り敢えず置いとくとして、 それに対するフォローアップが、

「コンピュータのメモリとアドレスの概念について勉強するのがいいですよ」

とか、

「先にアセンブラを勉強したらどうでしょうか」

みたいな調子だったのを見て、ちょっと暗澹たる気持ちになりました。

確かに、 この回答者さん達のフォローは決して外してはいないのだろうとは思います。 誰にでも低レベルのことの方が理解が早いものです。 私自身、Cを勉強する前にアセンブラをちょっとやってたので(Z80だけど)、 だからCのポインタを理解できたのかも知れません。

でも、現実にC言語でアプリケーションプログラムを書く上では、 ポインタがアドレスであることを意識する必要など全くない ものです。いざバグるその時までは... って、 この辺がヨワヨワなんですが(^^;

なのに、

「C言語のポインタはアドレスだ」
「メモリを常に意識しないと、C言語のプログラミングはできない」
「Cなんて構造化アセンブラじゃないか」
「Cなんて低級言語じゃないか」

等々と言われると(これらのことは至る所で非常によく言われています)、 Cでアプリケーションプログラムを書いていた(過去形... 最近はJavaなんて言語を使わされてますが)私としては、 やっぱり悲しい、と思うわけで、 上記のようなフォローは、そういう認識をさらに助長してしまいそうで、 「暗澹たる気持ち」になってしまったわけです。

Cでは、確かにほとんどの処理系において、ポインタの値は、 アドレスそのものなのだろうと思います(このアドレス自体、 OSにより提供される仮想アドレスであるという話は置いといて)。 でも、それは、 あくまでポインタを実現するための「実装手段」として たまたまアドレスを使用しているということに過ぎず、 ポインタという概念自体は、もっと抽象的なものである筈です。 すなわち、「何かを指し示すモノ」。

手元の、「C言語を256倍使う本」には、 以下のようなコードが載っていたりして(p.119)、

  unsigned far *p = (unsigned far *)0xA0000000L;

  for (i = 0; i < 0x1000; ++i)
      p[i] = 0;

PC-9801でテキストVRAMをクリアするコードだそうなんですが、 この本はずいぶん昔の本(初版は1990年9月11日)なわけで、 これはあくまで昔の話、今時こんなコーディングはしないでしょう。 まずコンパイラに怒られますし、それをキャストで黙らせたとしても、 OSのメモリ保護がちゃんとしていれば「できない」技ですし。

ちなみに、規格では、「ポインタ」を最初に定義している所は、 おそらく「6.1.2.5 型」における以下の説明だと思うのですが、

ポインタ型は、被参照型の実体を参照するための値を持つオブジェクトを表す。

要するに、実体を参照することさえ出来れば、 実装手段としては別段アドレスを使わなくても良いってことですね。

ついでに、&演算子については、

単項&(〜のアドレス)演算子の結果は、 そのオペランドが指し示すオブジェクト又は関数へのポインタとする。

だそうです(6.3.3.2)。&を「アドレス演算子」と呼んでる所は アレレなんですが(^^;、この演算子が返すのは、あくまで「ポインタ」であって、 「アドレス」ではないということです。

printf()の %pの出力は、

そのポインタの値を処理系定義の方法で表示可能文字の列に変換する。

と定められているのみです(7.9.6.1)。

低レベルの概念を、ライブラリでラップすること等により隠蔽し、 より抽象化した概念として使用することは、プログラムの設計において 日常的に行なわれることです(Cの場合、隠蔽しきれてないわけですが)。 Cのポインタが、どうしてもアドレスにしか見えない人ってのは、 このような「それにより提供される機能と、その実装手段との分離」が ちゃんと出来ていないとしか思えません。

自動車を運転するには、アクセル踏めば走ることを知っていれば良く、 その車がガソリン車なのかディーゼル車なのか電気自動車なのかを 知る必要は必ずしもないように、 ポインタを使う際には、その実現手段が仮にアドレスだったとしても、 それを意識する必要は特にない筈です。

Cは確かに低級言語かも知れませんが、 高級言語を使いたいのに、C言語を使わざるを得ない場合に、 「Cなんて低級言語じゃないか。けっ」なんて屈折して斜に構えても 得る物があるわけじゃなし、 たとえその処理系でポインタがアドレスであったとしても、 そんなことは忘れてしまえばいいじゃないですか。 Cは、高級言語らしく使おうと思えば結構高級言語らしく使える言語です。 いざバグるその時までは。あれ?

Cインタプリタを考える

ちょっと昔のことになりますが、私は、Cのサブセットのインタプリタを 書いたことがあります。正確には、中間コードへのコンパイラと、 その中間コードをインタープリトするVM(Virtual Machine)を書いたわけですが。

その際、ポインタの内部表現は、おおむねこんな感じにしました。

typedef enum {
    NULL_SEGMENT,
    STACK_SEGMENT,
    TEXT_SEGMENT,
    DATA_SEGMENT,
    NATIVE_SEGMENT
} SegmentType;

typedef struct {
    SegmentType     segment;
    union {
        unsigned int    index;      /* NATIVE_SEGMENT以外のとき */
        void            *pointer;   /* NATIVE_SEGMENTのとき */
    } value;
} Pointer;

このVMでは、メモリを「スタックセグメント」「テキストセグメント」 「データセグメント」に分けて管理しており(この呼び方はUNIXからのパクリ)、 「スタックセグメント」は、スタック、 すなわちローカル変数やら式の計算途中の値やらリターンアドレスやらを 置いておく所、 「テキストセグメント」は実質文字列リテラルの置き場所 (命令コードは別扱いにしていたし、constもサポートしなかったので)、 「データセグメント」は、グローバル変数の置き場所 (staticはサポートしなかったので)、 そしてそれぞれ charの配列で巨大な領域を取り、もし伸長する場合には realloc()していました (一応ダイナミックロードだったので、スタックに限らず伸長の可能性があった)。 そして、共用体valueのindex側を使用して、その領域の特定の場所を ポイントしていたわけです。 ヒープに関しては別扱いで、直接ライブラリ関数のmalloc()をコール、 それによって取得したポインタを「NATIVE_SEGMENT」として 共用体valueのpointer側に格納していました。 何らかの事情でネイティブ側のポインタが必要な場合(FILE*とか)も NATIVE_SEGMENTを使いました。

# 細かいことを言うと、TEXT_SEGMENTは、VMに対してひとつの領域が対応していた
# わけではなくて、ひとつのバイトコードファイル(.cと1対1対応)毎に存在し、
# インデックスもその内部でのものでした。
# あと、ヒープについては、間接参照テーブルを経由してコンパクションを実現
# してやろうかと企んだりしたのですが、結局そこまでは実装しませんでした。
# 今にして思えば、スタックセグメントはともかくとして、データセグメントは、
# 何も連続した巨大領域をrealloc()でニョキニョキ伸ばさなくても、分断された
# 領域を連結リストか何かで管理して、valueのpointer側を使って直接指してりゃ
# 良かったと思うんですがねえ。

このVM上で動くCでは、「ポインタ」はあくまで上記の構造体であり、 「アドレス」ではありません。でも、「ポインタ」は、通常のC言語と 同じように使えます。 方法は他にも色々あるでしょうが、インタプリタのCなら、 やっぱりポインタには生アドレス以外のデータ構造を当てていると思います。

つまり、ポインタは、別段アドレスとは限らんということです。

ポインタ演算

Cのポインタが他の言語(PascalとかJavaとかLispとかSmalltalkとか) のポインタと比べて異なっていることとして、 Cには、「ポインタ演算」という妙な機能があることが 挙げられます(でも、最近のPascal(の亜流)であるDelphiでは、 ポインタ演算の機能もあるそうな)。

Cでは、ポインタに対して整数の値を足したり引いたり、 ポインタ同士で引き算したりできます。 でも、これをもって、「ほらやっぱりアドレスじゃないか」 なんて言うのは外しています。

ポインタに1足した時、その指すトコロは必ずしも1byte進むわけではなく、 その指す型のサイズだけ進みます。 つまり、これは「アドレス」に対して加算を行なったと考えるべきではなく、 ここでのポインタは「配列の代用品である」と考えるべきなのでしょう。

C言語では、配列とポインタの間に妙な交換性があります。 これについては、 「 配列とポインタの完全征覇」の方に詳しく書いたので ここで詳細を繰り返しはしませんけど、おおざっぱに言えば、 Cでは、配列を定義/宣言することは出来ても その内容を「配列として」アクセスする手段は提供されておらず、 常にポインタを使用してアクセスすることになっています。 p[i]という書き方は出来ますが、これはあくまで *(p + i)の syntax sugarに過ぎません。

Cに、ポインタ演算という妙な機能があるのは、 初期のコンパイラを作成する際に「手を抜いて」最適化の機能を 組み込まなかったためだと思われます。 念のために補足しますと、ここでの「手を抜いて」は誉め言葉です。 C言語が開発された目的(UNIXというOSを記述したかった)を考えれば、 これはその時点では合理的な選択だったのでしょう。

確かに、最適化を全く行なわないコンパイラなら、 ポインタ演算を使ってポインタ自体を進め、*pのように参照すれば、 p[i]と書いて毎回 p + iの加算を行なうよりも高速なコードを 出力することが期待できます。

ただし、今時のコンパイラは最適化が進んでおり、 ポインタ演算を使ってポインタ自体を進めようが、 p[i]というsyntax sugarを使って添字アクセスしようが、 特に変わらない速度の実行コードを出力します。 そして、多くの人にとって、ポインタ演算を使ったプログラムより、 添字でアクセスするプログラムの方が読みやすいものです。

だったら... もう、 ポインタ演算なんて使うのはやめてしまって、素直に添字アクセスすればいい のではないでしょうか?

K&Rは、多くの人がバイブルとして信奉するテキストですが、 私は、この本は、例えば新人研修のテキストとしては使いたくありません。 サンプルプログラムの中で、あまりにもポインタ演算を乱用しているからです。

*++argv[0] なんてワケのわからない表現を嬉しそうに(?)使われても困ります。

だいたい、argvは、システムからもらった大事な引数なんだから、 そんなものを迂闊に更新すべきではないでしょう。 一般的に言っても、ポインタをインクリメントしながら使う場合ってのは、 配列の先頭要素へのポインタを関数の引数として受け取り、 そのポインタを進めるような使い方が多いような気がするんですが、 確かにそうすれば、別途添字用の変数を用意する必要がなくなるんですが、 引数ってのは関数にとって呼び出し元からもらった大事な情報なので、 迂闊に更新すべきではありません(後で使うことになるかも知れないし)。 だからって、いったん別のポインタにコピーしてからポインタ演算、 するぐらいなら、素直に添字でアクセスする方がわかりやすいってものです。

# XtのXtAppInitializeみたいに、&argcとargvを受け取り、自分とこに
# 関係するオプションだけを抜き取って返す(つまりargcとargvに破壊的
# な変更を加える)、みたいな使い方もあるにはあります。これはこれで
# わかりやすいんですが。

K&Rには、strcpyの実装として以下のような例が載っていて(p.129)、

  /* strcpy: t を s にコピーする; ポインタ版3 */
  void strcpy(char *s, char *t)
  {
      while (*s++ = *t++)
          ;
  }
これは一見してわかりにくいように見えるが、 この記法はかなり便利なものであり、 Cプログラムでよく見かけるという理由から、 こうした慣用法はマスターすべきである。

だそうなんですが、「一見してわかりにくいように見える」なら、 やっぱり書くべきではないでしょう。

もっとも、過去のコードを読む際のことを考えれば、 こういった記法もマスターしなきゃいけない面もあるかとは思いますが...

もちろん、わかりやすいわかりにくいというのは主観の問題ではありますが、 以前、新人研修の講師をやった際、12人の新人君に、ポインタ演算の例と 添字アクセスの例の両方を教えた上で、「どっちがわかりやすいと思う?」と 聞いたら、12人全員が添字の方に手を挙げました。

添字でアクセスするより、p++の方が、なんだかイテレータみたいで わかりやすいじゃん、という意見も聞いたことがありますが、 イテレータってのはこの場合「配列」という実現手段を隠蔽しなきゃ 意味がないと思うんですけど、ポインタ演算している時点で、 対象が配列であるってことが限定されちゃっているわけですから、 それじゃダメでしょう。

配列のレンジチェック

Cのポインタについてもうひとつ言われることとして、

ポインタは危険だ

というのもあります。

これには大きく分けてふたつの意味があって、

後者は、ポインタが使える言語なら、常に起こり得ることです。 ポインタそのものの難しさと言えるでしょう。 Javaにだってポインタはあるので 当然この問題は発生し得るのに、「Javaにはポインタがない」なんていう 悪質なデマが蔓延しているおかげで 多くの混乱を招いているような気がするのですが、 これについて書き始めると終わらないので置いといて、

特に C において問題になる前者の問題について見ていくとしましょう。

Cで、ポインタが不正な値になってしまう原因としては、 以下のものが挙げられます。

  1. auto変数は通常初期化されないので、ポインタ変数をautoで確保すると、 初期化するまではそのポインタは不正である。
  2. mallocで確保した領域は通常初期化されないので、 その領域の一部または全部をポインタとして使用する場合、 初期化するまではそのポインタは不正である。
  3. Cでは通常配列にレンジチェックがなく、レンジを越えた所も 指せてしまう。
  4. Cでは、auto変数(staticでないローカル変数)へのポインタも 取れてしまうけれども、 auto変数は関数を抜けると解放されてしまうため、 もし、auto変数を指すポインタがその後も残っていれば、 それは不正なポインタになってしまう。
  5. malloc()で確保した領域をfree()した場合、もしその領域をまだ指している ポインタがあるとすれば、それは不正なポインタになってしまう。

例えば、Cのインタプリタを作るとして、処理系でこれらの問題が どう回避できるかを考えると...

1, 2は、どちらも初期化の問題です。 処理系があらかじめNULLにしておけば問題ないでしょう (NULLがビットパターンゼロではない処理系において、 「中に何を詰めるつもりかわからない」mallocで確保する領域を、 どうやってあらかじめNULLにするのか、という問題はあるなあ...)

4は、そう簡単には対処できません。 関数を抜ける時点で、これから解放するauto変数を指している ポインタを全部追跡して(どうやって?)、NULLに書き換えるとか、 全てのauto変数に参照カウンタを付けといて、 参照中のポインタがあれば実行時エラーにするとかの案が考えられますが、 どちらも実行速度に悪影響を与えそうです。 例えばPascalなんかだと、 ポインタはヒープに確保した領域を指すことしか出来ず、 Cみたいに変数へのポインタをポイポイ取ることはできないので、 こういう問題は発生しないのですが。 変数引数の実装が「ポインタ渡し」だったとしても、 そのポインタの生存期間は、それによって指されるものより 絶対に長くはなりませんし。

5も、そう簡単には対処できません。 これは、結局、CやPascalのように、 メモリの解放をプログラマ任せにしている場合には避けられない問題です。 本当に解決しようと思ったら、free()をプログラマ任せにせず、 言語側がガベージコレクションをサポートする必要があります。 Cの場合、邪悪なポインタ操作ができることを逆手にとって、 ライブラリレベルで、 ある程度プログラマの負担を減らすことは可能ではありますが。

で... 何とかなりそうなのは、3の「配列のレンジチェック」です。

先に挙げた、私が以前作ったCのインタプリタでは、 配列のレンジチェックはやってなかったのですが(何のために インタプリタにしたのやら(^^;) 原理的には可能な筈なので、ここでちょっと方針を考えてみます。 試したわけではないので、考えの浅い所等ありましたらご指摘願います(_o_) > このページを読んだ方

まず、Cでは、配列は、定義/宣言することは出来ても、 それをアクセスする時には常にポインタを使用する、 ということを認識する必要があります。

特に Cでは、関数の引数として配列を渡すことは出来ず、 単にポインタが渡って来るだけであり、呼び出され側をコンパイルする時点では、 呼び出し側の配列のサイズを知る方法はありません。

ということは、結局、実現すべきは「配列のレンジチェック」というよりは、 「ポインタ演算のレンジチェック」だということになり、 ポインタ自体が実行時にレンジを保持すべきだ、 ということになります。

  typedef struct {
      void      *p;
      int       lower_limit;
      int       upper_limit;
  } Pointer;

この pの所を、このサンプルの通りvoid*にするのか、 先に挙げた例のように構造体にするのか、 というのはこの場合関係ないので置いといて...

細かい仕様は色々考えられますけど、取り敢えずここでは、

としましょう。

で、int a[10]; と定義された配列があったとき、 aは、式の中ではポインタに読み換えられますが、その際、 lower_limitを0に、upper_limitを9にセットします。

a[10]の時のaの図

ポインタ演算する際には、オペランドがlower_limit, upper_limitで 指定された範囲に入っているかをチェックし、 演算の結果として生成されるポインタは、 pに加減算を行なうだけでなく、lower_limit, upper_limitも その分だけずらしてやります。

aに対してポインタ演算した図

単独の変数に&を付けてポインタを取得する場合、 lower_limit, upper_limit共にゼロにします。

malloc()で確保した領域は、void*なので、バイト単位で lower_limit, upper_limitを保持しています。 これに対して(暗黙かも知れないけど)ポインタのキャストを行なって、 適切な型のポインタに代入するわけですが、 ポインタをキャストする時点で、そのポインタの指す型のサイズで lower_limit, upper_limitを割ってやります(正確な表現じゃないですが)。 この割り算で余りが出たら... 実行時エラーでいいような気がする。

upper_limitが-1を取り得るというのは、Cの場合、 ポインタは、配列の範囲を1個超えた所までは指すことができるように 規格で定められているからです(6.3.6)。

なんでそんなわけのわからない仕様になっているかというと、 例えばANSI-C Rationaleで挙げられているサンプルですが、

  SOMETYPE array[SPAN];
  /* ... */
  for (p = &array[0]; p < &array[SPAN]; p++)

みたいなコードがあったとき、ループの終了時には、 pはarray[SPAN]、つまり配列の範囲を1個超えた所を指しているからです。 先に挙げたstrcpyの実装でもそうですね。

# だから始めっから添字で参照すればいいのに :-(

ポインタは配列の範囲を1個超えた所まで存在することが出来るのですが、 もちろんそこに書き込んではいけません。読む方も、やめた方がいいでしょう。 よって、*演算子を適用する際に、upper_limitをチェックして、 -1なら実行時エラーにするんでしょうね。

ポインタの引き算は、p同士の引き算ってことになりますが、 この時、オペランドが両方とも同じ配列の上にあることを チェックすることもできますね。

さて、効率を考えなければ(ポインタがこんなにでかいんじゃねえ)、 Cで配列にレンジチェックをかけるのは、 こんな風に割と簡単に実現できると思うのですけれども、

ちょっと待て。俺のプログラムではこんなことやってるんだが、 これって動かなくなるんじゃないか?
    typedef struct {
        double x;
        double y;
    } Point;

    typedef Polyline {
        int    point_num;
        Point  point[1];
    }

    ...

    polyline = malloc(sizeof(Polyline) + sizeof(Point) * (point_num-1));

という人もいるかもしれません。ただし、残念ながら、 このプログラムはもともと規格違反です。

FORTRANのプログラムをCに移植する際に、

  int     a_temp[10];
  int     *a = a_temp-1;

みたいなテクニックを使って1から始まる配列を作ることもありますけど (f2cはこうやってた)、 これも、「配列のレンジチェック機能付きC」では動かなくなっちゃいますが、 やっぱりもともと規格違反です。

# だからこういうコードは書いちゃダメ、と言っているわけではありません。
# 「規格厳密一致プログラム(strictly conforming program)」を書くことに、
# それほど意味があるとは思えませんし。

C on JVM?

さて、何らかのVMの上で動くCってのを考えてみましたが、 今最も普及しているVMと言えば、やっぱりJavaのVM(JVM)でしょう。

もともと私が長々とこんな文章を書いてきたのは、 Javaな人達が、Cに対して、 やれポインタが生アドレスだから危険だの、 領域ブチ壊すから危険だの、 GCがないからメモリを全部リークするだの、 それに比べてJavaはVMの上で動くから安全だね、 みたいなことばっかり言ってくれるからムカついた、 というのが主な理由だったりするわけで(^^; 「じゃあ、JVMの上で動くCがあれば文句ないだろ」というわけで、 ちょっと考えてみることにします。

# でも、私の知る限り、JVMの上で動くCって聞いたことがないんですよ。
# なんでなんだろう? Adaとか、Smalltalkとか、Lispとかはあるようですが。

以下、これも試したわけではないので、考えの浅い所等ありましたら ご指摘願います(_o_) > このページを読んだ方

JVMの仕様を見ますと、Cのポインタを直接表現できるような 機能が見当たりません。

そうなると、まず考えられるのは、

byteの巨大な配列を確保し、そこをメモリ空間と考える。 ポインタはこの配列のインデックス。

できないことはないんでしょうが、この方針ではあんまりです。 あまりにも低レベルというか、機械語的です。 intひとつ取り出すのに、4つのbyteをアクセスし、 左にビットシフトしながら足していくのだとしたら、効率悪過ぎますし。

というわけでもうちょっとまともにVMを使う案を考えるとします。

例えば以下のようにローカル変数が宣言されていたとすると、

  int     a;
  int     b[10];
  Hoge    hoge; /* Hogeは構造体のtypedef */

これを、Javaのローカル変数と同じように、 JVMのJavaスタックに格納するとすると、

じゃあ妥協して、配列や構造体は、全部Javaスタック上に展開してしまって、 かつ、ポインタが取れるように、そいつらを全部参照にしてしまうか。

妥協案1(失敗)

ああダメだ、これじゃあ、配列の順次アクセスが出来ない。 構造体も、要素のポインタを取ることが出来るようになった代わりに、 構造体全体のポインタが取れなくなっちゃっている。

じゃあさらに妥協して、 Javaスタックから直接参照を取るんじゃなくて、配列を間にかますか。

妥協案2

この配列は、関数突入時にnewするとして、型は、 java.lang.Objectの配列でも別にいいけど、 どうせなら、CScalar(Cのスカラ、の意)みたいなクラスを別途作って、 それを継承してIntとか、Doubleとか、Pointerとかを作ればいいのかな。 で、Pointerの内部表現はこう。

  class Pointer {
      public CScalar[] base;
      public int       index;
  }

こうすれば、CScalarの配列の中の特定の要素を指せるし、 ポインタ演算だって配列参照だって出来るはず。 その代わり、Javaの配列のレンジチェック機構は使えず、 同じCScalarの配列の中なら壊し放題... 悲しい。 参照を使っている以上、C on JVMでは、sizeof(char)もsizeof(int)も sizeof(double)も、全部1ってことになるのかな。

IntとかDoubleとかPointerとかは、 Javaのラッパークラスみたいなimmutableなクラスと違って、 内容を変更可能にしておき、関数に突入した時点で全部貼り付ければ良いのかな。 関数内部のブロックの中で変数を宣言してたとしても、 関数突入時に全部確保して悪いってことはないし(そうなっている処理系も多いし)、 だとすれば、CScalarの配列の、ある要素について、 そこに格納されるデータの型は静的に決まっていると考えていいもんね...

...って、いかんいかん、共用体を忘れてました。 共用体なんて、こっそり構造体に読み替えてしまっても、 メモリがもったいないだけでそうそう困らないと思うんですが(おい)、 一応、規格では、「共用体オブジェクトへのポインタは、 適切に変換すれば、そのそれぞれのメンバ(又はビットフィールドならば、 それが置かれた単位)を指す。更に、逆も成り立つ。」と決められているので、 今の方針ではちょっと面倒そうです (キャストの段階でごまかすという手はあるものの)。 さらに、今まではauto変数のことしか考えてなかったですけど、 Cにはmalloc()という、「そこに何を詰めるつもりなのか さっぱりわからない」凶悪な機構があるのでした。

そういうことも考えると... さらに妥協して、CScalarの配列に張り付いているオブジェクトを 全部immutable的な扱いにして、 必要に応じてnewしてくっつけるようにした方が簡単ですね。 そうすると、未初期化変数を参照すると、NullPointerExceptionになります。 プログラマにとってはその方が便利ですね。

関数を抜ける時、その関数用のauto変数の領域(CScalarの配列)をどうすべきかですが、 本来なら、別に何もしなくてもそのうちにGCが回収する筈です。 が、auto変数へのポインタを、アプリケーションが不正に保持していた場合、 CScalarの配列自体が解放されません。 Cならあっさりdangling pointerですが、C on JVMだと安全でいいね、 と喜ぶ向きもあるかも知れませんが、 私には、これは単にバグの発見を遅らせるだけのように思えます。 例えばコンパイル時にデバッグオプションを付けた場合には、 CScalarの配列をnullでクリアしておく、 ぐらいの配慮はあった方がいいでしょう。

あとは、グローバル変数・static変数ですが、 手頃な所でひとつのソースファイル(.cファイル)をひとつのクラスファイルに 変換するとして、関数は全てstaticなメソッドにするとして、 static変数は、ファイルスコープのものも関数ローカルのものもまとめて、 staticフィールドにCScalarの配列を持ってしまえばいいでしょう。 グローバル変数は、GlobalVariableクラスにCScalarの配列を持たせるとして、 コンパイルの後に1パス入れて、全ソースをナメてGlobalVariableクラスを 生成しなければならないと思うんですが、各クラスファイルの方からは、 自分が参照しているグローバル変数のGlobalVariableの中でのインデックスは 知りようがないので、各クラスのクラスイニシャライザで、 ローカルの名前表(のインデックス)からグローバルなインデックスへの 変換テーブルを作ることになるのかな。

そういえば、Cでは、static/グローバル変数については、 何も書かなくてもゼロで初期化されていることになっていますし、 もちろん初期化子を書けば任意の初期化が出来るので、 static/グローバル変数の領域については、CScalarの配列の各要素は nullではなく、クラスイニシャライザでnewして貼っておく必要がありますね。 この場合、型は決定していますし、 共用体も、先頭で宣言してあるメンバで初期化すればいいのでした。

おっといけない、関数のポインタというものがありました。 これは... 取り敢えず速度を求めないのなら、方法はありますね。 そう、リフレクションです。

さっきのPointerのスーパークラスとしてAbstractPointerを作って、 それを継承してPointerとFunctionPointerを作ればいいですね。

  class FunctionPointer extends AbstractPointer {
      Method p;
  }

規格だと、()演算子は「関数へのポインタ」をオペランドにすることになってますが、 実際には、「関数へのポインタを格納した変数」なのか、 「関数名直指定」なのかは、 コンパイラは区別が付くわけで、 前者の場合のみ、いくらか遅くなるのはまあしょうがないでしょう。

# 2000/3/2付記
# よく考えてみたら、1つの.cファイルを1クラスに展開するとなると、クラスの
# 名前がわからない限り関数(staticメソッド)呼び出しができませんね(^^;
# そうすると、やっぱり、コンパイルの後にリンクのパスを入れて、そこで
# 相手を探してコンスタントプールの中のクラス名を書き変える以外ないかなあ...
# あと、setjmp, longjmpなんてのもあったのでした。これはやっぱり例外処理
# 機構を使うのかなあ。関数全体でcatchして、例外ハンドラの中からsetjmpの
# 直後にジャンプするようなコードでいいんかしらん。途中の関数はその事情を
# 知らないからthrows節が吐けないけれど、VMでの例外の整合性のチェックは、
# しなきゃしなきゃと言いつつやっぱやーめた、という状態のようなので(?)
# それでもいいのかな。

可変長引数も実現しなきゃいけませんが、これは単純に「CScalarの配列」を 最後の引数として渡してやれば良さそうです。

以上、だらだらと書いてきましたが、まとめると、

  1. Cで使用する全てのデータは、「CScalarの配列」からの参照で保持する。
  2. 「CScalarの配列」の各要素には、Cのスカラが一個だけ貼り付く。 構造体・配列の参照は、コンパイラがインデクシングする。
  3. 「CScalarの配列」の各要素は、初期化されるまではnullで、 代入される度にimmutableなオブジェクトが貼り付く。 グローバル変数とstatic変数は最初から初期化されていると考える。
  4. 「CScalarの配列」を確保するタイミングは、
    1. プログラム起動時。最初にロードされたクラスのクラスイニシャライザが、 GlobalVariableクラスを参照し、 GlobalVariableクラスのクラスイニシャライザで、 グローバル変数用の「CScalarの配列」を確保する。
    2. 各クラスファイル(.cファイルに相当)のロード時。 static変数用の「CScalarの配列」を確保する。
    3. 関数突入時。auto変数用に確保する。
    4. malloc()時。
    5. 可変長引数を渡す時。
  5. ポインタの内部表現は、関数へのポインタ以外は、 「CScalarの配列への参照」と、「CScalarの配列でのインデックス」とする。 &や*演算子の操作は、コンパイラが対応コードをその場に展開する。
  6. 関数へのポインタは、Methodクラスのオブジェクトとする。
  7. NULLポインタは、「CScalarの配列への参照」をnullにする。

この方針だと、参照を使いまくっているため遅そうではありますが、 そこそこ簡単に、C on JVMのコンパイラが書けそうな気がするのですが...

プログラマの立場からすれば、GCはJVMのものが使えますし、 memset()とかcalloc()とかでクリアした領域を、 ゼロという値として使用するようなタコなコードも廃絶できます。

細かい所で規格違反はありそうですが、 善良な(?)Cプログラマなら意識しなくて良いレベルでCしてると思うんですけど、 どうでしょう?

おわりに

なんかとりとめもないことをだらだらと書いてきましたけど、 結局何を言いかったのかというと、

ポインタは確かにアドレスかも知れんけど、 別にことさらアドレスだと思う必要ないじゃない。

ということです。いざバグるその時までは(だーかーらー)。

現実問題として、インタプリタでない、普通のCの処理系では、 領域破壊にまつわるバグが発生した時には、 ポインタをアドレスとして意識しなければならない局面がどうしても出てきます。 やれスタック壊しただのヒープの管理領域壊しただの... 大変です。ホントに。(;_;)

それに、初心者さんにとっては、ポインタをはっきりアドレスとして認識し、 printfの%pとかで値を表示しながら勉強した方が、 なんか抽象的でつかみどころのない「何かを指し示すモノ」という概念よりも、 具体的に目に見えるだけにわかりやすいような気もします。 だとすると、ポインタをアドレスとして見ることができないJavaとか SmalltalkとかLispとかの言語は、とっても習得が困難だということに なるような気もしますが。

でも、アプリケーションプログラマなら、少なくとも設計/コーディングの間は、 ポインタがアドレスであることを忘れていてもいい筈なわけで、 「Cは、ポインタでメモリを直にいじくる低級言語だ」と思っている Cプログラマさん達には、

何もそんなに自虐的になることないんじゃない?

と言いたかったりするわけです。


ひとつ上のページに戻る | ひとつ前の話 | ひとつ後の話 | トップページに戻る