第7章: 配列と文字列

7.1 配列の理論

配列とは

配列(Array)は、同じ型の要素が連続したメモリ領域に配置されたデータ構造である。

計算機科学的定義:

  • ランダムアクセス: O(1)で任意の要素にアクセス可能
  • 固定サイズ: サイズは宣言時に決定(C99のVLAを除く)
  • 連続メモリ: 要素は隣接して配置
  • 配列のメモリレイアウト:
    int arr[5] = {10, 20, 30, 40, 50};
    
    アドレス:  0x100   0x104   0x108   0x10C   0x110
               ┌───────┬───────┬───────┬───────┬───────┐
               │  10   │  20   │  30   │  40   │  50   │
               └───────┴───────┴───────┴───────┴───────┘
    インデックス: [0]     [1]     [2]     [3]     [4]
    

    7.2 配列の宣言と初期化

    基本的な宣言

    /* 宣言のみ(初期化なし) */
    int arr[10];  /* 自動変数: 値は不定 */
    
    /* 静的配列は0で初期化される */
    static int static_arr[10];  /* 全て0 */
    
    /* サイズを指定して初期化 */
    int arr1[5] = {1, 2, 3, 4, 5};
    
    /* 部分初期化(残りは0) */
    int arr2[5] = {1, 2};  /* {1, 2, 0, 0, 0} */
    
    /* 全て0で初期化 */
    int arr3[5] = {0};
    
    /* サイズを省略(初期化子から推論) */
    int arr4[] = {1, 2, 3};  /* サイズは3 */
    

    指定初期化子(C99)

    /* 特定のインデックスを初期化 */
    int arr[10] = {[5] = 100, [9] = 200};
    /* {0, 0, 0, 0, 0, 100, 0, 0, 0, 200} */
    
    /* 範囲指定(GCC拡張) */
    int arr2[10] = {[0 ... 4] = 1, [5 ... 9] = 2};
    

    可変長配列(VLA、C99)

    void func(int n)
    {
        int vla[n];  /* 実行時にサイズが決定 */
    
        /* VLAの制限 */
        /* - スタックに確保される */
        /* - サイズが大きいとスタックオーバーフロー */
        /* - 初期化子を使用できない */
        /* - C11ではオプショナル */
    }
    

    7.3 多次元配列

    2次元配列

    /* 2次元配列の宣言 */
    int matrix[3][4];  /* 3行4列 */
    
    /* 初期化 */
    int matrix[3][4] = {
        {1, 2, 3, 4},
        {5, 6, 7, 8},
        {9, 10, 11, 12}
    };
    
    /* ネストしたブレースを省略可能 */
    int matrix2[3][4] = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12};
    

    メモリレイアウト

    int matrix[3][4] の行優先(row-major)レイアウト:
    
              col 0   col 1   col 2   col 3
            ┌───────┬───────┬───────┬───────┐
    row 0   │ [0][0]│ [0][1]│ [0][2]│ [0][3]│
            ├───────┼───────┼───────┼───────┤
    row 1   │ [1][0]│ [1][1]│ [1][2]│ [1][3]│
            ├───────┼───────┼───────┼───────┤
    row 2   │ [2][0]│ [2][1]│ [2][2]│ [2][3]│
            └───────┴───────┴───────┴───────┘
    
    メモリ内での配置(連続):
    [0][0] [0][1] [0][2] [0][3] [1][0] [1][1] [1][2] [1][3] [2][0] ...
    
    アドレス計算: &matrix[i][j] = base + (i * 4 + j) * sizeof(int)
    

    配列へのポインタ

    int matrix[3][4];
    
    /* 配列へのポインタ */
    int (*ptr)[4] = matrix;  /* 「4つのintの配列」へのポインタ */
    
    /* アクセス */
    ptr[1][2];     /* matrix[1][2] と同じ */
    *(*(ptr + 1) + 2);  /* 同じ */
    

    ポインタの配列 vs 配列へのポインタ

    int *ptr_array[4];   /* 「intへのポインタ」の配列(4要素) */
    int (*array_ptr)[4]; /* 「4つのintの配列」へのポインタ */
    
    /* 読み方のコツ: 名前から始めて右へ、[]があれば配列、()で*があればポインタ */
    

    7.4 文字列の理論

    C言語の文字列

    C言語には組み込みの文字列型がない。文字列はヌル終端文字配列として表現される。

    char str[] = "hello";
    /* 実際の内容: {'h', 'e', 'l', 'l', 'o', '\0'} */
    /* サイズ: 6バイト(ヌル文字含む) */
    

    文字列リテラル

    /* 文字列リテラルは静的記憶域に配置 */
    char *str = "hello";  /* 文字列リテラルへのポインタ */
    
    /* 文字列リテラルは変更不可 */
    str[0] = 'H';  /* 未定義動作! */
    
    /* const修飾子を付けるべき */
    const char *str = "hello";
    

    文字配列 vs 文字列リテラル

    /* 文字配列: コピーが作成される、変更可能 */
    char arr[] = "hello";
    arr[0] = 'H';  /* OK */
    
    /* 文字列リテラル: 変更不可 */
    char *ptr = "hello";
    ptr[0] = 'H';  /* 未定義動作 */
    

    7.5 文字列操作関数

    長さと比較

    #include <string.h>
    
    /* strlen: 長さ(ヌル文字を含まない) */
    size_t len = strlen("hello");  /* 5 */
    
    /* strcmp: 比較 */
    int cmp = strcmp("abc", "abd");  /* 負の値('c' < 'd') */
    
    /* strncmp: 最大n文字を比較 */
    int cmp2 = strncmp("hello", "help", 3);  /* 0 */
    

    コピーと連結

    char dest[100];
    
    /* strcpy: コピー(危険!) */
    strcpy(dest, "hello");
    
    /* strncpy: 最大n文字をコピー */
    strncpy(dest, "hello", sizeof(dest) - 1);
    dest[sizeof(dest) - 1] = '\0';  /* 明示的にヌル終端 */
    
    /* strcat: 連結(危険!) */
    strcat(dest, " world");
    
    /* strncat: 最大n文字を連結 */
    strncat(dest, " world", sizeof(dest) - strlen(dest) - 1);
    

    検索

    char str[] = "hello world";
    
    /* strchr: 文字を検索 */
    char *p1 = strchr(str, 'o');  /* "o world" */
    
    /* strrchr: 末尾から検索 */
    char *p2 = strrchr(str, 'o');  /* "orld" */
    
    /* strstr: 部分文字列を検索 */
    char *p3 = strstr(str, "wor");  /* "world" */
    

    分割

    char str[] = "one,two,three";
    char *token;
    
    /* strtok: トークン分割(元の文字列を変更する!) */
    token = strtok(str, ",");
    while (token) {
        printf("%s\n", token);
        token = strtok(NULL, ",");
    }
    /* strtokは内部状態を持つ(スレッドセーフでない) */
    

    7.6 安全な文字列操作

    バッファオーバーフロー

    /* 危険な例 */
    char buf[10];
    strcpy(buf, "this string is too long");  /* オーバーフロー! */
    
    /* 安全な例 */
    char buf[10];
    strncpy(buf, "this string is too long", sizeof(buf) - 1);
    buf[sizeof(buf) - 1] = '\0';
    

    snprintf

    char buf[100];
    
    /* snprintf: 安全なフォーマット */
    int ret = snprintf(buf, sizeof(buf), "Value: %d", 42);
    
    /* 戻り値は「書き込まれるはずだった文字数」 */
    if (ret >= sizeof(buf)) {
        /* 切り詰めが発生した */
    }
    

    strlcpy と strlcat(BSD拡張)

    /* OpenBSD発祥、より安全な関数 */
    #include <bsd/string.h>  /* Linux: libbsd */
    
    size_t strlcpy(char *dst, const char *src, size_t size);
    size_t strlcat(char *dst, const char *src, size_t size);
    
    /* 常にヌル終端される(sizeが0でない限り) */
    /* 戻り値は「必要だったサイズ」 */
    char buf[10];
    size_t ret = strlcpy(buf, "hello world", sizeof(buf));
    if (ret >= sizeof(buf)) {
        /* 切り詰めが発生した */
    }
    

    7.7 文字列変換

    数値から文字列

    #include <stdio.h>
    #include <stdlib.h>
    
    char buf[100];
    
    /* sprintf(危険)/ snprintf(安全) */
    snprintf(buf, sizeof(buf), "%d", 42);
    
    /* itoa(非標準)*/
    /* 使用を避け、snprintfを使う */
    

    文字列から数値

    #include <stdlib.h>
    
    /* atoi: 文字列 → int(エラー検出なし) */
    int i = atoi("42");
    
    /* strtol: 文字列 → long(エラー検出あり) */
    char *endptr;
    long l = strtol("42abc", &endptr, 10);
    if (*endptr != '\0') {
        /* 変換されなかった文字がある */
    }
    
    /* strtod: 文字列 → double */
    double d = strtod("3.14", NULL);
    

    7.8 ワイド文字とマルチバイト文字

    ワイド文字

    #include <wchar.h>
    
    /* wchar_t: 固定幅の文字型 */
    wchar_t wc = L'あ';
    wchar_t ws[] = L"こんにちは";
    
    /* ワイド文字関数 */
    wcslen(ws);       /* strlen相当 */
    wcscpy(dest, ws); /* strcpy相当 */
    

    マルチバイト文字

    #include <stdlib.h>
    #include <locale.h>
    
    /* ロケール設定 */
    setlocale(LC_ALL, "");
    
    /* マルチバイト → ワイド文字 */
    wchar_t wc;
    int len = mbtowc(&wc, "あ", MB_CUR_MAX);
    
    /* ワイド文字 → マルチバイト */
    char mb[MB_CUR_MAX];
    int n = wctomb(mb, L'あ');
    

    UTF-8

    /* UTF-8はマルチバイトエンコーディング */
    /* ASCII互換: 1バイト文字はASCIIと同一 */
    /* 非ASCII: 2〜4バイト */
    
    char utf8[] = "日本語";  /* 9バイト(3文字×3バイト) */
    size_t len = strlen(utf8);  /* 9(バイト数) */
    /* 文字数を得るには別途処理が必要 */
    

    7.9 配列とポインタの関数引数

    配列の引数

    /* 以下は全て同じ意味 */
    void func(int arr[10]);  /* サイズは無視される */
    void func(int arr[]);
    void func(int *arr);
    
    /* サイズを別途渡す */
    void process(int *arr, size_t size)
    {
        for (size_t i = 0; i < size; i++)
            arr[i] *= 2;
    }
    

    多次元配列の引数

    /* 2次元配列: 2番目以降のサイズを指定する必要がある */
    void func(int arr[][4], size_t rows);
    void func(int (*arr)[4], size_t rows);
    
    /* 任意のサイズ(VLA、C99) */
    void func(size_t rows, size_t cols, int arr[rows][cols]);
    

    7.10 複合リテラル(C99)

    /* 配列の複合リテラル */
    int *p = (int[]){1, 2, 3};
    
    /* 文字列の複合リテラル */
    const char *s = (char[]){"hello"};
    
    /* 関数引数として */
    print_array((int[]){10, 20, 30}, 3);
    

    7.11 まとめ

    本章では、配列と文字列について学んだ:

  • 配列: 連続メモリ、固定サイズ
  • 多次元配列: 行優先レイアウト
  • 文字列: ヌル終端文字配列
  • 文字列関数: strlen, strcpy, strcmp等
  • 安全な操作: snprintf, strlcpy
  • 文字列変換: atoi, strtol, sprintf
  • ワイド文字: wchar_t, マルチバイト
  • 関数引数: 配列の減衰
  • 次章では、構造体と共用体について学ぶ。

    ---

    参考文献

  • ISO/IEC 9899:2011, Programming languages — C
  • Miller, T. C., & de Raadt, T. (1999). "strlcpy and strlcat – consistent, safe, string copy and concatenation", USENIX Annual Technical Conference
  • Viega, J., & Messier, M. (2003). "Secure Programming Cookbook for C and C++", O'Reilly Media