05 配列とポインタ・多重ポインタ

ポインタは「アドレスを格納する変数」である。データそのものではなく、データが置かれているメモリ上の位置(アドレス)を扱う。配列名が先頭アドレスを表すこと、*(p+1)(*p+1) の違い、多重ポインタ int ** まで、2級で最も差がつく単元である。

✅ この章のゴール:① ポインタ変数=アドレスの器、*p=中身、と説明できる ② &(アドレス演算子)と *(宣言時/間接参照の2用法)を区別できる ③ 配列名=先頭アドレス、要素番号=先頭からの距離が分かる ④ *(p+1)(*p+1) の違いを言える ⑤ 多重ポインタ int **pp とポインタ配列 char *[] が読める。

05-1 ポインタとは(アドレスを扱う仕組み)

「ポインタ」は指し棒をイメージすると分かりやすい。ポインタは「データそのもの」ではなく、データが格納されている「アドレス」を扱う。

変数や配列はメモリ上に配置される。メモリ上に置かれるということは、そこには必ず「アドレス」が存在する。C言語ではこのアドレスを特殊な変数に格納し、そのアドレスが指す先のデータを参照できる。これが「ポインタ」の仕組みである。

⚠ ポインタは「わかった気になっても、なんだかわからない」と感じやすい厄介な存在。「アドレス」と「中身」を毎回区別することを意識すれば、必ず読み解ける。

05-2 ポインタ変数とアドレス演算子 &

ポインタ変数とは、他の変数やデータの「アドレス」を格納できる変数である。データそのものではなく、メモリ上の位置を操作できる。

int num = 3;
int *p;
p = #   /* num のアドレスをポインタ変数 p に格納 */

ここで pnum のアドレスを保持し、*pnum の値を参照できる。

アドレス演算子 &

&(アンパサンド)を変数の前につけると、「その変数が格納されたメモリアドレス」になる。

int num = 3;
int *p = #   /* &num = num のアドレス。それを p に代入 */
int *p = #p に入るのは num のアドレス(num の値「3」ではない)。

05-3 アスタリスク * の2つの意味

アスタリスク * には2つの使い方がある。同じ記号なので混同しやすいが、出てくる場所で見分ける。

用法意味
型修飾子としての *「ポインタ変数」を宣言する(〜へのポインタ型)int *p;
間接参照演算子としての *(デリファレンス)ポインタが指すアドレスの「中身(値)」を取り出す*p

