第4章:数値表現の理論 - 2の補数とコンピュータ算術の科学
導入:数を表現するとは
人類は数千年にわたり、数を表現する方法を模索してきました。バビロニアの60進法、マヤの20進法、そして現代のコンピュータで使われる2進法。ft_printfで%dや%uを実装する際、私たちはこの長い歴史の最先端に立っています。
この章では、数値表現の理論的基礎を学び、なぜコンピュータが2の補数表現を採用したのか、そしてその設計が私たちの実装にどのような影響を与えるのかを深く理解します。
---
第1部:位取り記数法の数学
1.1 位取り記数法の原理
現代の数の表記法は「位取り記数法(Positional Notation)」に基づいています。これは紀元前3000年頃のバビロニアで発明され、インドで10進法として完成し、アラビアを経由してヨーロッパに伝わりました。
位取り記数法の一般形:
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
基数 b で表現された n 桁の数 d_{n-1}d_{n-2}...d_1d_0 の値:
N = Σ(i=0 to n-1) d_i × b^i
= d_{n-1} × b^{n-1} + d_{n-2} × b^{n-2} + ... + d_1 × b^1 + d_0 × b^0
例:10進数 "1234" の場合(b = 10)
1234 = 1×10³ + 2×10² + 3×10¹ + 4×10⁰
= 1000 + 200 + 30 + 4
= 1234
例:2進数 "1101" の場合(b = 2)
1101₂ = 1×2³ + 1×2² + 0×2¹ + 1×2⁰
= 8 + 4 + 0 + 1
= 13₁₀
1.2 基数変換の数学
10進数から任意の基数への変換は、連続除算アルゴリズムで行います。これはft_printfにおける整数出力の核心となるアルゴリズムです。
10進数 → b進数への変換アルゴリズム:
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
入力:10進数 N、基数 b
出力:b進数表現の各桁 d_0, d_1, ..., d_{k-1}
手順:
i = 0
while N > 0:
d_i = N mod b // 最下位桁から取得
N = N div b // 商を次の入力に
i = i + 1
例:42 を2進数に変換
42 ÷ 2 = 21 余り 0 → d₀ = 0
21 ÷ 2 = 10 余り 1 → d₁ = 1
10 ÷ 2 = 5 余り 0 → d₂ = 0
5 ÷ 2 = 2 余り 1 → d₃ = 1
2 ÷ 2 = 1 余り 0 → d₄ = 0
1 ÷ 2 = 0 余り 1 → d₅ = 1
結果:42₁₀ = 101010₂
1.3 除算と剰余の性質
C言語における整数除算の挙動を正確に理解することは、整数出力の実装に不可欠です:
/* 除算と剰余の数学的定義 */
// 任意の整数 a と正の整数 b に対して:
// a = (a / b) * b + (a % b)
// C99以降の定義(0方向への切り捨て):
// a / b は商を0方向に切り捨てる
// a % b は a と同じ符号を持つ
#include <stdio.h>
void division_examples(void)
{
// 正の数同士
printf(" 7 / 3 = %d, 7 %% 3 = %d\n", 7 / 3, 7 % 3); // 2, 1
printf(" 7 / -3 = %d, 7 %% -3 = %d\n", 7 / -3, 7 % -3); // -2, 1
// 負の数の除算
printf("-7 / 3 = %d, -7 %% 3 = %d\n", -7 / 3, -7 % 3); // -2, -1
printf("-7 / -3 = %d, -7 %% -3 = %d\n",-7 / -3, -7 % -3); // 2, -1
// 検証: a = (a/b)*b + (a%b)
int a = -7, b = 3;
printf("%d = (%d/%d)*%d + (%d%%%d) = %d*%d + %d = %d\n",
a, a, b, b, a, b, a/b, b, a%b, (a/b)*b + (a%b));
// -7 = (-7/3)*3 + (-7%3) = -2*3 + -1 = -7 ✓
}
この性質は、負の数を処理する際に重要になります:
/* 負の数から桁を取得する */
// -42 から桁を取得
// -42 % 10 = -2 (負の剰余!)
// -(-42 % 10) = 2 (正の桁を得る)
// または絶対値を先に取得
// unsigned int num = (unsigned int)(-(-42)) = 42
// 42 % 10 = 2
---
第2部:2進数表現と2の補数
2.1 符号なし2進数
コンピュータは電気的なON/OFF状態を使って情報を表現するため、2進数が自然な選択です。n ビットで表現できる符号なし整数の範囲:
n ビット符号なし整数:
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
範囲:0 〜 2^n - 1
8ビット: 0 〜 255 (2^8 - 1)
16ビット: 0 〜 65,535 (2^16 - 1)
32ビット: 0 〜 4,294,967,295 (2^32 - 1)
64ビット: 0 〜 18,446,744,073,709,551,615 (2^64 - 1)
ビットパターンと値の対応(8ビット):
0000 0000 → 0
0000 0001 → 1
0111 1111 → 127
1000 0000 → 128
1111 1111 → 255
2.2 符号付き整数の表現方法
負の数を表現するために、歴史的に3つの主要な方法が考案されました:
符号付き整数の表現方法:
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
1. 符号・絶対値表現(Sign-Magnitude)
─────────────────────────────
最上位ビット(MSB)を符号として使用
0 = 正、1 = 負
例(8ビット):
+5 = 0 000 0101
-5 = 1 000 0101
問題点:
- ゼロが2つ存在(+0 = 0000 0000、-0 = 1000 0000)
- 加算器の設計が複雑
- 現代では浮動小数点数で使用
2. 1の補数(One's Complement)
─────────────────────────────
負の数 = 正の数の全ビットを反転
例(8ビット):
+5 = 0000 0101
-5 = 1111 1010
問題点:
- やはりゼロが2つ(+0 = 0000 0000、-0 = 1111 1111)
- キャリーの循環処理が必要
- 初期のコンピュータ(UNIVAC)で使用
3. 2の補数(Two's Complement)
─────────────────────────────
負の数 = 1の補数 + 1
例(8ビット):
+5 = 0000 0101
-5 = 1111 1011
利点:
- ゼロが1つだけ(0000 0000)
- 加算器をそのまま減算に使用可能
- 現代のほぼ全てのコンピュータで採用
2.3 2の補数の数学的基礎
2の補数表現は、モジュラー算術に基づいています。John von Neumannらが1945年のEDVAC報告書で採用を推奨しました。
2の補数の数学的定義:
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
n ビットの整数 x の2の補数表現:
x ≥ 0 の場合:そのまま2進数表現
x < 0 の場合:2^n + x(モジュラー表現)
例:8ビットで -5 を表現
2^8 + (-5) = 256 - 5 = 251
251 = 1111 1011₂
検証:
+5 = 0000 0101
~5 = 1111 1010 (ビット反転、1の補数)
~5+1= 1111 1011 (+1で2の補数)
これは251と同じ!
2の補数の重み付け解釈:
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
n ビットの2の補数表現 b_{n-1}b_{n-2}...b_1b_0 の値:
N = -b_{n-1} × 2^{n-1} + Σ(i=0 to n-2) b_i × 2^i
最上位ビットの重みが負になる!
例:8ビットで 1111 1011 の値
= -1×2^7 + 1×2^6 + 1×2^5 + 1×2^4 + 1×2^3 + 0×2^2 + 1×2^1 + 1×2^0
= -128 + 64 + 32 + 16 + 8 + 0 + 2 + 1
= -128 + 123
= -5 ✓
2.4 2の補数の範囲と非対称性
2の補数表現には重要な非対称性があります。これがINT_MIN問題の根本原因です:
n ビット2の補数の範囲:
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
最小値:-2^{n-1}
最大値:2^{n-1} - 1
8ビット: -128 〜 127
16ビット: -32,768 〜 32,767
32ビット: -2,147,483,648 〜 2,147,483,647
64ビット: -9,223,372,036,854,775,808 〜 9,223,372,036,854,775,807
非対称性の理由:
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
2^n 個のビットパターンを分配:
- 0 が1つのパターンを使用(0000...0000)
- 残り 2^n - 1 個を正負に分配
- 負の数が1つ多くなる
8ビットの全パターン(256個):
0000 0000 → 0 (1つ)
0000 0001 → 1
...
0111 1111 → 127 (正の数:127個)
1000 0000 → -128 (最小値!対応する正の数がない)
1000 0001 → -127
...
1111 1111 → -1 (負の数:128個)
2.5 INT_MIN問題の本質
32ビット整数におけるINT_MIN問題を理解しましょう:
/* INT_MIN問題の詳細 */
#include <limits.h>
#include <stdio.h>
void demonstrate_int_min_problem(void)
{
int min = INT_MIN; // -2,147,483,648
int max = INT_MAX; // 2,147,483,647
printf("INT_MIN = %d\n", min);
printf("INT_MAX = %d\n", max);
// INT_MAXに1を足すとオーバーフロー
// 2,147,483,647 + 1 = -2,147,483,648(環状に回る)
// INT_MINの符号を反転しようとすると...
// -(-2,147,483,648) = 2,147,483,648
// しかし32ビット符号付きでは表現できない!
// 以下は未定義動作!
// int positive = -min; // オーバーフロー
// 正しい処理方法:unsigned型を使用
unsigned int positive = (unsigned int)(-((long long)min));
// または
unsigned int positive2 = (unsigned int)min * -1; // 実装依存
printf("As unsigned: %u\n", positive);
}
---
第3部:コンピュータにおける算術演算
3.1 加算器の設計
2の補数の最大の利点は、加算と減算を同じハードウェアで処理できることです:
半加算器(Half Adder):
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
入力:A, B(各1ビット)
出力:Sum, Carry
真理値表:
A | B | Sum | Carry
──┼───┼─────┼───────
0 | 0 | 0 | 0
0 | 1 | 1 | 0
1 | 0 | 1 | 0
1 | 1 | 0 | 1
論理式:
Sum = A XOR B
Carry = A AND B
全加算器(Full Adder):
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
入力:A, B, Cin(下位からのキャリー)
出力:Sum, Cout
論理式:
Sum = A XOR B XOR Cin
Cout = (A AND B) OR (Cin AND (A XOR B))
リップルキャリー加算器:
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
n個の全加算器を連結
キャリーが順次伝播(遅延の原因)
A₃ B₃ A₂ B₂ A₁ B₁ A₀ B₀
│ │ │ │ │ │ │ │
┌┴─┴┐ ┌┴─┴┐ ┌┴─┴┐ ┌┴─┴┐
←─┤ FA ├←─┤ FA ├←─┤ FA ├←─┤ FA ├←─ 0
└──┬─┘ └──┬─┘ └──┬─┘ └──┬─┘
│ │ │ │
S₃ S₂ S₁ S₀
3.2 減算と2の補数
減算は、引く数の2の補数を足すことで実現できます:
減算の実現:A - B
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
A - B = A + (-B) = A + (NOT B + 1)
ハードウェア実装:
1. Bの各ビットを反転(XORゲートを使用)
2. キャリー入力を1にセット(+1の効果)
3. 通常の加算を実行
例:8ビットで 5 - 3 を計算
A = 5 = 0000 0101
B = 3 = 0000 0011
~B = 1111 1100
~B + 1 = 1111 1101 (-3の2の補数)
0000 0101 (5)
+ 1111 1101 (-3)
──────────
1 0000 0010 (キャリーアウトは無視)
= 2 ✓
3.3 オーバーフローの検出
符号付き整数演算でのオーバーフローは、結果が表現可能な範囲を超えた場合に発生します:
/* オーバーフローの検出 */
// オーバーフローの条件:
// 1. 正 + 正 = 負(正のオーバーフロー)
// 2. 負 + 負 = 正(負のオーバーフロー)
#include <stdbool.h>
bool will_overflow_add(int a, int b)
{
// 正のオーバーフロー:a > 0 && b > INT_MAX - a
// 負のオーバーフロー:a < 0 && b < INT_MIN - a
if (a > 0 && b > 0 && b > INT_MAX - a)
return true;
if (a < 0 && b < 0 && b < INT_MIN - a)
return true;
return false;
}
// ハードウェアでの検出:
// Overflow = Carry_in XOR Carry_out(最上位ビットで)
/* 符号なしでのキャリー/オーバーフロー */
bool will_carry_add_unsigned(unsigned int a, unsigned int b)
{
return b > UINT_MAX - a;
}
3.4 乗算と除算
乗算と除算はより複雑な演算ですが、シフト演算と組み合わせることで効率化できます:
/* 2の冪乗による乗除算の最適化 */
// x * 2^n = x << n
// x / 2^n = x >> n(符号なしの場合)
// 例:x * 10 = x * 8 + x * 2 = (x << 3) + (x << 1)
int multiply_by_10(int x)
{
return (x << 3) + (x << 1);
}
// 10での除算(シフトでは表現できない)
// コンパイラは乗算の逆数を使った最適化を行う
// x / 10 ≈ x * (2^35 / 10) >> 35
---
第4部:C言語における整数型
4.1 整数型の歴史と標準化
C言語の整数型は、様々なアーキテクチャに対応するため、意図的に曖昧に定義されています:
C言語整数型の保証(C99以降):
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
最小ビット幅の保証:
char : 少なくとも 8ビット
short : 少なくとも 16ビット
int : 少なくとも 16ビット
long : 少なくとも 32ビット
long long : 少なくとも 64ビット
サイズの順序関係:
sizeof(char) ≤ sizeof(short) ≤ sizeof(int) ≤ sizeof(long) ≤ sizeof(long long)
実際の一般的なサイズ(LP64モデル、現代の64ビットUnix):
char : 8ビット (1バイト)
short : 16ビット (2バイト)
int : 32ビット (4バイト)
long : 64ビット (8バイト)
long long : 64ビット (8バイト)
Windows 64ビット(LLP64モデル):
long : 32ビット (4バイト) ← 異なる!
4.2 整数昇格(Integer Promotion)
C言語では、演算時に小さな整数型が自動的にintに昇格します:
/* 整数昇格の規則 */
// charやshortは演算前にintに昇格される
char a = 100;
char b = 50;
char c = a + b; // 実際の計算:(int)a + (int)b = 150、その後charに戻す
// 昇格の例
unsigned char uc = 255;
int result = uc + 1; // ucはintに昇格、結果は256
// 驚くべき挙動
unsigned char x = 255;
unsigned char y = 1;
if (x + y > 255) // x+yはintとして計算されるので256、条件は真
printf("Promoted to int!\n");
// va_argでの影響
// char, short は常に int として渡される
// %c の実装では va_arg(args, int) を使用する必要がある
4.3 符号付きと符号なしの変換
符号付きと符号なしの混合演算には注意が必要です:
/* 符号変換の落とし穴 */
#include <stdio.h>
void signed_unsigned_pitfalls(void)
{
int negative = -1;
unsigned int positive = 1;
// 驚くべき結果!
if (negative < positive)
printf("Expected: -1 < 1\n");
else
printf("Surprise: -1 > 1 ???\n"); // これが出力される!
// 理由:比較時にnegativeがunsignedに変換される
// (unsigned int)(-1) = 4294967295 > 1
// 安全な比較方法
if (negative < 0 || (unsigned int)negative < positive)
printf("Correct comparison\n");
// %u で負数を出力すると...
printf("%%u of -1: %u\n", -1); // 4294967295
// ビットパターンは同じ、解釈が異なる
}
4.4 未定義動作と実装定義動作
整数演算には、未定義動作(UB)と実装定義動作(IB)があります:
/* 未定義動作(Undefined Behavior)の例 */
// 1. 符号付き整数のオーバーフロー
int a = INT_MAX;
int b = a + 1; // UB!コンパイラは何でもできる
// 2. ゼロ除算
int c = 42 / 0; // UB!
// 3. シフト量が型のビット幅以上
int d = 1 << 32; // intが32ビットならUB
// 4. 負数の左シフト(C99以前)
int e = -1 << 1; // C99ではUB、C11で定義された
/* 実装定義動作(Implementation-defined Behavior)の例 */
// 1. charが符号付きか符号なしか
char ch = -1; // 符号付きなら-1、符号なしなら255
// 2. 右シフトの符号拡張
int f = -8 >> 1; // 算術シフト(-4)か論理シフト(大きな正の数)か
// 3. 整数型のサイズ
sizeof(long); // 4か8か(プラットフォーム依存)
---
第5部:数値から文字列への変換
5.1 変換アルゴリズムの理論
数値を文字列に変換する問題は、基本的に基数変換です。各桁を順に取り出し、対応する文字に変換します:
数値→文字列変換の数学:
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
入力:非負整数 N、基数 b(通常は10)
出力:各桁を表す文字の列
アルゴリズム:
digits = []
while N > 0:
d = N mod b // 最下位桁
digits.prepend(d) // 先頭に追加(または後で反転)
N = N div b
return digits
重要な観察:
- 剰余演算で最下位桁から順に取得
- 結果は逆順になる → 反転が必要
- N = 0 の特別扱いが必要
5.2 再帰的実装の分析
再帰的アプローチは数学的に自然ですが、トレードオフがあります:
/* 再帰的実装 */
#include <unistd.h>
int print_unsigned_recursive(unsigned int n)
{
int count = 0;
if (n >= 10)
count = print_unsigned_recursive(n / 10);
char digit = '0' + (n % 10);
write(1, &digit, 1);
return count + 1;
}
/*
* スタックの状態(n = 12345 の場合):
*
* call stack | action after return
* ─────────────────────────────────────────
* print(12345) | output '5'
* print(1234) | output '4'
* print(123) | output '3'
* print(12) | output '2'
* print(1) | output '1'
*
* 出力順序:1 → 2 → 3 → 4 → 5 = "12345" ✓
*
* 再帰の深さ:桁数に比例(最大10段、32ビット整数で)
* 各呼び出しでスタックフレーム(約32〜64バイト)を消費
*/
5.3 反復的実装と最適化
配列バッファを使った反復的実装は、より効率的です:
/* 反復的実装(最適化版) */
int print_unsigned_iterative(unsigned int n)
{
char buffer[11]; // 4294967295 = 10桁 + 終端
int pos;
int len;
// 逆順に桁を格納
pos = 10;
buffer[pos] = '\0';
if (n == 0)
{
buffer[--pos] = '0';
}
else
{
while (n > 0)
{
buffer[--pos] = '0' + (n % 10);
n /= 10;
}
}
len = 10 - pos;
write(1, &buffer[pos], len);
return len;
}
/*
* メモリレイアウト(n = 12345 の場合):
*
* buffer[]: [?][?][?][?][?][1][2][3][4][5][\0]
* index: 0 1 2 3 4 5 6 7 8 9 10
* ↑
* pos = 5
*
* 処理の流れ:
* - 12345 % 10 = 5, buffer[9] = '5', n = 1234
* - 1234 % 10 = 4, buffer[8] = '4', n = 123
* - 123 % 10 = 3, buffer[7] = '3', n = 12
* - 12 % 10 = 2, buffer[6] = '2', n = 1
* - 1 % 10 = 1, buffer[5] = '1', n = 0
*
* システムコール:1回のみ(再帰版は5回)
*/
5.4 INT_MIN問題の解決
INT_MINを安全に処理する方法:
/* INT_MIN問題の解決策 */
int print_signed_int(int n)
{
unsigned int num;
int count = 0;
if (n < 0)
{
write(1, "-", 1);
count++;
// 核心:符号反転をunsigned型で行う
// これにより、-(-2147483648) = 2147483648 が正しく表現される
num = (unsigned int)(-(long long)n);
// または単に:
// num = -(unsigned int)n; // 実装定義だが多くの環境で動作
}
else
{
num = (unsigned int)n;
}
count += print_unsigned_iterative(num);
return count;
}
/*
* なぜこれが動作するか:
*
* int n = -2147483648; // 0x80000000
*
* 方法1:long longを経由
* (long long)n = -2147483648LL
* -(long long)n = 2147483648LL
* (unsigned int)(2147483648LL) = 2147483648U // 正しく表現可能
*
* 方法2:直接unsigned変換(多くの環境で動作)
* -(unsigned int)n
* = -(unsigned int)(0x80000000)
* = -2147483648U // これはモジュラー算術で処理される
* = 4294967296U - 2147483648U
* = 2147483648U // 正しい!
*/
---
第6部:フラグと書式指定の実装
6.1 整数出力に関わるフラグ
printfの整数出力では、以下のフラグが使用できます:
整数出力のフラグ:
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
'-' : 左揃え(デフォルトは右揃え)
'+' : 常に符号を表示(正の数に+を付ける)
' ' : 正の数の前にスペース(+フラグと併用で+が優先)
'0' : 0でパディング(-フラグと併用で無効)
'#' : 代替形式(整数では未使用、16進数で0xプレフィックス)
幅(width):
最小フィールド幅を指定
出力がこれより短い場合、パディングで埋める
精度(precision):
最小桁数を指定
出力がこれより短い場合、0で埋める
精度0で値が0の場合、何も出力しない
フラグの優先順位:
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
1. '-' フラグは '0' フラグを無効化
理由:左揃えで0パディングは意味がない
2. 精度指定は '0' フラグを無効化
理由:精度による0パディングと幅による0パディングは競合
3. '+' フラグは ' ' フラグを無効化
理由:両方指定されたら+が優先
6.2 フラグ処理の実装
/* フラグ構造体と正規化 */
typedef struct s_flags
{
int minus; // '-' フラグ
int plus; // '+' フラグ
int space; // ' ' フラグ
int zero; // '0' フラグ
int width; // 最小フィールド幅
int precision; // 精度(-1で未指定)
} t_flags;
void normalize_flags(t_flags *flags)
{
// '-' は '0' を無効化
if (flags->minus)
flags->zero = 0;
// 精度指定は '0' を無効化
if (flags->precision >= 0)
flags->zero = 0;
// '+' は ' ' を無効化
if (flags->plus)
flags->space = 0;
}
6.3 出力長の計算
正確な出力長を事前に計算することは、正しいパディングに不可欠です:
/* 出力長の計算 */
typedef struct s_int_format
{
unsigned int num; // 絶対値
char sign; // 符号文字('\0', '+', '-', ' ')
int num_digits; // 数値の桁数
int zeros; // 精度による0パディング
int total_len; // 全体の長さ
} t_int_format;
int count_digits(unsigned int n)
{
int count = 0;
if (n == 0)
return 1;
while (n > 0)
{
count++;
n /= 10;
}
return count;
}
void calculate_format(t_int_format *fmt, int n, t_flags flags)
{
// 符号の決定
if (n < 0)
{
fmt->sign = '-';
fmt->num = (unsigned int)(-(long long)n);
}
else if (flags.plus)
{
fmt->sign = '+';
fmt->num = (unsigned int)n;
}
else if (flags.space)
{
fmt->sign = ' ';
fmt->num = (unsigned int)n;
}
else
{
fmt->sign = '\0';
fmt->num = (unsigned int)n;
}
// 桁数の計算
fmt->num_digits = count_digits(fmt->num);
// 精度0で値が0の特殊ケース
if (flags.precision == 0 && fmt->num == 0)
fmt->num_digits = 0;
// 精度による0パディング
if (flags.precision > fmt->num_digits)
fmt->zeros = flags.precision - fmt->num_digits;
else
fmt->zeros = 0;
// 全体の長さ
fmt->total_len = (fmt->sign ? 1 : 0) + fmt->zeros + fmt->num_digits;
}
6.4 完全な整数出力関数
/* 完全な整数出力実装 */
int print_integer(int n, t_flags flags)
{
t_int_format fmt;
int count = 0;
int padding;
normalize_flags(&flags);
calculate_format(&fmt, n, flags);
// パディング幅の計算
padding = flags.width - fmt.total_len;
if (padding < 0)
padding = 0;
// 右揃えの場合:パディングを先に出力
if (!flags.minus && padding > 0)
{
if (flags.zero)
{
// 0パディング:符号 → 0 → 数値
if (fmt.sign)
{
write(1, &fmt.sign, 1);
count++;
}
count += print_chars('0', padding);
}
else
{
// スペースパディング:スペース → 符号 → 数値
count += print_chars(' ', padding);
if (fmt.sign)
{
write(1, &fmt.sign, 1);
count++;
}
}
}
else if (fmt.sign)
{
// 左揃えまたはパディングなし:符号を先に
write(1, &fmt.sign, 1);
count++;
}
// 精度による0パディング
count += print_chars('0', fmt.zeros);
// 数値本体の出力
if (fmt.num_digits > 0)
count += print_unsigned_iterative(fmt.num);
// 左揃えの場合:後ろにスペースパディング
if (flags.minus && padding > 0)
count += print_chars(' ', padding);
return count;
}
int print_chars(char c, int count)
{
int i;
for (i = 0; i < count; i++)
write(1, &c, 1);
return count;
}
---
第7部:%d、%i、%u の完全実装
7.1 %d と %i の実装
%dと%iはprintfでは全く同じ動作をします(scanfでは異なる):
/* %d と %i の処理 */
int handle_d_i(va_list args, t_flags flags)
{
int value;
value = va_arg(args, int);
return print_integer(value, flags);
}
7.2 %u の実装
%uは符号なし整数を出力します:
/* %u の処理 */
int print_unsigned_with_flags(unsigned int n, t_flags flags)
{
int num_digits;
int zeros;
int total_len;
int padding;
int count = 0;
normalize_flags(&flags);
// %u では符号フラグは無視
flags.plus = 0;
flags.space = 0;
// 桁数計算
num_digits = count_digits(n);
if (flags.precision == 0 && n == 0)
num_digits = 0;
// 精度による0
zeros = (flags.precision > num_digits) ?
flags.precision - num_digits : 0;
total_len = zeros + num_digits;
padding = flags.width - total_len;
if (padding < 0)
padding = 0;
// 右揃え:パディング → 0 → 数値
if (!flags.minus && padding > 0)
count += print_chars(flags.zero ? '0' : ' ', padding);
count += print_chars('0', zeros);
if (num_digits > 0)
count += print_unsigned_iterative(n);
// 左揃え:数値 → パディング
if (flags.minus && padding > 0)
count += print_chars(' ', padding);
return count;
}
int handle_u(va_list args, t_flags flags)
{
unsigned int value;
value = va_arg(args, unsigned int);
return print_unsigned_with_flags(value, flags);
}
---
第8部:テストと検証
8.1 境界値テスト
/* 境界値のテスト */
#include <limits.h>
#include <stdio.h>
void test_boundary_values(void)
{
printf("=== Boundary Value Tests ===\n");
// INT_MIN
printf("INT_MIN: ");
int ret1 = ft_printf("%d", INT_MIN);
printf(" [%d]\n", ret1);
printf("Expected: ");
int ret2 = printf("%d", INT_MIN);
printf(" [%d]\n", ret2);
// INT_MAX
printf("INT_MAX: ");
ret1 = ft_printf("%d", INT_MAX);
printf(" [%d]\n", ret1);
printf("Expected: ");
ret2 = printf("%d", INT_MAX);
printf(" [%d]\n", ret2);
// UINT_MAX
printf("UINT_MAX: ");
ret1 = ft_printf("%u", UINT_MAX);
printf(" [%d]\n", ret1);
printf("Expected: ");
ret2 = printf("%u", UINT_MAX);
printf(" [%d]\n", ret2);
// ゼロ
printf("Zero: ");
ret1 = ft_printf("%d", 0);
printf(" [%d]\n", ret1);
}
8.2 フラグ組み合わせテスト
/* フラグの組み合わせテスト */
void test_flag_combinations(void)
{
int n = 42;
printf("=== Flag Combinations ===\n");
// 基本フラグ
printf("%%d: [%d]\n", n);
printf("%%+d: [%+d]\n", n);
printf("%% d: [% d]\n", n);
printf("%%05d: [%05d]\n", n);
printf("%%-5d: [%-5d]\n", n);
// 組み合わせ
printf("%%+05d: [%+05d]\n", n);
printf("%%-+5d: [%-+5d]\n", n);
printf("%% 05d: [% 05d]\n", n);
// 負の数
printf("=== Negative Numbers ===\n");
n = -42;
printf("%%d: [%d]\n", n);
printf("%%+d: [%+d]\n", n);
printf("%% d: [% d]\n", n);
printf("%%05d: [%05d]\n", n);
printf("%%-5d: [%-5d]\n", n);
printf("%%+05d: [%+05d]\n", n);
}
8.3 幅と精度のテスト
/* 幅と精度のテスト */
void test_width_precision(void)
{
printf("=== Width and Precision ===\n");
// 幅のみ
printf("%%5d: [%5d]\n", 42);
printf("%%5d: [%5d]\n", -42);
printf("%%5d: [%5d]\n", 12345);
printf("%%5d: [%5d]\n", 123456);
// 精度のみ
printf("%%.5d: [%.5d]\n", 42);
printf("%%.5d: [%.5d]\n", -42);
printf("%%.0d: [%.0d]\n", 0); // 空!
printf("%%.0d: [%.0d]\n", 42);
// 幅と精度の組み合わせ
printf("%%10.5d: [%10.5d]\n", 42);
printf("%%10.5d: [%10.5d]\n", -42);
printf("%%5.10d: [%5.10d]\n", 42);
printf("%%5.0d: [%5.0d]\n", 0); // 5スペース
}
8.4 %u の特殊ケース
/* %u の特殊ケーステスト */
void test_unsigned_special(void)
{
printf("=== Unsigned Special Cases ===\n");
// 負数を%uで出力
printf("%%u of -1: %u\n", (unsigned int)-1); // 4294967295
printf("%%u of -42: %u\n", (unsigned int)-42);
// 大きな値
printf("%%u: [%u]\n", 4294967295U);
printf("%%10u: [%10u]\n", 4294967295U);
printf("%%.15u: [%.15u]\n", 4294967295U);
printf("%%20.15u: [%20.15u]\n", 4294967295U);
}
---
まとめ:数値表現の統一的理解
この章では、以下の知識を統合しました:
歴史的発展:
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
紀元前3000年 バビロニア60進法 - 位取り記数法の原点
紀元後500年頃 インドの10進法 - 0の発明と完成
1945年 EDVAC報告書 - 2の補数表現の採用
1978年 C言語K&R版 - 整数型の標準化
数学的基礎:
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
- 位取り記数法の一般形
- 基数変換アルゴリズム(連続除算)
- 2の補数の数学的定義
- モジュラー算術との関係
ハードウェア理解:
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
- 加算器の設計(半加算器、全加算器)
- 2の補数による減算の実現
- オーバーフロー検出の仕組み
C言語の仕様:
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
- 整数型のサイズ保証
- 整数昇格の規則
- 未定義動作と実装定義動作
- 符号付き/符号なしの変換
実装技法:
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
- 再帰的vs反復的アプローチ
- INT_MIN問題の解決
- フラグと精度の処理
- 効率的な出力長計算
次章では、16進数とポインタ表現(%x、%X、%p)について学びます。メモリアドレスの概念、16進表記の歴史、そしてポインタ出力の実装技法を探求します。