第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 まとめ
本章では、配列と文字列について学んだ:
次章では、構造体と共用体について学ぶ。
---