🚀 C言語:ポインタから構造体・連結リストまでの完全まとめ
1. ポインタの階層構造(マトリョーシカ)
C言語のポインタは、データの「階層(次元)」が増えるたびに * の数が増えていきます。全体をいくつかのグループ(群)で束ねていく階層構造として捉えるのが正解です。
| 階層 | 概念・役割 | 宣言の例 | メモリの実態イメージ |
|---|---|---|---|
| 単体 | データそのもの | int score = 10; |
1つの数値や文字の箱 |
1階層 (*) |
A群(データの集まり) | int *p = &score; |
通常の変数のアドレスを指すポインタ(1次元) |
2階層 (**) |
B群(A群をまとめる) | int **pp = &p; |
ポインタ変数のアドレスを指すポインタ(2次元) |
3階層 (***) |
C群(B群をまとめる) | int ***ppp = &pp; |
ダブルポインタのアドレスを指すポインタ(3次元) |
2. 「ポインタ配列」と「ダブルポインタ」の決定的な違い
どちらも複数のポインタを扱うために使われますが、特定の場所(メモリ領域)を指すか指さないかという柔軟性に決定的な違いがあります。
📊 性能・特徴の比較
| 比較項目 | ポインタ配列 (Type *arr[N]) |
ポインタへのポインタ (Type **pp) |
|---|---|---|
| 本質 | 配列(メモリ領域そのもの) | 変数(ポインタ)(アドレスを格納する器) |
| 特定の場所 | 指す(固定)。作った時点でサイズが決まる。 | 指さない(自由)。最初はどこも指していない。 |
| 柔軟性 | ❌ 後から要素数を増やしたり減らしたりできない。 | ⭕ malloc/reallocでサイズを自由に変更可能。 |
| 主な用途 | 文字列の配列(長さの異なる文字列の管理) | 関数の引数でポインタを書き換える、動的2次元配列 |
3. 構造体(struct)の基本
構造体は、配列と違って**「種類の異なる複数の型」**を1つのグループとしてまとめて管理する仕組みです。
-
.(ドット演算子): 構造体の実体(変数)から中身にアクセスする。 -
->(アロー演算子): 構造体を指すポインタ経由で中身にアクセスする。-
ptr->ageは(*ptr).ageをスマートに書いたもの。
-
4. 複数データの管理:「構造体配列」vs「連結リスト」
複数の構造体データをまとめて管理する方法には、一長一短(トレードオフ)があります。
📊 メモリ構造と性能の比較
| 比較項目 | 構造体配列 (Student class) |
連結リスト (struct Node *head) |
|---|---|---|
| メモリ配置 | メモリ上に連続して一列に並ぶ | メモリ上のバラバラの場所に散らばる |
| データの追加・削除 | ❌ 苦手(途中に割り込むと後ろを全部ずらす) | ⭕ 大得意(ポインタの矢印を付け替えるだけ) |
| 特定データへのアクセス | ⭕ 超高速(100番目に一発でジャンプできる) | ❌ 遅い(先頭から100回矢印をたどる必要がある) |
| メモリの無駄 | ❌ 出やすい(使わない枠も最初に確保する) | ⭕ ゼロ(増えた瞬間だけピンポイントで確保) |
5. 連結リストの実装と重要ルール
🛠️ データを末尾に自動追加する関数例
先頭のポインタ(head)自体を関数内で書き換える可能性があるため、引数には**ポインタのポインタ(**head_ref)**を使用します。
struct Node {
char name[50];
struct Node *next; // 次の部屋を指す魔法の矢印
};
void append(struct Node **head_ref, const char *new_name) {
// 1. 新しい部屋を malloc で作る
struct Node *new_node = (struct Node *)malloc(sizeof(struct Node));
strcpy(new_node->name, new_name);
new_node->next = NULL;
// 2. リストが空なら、この部屋を先頭にする
if (*head_ref == NULL) {
*head_ref = new_node;
return;
}
// 3. 最後の部屋(next が NULL の部屋)を探す
struct Node *last = *head_ref;
while (last->next != NULL) {
last = last->next;
}
// 4. 最後の部屋の後ろに新しい部屋を繋ぐ
last->next = new_node;
}
⚠️ メモリ解放(free)の鉄則:「逆マトリョーシカ」
動的に確保したメモリを解放するときは、必ず内側(奥のデータ)から外側へ順番に消さなければなりません。
-
理由: 外側(住所録や先頭ポインタ)を先に
freeしてしまうと、奥にあるデータの場所を知る手段(参照方法)が完全に消滅し、二度と解放できないゴミ(メモリリーク)になるため。 -
対策: 連結リストを消すときは、必ず「次の場所」を一時的なポインタに避難させてから、現在の部屋を
freeします。