Cにおける前方参照のお話 [CodeWarrior]
Cのコンパイラは、ワンパスが前提となっているので、何かを使おうとしたら、その「何か」をあらかじめ宣言しておく必要があります。 でも、なんでもかんでも宣言するのは面倒なので、「宣言しなくても使える」仕組みが残されているのです。
ワンパスって、何?
序文でさらりと書いた「ワンパス」ですが、これを説明するには、歴史の教科書が必要になってきます。 そもそも、コンパイラというのは、ソースコードを機械語の羅列に変換するプログラムです。 そのため、コンパイラには、「ソースコードを読む」機能が備わっていなくてはなりません。
では、ソースコードは、どこに入っていたかというと、昔は高価なハードディスクなどには入っていませんでした。 使われていたのは、パンチカード、紙テープ、磁気テープなどです。 フロッピーディスクが使われ始めたのは、最近の話です。
これらのメディアは、二つの種類に分けられます。 シーケンシャルなアクセスしか許されないものとランダムアクセス可能なものです。 UNIX世界では、シーケンシャルなアクセスしか許されないファイルシステムをキャラクタ・デバイス、ランダムアクセス可能なファイルシステムをブロック・デバイスと呼んでいます。
今では、考えられないことですが、ソースコードのほとんどは、キャラクタ・デバイスに入っていました。 そのため、ソースコードを一度にメモリに読み込んでランダムアクセスを行うと自由にコンパイルが出来るようになります。 ところが、ギッチョン、当時のコンピュータにはソースコードを一度に保存できるほどのメモリ容量がありませんでした。 そのため、キャラクタデバイスからソースコードを読みながら、コンパイルを行い、出てきた機械語プログラムの結果を別のキャラクタデバイスに書き出すという「省メモリに徹したコンパイラ」を作らざるを得ませんでした。
ワンパスという言葉は、この時に生まれました。 キャラクタデバイスからソースコードを一度読んだだけで、コンパイラが機械語を生成できることを「ワンパス」と呼びました。
もちろん、「ツーパス」のコンパイラも存在しました。 このコンパイラの場合には、同じソースコードを二回読み込む必要がありました。 一回目の読み込みで必要な情報を収集し、二回目の読み込みで機械語のプログラムを作成します。 二回目に同じソースコードを読み込むためには、パンチカードをもう一度デッキにセットしなおしたり、紙テープを巻き戻したり、磁気テープを巻き戻したりする必要があります。 これらの作業は、もちろん、人間の手作業によって行われます。
こう考えていくと、「ワンパス」と「ツーパス」のどちらが優れているかは、明白だと思います。 そのため、「ワンパス」である事を売りにしたコンパイラが続々と出てきたのです。
きちんと宣言したプログラムの場合
ワンパスという言葉の意味がわかった所で、「ワンパスのコンパイラに優しい」プログラムの例を挙げます。
#include <hidef.h> /* for EnableInterrupts macro */ #include "derivative.h" /* include peripheral declarations */ word sub1(byte p1, word p2) { MTIMMOD = p1; TPMC1V = p2; return MTIMCNT; } volatile word value; void main(void) { value = sub1(50, 1200); for(;;) { } /* loop forever */ /* please make sure that you never leave main */ }
このサンプルプログラムは、 MC9S08QG8 をターゲットとしたプログラムで、 main 関数から sub1 という関数を呼び出しています。 処理の内容に意味は無いので気にしないように。
このプログラムでは、「sub1 という関数が byte 型の p1 と word 型の p2 という二つの引数を受け取り、 word 型の結果を返す。」ことがあらかじめ宣言されています。 そのため、何のためらいも無く main 関数で sub1 関数を使うプログラムが生成されます。
15: value = sub1(50, 1200); 0000 a632 [2] LDA #50 0002 4504b0 [3] LDHX #1200 0005 ad00 [5] BSR sub1 0007 960000 [5] STHX value
関数を後で定義したらどうなるか
先の例では、 sub1 関数を main 関数で使用する前に宣言していました。 もし、逆に、 sub1 関数を main 関数の後に配置したらどうなるでしょうか。
#include/* for EnableInterrupts macro */ #include "derivative.h" /* include peripheral declarations */ volatile word value; void main(void) { value = sub1(50, 1200); for(;;) { } /* loop forever */ /* please make sure that you never leave main */ } word sub1(byte p1, word p2) { MTIMMOD = p1; TPMC1V = p2; return MTIMCNT; }
このプログラムをコンパイルすると、以下のようなエラーメッセージが表示されます。
Warning : C1801: Implicit parameter-declaration for 'sub1' main.c line 8 Warning : C1140: This function is already declared and has a different prototype main.c line 15 Error : C1019: Incompatible type to previous declaration (found 'unsigned int (*) (unsigned char ,unsigned int )', expected 'int (*) (int ,int )') main.c line 15 C1440: This is causing previous message 1019 main.c line 8 Error : C2801: ';' missing main.c line 18 Error : Compile failed
main.c 8 行目「value = sub1(50, 1200);」では、"C1801"という警告が出ています。 これは、 sub1 関数が前もって宣言されていないために、「暗黙的な引数宣言を行った」という意味です。 つまり、ソースコードに書いていない事を勝手に解釈してコンパイルしちまったぜ、というとんでもない警告なのです。
ただ、このメッセージには続きがあって、 main.c 15 行目で、「前に宣言した型と違うじゃないか」とエラーを発しています。 エラーが発生すると、コンパイルは完了しないので、機械語のプログラムは生成されず、問題も発生しません。
このメッセージの解説によると、 main.c 8 行目でコンパイラが勝手に解釈していた sub1 関数は、「int sub1(void)」と解釈されたことがわかります。
コンパイラの勝手な解釈が現実と一致した場合
それでは、 sub1 関数がコンパイラが勝手に解釈していた「int sub1(void)」と同じだったら、何が起こるでしょうか。
#include <hidef.h> /* for EnableInterrupts macro */ #include "derivative.h" /* include peripheral declarations */ volatile int value; void main(void) { value = sub1(); for(;;) { } /* loop forever */ /* please make sure that you never leave main */ } int sub1(void) { return MTIMCNT; }
コンパイルの結果、エラーは無くなりました。
Warning : C1801: Implicit parameter-declaration for 'sub1' main.c line 8
でも、相変わらず、警告は出ています。 コンパイルの結果は、こうなりました。
0000 cd0000 [6] JSR sub1 0003 960000 [5] STHX value
確かに間違ったことはしていません。 正常に働くプログラムが出来ることでしょう。
警告を消したい
ですが、警告といえども、メッセージが表示されるのは、気持ちがいいものではありません。 何とか消す方法はないでしょうか。 それが、プロトタイプ宣言です。
#include <hidef.h> /* for EnableInterrupts macro */ #include "derivative.h" /* include peripheral declarations */ int sub1(void); /* prototype */ volatile int value; void main(void) { value = sub1(); for(;;) { } /* loop forever */ /* please make sure that you never leave main */ } int sub1(void) { return MTIMCNT; }
追加した一行「int sub1(void);」がプロトタイプ宣言と呼ばれるものです。 この一行を加えたことで、警告も出なくなりました。 これで、万事解決です。
引数が違っていても大丈夫
上の例は、コンパイラの都合に合わせて、 sub1 関数の宣言を変えてしまったのですが、プロトタイプ宣言を行えば、引数や戻り値の型が異なっていても構いません。
#include <hidef.h> /* for EnableInterrupts macro */ #include "derivative.h" /* include peripheral declarations */ word sub1(byte p1, word p2); /* prototype */ volatile word value; void main(void) { value = sub1(50, 1200); for(;;) { } /* loop forever */ /* please make sure that you never leave main */ } word sub1(byte p1, word p2) { MTIMMOD = p1; TPMC1V = p2; return MTIMCNT; }
別のソースコードに含まれる関数を参照する場合については、別の機会に。
こんばんは。
これはありがたい、よく分かりました。
ワンパス、ツーパスは知っていましたが、SUBを書く位置に影響すると理解していませんでした。言われてみればそうですよね。
次も楽しみです。
by DAI (2009-08-12 20:49)
C言語を始めた頃、まさにこんな書き方をしていました、恥ずかしい・・・。
当時はvolatile修飾子も知らなかったもんなぁ。
今は、新人君に「volatileは、コンパイラに最適化を抑制させるための修飾子だよ」って言って「????(汗」って顔をさせるのが好きです(笑)
あ、そういえばSwordfishコンパイラ(PICマイコンのBASICコンパイラ)が前方参照もプロトタイプ宣言(DECLARE)もできないんで、いまだにこの書き方を強要してます(汗)
by Ponta (2009-08-14 00:51)
そもそも、K&Rの時代には、「プロトタイプ宣言」という概念が無く、単純に「関数の宣言」の意味しかありませんでした。そのため、引数がどれほど並んでいても「word sub1();」で済んじゃいます。この書き方は、K&Rでは誤りではありません。
ところが、ANSIの時代になると、型のチェックが厳密になってきて、「引数の型が違うものは、別の関数である」という判断がなされます。C1801は、そういった、ANSI準拠で無いソースコードに対する警告です。
そのため、K&R時代のソースコードをコンパイルしたときに出てくるC1801と、ANSI準拠で書いたソースコードをコンパイルして出てくるC1801は、その重みが全然違います。
そういえば、K&Rの時代には、可変長な引数が普通に使われていたもんな。
by noritan (2009-08-14 13:20)
そういえば、僕がC言語始めた頃はK&Rしかなかったかも・・・。
Cマガジン(ソフトバンク)創刊時から読んでたんですが・・・。
なんか記憶が交錯してます(汗)
しかし、K&RとANSIの違いを、さらりと説明できるnotitanさんは流石。
可変長引数は便利ですが、たまにスタック破壊を・・・(←使い方が悪いだけ)
by Ponta (2009-08-14 23:40)
昔のコンパイラでは、引数は必ずスタックに積まれていました。ところが、最近のRISCのように、レジスタの数が多くなってくると引数をレジスタで渡したくなってきます。個の記事の例を見てわかるように、 CodeWarrior でさえ、レジスタ渡しの引数が標準になっています。また、SPARCなんかでは、引数を渡す仕組みをレジスタファイルが持っていたりします。また、たとえスタックに積まれたとしても、スタックの消費量が少なくなるように引数の並べ替え(最適化)をしてくれるコンパイラもあります。
可変長引数は、引数がスタック上に宣言の通りに並んでいることが前提となっているので、今の時代のコンパイラでは、通常はよっぽど運が良くないと使えないでしょう。そのために、 VAR_ARGS という特別な宣言が存在しています。
by noritan (2009-08-15 13:51)
> 最近のRISCのように、レジスタの数が多くなってくると引数をレジスタで渡したくなってきます。
そうそう8bitなのに32個も汎用レジスタが有って、割り込みの度にこれらレジスタの退避に関するでかいオーバーヘッドが発生する、「本末転倒じゃねえか!」ってマイコンも有りますな。
by hamayan (2009-08-16 00:07)