printf() といえば言わずと知れたC言語のフォーマット型の 文字列出力関数です。
Cを初めて学んだその日から常にお世話になる関数ですが、 一体 printf() の中ではどんな処理をしているのか勉強するため 実際に作ってみます。
ソースコードは こちら からダウンロードできます。
目標
本家printf() のめぼしい機能を大体実現する。
出力変換指定子
printfの出力変換指定は以下の通りです。
%[flags][width][precision][modifier]type
〜〜今回対応した指定子〜〜
flags
出力時の形式をフラグ式に決定します。
- ・・・ 出力データより width が大きい倍左詰めで出力する
- ・・・ 正の数値の先頭に + を表示する [空白 ] ・・・ (空白) + と同様の効果 # ・・・ 型を明示して数値を出力(例 16進数なら頭に 0x がつく) , ・・・ 整数で3桁ごとにカンマで区切る
width
(数字) ・・・ 出力全体の桁数を指定する。 先頭に0をつけると余白を0で埋める。
- ・・・ 引数で渡された値を width として使用する
precision
(数字) ・・・ 数値の出力の桁数を指定する。 先頭に0をつけると余白を0で埋める。
- ・・・ 引数で渡された値を width として使用する
modifier
hh・・・ 引数が char型であると明示 h ・・・ 引数が short型であると明示 l ・・・ 引数がlong型であると明示 ll ・・・ 引数がlong long型であると明示 j ・・・ 引数がintmax型に収まると明示 z ・・・ 引数がsize_t型に収まると明示 t ・・・ 引数が ptrdiff_t型に収まると明示
type
d,i ・・・ 符号付き10進数 u ・・・ 符号なし10進数 x ・・・ 符号なし16進数 a-f 表記 X ・・・ 符号なし16進数 A-F表記 o ・・・ 符号なし8進数 s ・・・ ヌル終端文字列ポインタを用いて文字列出力 c ・・・ int値を文字として出力 p ・・・ ポインタ値を出力
他
%% ・・・ % を表示
double/float 関連以外はだいたい網羅されています。
実装
まずはサブ関数をいくつか作ります。 今回は標準関数をできるだけ使わない方針でいきたいので strchr() と同等な mystrchr を用意します。
char * mystrchr(const char *s, int c)
{
char ch = (char) c;
while (*s) {
if (*s == ch)
return (char *) s;
s++;
}
return NULL;
}
続いて
以下のような列挙体を定義しておきます。 型のサイズを大きい順に並べてあります。
typedef enum INTEGER_t {
LL = 2, //Long Long
L = 1, //Long
I = 0, //Int
S = -1, //Short
C = -2 //Char
} INTEGER;
この型サイズを指定して可変個引数のリストから 指定した型のデータをとってくる関数を用意しておきます。 signed/unsignedの2種類を用意しておきます。
static long long get_signed(va_list ap, INTEGER type)
{
INTEGER t; t = type;
if(t >= LL) t = LL;
if(t <= C) t = C;
switch(t)
{
case LL: return va_arg(ap, long long); break;
case L: return va_arg(ap, long); break;
case I: return va_arg(ap, int); break;
case S: return (short) va_arg(ap, unsigned ); break;
case C: return (signed char) va_arg(ap, unsigned ); break;
}
return (signed char) va_arg(ap, unsigned );
}
static unsigned long long get_unsigned(va_list ap, INTEGER type)
{
INTEGER t; t = type;
if(t >= LL) t = LL;
if(t <= C) t = C;
switch(t)
{
case LL: return va_arg(ap, unsigned long long); break;
case L: return va_arg(ap, unsigned long); break;
case I: return va_arg(ap, unsigned int); break;
case S: return (unsigned short) va_arg(ap, unsigned ); break;
case C: return (unsigned char) va_arg(ap, unsigned ); break;
}
return (unsigned char) va_arg(ap, unsigned );
}
最後に 整数型のデータを出力する put_integer() を作ります。
static void put_integer(void (*__putc)(int), unsigned long long n, int radix, int length, char sign, int flags)
{
static char *symbols_s = "0123456789abcdef";
static char *symbols_c = "0123456789ABCDEF";
char buf[80];
int i = 0;
int pad = ' ';
char *symbols = symbols_s;
if(flags & CAPITAL_LETTER) symbols = symbols_c;
do {
buf[i++] = symbols[n % radix];
if( (flags & THOUSAND_GROUP) && (i%4)==3) buf[i++] = ',';
} while (n /= radix);
length -= i;
if (!(flags & LEFT_JUSTIFIED)) {
if(flags & ZERO_PADDING) pad = '0';
while (length > 0) { length--; buf[i++] = pad;}
}
if (sign && radix == 10) buf[i++] = sign;
if (flags & ALTERNATIVE)
{
if (radix == 8) buf[i++] = '0';
else if (radix == 16)
{
buf[i++] = 'x';
buf[i++] = '0';
}
}
while ( i > 0 ) { __putc(buf[--i]); }
while ( length > 0 ) { length--; __putc(pad); }
}
引数は 1: 1文字出力関数putc への関数ポインタ 2: 表示したい数値(正) 3: 基数 4: 表示する長さ 5: 符号(+/-) 6: フラグ です。 フラグは
#define ZERO_PADDING (1<<1)
#define ALTERNATIVE (1<<2)
#define THOUSAND_GROUP (1<<3)
#define CAPITAL_LETTER (1<<4)
#define WITH_SIGN_CHAR (1<<5)
#define LEFT_JUSTIFIED (1<<6)
こんなかんじです。
put_integer() では まず最初の do-whileで itoa() 関数のように与えられた数値を文字列に変換して バッファに保存します。この時、低い位から調べていくため、 バッファ内の文字列は前後が入れ替わって入ります。
例) 0x523a do-while部終了時
buf[0] = a buf[1] = 3 buf[2] = 2 buf[3] = 5 (i=3)
となります。ちなみに簡易的な対応で THOUSAND_GROUP のフラグが有効な 場合3文字おきにカンマが挿入されるようにしてあります。
次に左寄せのフラグがない場合で、 表示桁数の指定がある場合にパッディングが必要であれば 挿入します。
do-whileで増加させた i は現在の文字数に等しいので (length - i) が必要なパディングの文字数です。 ZERO_PADDINGのフラグでパディングにスペースを使うか 0 を使うかを切り替えています。
ここまでで数値の配置が完了したので 最後に符号等の処理です。 10進数で符号の指定がある場合は 符号を追加 8/16進数で ALTERNATIVEのフラグが立っている場合は 先頭に 0 / 0x を追加します。
例 "%05x" , 0x523a
buf[0] = a buf[1] = 3 buf[2] = 2 buf[3] = 5 buf[4] = 0 buf[5] = x buf[6] = 0 (i=6)
これで buf[] が綺麗に整列したので i を小さくしながら一文字ずつ 表示すれば画面に 0x0523a と表示されます。
LEFT_JUSTIFIED のフラグが立っている場合はまだ length を消化していないので、表示幅に残りがあれば表示します。
これで必要なパーツは揃いました。
void myvprintf(void (__putc)(int), const char fmt, va_list ap)
サブ関数ができたので本丸も実装します。 基本的に 素直に行きます。
myvprintf が myprintfの本体です。 まずは
while (*fmt && *fmt != '%') __putc(*fmt++);
if (*fmt == '\0') { va_end(ap); break; }
fmt++;
文字列の末端 or % が来るまでは素直に一文字ずつ表示していきます。 末端(\0) であれば終了してループを抜けます。 % が来たら fmt++; で%を読み飛ばして オプションの解析に入ります。
まずは flags を読み取ります。
while (mystrchr("'-+ #0", *fmt)==*) {
switch (*fmt++) {
case '\'': flags |= THOUSAND_GROUP; break;
case '-': flags |= LEFT_JUSTIFIED; break;
case '+': flags |= WITH_SIGN_CHAR; sign = '+'; break;
case '#': flags |= ALTERNATIVE; break;
case '0': flags |= ZERO_PADDING; break;
case ' ': flags |= WITH_SIGN_CHAR; sign = ' '; break;
}
}
先頭の文字に flags関連の文字があれば switchでフラグを立てていきます。
続いて widthとprecisionです。 引数の数値(*)で指定された時は 引数と見に行き、直接数値が指定されていれば その数値を length と precision にそれぞれ保存します。
if(*fmt == '*'){ length = va_arg(ap,int); fmt++; }
else { while( _isnumc(*fmt) ) length = (length*10)+_ctoi(*fmt++); }
if (*fmt == '.')
{
fmt++;
if (*fmt == '*'){ fmt++; precision = va_arg(ap, int);}
else { while (_isnumc(*fmt) ) precision = precision * 10 + _ctoi(*fmt++); }
}
ここで2つマクロを使用しています。
#define _isnumc(x) ( (x) >= '0' && (x) <= '9' )
#define _ctoi(x) ( (x) - '0' )
isnumc は x が文字コードで '0'〜'9' の間であるかを調べるもので ctoi は x - '0' で文字コードと数値を変換しています。
ASCIIコードの数値は '0' = 0x30 '1' = 0x31 '2' = 0x32 '3' = 0x33 ・・・ '9' = 0x39
と連番になっているので (文字コード - '0' ) で数値と変換できます。
一文字読み取ってはもう一周するので もし2桁以上が指定されていた場合は 以前の数値を10倍 します。
例 "%12x"
一周目 length = ctoa('1') = 1
二周目 length = 1 x 10 + ctoa('2') = 12
これで長さが取得できました。 コンマを挟んで precisionも同様に長さを取得します。
続いて ●modifier は flags と同じようにできます。
while (mystrchr("hljzt", *fmt)) {
switch (*fmt++) {
case 'h': int_type--; break;
case 'l': int_type++; break;
case 'j': /*intmax : long */
case 'z': /*size : long */
case 't': /*ptrdiff : long */
int_type=L; break;
}
}
ここでちょっとしたテクニックを使っています。 l,h は2つ並べてより大きな(小さな)型を指すので enumで宣言した INTEGER int_type を加算することで 型の大きさを行き来するようにしてまとめてあります。
例) %ll int_type = 0 = I(アイ) ・・・初期化
l(エル) , l(エル) → int_type++, int_type++
int_type = 2 = LL
これで特殊なオプションの処理が終わりました。 最後は普通に x や dといった type が指定されているはずなので
switch (*fmt) {
case 'd':
case 'i':
i = get_signed(ap, int_type);
if (i < 0) { i = -i; sign = '-'; }
put_integer(__putc, i, 10, length, sign, flags);
break;
case 'u':
ui = get_unsigned(ap, int_type);
put_integer(__putc, ui, 10, length, sign, flags);
break;
case 'o':
・・・以下略
符号などに注意してそれぞれのタイプを表示してやればOKです。
注意が必要なのは ポインタ(%p) です。 32bit/64bit 環境で必要な桁数が違います。 32bitでは 8桁、64bitでは16桁が必要ですが、 コンパイル時にいちいち指定するのは面倒なのでちょっと テクニックを使います。
Unix系のOSでは データモデルとして 32bitでは ILP32 64bitでは LP64 を採用しています。 このデータモデルだと int などは同じ大きさですが
ILP32 long=32bit sizeof(long)=4 LP64 long=64bit sizeof(long)=8
が違います。 これを用いることで
case 'p':
length = sizeof(long) * 2;
int_type = L;
sign = 0;
flags = ZERO_PADDING | ALTERNATIVE;
こうするだけで32bit/64bit どちらでも自動で最適な桁数になります。 (x86系以外では使えない場合もあるので注意)
以上で実装完了です。 実際にテストしてみます。 myvprintf() は 一文字出力関数への関数ポインタをとるので putc(c, stdout) をラップした myputc を用意して myprintf() を完成させます。
test.c
#include
#include
#include "myvprintf.h"
void myputc(int c)
{
putc(c, stdout);
}
void myprintf(const char *fmt, ...)
{
va_list ap;
va_start(ap, fmt);
myvprintf(myputc,fmt, ap);
va_end(ap);
}
void main()
{
char *msg = "hello world!";
myprintf("%s\n", msg);
myprintf("%'d\n", 1000000);
myprintf("%07x\n", 21050);
myprintf("%#x\n", 255);
myprintf("%#X\n", 255);
}
このとおり、ちゃんと動作しているようです。 この myvfprintf() は 一文字出力関数だけ用意すれば 動くので
コピーするだけで色々な環境に組み込めます。
普段お世話になっている printf ですが、自分で実装するとなると 大変ですね。。。 double/fload も処理しようとすると大変なことになりそうです。。。
ソースコードは こちら からダウンロードできます。