printf出力書式まとめ

作成:

長さ修飾子

出力書式について、前回は以下の書式のうち、「変換指定子」について説明した。

%[フラグ][最小フィールド幅].[精度][長さ修飾子][変換指定子]

今回は「長さ修飾子」の説明を行う。

以下の一覧は、変換指定子と同じく、manの説明の抜粋+私の注釈である。 また、説明の上で、変換指定子を用いて、○変換と表現した場合、変換指定子○を用いた変換を指す。 例えば、d変換と表現すれば、変換指定子dを用いた変換を意味している。 変換指定子nの場合、厳密には変換ではないが同様にn変換と表現する。

また、複数の変換指定子をまとめた呼び名として、いくつか用語を使っている。 「整数変換」とは、符号付きもしくは符号なし整数を引数とする変換指定子、d, i, o, u, x, X を用いた変換を指す。 「実数変換」とは、doubleを引数とする変換指定子、a, A, e, E, f, F, g, G を用いた変換を指す。

長さ修飾子意味
hh 整数変換に対して、引数がsigned char型かunsigned char型であること。
n変換に対して、引数が、signed char*型であることを示す。
h 整数変換に対して、引数がshort型かunsigned short型であること。
n変換に対して、引数が、short*型であることを示す。
l 整数変換に対して、引数がlong型かunsigned long型であること。
n変換に対して、引数が、long*型であること。
c変換に対して、引数が、wint_t型であること。
s変換に対して、引数が、wchat_t*型であることを示す。
ll 整数変換に対して、引数がlong long型かunsigned long long型であること。
n変換に対して、引数が、long long*型であることを示す。
L 実数変換に対して、引き数がlong doubleであることを示す。
j 整数変換に対して、引数がintmax_t型かuintmax_t型であることを示す。 intmax_t型は最大幅の符号付き整数、 uintmax_t型は最大幅の符号なし整数の型を示す定義型である。
z 整数変換に対して、引数がsize_t型、もしくはssize_t型であることを示す。 size_t型はサイズを表現するための整数型で、 ssize_t型はサイズ及びエラーを表現する整数型であり、 多くの場合、前者は符号なし整数、後者は符号付き整数の定義型である。
t 整数変換に対して、引数がptrdiff_t型であることを示す。 ptrdiff_t型はポインタ値の差を表現するための整数の定義型である。

定義を調べて書き出してみると、思ったより種類がある。

適切な出力を得るためにはこの修飾子を適切に使いこなす必要があるが、なくても「それなりに」動く。

printf、というか、この仕組のベースとなっている可変長引数の仕組み上、「既定の実引数拡張」が行われ、 int型より小さい整数型はint型に、float型はdouble型に変換されてから渡される。 そのため、intより小さい整数型は特になにもしなくても、intとして扱ってしまって問題は起こらない。 また、こちらは仕様としてどうなのかはおいておいて、表示しようとしているサイズより大きい引数だが、 表示しようとしているサイズで表現できる整数値であれば、たいてい問題なく動作したりする。 表示しようとしているサイズでは表現できない値が格納された引数だった場合に問題が起こる。

そのため、それなりにプログラミングを経験した人でも、 あまりこれらの修飾子を使いこなしていないという人も多くいると思う。 せいぜい、64bit変数を扱うときに気をつけて、lとかllとかをつける。ぐらいだろう。

とはいえ、最近のgccとかは、結果として概ね問題なく出力される組み合わせであっても、 formatから期待される引数と、実際の引数の型が違うという警告を出してくれる。 積極的に使いこなしていないという人も、警告として表示されている分ぐらいは的確に対応するようにしよう。 特に、最近は64bitの環境が増え、かつては全部32bitで扱っても問題のなかったものが、いつの間にか64bitになっていた。 ということが多いので、過去の横着ノウハウは役に立たなくなっている。注意しよう。

意義的にはビット幅などを明示するものだと思ってしまうかもしれないが、 修飾子はビット幅を明示的に示すものにはなっていない。 もともと、C言語の型は言語仕様上ビット幅などは決められていないので、 使用している型と対応がとれていれば、実際のビット数の違いはコンパイラ(ライブラリ)が解決してくれる。

例えば、 intlongが32bitで、long longが64bitという環境もあれば、 intが32bitで、longlong longが64bitという環境もある。 同じgccでも32bitだと前者で、64bitだと後者になり。longが32bitだったり、64bitだったりするのだが、 出力で%ldとして、引数がlongであれば、32bitの環境でも、64bitの環境でも適切に処理してくれ、 実際のデータサイズをプログラマがケアしなくても良いようになっている。

では、実際に長さ指定子が適切で無い場合にどうなるかを実例を元に見てみよう。

printf("%zd\n", sizeof(char));
printf("%zd\n", sizeof(short));
printf("%zd\n", sizeof(int));
printf("%zd\n", sizeof(long));
printf("%zd\n\n", sizeof(long long));

long long a = 0x123456789abcdef;
printf("%hhx\n", a);
printf("%hx\n", a);
printf("%x\n", a);
printf("%lx\n", a);
printf("%llx\n", a);
1
2
4
8
8

ef
cdef
89abcdef
123456789abcdef
123456789abcdef

前半にsizeofの結果を表示して、各型のデータサイズを確認している。 charが1byte、shortが2byte、intが4byte、 longと、long longが8byteの環境である。 これで、long long変数を様々な長さ指定子で出力してみると、このように、オーバーフローが発生した結果が出力される。 これは大方予想通りの結果だろう。

次に浮動小数点数の場合はどうなるか。

printf("%zd\n", sizeof(float));
printf("%zd\n", sizeof(double));
printf("%zd\n\n", sizeof(long double));

long double a = 1.1;
printf("%f\n", a);
printf("%Lf\n", a);
4
8
16

0.000000
1.100000

floatが4byte、doubleが8byte、long doubleが16byteの環境である。 この場合、long doubleの変数を、doubleにキャストした時とは異なり、 全く違う値として出力されてしまった。 また、doublelong doubleを表示する引数に渡した場合も正常に変換されない。 long doubleを扱う場合は、慎重になる必要がある。 floatを指定する修飾子はないが、 先に説明したとおり、引数にfloatがあっても、内部的にはdoubleになっているため、 doubleと全く同じ扱いで問題なく動作する。