第2回 hello, world!

今回は、またしても今更感がある hello, world! についてです。hello, world! は、C言語の入門書の多くで取り上げられている初歩的なプログラムです。それ自体は非常につまらないものですが、では、hello, world! のソースコードを本当に理解できている方がどれぐらいいるでしょうか?

#include <stdio.h>

int main(void)
{
  puts("hello, world!");
  return 0;
}

この単純なプログラムは、入門書の最初に出てくるだけに、十分に理解されないまま先に進んでしまっていることが多いはずです。ここでは、改めて hello, world! を取り上げることで、徹底的に解説してみたいと思います。

ヘッダの取り込み

それでは順を追って見ていきましょう。ソースコードの最初に現れるのは、#include 指令です。#include 指令は、ヘッダまたはソースファイルを取り込むための前処理指令です。ここで、「ヘッダまたはソースファイル」と書いたのは、規格上、両者は明確に区別されているからです。

すなわち、<...> で取り込むものがヘッダであり、"..." で取り込むものはソースファイルというわけです。ただし、"..." 形式で指定したソースファイルが見つからない場合、またはそもそも "..." 形式がサポートされていない場合はヘッダが取り込まれることになります。

ヘッダというのは、ソースファイルと区別していることからもわかるように、必ずしもファイルであるとは限りません。コンパイラが、ヘッダを取り込む #include 指令を見つけた際に、そのヘッダで宣言・定義される内容を有効にすることができれば、実現方法は何でもよいのです。実際、C言語のインタプリタなどでは、そのような方式が採られることがあるようです。

次に、#include 指令で取り込むヘッダやソースファイルはどのようにして探し出されるのでしょうか? 規格上は、どんなアルゴリズムで、どこを対象に探索するかは処理系定義ということになっています。そのため、ソースファイルのディレクトリ構成を考える上で、あまり複雑な依存関係を作ってしまうと、それだけで移植性がなくなってしまいます。

<stdio.h>

それでは、#include 指令で取り込んでいるヘッダ <stdio.h> について見ていきましょう。このヘッダは、puts 関数を使うために、その関数原型を宣言させるために取り込んでいます。

先ほど、ヘッダはファイルとは限らないと書きましたが、大多数の処理系はファイルとして実装されていますので、一度 stdio.h の中をのぞいてみてください。エディタの検索機能を使うなどして、puts の関数原型を探してみてください。

int puts(const char *);

のような関数原型が見つかりましたでしょうか? 実際には、こんなにシンプルな宣言ではなく、もっとゴテゴテとした記述になっているのではないでしょうか?

例えば、ある処理系では

_CRTIMP int __cdecl  puts (const char*);

のようになっていました。_CRTIMP とか、__cdecl とか、訳のわからないものが付いていますね。これらは、その処理系の独自拡張機能を制御するためのものです。ですから、処理系によって、どんなものが付いてくるかは異なります。

stdio.h を開いたついでに、puts の関数原型以外も眺めてみてください。FILE 型の定義や、printf の関数原型などもあるはずです。眺めれば眺めるほど、いろいろ疑問が出てくると思いますが、それらを一つ一つ調べていくことで、入門書では決して得られなかった知識が身に付くはずです。

関数main

次は、hello, world! プログラムの中では唯一の関数である main についてです。前回も触れましたが、プログラムが main から始まるのは、あくまでもホスト環境の場合です。フリースタンディング環境の場合には、どんな名前のどんな型を持つ関数から始まるかは処理系定義になります。

hello, world! プログラム自体、puts 関数を使っていることから考えても、明らかにホスト環境だけを対象にしています。フリースタンディング環境では、<stdio.h> もサポートされません。

main の返却型ですが、これは必ず int でなければなりません。ときどき void になっている入門書がありますが、main の返却型を void にできるのは、次の場合に限られます。

  • フリースタンディング環境の場合
  • C99の場合
  • 非標準処理系の場合

フリースタンディング環境では、プログラムはどんな名前のどんな型の関数から始まるかは処理系定義ですから、関数名が main で返却型が void であっても、処理系がそう定義しているのであれば問題ありません。

