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

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

ヘッダの取り込み

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

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

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

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

<stdio.h>

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

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

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

たとえば、ある処理系では次のような宣言になっていました。

_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型であることを許容する場合
  • 非標準処理系の場合

フリースタンディング環境では、プログラムはどんな名前のどんな型の関数から始まるかは処理系定義ですから、関数名が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関数が画面に出力するものだと思い込んでいると、失敗する状況は想像しにくいですが、標準出力が通信ポートやディスク上のファイルに結び付けられている場合だと、エラーが発生することは普通に起こりえます。

たとえば、SDカードや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 は再帰的に呼び出すことができるので。