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

## Hello, world! ✋

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

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

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

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

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

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

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

## 成果物 🏁

https://github.com/kamata-bug-factory/my-printf

## ポイント 💡

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

```c
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`

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

```c
va_list args;
```

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

#### 1.2 `va_start`

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

```c
va_start(args, format);
```

- `args` を初期化する
- `format`（最初の固定引数） の次の引数から取得できる

#### 1.3 `va_arg`

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

```c
int value = va_arg(args, int);
```

- 呼び出すたびに次の引数を取得する
- 型は `int`, `double`, `char*` など 正しく指定する必要がある

#### 1.4 `va_end`

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

```c
va_end(args);
```

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

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

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

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

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

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

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

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

```c
switch (*format) {
```

#### 2.3 `%d`（整数）の処理

整数型 `int` を `va_arg` で取得し、`my_putint` に渡します。

```c
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` に渡します。

```c
case 's':
    my_puts(va_arg(args, char*));
    break;
```

```c
void my_puts(const char *s) {
    write(1, s, strlen(s));
}
```

- `va_arg(args, char*)` によって `args` から `char *` 型（文字列の先頭アドレス）を取得する
- `my_puts` は `write(1, s, strlen(s));` で文字列全体を出力する

#### 2.5 `%c`（文字）の処理

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

```c
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);` で `%` をそのまま出力します。

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

#### 2.7 `default` の処理

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

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

#### ※ `write` の使い方 🧑‍🏫

```c
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 バイト確保します。

```c
char buf[12];
```

#### 3.2 負の数の扱い

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

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

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

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

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

```c
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", "-"]`

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

#### 3.5 バッファの反転

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

```c
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]` (`8` ↔ `5`) |
| `["-", "5", "7", "6", "8", "9"]` | `buf[2]` ↔ `buf[3]` (`7` ↔ `6`) |
| `["-", "5", "6", "7", "8", "9"]` | 完了 |

#### 3.6 文字列終端を追加

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

```c
buf[i] = '\0';
```

#### 3.7 `write` で出力

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

```c
write(1, buf, i);
```

### 4. UTF-8 を扱う 🇯🇵

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

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

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

```c
void my_put_utf8_char(const char **p) {
    const unsigned char *s = (const unsigned char *)(*p);
```

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

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

```c
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`
  - `0x80`（`10000000`）との論理積をとる
  - 結果が `0` なら ASCII（1 バイト）
  - ex. `'A'`（`01000001` → `0`）
- `(*s & 0xE0) == 0xC0`
  - `0xE0`（`11100000`）との論理積をとる
  - 結果が `0xC0`（`11000000`）なら 2 バイト
  - ex. `"あ"`（`11000011 10000010` → `11000000`）
- `(*s & 0xF0) == 0xE0`
  - 結果が `0xE0`（`11100000`）なら 3 バイト
  - ex. `"漢"`（`11101000 10000010 10010010` → `11100000`）
- `(*s & 0xF8) == 0xF0`
  - 結果が `0xF0`（`11110000`）なら 4 バイト
  - ex. 絵文字 `"🌍"`（`11110000 10011111 10011000 10001100` → `11110000`）

#### 4.3 文字を出力

```c
write(1, s, bytes);
```

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

#### 4.4 ポインタを進める

```c
*p += bytes;
```

- `*p`（呼び出し元のポインタ）を `bytes` バイトだけ進める
- これにより、次のループで次の文字の先頭バイトを処理できる

## 動作確認

```c
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;
}
```

### コンパイル

```bash
gcc my-printf.c -o my-printf
```

### 実行

```bash
./my-printf
```

### 実行結果

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

ちゃんと動きました 🙌

## おわりに

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

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

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

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

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

- C で計算! 基礎からはじめるプログラミング
  https://www.amazon.co.jp/C%E3%81%A7%E8%A8%88%E7%AE%97-%E2%80%95%E5%9F%BA%E7%A4%8E%E3%81%8B%E3%82%89%E3%81%AF%E3%81%98%E3%82%81%E3%82%8B%E3%83%97%E3%83%AD%E3%82%B0%E3%83%A9%E3%83%9F%E3%83%B3%E3%82%B0-%E9%A6%96%E8%97%A4-%E5%81%A5%E4%B8%80/dp/4563015903

- 苦しんで覚える C 言語
  https://9cguide.appspot.com/