2026年7月頃、Xではstdio.hが話題になっていました。Xにいるエンジニアは、こういったどうでもいい話で盛り上がっては一部の人が燃やし燃やされ、次の日にはどうでもよくなりがちです。Qiitaのコメ欄も似たようなものですが。
そんなstdio.hについて、いくつかの意見がありました。
- stdio.hはおまじないでいいよ
- stdio.hは読んでおくべきだね
- stdio.hを使えない環境だってあるんですよ!
- 一部の組み込み系ではstdio.hはオプションだよ
- 標準入出力とは何かを議論すべき
そもそもなんの話をしてたんだっけ?という、いかにもXらしいカオスな状況でしたが、そういえば私もstdio.hをちゃんと読んだことがなかったので、良い機会ですからちゃんと読んでみようと思った次第です。ただ、全部読むのはしんどそうなので、主にprintf()を調べてみます。
事前補足ですが、stdio.hはC言語で使われますけど、これはCコンパイラというよりライブラリの話ですね。例を挙げると、gccのstdio.hではなく、glibcのstdio.hと書いた方がよさそうです。
glibcのstdio.h
まずはLinuxでも使われているgcc/glibcから読んでみましょう。
Debian Sourcesのコードはこちらです。
975行となかなかのボリュームです。printf()は366行目あたりでしょうか。
/* Write formatted output to stdout.
This function is a possible cancellation point and therefore not
marked with __THROW. */
extern int printf (const char *__restrict __format, ...);
英文コメントは、スレッドのキャンセルポイントに関する内容です。stdio.hのprintf()プロトタイプ宣言を読み解くには、Linuxのスレッドのキャンセルポイントというものを理解していないといけないようです。
printfの引数に使われている__restrictと__formatも気になる点でしょうか。
__restrictはC99で導入されたrestrictのglibc版です。このポインタを通じてアクセスするオブジェクトには、他のポインタから同時にアクセスしないことを約束しています。__restrictをわざわざ定義しているのは、restrictを使えないコンパイラのことを考慮しているからです。なのでmain.cの先頭で
extern int printf (const char * restrict __format, ...);
と書いて「stdio.hをincludeしなくてもprintf()は使えるのですよ」としてしまうとrestrictを使えない環境下ではコンパイラエラーになってしまうわけです。「他の環境など知らん自分のとこではコンパイルできる」という生き方を通すのは自由ですが、あまり優しい世界ではなさそうですね。restrictを取っ払ってしまえ!というのもよいですけど、コンパイラが迷惑しますし、今だったらAI氏が「それはちょっと」と言ってきそうです。
そんな__restrictはどこで定義してあるのか辿っていくと、bits/libc-header-start.h → features.h → sys/cdefs.hに書いてあります。では順に読んでいき……いや、今回はstdio.hを読むのが目的なので、これは深堀りが過ぎました。
もう一つの__formatが何かというと、これはただの引数名です。紛らわしいなキミ。
このように、printf()だけをみても、glibcのstdio.hは、POSIX(スレッド)、ISO C(restrict)、GCC(__THROW)といった仕様を知っていないと読み解けないことがわかります。しんどい。
Darwin libc(Mac)のstdio.h
macOSのlibSystemで使われています。FreeBSD libcをベースにAppleが独自拡張したものです。
stdio.hはこちら。
9割がコメントで実コードはたったの4行です。
#include <_stdio.h>
#ifdef _USE_EXTENDED_LOCALES_
#include <xlocale/_stdio.h>
#endif /* _USE_EXTENDED_LOCALES_ */
あっさりしていて読みやすいですね。では次のstdio.hに……というわけにもいきませんね、_stdio.hが本体でしょうから目を通してみます。
printf()はどこかなーと探してみると……なんと!定義されていません。怪しいのはこの辺です。
#include <_printf.h>
_printf.hを見てみましょうか。
__BEGIN_DECLS
int printf(const char * __restrict, ...) __printflike(1, 2);
__END_DECLS
printf()は__printflike()に置き換えられるようです。では、__printflike()はなにかというと、いろいろと展開されて最終的にこんな感じになるようです。
#define __printflike(fmtarg, firstvararg) \
__attribute__((format(printf, fmtarg, firstvararg)))
__printflike()は、「1番目の引数が書式文字列で、2番目の引数から可変引数が始まりますよ」とコンパイラに教えるための目印です。GCCやClangはこの__attribute__((format(printf, ...)))を見て、実際に渡された引数が書式文字列と食い違っていないかをコンパイル時にチェックしてくれます。書式文字列がらみのバグや脆弱性を防ぐための仕組みです。__attribute__と__format__は後ほど改めて。こうなるといつものprintf()とは違うものになってしまっていますね。
ところで、_printf.hを探してみると、printf.hというものも見つかります。
ややこしいですが、"printf フック APIを宣言するためのもの"らしいです。中身を見るとコピーライトが2005 Poul-Henning Kampとなっていて、どうやらAppleが独自に考えたものではなく、FreeBSD由来のコードをそのまま引き継いでいるようですが、もうここまでくるとさっぱりわかりません。この辺のファイル分割の思想や、定義の書式はBSDの文化のようです。
stdio.hの話をする時に「stdio.hにはprintf()とか定義してあるんよ」と言う人がいますけど、「それBSDの前でも言える?」って思いました。
Windowsコンソールアプリのstdio.h
オープンソースではないので、ブラウザで閲覧することはできません。正確には、WindowsSDKに含まれるUniversal CRT(UCRT)というライブラリのようです。なので今回はVisualStudioでコンソールアプリを作成してからstdio.hを開いてみました。全部で2448行もあります。
コードを見たい人はVisualStduioかWindowsSDKをMicrosoftの公式サイトからインストールしてね!
どうでもいいですが、攻撃力の高そうな(-+-)コメントブロックになっています。
気になるキーワードは、_Check_return_opt_、_CRT_STDIO_INLINE、__CRTDECL、_In_z_、_Printf_format_string_あたりでしょうか。表にまとめちゃいましょう。
| keyword | 用途 | 誰のため? |
|---|---|---|
_Check_return_opt_ |
戻り値はチェックした方がよいよ (必須じゃない) |
VisualStudioの静的解析 |
_CRT_STDIO_INLINE |
この関数はインライン化して | コンパイラ |
__CRTDECL |
呼び出し規約 | コンパイラ |
_In_z_ |
入力専用&NULL終端された文字列だよ | VisualStudioの静的解析 |
_Printf_format_string_ |
この引数はprintfの書式文字列だよ | VisualStudioの静的解析 |
人間様が読まなくてもよいものばかりです。stdio.hは人が目にしていいものではないのかもしれません。
ところで、char const* constは理解できますか?C言語完全に理解してますか。
Windowsデスクトップアプリでstdio.h
このセクションは余談です。
VisualStudioでWindowsデスクトップアプリを作成して、#include <stdio.h>を書くとprintf()を使うことが出来ます。この時、文字列はどこに出力されるかというと「どこにも表示されません」。出力先がありませんからね。ということはコンソールウィンドウを生成すれば出力されるということです。
WinMain(...)
{
~
AllocConsole();
FILE* fp;
freopen_s(&fp, "CONOUT$", "w", stdout);
printf("Hello World\n");
setvbuf(stdout, NULL, _IONBF, 0);
~
ここで使われるstdio.hは先ほどのコンソールアプリ版と同じものです。
ついでに説明すると、VisualStudioの出力ウィンドウに文字列を出したいのであればOutputDebugString()を使えばよいですよ。
musl libcのstdio.h
glibcよりもちっちゃいライブラリだそうです。そんなライブラリのstdio.hはこちら。
int printf(const char *__restrict, ...);
こういうのでいいんだよおじさんもニッコリの定義です。
コメントや段落分けの改行くらいは使ってもよさそうですけど、こういったところに、こだわりというか思想を感じます。
こうなると、glibcのstdio.hはなんであんなにゴッチャリなのか?という疑問が出てきますが、思想だけではなく、歴史的経緯を含んだ結果なのではないかと私は思います。というのも、glibcは1980年代に開発が始まり、今に至るまで開発が続けられていて、ANSI C、ISO C、POSIX、X/Open、SVID...などを取り込んで、今でも過去のコードとの互換性を限りなく保持しようという思想が読み取れます。
一方で、muslは2011年のリリースで、ISOとPOSIXだけは守るという思想ですから、stdio.hもシンプルに充分なのですね。わざわざglibc互換のライブラリを再開発する理由もないでしょうし。
つまり、stdio.hを理解するにはC言語の歴史も合わせて学ばねばならないのです。
それにしても、LinuxだけでなくWindowsもですが、ソースコードだけでなくバイナリレベルで互換性を保持していこうとする姿勢は素晴らしいですね。
newlibのstdio.h
Newlibは、主に組み込みシステムで使われる標準Cライブラリです。Raspberry PiのPico SDKなどにも採用されているようで、私も知らずに使っていました。
Newlibのstdio.hはこちらで見ることができます。
printf()の宣言はこのようになっています。
int _EXFUN(printf, (const char *__restrict, ...)
_ATTRIBUTE ((__format__ (__printf__, 1, 2))));
見た目はあっさりですが、気になるキーワードをまとめて説明しましょう。
| keyword | 用途 | 誰のため? |
|---|---|---|
_EXFUN |
古いコードへの配慮? | 人向け? |
_ATTRIBUTE |
__attribute__のラッパー |
コンパイラ |
__format__ |
formatのラッパー / 書式付き関数ですよ |
コンパイラ |
__printf__ |
printf形式の書式文字列ですよ | コンパイラ |
そもそも__attribute__がわからない人はstdio.hを読んでいる場合ではないのですが、知らなくてもそんなに困りませんので、気が向いたら調べてみるとよいと思います。自作関数の宣言で使うこともできます。
同じように、stdio.hを完全に理解するためには__を使う理由も知っておくべきかもしれませんね。
_EXFUNは、正直、よくわかりませんでした。
シンプルに見えて、意外と奥深いnewlibのstdio.hでしたね。
ところで、newlibは組み込み専用というわけではありません。ライブラリ群なので、LinuxでもWindowsでもコードを整えてビルドすれば使うことはできます。ただ、これをOS上で使う理由があるかという話で『glibc / ucrtがあるし……』ってなりますね。
また、newlibはCygnus Solutionsが開発したもので、Windowsのアプリ開発者ならご存じのCygwinも実はnewlib -> cygwin1.dll -> Windows APIという構成になっていることからも、newlibは組み込み専用ではないことがわかります。
私の意見としては、newlibは組み込み機器専用というより、組み込みに向いているlibcという認識です。
AVR Libcのstdio.h
Arduinoで使われています。これは私も触れたことがあります。Arduino言語の中身はC/C++ぽいという程度の理解度です。
AVR Libcのstdio.hはこちら。
まさかの1000行超え!軽く済ませるつもりだったのに。
でも、printf()の宣言部はわかりやすいですね。
/**
The function \c printf performs formatted output to stream
\c stdout. See \c vfprintf() for details.
*/
extern int printf(const char *__fmt, ...);
stdio.hを全体的に眺めてみると、先ほどのnewlibとAVR Libcは組み込み向けなのにI/Oの抽象化の方法がまったく違うことに気が付きますが、これは今回の記事の本題とは少し離れてしまうので省略します。
それから、このstdio.hの中身はマニュアルを兼ねていました。Arduinoをより深く理解したい人はstdio.hを読みましょう。これはおまじないで済ませてはいけないstdio.hです。あ、でも、ライブラリを使うだけであれば、読みやすいマニュアルは別のところで用意されていますから、頑張ってstdio.hを読まなくても大丈夫ですし、そもそもコードを読む前にマニュアルを読みましょう。
古き良き時代のstdio.h
最後に UNIX Version 7 のstdio.hを見てみましょう。
すべての始まりです。
printf()の宣言が無いだと……!?
これはprintf()が使えなかったわけではなく、わざわざ宣言する必要がなかったからです。この時代のC言語(K&R)は引数リストを宣言できませんでしたし、戻り値がintの関数はデフォルト動作なので、宣言する理由がありません。関数名が一致していればコンパイル&リンクは通りますから。
では、引数の型や個数が違ったらどうなるかというと暴走するので頑張ってデバッグします。
ANSI C以降は引数のプロトタイプ宣言を導入することで、コンパイル時にチェックができるようになり、現代ではコードを書いてる時にIDEが教えてくれますし、それどころかAIが「うわそこ引数が違いますよあぁもう見てらんない、もう手を出さないでください、私がコードを作成します」って代わりに働いてくれるのでいい時代になりました。
ここで疑問なのは、当時のエンジニアは、stdio.hにprintf()が定義されていないのに、どうやってlibcに含まれている関数を把握していたのかということですが、それはマニュアルに書いてあります。manコマンドを使うもよし、紙のマニュアルを読むもよしです。マニュアルが仕様&ソースコードが実装という文化ですね。マニュアルを読まずにいきなりstdio.hやlibcのコードを読むのはよろしくないのです。今ではソースコードが仕様というのも珍しくない話ですが。
感想
初学者が画面に"Hello World"と表示したいのであれば、stdio.hは後回しでいいでしょう。関数の書き方を学んだ時にprintf()を理解して、ファイルの分割を学んだ時にincludeを理解して、それからある日ふと『このstdio.hってなんだろう?』と疑問に思った時にstdio.hを開いて絶望するのがよいのではないでしょうか。
それにしても、今回、stdio.hを調べて改めて感じたのは、どのような開発環境でも、このコードをコンパイルして実行できるということが本当に素晴らしいということでした。そんな当たり前のことを実現するためにstdio.hはとても複雑になっているのだなと思った次第です。
#include <stdio.h>
main( )
{
printf("hello, world\n");
}
環境の相違は全部stdio.hに任せておけばよいので、皆さんもstdio.hに感謝しながら#Inculude <studio.h>と心を込めて一文字一文字入力しましょうね。
stdio.hを読むべき(強火)という意見についてですが、これまで見てきたように、読めるstdio.hと読めないstdio.hと読まなくていいstdio.hがあり、環境によって別物と言えるくらいに違いがあります。自分のところのstdio.hは読めても、そうでない環境もあるということです。ここまで振れ幅があるものを「読んで当然」と一括りにするのは無理がありますから、だったら普段は#include <stdio.h>をおまじないとして扱っておいて、気になった時だけstdio.hを覗くくらいがちょうどよさそうに思います。
1980年代のstdio.hしか読んだことのない今では現場を離れたエンジニアさんや、組み込み系専門のエンジニアさんが「stdio.hは読んで当然!」と言う前に、今どきのOS向けstdio.hにも一度目を通してからにして欲しいかなと思ったりもしました。その上で「最新のstdio.hは理解している。そんなものは読んで当然である」と言われてしまうと眩しすぎるので、もう少し手心を加えていただきたく。
stdio.hの複雑さは、ほとんどの場合、歴史によるものだということも改めて理解できました。K&R時代にプロトタイプ宣言が無かった理由も、glibcが今なおPOSIXやSVIDを引きずっている理由も、AppleのstdioがBSD由来である理由も、過去の経緯を知らなければ「なぜこうなっているのか」にはたどり着けません。stdio.hを理解するということは、結局のところC言語とその周辺の歴史を学ぶことと同じなのだと思います。過去を切り捨てて今の姿だけを見ても、stdio.hはただ複雑で読みにくいコードにしか見えないのでしょう。
というわけで、今回7つのstdio.hを覗いてみて思ったのは、#include <stdio.h>は今まで通り「おまじない」でいいよね!ということです。中身がこれだけ複雑で、環境ごとに別物なのですから、全部理解してから使おうとする方がどうかしてます。
#include <stdio.h>は、今日も変わらずおまじないです。
余談 - 標準ってなんなんだ
ところで、stdio.hって無法地帯なのかというとそんなことはなく、C89から基本的な入出力機能が定義されていて、提供すべき関数にprintfが含まれています。それぞれのライブラリは、その定義を守りつつ、独自に拡張しているわけですね。
もちろん、ANSIやISOに従わない、オレオレC言語、オレオレlibcだって存在しても構わないので、stdio.hを使わなくてもよいという議論は成り立ちます。
標準出力についても、ライブラリの標準出力の出力先が「そこはエンジニアが自分で定義しろ」というものもあれば、「OSのstdoutにしといたから」というものまであるので、会話の線引きをちゃんとしないとカオスになって当然です。
仕様や規格については、日本にはJIS X3010というC言語のルールがありますが、話がややこしくなるので触れないでおきます。大阪弁と京都弁を混ぜて議論するようなものです。
余談 - 組み込み系の標準ライブラリ
「一部の組み込み系ではstdio.hはオプションだよ」という意見もありました。これもちゃんと根拠がある話で、C言語の実行環境には、OSがある「ホステッド環境」と、OSを前提としない「フリースタンディング環境」の2種類が規格上存在します。
フリースタンディング環境ではstdio.hを含む標準ライブラリの提供はそもそも必須ではありません。規格が要求するのは<float.h>や<stddef.h>などごく一部だけです。マイコン向けの開発でstdio.hが無かったり独自実装だったりするのは、フリースタンディング環境に該当するからなんですね。
ただし、組み込み系だからといって標準出力そのものが無いとは限りません。UARTやシリアル経由でホスト側のターミナルに文字を送ったり、AVR Libcのようにfdevopen()でストリームを自前のデバイスに紐付けたりすれば、printf()は普通に使えます。
「OSが無い=出力先が無い」わけではなく、「OSが前提とする標準入出力の仕組みが無い」というだけの話で、その先をどう実装するかは各libcやプロジェクト次第、というのが正確なところだと思います。組み込み系のバックグラウンドは、ホステッド/フリースタンディングという規格上の話だけでは片付かない分、意見が衝突しやすいのかもしれません。
そう考えると、glibcのようにLinuxカーネルのシステムコールをラップすることが本質のlibcは、OSと切り離せない存在です。Wikipediaでは「Linuxカーネル+glibcで初めてLinux APIになる」と説明されているくらいで、glibcのstdio.hを読むということは、Linuxというカーネルとセットで理解するということでもあります。
Windowsは、UCRTやWin32 APIがOSとの仲介役として間に入っています。
macOSは、カーネルのBSD部分がシステムコールを提供している点ではLinux寄りですが、生のシステムコールを直接呼ばせず、必ずlibSystem(Darwin libc)経由にさせる点ではWindowsに近く、両者の中間のような立ち位置です。
同じ「OS依存」でも、ライブラリとOSの距離感がまったく違うというのも今回あらためて面白いと思った点でした。