① ポインタの宣言(型修飾子としての *

int num = 3;
int *p = #   /* num のアドレスをポインタ p に代入 */

② 参照先の値を取得(間接参照演算子としての *

int num = 10;
int *p = #          /* num のアドレスを p に格納 */
printf("%d¥n", *p);      /* 10 を出力(p が指す先の中身) */
「ポインタ変数は器(うつわ)」と考えるとよい。
p(ポインタ変数そのもの)= アドレス=うつわ
*p(間接参照)= うつわの中身

メモリマップ例

宣言 int num = 3; int *p = # はメモリ上で次のようになる。

0xFFFF1230
p(numの番地)
3
num(中身)
num は番地 0xFFFF1230 に値 3p 自身は番地 0xFFFF4560 に置かれ、その中身は num の番地 0xFFFF1230

つまり p の中身は 0xFFFF1230(num のアドレス)、*p はそのアドレスの中身 3 になる。


05-4 ポインタ変数の宣言

宣言方法

int  *p;     /* 整数型へのポインタ */
char *str;   /* 文字型へのポインタ */

注意点

int x = 20;
int *p = &x;   /* x のアドレスを p に格納 */

メモリマップ例

宣言 int x = 20; int *p = &x;

0xFFFF1200
p(xの番地)
20
x(中身)
x は番地 0xFFFF1200 に値 20p 自身は番地 0xFFFF1250 に置かれ、その中身は x の番地 0xFFFF1200
⚠ ポインタ変数を宣言しただけ(int *p; だけ)で *p を読み書きすると、どこを指しているか分からない(未初期化)ため未定義動作になる。必ず &変数 などで初期化してから使う。

05-5 配列とポインタの関係(配列名=先頭アドレス)

配列とアドレスは密接な関係がある。最重要ルールは 「配列名は、その配列の先頭アドレス」 である。

配列名と先頭アドレス

int arr[3] = {1, 2, 3};
printf("%p¥n", arr);       /* 配列の先頭アドレス */
printf("%p¥n", &arr[0]);   /* 同じ値(先頭要素のアドレス) */
1
arr[0]
0xFFFF1300
2
arr[1]
0xFFFF1304
3
arr[2]
0xFFFF1308
配列名 arr = 先頭 arr[0] のアドレス 0xFFFF1300int は4バイトなのでアドレスは4ずつ進む。
✅ コード上の arr はアドレス値 0xFFFF1300 を表す。arr&arr[0]同じ(どちらも先頭アドレス)。

要素番号とアドレスの関係

要素番号は、先頭アドレスからの「距離」である。

int arr[3] = {1, 2, 3};
printf("%p¥n", &arr[1]);   /* 先頭から1つ分進んだアドレス       = 0xFFFF1304 */
printf("%d¥n",  arr[1]);    /* 先頭から1つ分進んだアドレスの中身 = 2 */

int 型を4バイトとすると、arr の各要素は4バイト。要素を1つ先に進めると、アドレスは4つずつ飛ぶ


05-6 ポインタで配列を操作(*(p+1)(*p+1) の違い)

アドレスに対して +1 すると、そのデータ型のバイト数だけ加算されたアドレスを自動計算する(int * なら +4 バイト)。

int arr[3] = {10, 20, 30};
int *p = arr;
printf("%d¥n", *(p + 1));   /* 20 を出力 */

メモリマップ例

10
arr (arr+0)
0xFFFF1400
20
arr+1
0xFFFF1404
30
arr+2
0xFFFF1408
各マスの上が箱の中身(値)、下が式とアドレス。*(p+1)arr+1 の中身 = 20

順を追って整理する。

最頻出の差がつくポイント。カッコの位置で意味が変わる。
*(p + 1)アドレスを +1 してから中身 → arr[1] = 20
(*p + 1)中身(arr[0]=10)を取り出してから +111

改めて:ポインタ演算の2系統

データに対する演算(参照した「値」に対して演算する):

int x = 5;
int *p = &x;
(*p)++;   /* x の値が 6 になる(中身を取り出して +1) */

ポインタに対する演算(ポインタ自体を動かすと次のメモリ位置を指す):

int arr[3] = {10, 20, 30};
int *p = arr;
p++;                  /* 次の要素を指す */
printf("%d¥n", *p);   /* 20 */

混乱しやすい例(アドレスと値の違い):

int arr[3] = {1, 2, 3};
int *p = arr;
printf("%p¥n", p);        /* アドレスを出力 */
printf("%d¥n", *p);       /* 値(1)を出力 */
printf("%d¥n", *(p+1));   /* 次の値(2)を出力 */

05-7 文字列の書き換え(char ポインタ)

配列名が先頭アドレスである性質は、文字列(char 配列)でも同じ。ポインタ経由で中身を書き換えることもできる。

#include <stdio.h>

int main(void)
{
    char str[] = "Hello";
    char *p = str;

    *(p + 1) = 'a';        /* 2文字目(index 1)を 'a' に変更 */
    printf("%s¥n", str);   /* 出力: Hallo */

    return 0;
}

str の内容 "Hello"e(index 1)を a に書き換えるので、結果は Hallo になる。

char *p = str; は「p に str の先頭アドレスを入れる」。*(p+1) は str の2文字目そのもの(同じメモリ)なので、代入すると元の str が変わる。

05-8 多重ポインタ(int **

ポインタが別のポインタ変数を指す場合、この関係を多重ポインタと呼ぶ。多重ポインタはポインタ自体のアドレスを保持し、間接的にデータを参照する。

基本構造

int a = 10;
int *p = &a;      /* p は a のアドレスを保持するポインタ */
int **pp = &p;    /* pp は p のアドレスを保持するポインタ(多重ポインタ) */
変数説明
a10通常の整数型変数の値
p&aa のアドレスを保持するポインタ
pp&pp のアドレスを保持するポインタ(多重ポインタ)

アクセス方法

#include <stdio.h>

int main(void)
{
    int a = 10;
    int *p = &a;      /* p は a を指す */
    int **pp = &p;    /* pp は p を指す */

    /* 値の出力 */
    printf("a:    %d¥n", a);      /* 直接アクセス            → 10 */
    printf("*p:   %d¥n", *p);     /* p を介してアクセス       → 10 */
    printf("**pp: %d¥n", **pp);   /* pp を介して間接アクセス  → 10 */

    /* アドレスの出力 */
    printf("&a: %p¥n", (void *)&a);    /* a のアドレス */
    printf("p:  %p¥n", (void *)p);      /* p に格納されているアドレス */
    printf("*pp: %p¥n", (void *)*pp);   /* pp を介して得た p の中身(=a のアドレス) */

    return 0;
}

実行結果(値はすべて 10)

a:    10
*p:   10
**pp: 10
&a: 0x7ffee7e5a97c
p:  0x7ffee7e5a97c
*pp: 0x7ffee7e5a97c
pp は p のアドレス、*pp は p の中身(=a のアドレス)、**pp はそのまた中身(=a の値 10)。* を1つ外すごとに「1段だけ中身に潜る」と覚える。

05-9 ポインタ配列(char *[]

文字列(の先頭アドレス)をまとめて持つにはポインタ配列 char *[] を使う。各要素が「文字列の先頭アドレス」になる。配列名は先頭を指すので char ** で受けられる。

#include <stdio.h>

int main(void)
{
    char *instruments[] = {"violin", "cello", "flute", "horn"};
    char **ptr = instruments;   /* 配列の先頭アドレスを指すポインタ */
    int i;

    for (i = 0; i < 4; i++) {
        printf("%s¥n", *(ptr + i));   /* 各要素(文字列)を出力 */
    }

    return 0;
}

ptrinstruments 配列の先頭を指し、ポインタ演算 *(ptr + i) で各文字列を順に取り出す。

同じ考え方の例(こちらも char ** でポインタ配列の先頭を受ける):

#include <stdio.h>

int main(void)
{
    char *strings[] = {"apple", "banana", "cherry"};
    char **ptr = strings;   /* ポインタ配列の先頭を指す */
    int i;

    for (i = 0; i < 3; i++) {
        printf("%s¥n", *(ptr + i));
    }

    return 0;
}

05-10 応用:malloc による2次元配列(参考)

多重ポインタは動的メモリ管理や2次元配列の操作にも使える。「ポインタの配列」を確保し、その各要素にさらに行を確保する。やや難しいので、まずは「int ** で行を、int * で各行を確保する」という形だけ押さえればよい。

#include <stdio.h>
#include <stdlib.h>

int main(void)
{
    int rows = 2, cols = 3;
    int **matrix;
    int i, j;

    matrix = (int **)malloc(rows * sizeof(int *));   /* 行ぶんのポインタを確保 */

    for (i = 0; i < rows; i++) {
        matrix[i] = (int *)malloc(cols * sizeof(int));   /* 各行を確保 */
        for (j = 0; j < cols; j++) {
            matrix[i][j] = i + j;
        }
    }

    for (i = 0; i < rows; i++) {
        for (j = 0; j < cols; j++) {
            printf("%d ", matrix[i][j]);
        }
        printf("¥n");
    }

    for (i = 0; i < rows; i++) {
        free(matrix[i]);   /* 各行を解放 */
    }
    free(matrix);          /* 行ポインタの配列を解放 */

    return 0;
}
⚠ 動的に確保したメモリは必ず free で解放する(メモリリーク防止)。確保した順と逆順で、各行を解放してから matrix 本体を解放する。

05-11 注意点(ポインタの落とし穴)


05-12 📝 過去問で確認

過去問(配列とポインタ)— まず自分で解いてみよう
配列とポインタの過去問 問題①
問題 ①
配列とポインタの過去問 問題②
問題 ②
解説を表示
配列とポインタの過去問 解説①
解説 ①
配列とポインタの過去問 解説②
解説 ②

05-13 まとめ