3
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

【C言語】 Hello, world! 以前 〜 printf 関数を自作してみた〜

Last updated at Posted at 2025-03-28

Hello, world! ✋

printf("Hello, world!\n");

7 年前、ここから私のプログラミングライフが始まりました。

プログラミングの基本のキとして扱われがちな Helllo, world! ですが、ここで使っている標準出力関数ってかなりリッチですよね?

例えば、C 言語の printf 関数は、

  • 可変長引数を受け取れる
  • フォーマット指定子(%d など)に変数を埋め込める
  • 文字によってバイトサイズの異なる UTF-8 を扱える

etc...
かなり複雑な作りになっていることが想像できます 🤔

上記 3 点を満たす簡易版 printf 関数を自作してみました!

成果物 🏁

ポイント 💡

1. 可変長引数を受け取る 🎁

void my_printf(const char *format, ...) {
    va_list args;
    va_start(args, format);

    while (*format) {
        if (*format == '%') {
            format++;
            switch (*format) {
                case 'd': my_putint(va_arg(args, int)); break;
                case 's': my_puts(va_arg(args, char*)); break;
                case 'c': {
                    char c = (char)va_arg(args, int);
                    write(1, &c, 1);
                    break;
                }
                case '%': write(1, "%", 1); break;
                default: write(1, "?", 1); break;
            }
        } else {
            my_put_utf8_char(&format);
            continue;
        }
        format++;
    }

    va_end(args);
}

1.1 va_list

可変長引数を格納するための変数。

va_list args;

これを使って可変長引数を操作します。

1.2 va_start

可変長引数の処理を開始します。

va_start(args, format);
  • args を初期化する
  • format(最初の固定引数) の次の引数から取得できる

1.3 va_arg

可変長引数の次の引数を取得します。

int value = va_arg(args, int);
  • 呼び出すたびに次の引数を取得する
  • 型は int, double, char* など 正しく指定する必要がある

1.4 va_end

可変長引数の処理を終了します。
va_start を呼んだら必ず va_end で終了します。

va_end(args);

2. フォーマット指定子に変数を埋め込む 👷

my_printf 関数では、format の文字を 1 文字ずつ処理します。
format++ でポインタをインクリメントすることで、1 バイトずつ処理できます。

以下の流れでフォーマット指定子を処理します。

2.1 if (*format == '%') でフォーマット指定子を検出

format の現在の文字が '%' である場合、次の文字を確認するために format++ します。

