C言語で組み込みのプログラムを書いていたところ、printf()があるとバグが発生してprintf()が無いとバグが発生しない という現象に遭遇。
バグの原因はprintf()ではなく別の関数によるメモリ破壊だったのだけど、デバッグの過程で色々と調べたのでまとめることにした。
printf()
といえばプログラミング学習で一番最初にHello Worldで動作させる超基本関数。一見簡単そうに見えて実は奥が深いやつ。
printf()
にはsprintf()
やfprintf()
という兄弟みたいな関数がいる。
これらの違い、それぞれの扱い方や内容について簡単にまとめていく。
3つの違いを簡単にまとめると
これら3つの違いを超簡単にまとめると、下記のようになります。
どの”printf”も出力される内容に大きく差はない。違うのは「どこに出力されるか」ということ。
Hello Worldでおなじみprintf()
はコンソールへの出力。sprintf()
はprintf()で出力するような文字列を配列に出力することができる。fprintf()
はprintf()で出力するような文字列をファイルに出力することができる。
1 2 3 4 5 6 7 |
#include <stdio.h> int main(){ int a =10; printf("aの値は%d\n", a);//出力結果:aの値は10 return 0; } |
printf()はただ単に文字列を出力するだけでなく、上記のように文字列のなかに変数などを組み込んで一つの文字列として出力することができる。
このフォーマット変換の機能は3つすべてのprint関数で同じ。違うところは出力先のみ。
printf()の使い方
なんかもう、printfは言及するまでもないと無いと思うんだけど、他とのバランスも考えて一応記載。
第一引数は文字列のポインタを受け取る。文字列の中に%d
など変換指定子を入れることで変数を文字列に変換して挿入することができる。
第二引数の...
は可変長引数といって、型も引数名も指定せず任意の数の引数を受け取ることができる。
こう見てみると、printfはかなり特殊な仕様の関数だなと思う。
sprintf()の使い方
sprintf()にはprintf()から引数が一つ増えていて、これが出力先となる。
使い方は次の通り。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
#include <stdio.h> int main(){ char str[32];//出力先のchar配列 int a = 2; int b = 3; int c; c = a + b; sprintf(str, "The sum of %d and %d is %d", a, b, c); printf("str is %s"); return 0; } |
まず、出力先となるchar型配列の宣言が必要。ここでは、文字列を格納するのに十分な要素数を確保しておく。
sprintfの第一引数には配列の先頭要素ポインタ(str = &str[0])を渡す。
fprintf()の使い方
fprintfはsprintfの出力先を「ファイル」にしたもの。第一引数にはファイルポインタ(ファイル構造体のポインタ)を渡す。
使い方は下記の通り。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
#include <stdio.h> int main(){ FILE* fp;//出力先のファイルポインタ int a = 2; int b = 3; int c; c = a + b; fp=fopen("./test.txt", "w"); fprintf(fp, "The sum of %d and %d is %d", a, b, c); fclose(fp); return 0; } |
最初に出力先となるファイルポインタを宣言する。fopen()
によって指定したファイルをファイルポインタfp
に割りあて、それをfprintfの第一引数として渡す。
入力する引数の型こそ異なるものの、sprintfとほぼ同じ使い方。
printf()をもう少し掘り下げてみる
ストリームバッファ
標準出力はIDEのコンソールだったり、組み込みであればUARTが出力だったり、システムによって出力先が異なる。しかし、どんなシステムでもprintf()
という関数を叩けばそれぞれの標準出力に出力することができる。
printfから標準出力コンソールまでどのように処理が行われるのかを追ってみる。まずはprintf()
の中身(glibcのprintf.c)を調べてみる。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
/* Write formatted output to stdout from the format string FORMAT. */ /* VARARGS1 */ int __printf (const char *format, ...) { va_list arg; int done; va_start (arg, format); done = vfprintf (stdout, format, arg); va_end (arg); return done; } |
まず、va_list arg
は可変長引数を格納するための変数。
printfに入力された引数をvfprintf()
に渡している。vfprintfはfprintfの引数の形式をちょっとだけ変更したものだけど、ほぼ同じものと考えてもいい。
そうすると、第一引数に与えられているstdoutはFILE構造体ポインタ(ファイルポインタ)ということになる。
つまり、printfは「標準出力コンソールに出力する」という動作を「ファイルへ出力する」という動作として行なっている。
printfの引数に渡した文字列は出力ストリームバッファを通して標準出力のハードウェアに出力される。ストリームバッファとは受け取ったデータを一旦ためておき、ある条件(例えば改行コードがくるとか)でデータを掃き出すためのバッファ。
この出力ストリームバッファはファイルに見立てられていて、printfはstdoutという”ファイル”へ文字列を書き込む動作を行なっている。
システムは、標準出力へのデータパスを抽象化してユーザープログラムからはファイルに見えるようにしている。UARTだろうがディスプレイのコンソールだろうが、標準出力に設定されたものは全部ファイルとしてアクセスする。
実はprintf()とfprintf()は同じことをしてる?
printfはファイルに見立てて抽象化されたストリームバッファにデータを送ることで出力を行っていることが分かった。
ここで一つのことに気づく。
「あれ、ファイルとしてアクセスしてるってことはfprintfと同じじゃね?」
fprintf()
の機能は以下の二つ。
- 文字列中の変換指定子などを変換して文字列を整形
- 整形されたデータをファイルへ出力する。
printfも全く同じことをしている。唯一違うところは、printfには出力ファイルを指定する引数がないということ。printfが扱うことができるのはstdoutだけ。
まとめると
printf()は「出力先をstdoutに限定されたfprintf()」と等価である
という事ができる。