第5章:メモリアドレッシングと16進表記 - コンピュータアーキテクチャの基礎
導入:なぜ16進数なのか
コンピュータサイエンスにおいて16進数(hexadecimal)が広く使われる理由は、単なる慣習ではありません。2進数との数学的な関係性、人間の認知能力との相性、そしてハードウェア設計との親和性が、16進数を「コンピュータの言語」として確立させました。
ft_printfで%x、%X、%pを実装する際、私たちはコンピュータアーキテクチャの根幹に触れることになります。この章では、メモリアドレッシングの理論から始めて、なぜポインタを16進数で表示するのかを深く理解します。
---
第1部:von Neumannアーキテクチャとメモリ
1.1 ストアドプログラム方式の誕生
1945年、John von Neumannは「EDVACに関する報告書の第一草稿」で、現代コンピュータの基礎となるアーキテクチャを提案しました。その核心は「プログラムとデータを同じメモリに格納する」というアイデアでした。
von Neumannアーキテクチャの構成要素:
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
1. 中央処理装置(CPU)
- 制御装置(Control Unit)
- 算術論理演算装置(ALU)
- レジスタ群
2. 主記憶装置(Main Memory)
- プログラム(命令)を格納
- データを格納
- アドレスによってアクセス
3. 入出力装置(I/O)
4. バス(Bus)
- データバス
- アドレスバス
- 制御バス
メモリモデル:
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
アドレス 内容
─────────────────
0x0000 │ 命令1 │
0x0004 │ 命令2 │
0x0008 │ 命令3 │
... │ ... │
0x1000 │ データ1 │
0x1004 │ データ2 │
... │ ... │
プログラムとデータが同じアドレス空間に存在
→ これがポインタ概念の基礎
1.2 メモリアドレスの概念
メモリは「番地付きの箱の並び」として抽象化できます。各箱(セル)には一意のアドレスが割り当てられています:
メモリの抽象モデル:
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
物理的視点:
連続した記憶素子の配列
論理的視点:
アドレス → 内容 の写像関数
M: Address → Value
バイトアドレッシング:
現代のほとんどのアーキテクチャでは、
最小のアドレス可能単位は1バイト(8ビット)
アドレス空間のサイズ:
n ビットのアドレスバス → 2^n 個のアドレス
8ビットCPU: 2^8 = 256 バイト = 256 B
16ビットCPU: 2^16 = 65,536 バイト = 64 KB
32ビットCPU: 2^32 = 4,294,967,296 バイト = 4 GB
64ビットCPU: 2^64 = 18,446,744,073,709,551,616 バイト = 16 EB
1.3 仮想メモリとアドレス変換
現代のオペレーティングシステムは仮想メモリを使用し、各プロセスに独立したアドレス空間を提供します:
仮想メモリの概念:
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
プロセスA プロセスB 物理メモリ
┌──────────┐ ┌──────────┐ ┌──────────┐
│ 0x0000 │ │ 0x0000 │ │ 物理 0x0 │
│ ... │ │ ... │ │ ... │
│ 0x1000 ──┼───┼──────────┼───→ │ 0x5000 │
│ ... │ │ 0x1000 ──┼───→ │ 0x8000 │
└──────────┘ └──────────┘ └──────────┘
仮想アドレス ──[MMU]──→ 物理アドレス
利点:
- プロセス間のメモリ保護
- アドレス空間の独立性
- メモリの効率的な利用(ページング)
- 物理メモリ以上のアドレス空間
printfで表示されるアドレス:
- 通常は仮想アドレス
- 同じプログラムでも実行ごとに異なる場合がある
(ASLR: Address Space Layout Randomization)
---
第2部:16進数表記の数学と歴史
2.1 16進数と2進数の関係
16進数が選ばれた理由は、2進数との完璧な対応関係にあります:
2進数と16進数の対応:
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
16 = 2^4
つまり、16進数1桁 = 2進数4桁(1ニブル)
2進数 16進数
─────────────
0000 0
0001 1
0010 2
0011 3
0100 4
0101 5
0110 6
0111 7
1000 8
1001 9
1010 A (10)
1011 B (11)
1100 C (12)
1101 D (13)
1110 E (14)
1111 F (15)
例:32ビット値の変換
2進数: 1101 1110 1010 1101 1011 1110 1110 1111
16進数: D E A D B E E F
→ 0xDEADBEEF
この対応により:
- 2進数→16進数:4桁ずつグループ化
- 16進数→2進数:各桁を4ビットに展開
いずれも機械的に実行可能
2.2 なぜ8進数ではないのか
歴史的には8進数(octal)も使われていました:
8進数 vs 16進数:
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
8進数の利点(歴史的):
- 8 = 2^3、3ビットに対応
- 初期のコンピュータ(PDP-8など)との親和性
- UNIXのファイルパーミッション(chmod 755)
16進数の利点(現代的):
- 8ビット = 16進2桁(きれいに対応)
- 32ビット = 16進8桁
- 64ビット = 16進16桁
8進数だと:
- 8ビット = 2.67桁(きれいに割り切れない!)
- 32ビット = 10.67桁
- バイト境界と一致しない
バイト(8ビット)が標準単位となった現代では、
16進数がより自然な選択。
C言語での表記:
- 8進数: 0755(先頭に0)
- 16進数: 0xFF(先頭に0x)
2.3 16進数表記の歴史
16進数表記の発展:
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
1950年代以前:
- 様々な表記法が混在
- 一部のシステムでは0-9, u, v, w, x, y, z
1960年代:
- IBM System/360(1964年)が16進数を標準採用
- A-Fの表記が定着
表記の選択:
- なぜA-Fか?
→ 0-9の次のアルファベットを自然に使用
→ キーボードで入力しやすい
→ 印刷時に判読しやすい
大文字 vs 小文字:
- 歴史的に大文字が先(EBCDIC、初期ASCII)
- Unixの影響で小文字も普及
- C言語:どちらも有効(0xFFと0xff)
- printf:%Xで大文字、%xで小文字
---
第3部:バイト順序(エンディアン)
3.1 エンディアンの起源
「エンディアン」という用語は、ジョナサン・スウィフトの「ガリヴァー旅行記」に由来します。Danny Cohenが1980年の論文で、バイト順序の論争をこの小説の「卵の割り方論争」になぞらえました。
エンディアンの定義:
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
32ビット値 0x12345678 をメモリに格納する場合:
リトルエンディアン(Little-Endian):
下位バイトを低位アドレスに格納
アドレス: 0x00 0x01 0x02 0x03
内容: 0x78 0x56 0x34 0x12
↑最下位 ↑最上位
ビッグエンディアン(Big-Endian):
上位バイトを低位アドレスに格納
アドレス: 0x00 0x01 0x02 0x03
内容: 0x12 0x34 0x56 0x78
↑最上位 ↑最下位
主要アーキテクチャ:
リトルエンディアン: x86, x64, ARM(デフォルト)
ビッグエンディアン: SPARC, PowerPC(古いMac), ネットワーク順序
3.2 エンディアンとポインタ表示
/* エンディアンの確認と影響 */
#include <stdio.h>
#include <stdint.h>
void check_endianness(void)
{
uint32_t value = 0x12345678;
uint8_t *bytes = (uint8_t *)&value;
printf("Value: 0x%08X\n", value);
printf("Memory layout:\n");
printf(" Address+0: 0x%02X\n", bytes[0]);
printf(" Address+1: 0x%02X\n", bytes[1]);
printf(" Address+2: 0x%02X\n", bytes[2]);
printf(" Address+3: 0x%02X\n", bytes[3]);
if (bytes[0] == 0x78)
printf("Little-Endian system\n");
else
printf("Big-Endian system\n");
}
/*
* ポインタ表示への影響:
*
* ポインタ値(アドレス)自体は単一の整数値として扱われるため、
* %p での表示にはエンディアンは影響しない。
*
* しかし、メモリダンプやデバッグでは
* エンディアンを意識する必要がある。
*/
---
第4部:ポインタの本質
4.1 ポインタとは何か
C言語におけるポインタは、メモリアドレスを格納する変数です。しかし、その本質はより深いものです:
ポインタの多層的理解:
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
レベル1:実装的理解
ポインタ = メモリアドレスを格納する変数
レベル2:型理論的理解
ポインタ型 T* = 「T型の値が格納されているメモリ位置への参照」
レベル3:抽象的理解
ポインタ = 間接参照(indirection)の具現化
「何かを指し示す」という概念のメモリ上での実現
C言語でのポインタ:
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
int x = 42;
int *ptr = &x;
メモリ状態:
アドレス 変数名 値
0x1000 x 42
0x2000 ptr 0x1000
ptr の値は 0x1000(xのアドレス)
*ptr は 42(ptrが指す先の値)
ポインタのサイズ:
- 32ビットシステム: 4バイト
- 64ビットシステム: 8バイト
- ポインタが指す型のサイズとは無関係
4.2 ポインタ型とポインタ算術
/* ポインタ型の重要性 */
#include <stdio.h>
void pointer_arithmetic(void)
{
int arr_int[] = {1, 2, 3, 4, 5};
char arr_char[] = {'a', 'b', 'c', 'd', 'e'};
int *pi = arr_int;
char *pc = arr_char;
printf("sizeof(int) = %zu\n", sizeof(int)); // 4
printf("sizeof(char) = %zu\n", sizeof(char)); // 1
printf("int pointer:\n");
printf(" pi = %p\n", (void *)pi);
printf(" pi + 1 = %p\n", (void *)(pi + 1));
printf(" 差: %td バイト\n", (char *)(pi + 1) - (char *)pi);
// 差は sizeof(int) = 4 バイト
printf("char pointer:\n");
printf(" pc = %p\n", (void *)pc);
printf(" pc + 1 = %p\n", (void *)(pc + 1));
printf(" 差: %td バイト\n", (char *)(pc + 1) - (char *)pc);
// 差は sizeof(char) = 1 バイト
}
/*
* ポインタ算術の規則:
*
* ptr + n = ptr + (n * sizeof(*ptr))
*
* これにより、配列の要素アクセスが
* ポインタ算術で自然に表現できる:
* arr[i] は *(arr + i) と等価
*/
4.3 NULLポインタの意味
NULLポインタの歴史と意味:
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
歴史的背景:
- 1960年代、Tony Hoareが「null参照」を発明
- 後に彼自身が「10億ドルの間違い」と呼んだ
C言語でのNULL:
- <stddef.h> で定義
- 通常は ((void *)0) または 0
- 「何も指していない」ことを示す特別な値
アドレス0の意味:
- ほとんどのシステムで、アドレス0は
ユーザープログラムからアクセス不可
- NULLポインタのデリファレンスは
通常セグメンテーション違反
printfでのNULL表示:
- 実装依存
- "(nil)" または "0x0" または "(null)"
- ft_printfでは評価基準に合わせる
---
第5部:ft_printfにおける16進数出力
5.1 基数変換アルゴリズム
16進数への変換は、第4章で学んだ基数変換の応用です:
/* 16進数変換の実装 */
/*
* 方法1:除算による変換
*
* N を16で繰り返し割り、余りを取得
* 余りは最下位桁から順に得られる
*/
void convert_to_hex_division(unsigned int n)
{
const char *digits = "0123456789abcdef";
char buffer[9]; // 32ビット = 最大8桁 + 終端
int i = 0;
if (n == 0)
{
buffer[i++] = '0';
}
else
{
while (n > 0)
{
buffer[i++] = digits[n % 16];
n /= 16;
}
}
// bufferには逆順で格納されている
// 出力時に反転が必要
}
/*
* 方法2:ビット演算による変換
*
* 16 = 2^4 なので、4ビットずつ処理
* 除算より効率的(多くのCPUで)
*/
void convert_to_hex_bitwise(unsigned int n)
{
const char *digits = "0123456789abcdef";
char buffer[9];
int i = 0;
if (n == 0)
{
buffer[i++] = '0';
}
else
{
while (n > 0)
{
buffer[i++] = digits[n & 0xF]; // 下位4ビット
n >>= 4; // 4ビット右シフト
}
}
}
/*
* なぜビット演算が効率的か:
*
* n % 16 → 除算命令(多くのCPUで遅い)
* n & 0xF → AND命令(1サイクル)
*
* n / 16 → 除算命令
* n >> 4 → シフト命令(1サイクル)
*
* ただし、現代のコンパイラは2の冪乗での除算を
* 自動的にシフト演算に最適化することが多い
*/
5.2 %x と %X の実装
/* 完全な16進数出力実装 */
static int get_hex_len(unsigned int n)
{
int len;
if (n == 0)
return 1;
len = 0;
while (n > 0)
{
len++;
n >>= 4;
}
return len;
}
static int output_hex_digits(unsigned int n, int use_upper)
{
const char *base;
char buffer[9];
int i;
int len;
base = use_upper ? "0123456789ABCDEF" : "0123456789abcdef";
if (n == 0)
return write(1, "0", 1);
i = 0;
while (n > 0)
{
buffer[i++] = base[n & 0xF];
n >>= 4;
}
len = i;
while (--i >= 0)
write(1, &buffer[i], 1);
return len;
}
int print_hex(unsigned int n, t_flags *flags, char specifier)
{
int count;
int hex_len;
int prefix_len;
int total_len;
int padding;
int use_upper;
count = 0;
use_upper = (specifier == 'X');
/* 16進数の桁数を計算 */
hex_len = get_hex_len(n);
/* 精度0で値0の特殊ケース */
if (flags->precision == 0 && n == 0)
hex_len = 0;
/* #フラグによるプレフィックス */
prefix_len = 0;
if ((flags->hash) && n != 0)
prefix_len = 2; /* "0x" or "0X" */
/* 精度による最小桁数 */
if (flags->precision > hex_len)
hex_len = flags->precision;
total_len = prefix_len + hex_len;
/* パディング幅 */
padding = 0;
if (flags->width > total_len)
padding = flags->width - total_len;
/* 出力処理 */
if (!flags->minus)
{
if (flags->zero && flags->precision < 0)
{
/* 0パディング:プレフィックス → 0 → 数値 */
if (prefix_len)
count += write(1, use_upper ? "0X" : "0x", 2);
while (padding-- > 0)
count += write(1, "0", 1);
}
else
{
/* スペースパディング:スペース → プレフィックス → 数値 */
while (padding-- > 0)
count += write(1, " ", 1);
if (prefix_len)
count += write(1, use_upper ? "0X" : "0x", 2);
}
}
else
{
/* 左揃え */
if (prefix_len)
count += write(1, use_upper ? "0X" : "0x", 2);
}
/* 精度による0パディング */
{
int actual_len = get_hex_len(n);
int zeros = flags->precision - actual_len;
while (zeros-- > 0)
count += write(1, "0", 1);
}
/* 数値本体 */
if (hex_len > 0 && !(flags->precision == 0 && n == 0))
count += output_hex_digits(n, use_upper);
/* 左揃えの後パディング */
if (flags->minus)
{
while (padding-- > 0)
count += write(1, " ", 1);
}
return count;
}
5.3 #フラグの詳細
#フラグ(代替形式)の仕様:
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
%#x : 0以外の値に "0x" プレフィックス
%#X : 0以外の値に "0X" プレフィックス
重要な仕様:
1. 値が0の場合、プレフィックスは付かない
printf("%#x", 0); → "0"("0x0"ではない)
2. プレフィックスは幅に含まれる
printf("%#8x", 0x2a); → " 0x2a"(8文字)
3. 0パディングとの相互作用
printf("%#010x", 0x2a); → "0x0000002a"
プレフィックスの後に0が続く
4. 精度との相互作用
printf("%#.6x", 0x2a); → "0x00002a"
プレフィックスの後に精度分の0
---
第6部:ポインタ出力(%p)の実装
6.1 ポインタ型の安全な変換
/* ポインタから整数への変換 */
#include <stdint.h>
/*
* uintptr_t について:
*
* - <stdint.h> で定義
* - ポインタを格納できる符号なし整数型
* - ポインタ→uintptr_t→ポインタ の変換が保証される
*
* なぜunsigned longではないのか:
* - Windows 64ビット(LLP64)では
* sizeof(long) = 4、sizeof(void*) = 8
* - uintptr_tは常にポインタと同サイズ
*/
int print_pointer(void *ptr)
{
uintptr_t addr;
const char *base = "0123456789abcdef";
char buffer[17]; /* 64ビット: 最大16桁 + 終端 */
int i;
int count;
count = 0;
addr = (uintptr_t)ptr;
/* "0x" プレフィックス(常に付く) */
count += write(1, "0x", 2);
/* アドレスが0の場合 */
if (addr == 0)
{
count += write(1, "0", 1);
return count;
}
/* 16進数に変換 */
i = 0;
while (addr > 0)
{
buffer[i++] = base[addr & 0xF];
addr >>= 4;
}
/* 逆順で出力 */
while (--i >= 0)
count += write(1, &buffer[i], 1);
return count;
}
6.2 NULLポインタの処理
/* NULLポインタの表示 */
/*
* プラットフォーム間の違い:
*
* macOS (Apple clang):
* printf("%p", NULL); → "0x0"
*
* Linux (glibc):
* printf("%p", NULL); → "(nil)"
*
* 42の評価基準を確認し、適切な実装を選択
*/
/* macOS互換バージョン */
int print_pointer_macos(void *ptr)
{
/* NULLも通常のアドレスとして扱う */
return print_pointer(ptr); /* "0x0" を出力 */
}
/* Linux glibc互換バージョン */
int print_pointer_linux(void *ptr, t_flags *flags)
{
int count;
int padding;
const char *nil_str = "(nil)";
count = 0;
if (ptr == NULL)
{
/* "(nil)" を出力 */
padding = flags->width - 5;
if (!flags->minus)
{
while (padding-- > 0)
count += write(1, " ", 1);
}
count += write(1, nil_str, 5);
if (flags->minus)
{
while (padding-- > 0)
count += write(1, " ", 1);
}
return count;
}
return print_pointer(ptr);
}
/* 幅指定対応の完全版 */
int print_pointer_full(void *ptr, t_flags *flags)
{
uintptr_t addr;
int addr_len;
int total_len;
int padding;
int count;
count = 0;
addr = (uintptr_t)ptr;
/* アドレスの桁数を計算 */
if (addr == 0)
addr_len = 1;
else
{
addr_len = 0;
uintptr_t tmp = addr;
while (tmp > 0)
{
addr_len++;
tmp >>= 4;
}
}
/* "0x" + 桁数 */
total_len = 2 + addr_len;
/* パディング */
padding = flags->width - total_len;
if (padding < 0)
padding = 0;
/* 右揃え */
if (!flags->minus)
{
while (padding-- > 0)
count += write(1, " ", 1);
}
/* アドレス出力 */
count += print_pointer(ptr);
/* 左揃え */
if (flags->minus)
{
while (padding-- > 0)
count += write(1, " ", 1);
}
return count;
}
6.3 32ビットと64ビットの互換性
/* プラットフォーム非依存の実装 */
#include <stdint.h>
#include <limits.h>
/*
* ポインタサイズの確認
*/
void check_pointer_size(void)
{
printf("sizeof(void*) = %zu bytes\n", sizeof(void *));
printf("sizeof(uintptr_t) = %zu bytes\n", sizeof(uintptr_t));
#if UINTPTR_MAX == 0xFFFFFFFF
printf("32-bit pointer\n");
printf("Max hex digits: 8\n");
#elif UINTPTR_MAX == 0xFFFFFFFFFFFFFFFF
printf("64-bit pointer\n");
printf("Max hex digits: 16\n");
#endif
}
/*
* バッファサイズの安全な定義
*/
#if UINTPTR_MAX == 0xFFFFFFFF
#define PTR_HEX_DIGITS 8
#else
#define PTR_HEX_DIGITS 16
#endif
int print_pointer_safe(void *ptr)
{
char buffer[PTR_HEX_DIGITS + 1];
/* ... 以下実装 ... */
}
---
第7部:パフォーマンスと最適化
7.1 システムコールのコスト
/* 出力効率の比較 */
/*
* 非効率な実装:1文字ずつwrite
*/
int print_hex_slow(unsigned int n)
{
const char *base = "0123456789abcdef";
int count = 0;
if (n >= 16)
count = print_hex_slow(n >> 4);
char c = base[n & 0xF];
write(1, &c, 1); /* 各桁でシステムコール */
return count + 1;
}
/*
* 効率的な実装:バッファリング
*/
int print_hex_fast(unsigned int n)
{
const char *base = "0123456789abcdef";
char buffer[9];
int i;
if (n == 0)
return write(1, "0", 1);
i = 8;
buffer[i] = '\0';
while (n > 0)
{
buffer[--i] = base[n & 0xF];
n >>= 4;
}
return write(1, &buffer[i], 8 - i); /* 1回のシステムコール */
}
/*
* システムコールのオーバーヘッド:
*
* write() は:
* 1. ユーザーモード → カーネルモード遷移
* 2. カーネル内でのバッファ処理
* 3. カーネルモード → ユーザーモード復帰
*
* これは数百〜数千CPUサイクルを消費
*
* 8桁の16進数出力:
* - 1文字ずつ:8回のシステムコール
* - バッファリング:1回のシステムコール
* → 約8倍の効率向上
*/
7.2 ビット演算の最適化
/* 桁数計算の最適化 */
/* 方法1:ループ */
int hex_len_loop(unsigned int n)
{
int len = 0;
if (n == 0) return 1;
while (n > 0)
{
len++;
n >>= 4;
}
return len;
}
/* 方法2:二分探索(32ビット専用) */
int hex_len_binary(unsigned int n)
{
if (n == 0) return 1;
if (n < 0x10) return 1;
if (n < 0x100) return 2;
if (n < 0x1000) return 3;
if (n < 0x10000) return 4;
if (n < 0x100000) return 5;
if (n < 0x1000000) return 6;
if (n < 0x10000000) return 7;
return 8;
}
/* 方法3:CLZ(Count Leading Zeros)命令を使用 */
#ifdef __GNUC__
int hex_len_clz(unsigned int n)
{
if (n == 0) return 1;
int leading_zeros = __builtin_clz(n);
int bits = 32 - leading_zeros;
return (bits + 3) / 4; /* 4ビットで1桁 */
}
#endif
/*
* パフォーマンス比較(100万回の呼び出し):
*
* ループ: ~15ms
* 二分探索: ~5ms
* CLZ: ~3ms
*
* 実用上は二分探索で十分
* CLZはGCC/Clang依存
*/
---
第8部:テストと検証
8.1 境界値テスト
/* 包括的なテストケース */
#include <stdio.h>
#include <limits.h>
void test_hex_boundaries(void)
{
printf("=== Boundary Tests ===\n");
/* 基本値 */
printf("0: %%x = %x\n", 0);
printf("1: %%x = %x\n", 1);
printf("15: %%x = %x\n", 15); /* 1桁最大 */
printf("16: %%x = %x\n", 16); /* 2桁最小 */
/* バイト境界 */
printf("255: %%x = %x\n", 255); /* 2桁最大 */
printf("256: %%x = %x\n", 256); /* 3桁最小 */
/* ワード境界 */
printf("0xFFFF: %%x = %x\n", 0xFFFF);
printf("0x10000: %%x = %x\n", 0x10000);
/* 最大値 */
printf("UINT_MAX: %%x = %x\n", UINT_MAX);
printf("INT_MAX: %%x = %x\n", INT_MAX);
printf("INT_MIN: %%x = %x\n", INT_MIN);
}
void test_hex_flags(void)
{
unsigned int n = 0x2a;
printf("\n=== Flag Tests ===\n");
/* 大文字/小文字 */
printf("%%x: %x\n", n);
printf("%%X: %X\n", n);
/* #フラグ */
printf("%%#x: %#x\n", n);
printf("%%#X: %#X\n", n);
printf("%%#x 0: %#x\n", 0); /* 0は特別 */
/* 幅 */
printf("%%10x: |%10x|\n", n);
printf("%%-10x: |%-10x|\n", n);
printf("%%010x: |%010x|\n", n);
/* 精度 */
printf("%%.6x: |%.6x|\n", n);
printf("%%.0x: |%.0x|\n", n);
printf("%%.0x 0:|%.0x|\n", 0);
/* 組み合わせ */
printf("%%#10.6x: |%#10.6x|\n", n);
}
void test_pointer(void)
{
int x = 42;
void *ptr = &x;
void *null = NULL;
printf("\n=== Pointer Tests ===\n");
printf("%%p: %p\n", ptr);
printf("NULL: %p\n", null);
printf("%%20p: |%20p|\n", ptr);
printf("%%-20p: |%-20p|\n", ptr);
}
8.2 printfとの比較テスト
/* ft_printfとprintfの出力比較 */
#include <string.h>
#define TEST(fmt, val) \
do { \
char buf_std[256], buf_ft[256]; \
int len_std = sprintf(buf_std, fmt, val); \
/* ft_printf版は実際の実装に合わせる */ \
printf("Format: %-15s Value: %-12u\n", fmt, val); \
printf(" printf: |%s| (len=%d)\n", buf_std, len_std); \
} while(0)
void run_comparison_tests(void)
{
unsigned int values[] = {0, 1, 15, 16, 255, 256, 0xDEAD, 0xDEADBEEF};
int n = sizeof(values) / sizeof(values[0]);
for (int i = 0; i < n; i++)
{
TEST("%x", values[i]);
TEST("%X", values[i]);
TEST("%#x", values[i]);
TEST("%10x", values[i]);
TEST("%-10x", values[i]);
TEST("%010x", values[i]);
TEST("%.6x", values[i]);
TEST("%#10.6x", values[i]);
printf("\n");
}
}
---
まとめ:メモリとアドレッシングの統一的理解
この章では、以下の知識を統合しました:
アーキテクチャの基礎:
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
- von Neumannアーキテクチャ
- メモリアドレッシングの概念
- 仮想メモリとアドレス空間
- エンディアンとバイト順序
16進数の理論:
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
- 2進数との数学的関係(16 = 2^4)
- 歴史的発展(IBM System/360)
- 8進数との比較
ポインタの本質:
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
- アドレスを格納する変数
- 型理論的な意味
- NULLポインタの扱い
実装技法:
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
- ビット演算による16進変換
- uintptr_tの使用
- プラットフォーム互換性
- パフォーマンス最適化
次章では、ft_printfの統合とテスト戦略について学びます。すべての変換指定子を組み合わせた複雑なフォーマット文字列の処理、エラーハンドリング、そして体系的なテスト手法を探求します。