第6章: ポインタとメモリ

6.1 メモリモデルの理論

フォン・ノイマンアーキテクチャ

現代のコンピュータはフォン・ノイマンアーキテクチャ(1945年)に基づいている:

┌─────────────────────────────────────────────────────────┐
│                      メモリ                              │
│  ┌─────────────────────────────────────────────────┐   │
│  │ アドレス0: [データ]                              │   │
│  │ アドレス1: [データ]                              │   │
│  │ ...                                              │   │
│  │ アドレスN: [データ]                              │   │
│  └─────────────────────────────────────────────────┘   │
└─────────────────────────────────────────────────────────┘
              ↑↓
         ┌────────────┐
         │    CPU     │
         │ レジスタ   │
         │ ALU        │
         └────────────┘

メモリは線形のバイト配列として抽象化される:

  • 各バイトは一意のアドレスを持つ
  • アドレスは0から始まる非負整数
  • ポインタはこのアドレスを格納する変数
  • C言語のメモリモデル

    プロセスのメモリレイアウト(典型的):
    
    高アドレス ┌─────────────────┐
               │    スタック     │ ← 自動変数、戻りアドレス
               │       ↓        │    (下に向かって成長)
               │                 │
               │       ↑        │
               │     ヒープ      │ ← malloc()で確保
               ├─────────────────┤
               │      BSS       │ ← 初期化されていない静的変数
               ├─────────────────┤
               │     Data       │ ← 初期化された静的変数
               ├─────────────────┤
               │     Text       │ ← プログラムコード
    低アドレス └─────────────────┘
    

    6.2 ポインタの基礎

    ポインタとは

    ポインタ(Pointer)は、メモリアドレスを格納する変数である。

    int x = 42;      /* xはint型の変数 */
    int *p = &x;     /* pはxのアドレスを格納するポインタ */
    
    printf("%d\n", x);    /* 42: xの値 */
    printf("%p\n", (void *)&x);  /* 0x7fff...: xのアドレス */
    printf("%p\n", (void *)p);   /* 0x7fff...: pの値(xのアドレス) */
    printf("%d\n", *p);   /* 42: pが指す値 */
    

    アドレス演算子とデリファレンス演算子

    int x = 42;
    
    /* &: アドレス演算子 */
    int *p = &x;  /* xのアドレスを取得 */
    
    /* *: デリファレンス(間接参照)演算子 */
    int y = *p;   /* pが指す値を取得 */
    *p = 100;     /* pが指す場所に値を書き込む */
    

    ポインタのサイズ

    /* ポインタのサイズはシステムに依存 */
    printf("%zu\n", sizeof(int *));    /* 8(64bit)/ 4(32bit) */
    printf("%zu\n", sizeof(char *));   /* 同じ */
    printf("%zu\n", sizeof(void *));   /* 同じ */
    
    /* 全てのポインタは同じサイズ */
    

    6.3 ポインタ演算

    ポインタの加減算

    int arr[5] = {10, 20, 30, 40, 50};
    int *p = arr;
    
    printf("%d\n", *p);       /* 10 */
    printf("%d\n", *(p + 1)); /* 20 */
    printf("%d\n", *(p + 2)); /* 30 */
    
    /* p + n は「pが指す型のサイズ × n」だけ進む */
    /* int *p の場合、p + 1 は sizeof(int) = 4バイト先 */
    

    配列とポインタの関係

    int arr[5] = {10, 20, 30, 40, 50};
    
    /* 以下は全て等価 */
    arr[0]    ==  *arr
    arr[i]    ==  *(arr + i)
    &arr[i]   ==  arr + i
    
    /* しかし、配列とポインタは異なる */
    int *p = arr;
    sizeof(arr);  /* 20: 配列全体のサイズ */
    sizeof(p);    /* 8: ポインタのサイズ */
    

    ポインタの差

    int arr[5] = {10, 20, 30, 40, 50};
    int *p1 = &arr[1];
    int *p2 = &arr[4];
    
    ptrdiff_t diff = p2 - p1;  /* 3(要素数) */
    
    /* ポインタの差は要素数を返す */
    

    ポインタの比較

    int arr[5];
    int *p1 = &arr[0];
    int *p2 = &arr[4];
    
    if (p1 < p2)   /* 同じ配列内のポインタは比較可能 */
        printf("p1 is before p2\n");
    
    /* 異なる配列のポインタ比較は未定義動作 */
    

    6.4 NULLポインタ

    NULLの定義

    /* stddef.h または stdlib.h で定義 */
    #define NULL ((void *)0)
    
    /* C23では nullptr が追加 */
    int *p = nullptr;
    

    NULLチェック

    int *p = NULL;
    
    /* NULLポインタのデリファレンスは未定義動作 */
    int x = *p;  /* クラッシュ(通常) */
    
    /* NULLチェック */
    if (p != NULL) {
        x = *p;
    }
    
    /* 簡潔な形式 */
    if (p) {
        x = *p;
    }
    

    6.5 void ポインタ

    汎用ポインタ

    void *vp;  /* 任意の型を指せるポインタ */
    
    int i = 42;
    float f = 3.14f;
    char c = 'A';
    
    vp = &i;  /* int * → void * は暗黙変換 */
    vp = &f;  /* float * → void * も暗黙変換 */
    vp = &c;  /* char * → void * も暗黙変換 */
    
    /* void * → 他の型へは明示的キャストが推奨 */
    int *ip = (int *)vp;
    

    void ポインタの用途

    /* malloc()はvoid *を返す */
    int *arr = (int *)malloc(sizeof(int) * 10);
    
    /* qsort()はvoid *を引数に取る */
    int compare(const void *a, const void *b)
    {
        return (*(int *)a - *(int *)b);
    }
    

    void ポインタの制限

    void *vp;
    
    /* void *に対する演算は不可(サイズが不明) */
    vp + 1;  /* エラー(GCC拡張では1バイト加算) */
    *vp;     /* エラー */
    

    6.6 多重ポインタ

    ポインタへのポインタ

    int x = 42;
    int *p = &x;      /* xへのポインタ */
    int **pp = &p;    /* pへのポインタ */
    int ***ppp = &pp; /* ppへのポインタ */
    
    printf("%d\n", x);     /* 42 */
    printf("%d\n", *p);    /* 42 */
    printf("%d\n", **pp);  /* 42 */
    printf("%d\n", ***ppp);/* 42 */
    

    用途: 関数でポインタを変更

    void allocate(int **pp, size_t n)
    {
        *pp = malloc(sizeof(int) * n);
    }
    
    int main(void)
    {
        int *arr = NULL;
        allocate(&arr, 10);
        /* arrはmallocで確保されたメモリを指す */
        free(arr);
        return 0;
    }
    

    用途: 文字列の配列

    char **argv;  /* char *の配列(文字列の配列) */
    
    /* main関数の引数 */
    int main(int argc, char **argv)
    {
        for (int i = 0; i < argc; i++)
            printf("%s\n", argv[i]);
        return 0;
    }
    

    6.7 配列とポインタの違い

    重要な違い

    char arr[] = "hello";  /* 配列: メモリにコピーを持つ */
    char *ptr = "hello";   /* ポインタ: 文字列リテラルを指す */
    
    arr[0] = 'H';  /* OK: 配列は変更可能 */
    ptr[0] = 'H';  /* 未定義動作: 文字列リテラルは変更不可 */
    
    sizeof(arr);   /* 6: 配列のサイズ('\0'含む) */
    sizeof(ptr);   /* 8: ポインタのサイズ */
    
    &arr;          /* char (*)[6]: 配列へのポインタ */
    &ptr;          /* char **: ポインタへのポインタ */
    

    配列の減衰

    void func(int arr[])  /* int *arr と同じ */
    {
        sizeof(arr);  /* ポインタのサイズ */
    }
    
    int main(void)
    {
        int arr[10];
        sizeof(arr);  /* 配列のサイズ(40バイト) */
        func(arr);    /* arr は &arr[0] に減衰 */
        return 0;
    }
    

    6.8 constとポインタ

    4つのパターン

    int x = 42;
    
    /* 1. 普通のポインタ */
    int *p1 = &x;
    *p1 = 100;  /* OK */
    p1 = NULL;  /* OK */
    
    /* 2. ポインタが指す値がconst */
    const int *p2 = &x;
    int const *p2_alt = &x;  /* 同じ意味 */
    *p2 = 100;  /* エラー */
    p2 = NULL;  /* OK */
    
    /* 3. ポインタ自体がconst */
    int * const p3 = &x;
    *p3 = 100;  /* OK */
    p3 = NULL;  /* エラー */
    
    /* 4. 両方const */
    const int * const p4 = &x;
    *p4 = 100;  /* エラー */
    p4 = NULL;  /* エラー */
    

    読み方のコツ

    右から左に読む:
    const int *p    → "p is a pointer to const int"
    int * const p   → "p is a const pointer to int"
    const int * const p → "p is a const pointer to const int"
    

    6.9 関数ポインタ

    関数ポインタの宣言

    /* return_type (*name)(parameter_types) */
    int (*func_ptr)(int, int);
    
    /* typedefを使った簡略化 */
    typedef int (*operation_t)(int, int);
    operation_t op;
    

    使用例

    int add(int a, int b) { return a + b; }
    int sub(int a, int b) { return a - b; }
    
    int (*op)(int, int);
    
    op = add;
    printf("%d\n", op(5, 3));  /* 8 */
    
    op = sub;
    printf("%d\n", op(5, 3));  /* 2 */
    

    関数ポインタの配列

    int add(int a, int b) { return a + b; }
    int sub(int a, int b) { return a - b; }
    int mul(int a, int b) { return a * b; }
    int divide(int a, int b) { return a / b; }
    
    int (*ops[])(int, int) = {add, sub, mul, divide};
    
    for (int i = 0; i < 4; i++)
        printf("%d\n", ops[i](10, 2));
    

    6.10 restrict修飾子(C99)

    エイリアシング問題

    void add_arrays(int *dest, int *src, size_t n)
    {
        for (size_t i = 0; i < n; i++)
            dest[i] += src[i];
    }
    
    /* destとsrcが重なる可能性があるため、
       コンパイラは最適化を制限する */
    

    restrict による最適化

    void add_arrays(int * restrict dest, int * restrict src, size_t n)
    {
        for (size_t i = 0; i < n; i++)
            dest[i] += src[i];
    }
    
    /* プログラマがdestとsrcは重ならないと保証
       コンパイラはより積極的な最適化が可能 */
    

    6.11 動的メモリ管理

    malloc, calloc, realloc, free

    #include <stdlib.h>
    
    /* malloc: メモリを確保(初期化なし) */
    int *arr1 = malloc(sizeof(int) * 10);
    if (!arr1)
        return -1;  /* エラー処理 */
    
    /* calloc: メモリを確保し0で初期化 */
    int *arr2 = calloc(10, sizeof(int));
    
    /* realloc: サイズを変更 */
    int *arr3 = realloc(arr1, sizeof(int) * 20);
    if (!arr3) {
        free(arr1);  /* 元のメモリを解放 */
        return -1;
    }
    arr1 = arr3;
    
    /* free: メモリを解放 */
    free(arr1);
    arr1 = NULL;  /* ダングリングポインタを防ぐ */
    

    メモリリーク

    void memory_leak(void)
    {
        int *p = malloc(sizeof(int) * 100);
        /* freeを忘れるとメモリリーク */
    }
    
    /* valgrindで検出可能 */
    

    ダングリングポインタ

    int *dangling_pointer(void)
    {
        int local = 42;
        return &local;  /* ローカル変数へのポインタを返す */
    }
    /* 関数終了後、localは破棄される */
    
    int *use_after_free(void)
    {
        int *p = malloc(sizeof(int));
        *p = 42;
        free(p);
        return p;  /* 解放済みメモリへのポインタ */
    }
    

    二重解放

    void double_free(void)
    {
        int *p = malloc(sizeof(int));
        free(p);
        free(p);  /* 二重解放: 未定義動作 */
    }
    
    /* 対策: 解放後にNULLを代入 */
    free(p);
    p = NULL;
    free(p);  /* free(NULL)は安全 */
    

    6.12 ポインタの安全な使用

    防御的プログラミング

    /* 1. NULLチェック */
    void process(int *p)
    {
        if (!p)
            return;
        /* 処理 */
    }
    
    /* 2. malloc後のチェック */
    int *arr = malloc(sizeof(int) * n);
    if (!arr) {
        perror("malloc");
        exit(1);
    }
    
    /* 3. free後のNULL代入 */
    free(arr);
    arr = NULL;
    
    /* 4. 配列境界チェック */
    if (index < 0 || index >= size)
        return -1;
    arr[index] = value;
    

    アドレスサニタイザ

    # コンパイル時にアドレスサニタイザを有効化
    gcc -fsanitize=address -g program.c -o program
    
    # 実行時にメモリエラーを検出
    ./program
    

    6.13 まとめ

    本章では、ポインタとメモリについて学んだ:

  • メモリモデル: フォン・ノイマンアーキテクチャ
  • ポインタの基礎: アドレス、デリファレンス
  • ポインタ演算: 加減算、配列との関係
  • NULLポインタ: 特殊な値
  • voidポインタ: 汎用ポインタ
  • 多重ポインタ: ポインタへのポインタ
  • constとポインタ: 4つのパターン
  • 関数ポインタ: コールバック
  • restrict: 最適化ヒント
  • 動的メモリ: malloc, free
  • 安全な使用: 防御的プログラミング
  • 次章では、配列と文字列について学ぶ。

    ---

    参考文献

  • Neumann, J. von (1945). "First Draft of a Report on the EDVAC"
  • ISO/IEC 9899:2011, Programming languages — C
  • Seacord, R. C. (2014). "The CERT C Coding Standard", 2nd Edition, Addison-Wesley
  • Kernighan, B. W., & Ritchie, D. M. (1988). "The C Programming Language", 2nd Edition, Prentice Hall