第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 まとめ
本章では、ポインタとメモリについて学んだ:
次章では、配列と文字列について学ぶ。
---