C99では、main は処理系定義の形式が認められていますので、移植性さえあきらめれば、返却型が void でも double でも構造体でも、処理系がそう定義しているのであればかまいません。

非標準処理系の場合は、そもそも標準規格に適合していないので、処理系が対応してさえいれば何でも OK です。

C言語の入門書を選ぶ際には、main の関数が int になっているかどうかをまず調べるべきだと指摘する方もいます。マイコンのプログラミング入門とかであれば、(フリースタンディング環境を想定しているのでしょうから)main の返却型が void でもかまいませんが、そうでなければ入門書としてはやはり不適切かと思います。

関数puts

このページで取り上げた hello, world! プログラムでは、文字列を画面に出力するために puts を使用しています。しかし、puts は本来画面に文字列を出力するためのものではありません。これは printf などについても同じことがいえます。

では、puts は何をする関数かといえば、引数として渡した文字列を標準出力に出力するためのものです。そして、標準出力は、通常画面などに結び付けられているにすぎません。場合によっては、標準出力がシリアルポートに結び付けられているかもしれませんし、ディスク上のファイルに結び付けられているかもしれません。

確実に画面に出力するためには、多くの場合、ハードウェアか、最低限デバイスドライバを直接制御しなければなりません。OS のシステムコールレベルだと、デバイスドライバが摩り替わったり、その他のフック機能等の影響で、確実に画面に出力できるとは限らないからです。

ところで、多くの場合公然と無視されているのですが、puts にも返却値があります。puts の呼び出しが成功すれば 0 以上の値を、失敗すれば EOF が返されます。puts が画面に出力するものだと思い込んでいると、失敗する状況は想像しにくいですが、標準出力が通信ポートやディスク上のファイルに結び付けられている場合だと、エラーが発生することは普通に起こりえます。

たとえば、フロッピーディスクや USB メモリのファイルに出力する場合であれば、メディアが抜かれれば当然エラーが発生します。というわけで、本当に手堅くコーディングするのであれば、puts の返却値も調べた方がよいのです。

同様に、入門書や解説書のサンプルコードでは、流れを理解しやすいようにするため、意図的にエラー処理が省略されている場合が少なくありません。実際の使用状況と、どこまでの信頼性が要求されるかによって、無視してしまいがちなエラーも適切に処理する必要があります。

return 0

最後は return 0 です。main の返却型が int 型ですので、何らかの整数値を返す必要があります。ただし、C99の場合には、最後の return 0 を省略した場合は、暗黙的に return 0 が埋め込まれます。

最初に呼び出された*1mainからのリターンは、その返却値を実引数として exit を呼び出すことと同じです。すなわち、atexit で登録した関数が後入れ先出しで実行され、オープンされている全ストリームがクローズされ、オープンされていた一時ファイルが削除され、その後、処理系定義の終了処理が行われます。

main からの返却値はホスト環境に渡されますが、それが具体的にどのように使われるかは処理系定義です。したがって、返却値がシェルなどに通知される場合もあれば、単に捨てられる場合もありますし、それ以外の振る舞いをすることもあるわけです。

なお、返却値に 0 を指定した場合は、プログラムの成功を意味します。main が 0 を返したとしても、必ずしもホスト環境にそのまま 0 が渡されるとは限りません。0 は成功を意味する値なので、ホスト環境が 1 をプログラム成功の値として扱うのであれば、0 から 1 に変換されてからホスト環境に渡されることになります。

プログラムの成功を意味する返却値は 0 でしたが、失敗を意味する返却値は処理系定義です。移植性のある方法で失敗終了させるためには、まず、<stdlib.h> ヘッダを取り込んでから、EXIT_FAILURE マクロを使用する必要があります。なお、EXIT_FAILURE と対になるように、処理系によらず 0 に定義される EXIT_SUCCESS というマクロもあります。

いかがだったでしょうか? 入門書ではここまでくどい説明をしていることはまずありません。たかが hello world! といわず、掘り下げてみれば、まだまだ気づいていなかったことがいろいろあるはずです。


*1 main は再帰的に呼び出すことができるので。