if (*format == '%') {
    format++;

2.2 switch 文でフォーマット指定子を判定

switch (*format) で、次の文字が d(整数)、s(文字列)、c(文字)、%(リテラルの %)のどれなのかを判定します。

switch (*format) {

2.3 %d(整数)の処理

整数型 intva_arg で取得し、my_putint に渡します。

case 'd':
    my_putint(va_arg(args, int));
    break;
  • va_arg(args, int) によって args から int 型の値を取り出す
  • 取り出した値を my_putint に渡し、整数を文字列に変換して write で出力する

2.4 %s(文字列)の処理

文字列のポインタ(char *)を va_arg で取得し、my_puts に渡します。

case 's':
    my_puts(va_arg(args, char*));
    break;
void my_puts(const char *s) {
    write(1, s, strlen(s));
}
  • va_arg(args, char*) によって args から char * 型(文字列の先頭アドレス)を取得する
  • my_putswrite(1, s, strlen(s)); で文字列全体を出力する

2.5 %c(文字)の処理

文字 char を取得し、write で直接出力します。

case 'c': {
    char c = (char)va_arg(args, int);
    write(1, &c, 1);
    break;
}
  • va_arg(args, int)int 型の値を取得し、char 型にキャストする
    • 可変長引数を使うとき、char, short などの小さいデータ型は int 型に昇格するため(default argument promotion)
  • write(1, &c, 1); で 1 バイトだけ出力する

2.6 %%(リテラルの %)の処理

%% を見つけた場合、write(1, "%", 1);% をそのまま出力します。

case '%':
    write(1, "%", 1);
    break;

2.7 default の処理

サポートされていないフォーマット指定子の場合、? を出力します。

default:
    write(1, "?", 1);
    break;

write の使い方 🧑‍🏫

int write(int fd, void *buf, unsigned int byte)
引数
引数 説明
fd(ファイルディスクリプタ) 書き込む対象(1 = 標準出力, 2 = 標準エラー)
buf(データバッファ) 書き込むデータのアドレス
byte(バイト数) 書き込むデータのサイズ
戻り値
  • 成功: 書き込んだバイト数
  • 失敗: -1

3. my_putint の処理 🔢

3.1 バッファの準備

int 型は -2147483648 〜 2147483647 の最大 11 桁。
これに終端ヌル文字 \0 を加えた 12 バイト確保します。

char buf[12];

3.2 負の数の扱い

負の数フラグを立て、絶対値を保持します。

if (n < 0) {
    is_negative = 1;
    n = -n;
}

3.3 数値を逆順にバッファに格納

数値を逆順でバッファに格納します。

ex. 1234["4", "3", "2", "1"]

if (n == 0) {
    buf[i++] = '0';
} else {
    while (n > 0) {
        buf[i++] = (n % 10) + '0';
        n /= 10;
    }
}
  • n % 10 で一番右の桁を取得し、それを '0''9' に変換する
  • n /= 10 で次の桁へ移動する

3.4 負の数なら '-' を追加

負の数のとき、末尾に '-' を追加します。

ex. -56789["9", "8", "7", "6", "5", "-"]

if (is_negative) {
    buf[i++] = '-';
}

3.5 バッファの反転

バッファの左右を反転します。

for (int j = 0; j < i / 2; j++) {
    char temp = buf[j];
    buf[j] = buf[i - j - 1];
    buf[i - j - 1] = temp;
}

ex. -56789

buf の状態 操作
["9", "8", "7", "6", "5", "-"] buf[0]buf[5] (9-)
["-", "8", "7", "6", "5", "9"] buf[1]buf[4] (85)
["-", "5", "7", "6", "8", "9"] buf[2]buf[3] (76)
["-", "5", "6", "7", "8", "9"] 完了

3.6 文字列終端を追加

C 言語の文字列として扱うため、最後にヌル文字 '\0' を追加します。

buf[i] = '\0';

3.7 write で出力

整数を標準出力に書き込みます。
終端ヌル文字 \0 は除いています。

write(1, buf, i);

4. UTF-8 を扱う 🇯🇵

ASCII 文字は format++ でポインタを 1 バイトずつ進めることで 1 文字ずつ処理できますが、UTF-8 文字は 2 〜 4 バイトの文字も存在するため、何バイトの文字か判定する必要があります。

my_put_utf8_char では次の文字が何バイトの文字か判定し、1 文字ずつ文字を出力します。

4.1 文字列の先頭バイトを取得

void my_put_utf8_char(const char **p) {
    const unsigned char *s = (const unsigned char *)(*p);
  • pconst char**(文字列のポインタへのポインタ)
  • *p で指している先頭文字のポインタを取得
  • sunsigned char* にキャストして扱う
    • char は 符号付き(signed char) の可能性があり、ビットパターンが負の値として解釈されないようにするため
    • ex. "こ"の先頭バイト: 0xE3(11100011)-29

4.2 先頭バイトを見て、UTF-8 のバイト数を判定

if ((*s & 0x80) == 0)        bytes = 1;  // ASCII (1バイト)
else if ((*s & 0xE0) == 0xC0) bytes = 2;  // 2バイト文字
else if ((*s & 0xF0) == 0xE0) bytes = 3;  // 3バイト文字
else if ((*s & 0xF8) == 0xF0) bytes = 4;  // 4バイト文字
ビットパターンによる判定

以下のように、UTF-8 の文字は先頭バイトのビットパターンで、何バイトの文字かが決まっています。

文字の種類 先頭バイトのビットパターン バイト数
ASCII 0xxxxxxx 1
2 バイト 110xxxxx 2
3 バイト 1110xxxx 3
4 バイト 11110xxx 4

& 演算を使って、このビットパターンに一致するかどうかを判定しています。

各判定の意味
  • (*s & 0x80) == 0
    • 0x8010000000)との論理積をとる
    • 結果が 0 なら ASCII(1 バイト)
    • ex. 'A'010000010
  • (*s & 0xE0) == 0xC0
    • 0xE011100000)との論理積をとる
    • 結果が 0xC011000000)なら 2 バイト
    • ex. "あ"11000011 1000001011000000
  • (*s & 0xF0) == 0xE0
    • 結果が 0xE011100000)なら 3 バイト
    • ex. "漢"11101000 10000010 1001001011100000
  • (*s & 0xF8) == 0xF0
    • 結果が 0xF011110000)なら 4 バイト
    • ex. 絵文字 "🌍"11110000 10011111 10011000 1000110011110000

4.3 文字を出力

write(1, s, bytes);
  • write を使って判定したバイト数 bytes だけ出力する
  • s は先頭バイトのポインタなので、bytes バイトだけ出力することで 1 文字を正しく表示できる

4.4 ポインタを進める

*p += bytes;
  • *p(呼び出し元のポインタ)を bytes バイトだけ進める
  • これにより、次のループで次の文字の先頭バイトを処理できる

動作確認

int main() {
    setlocale(LC_ALL, "");  // UTF-8 のロケール設定

    my_printf("ゼロ: %d\n", 0);
    my_printf("正の数: %d\n", 1234);
    my_printf("負の数: %d\n", -56789);

    my_printf("英語: %s\n", "Hello, world!");
    my_printf("日本語: %s\n", "こんにちは 世界");

    my_printf("char: %c\n", 'A');
    my_printf("%%\n");
    my_printf("%a\n");

    return 0;
}

コンパイル

gcc my-printf.c -o my-printf

実行

./my-printf

実行結果

ゼロ: 0
正の数: 1234
負の数: -56789
英語: Hello, world!
日本語: こんにちは 世界
char: A
%
?

ちゃんと動きました 🙌

おわりに

printf 関数が実際はどのような実装になっているか知りませんが、かなり複雑なことをしていそうです 🙈

今回は以下を実装しませんでしたが、これらも実装しようとすると大変そうです 😵‍💫

  • 単精度浮動小数点数 %f、倍精度浮動小数点数 %lf などのフォーマット指定子
  • 幅・精度指定(ex. %5d, %.3f

プログラミングの世界への扉を叩いたときの気持ちを思い出して、引き続き精進します ✊

おすすめしたい C 言語の名著 📖

3
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
3
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?