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 = # はメモリ上で次のようになる。

          メモリ位置例 | データ
         -------------+------------
num       0xFFFF1230  |    3        (num の値)
p         0xFFFF4560  | 0xFFFF1230  (p に格納されたアドレス=num の位置)

つまり 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;

          メモリ位置例 | データ
         -------------+------------
x         0xFFFF1200  |   20        (x の値)
p         0xFFFF1250  | 0xFFFF1200  (p に格納されたアドレス)
⚠ ポインタ変数を宣言しただけ(int *p; だけ)で *p を読み書きすると、どこを指しているか分からない(未初期化)ため未定義動作になる。必ず &変数 などで初期化してから使う。

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

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

配列名と先頭アドレス

int arr[3] = {1, 2, 3};
printf("%p¥n", arr);       /* 配列の先頭アドレス */
printf("%p¥n", &arr[0]);   /* 同じ値(先頭要素のアドレス) */
  メモリ位置  | データ
  -----------+---------
  0xFFFF1300 |   1       ・・・ arr[0] = 1   (arr = 0xFFFF1300)
  0xFFFF1304 |   2       ・・・ arr[1] = 2
  0xFFFF1308 |   3       ・・・ arr[2] = 3
✅ コード上の 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 を出力 */

メモリマップ例

  メモリ位置  | データ
  -----------+---------
  0xFFFF1400 |   10      ・・・ arr[0] = 10   (arr   = 0xFFFF1400)
  0xFFFF1404 |   20      ・・・ arr[1] = 20   (arr+1 = 0xFFFF1404)
  0xFFFF1408 |   30      ・・・ arr[2] = 30   (arr+2 = 0xFFFF1408)

順を追って整理する。

最頻出の差がつくポイント。カッコの位置で意味が変わる。
*(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 まとめ


05-14 理解度チェック

Q1. int num = 5; int *p = &num; のとき、p に格納されるものはどれか。

解説を表示

正解:ウ(num のアドレス)

&num は num のアドレス。ポインタ変数 p にはアドレスが入る(値「5」が入るわけではない)。num の値を取り出したいときは *p とする。

Q2. int arr[3] = {10, 20, 30}; int *p = arr; のとき、*(p + 1) の値はどれか。

解説を表示

正解:イ(20)

parr の先頭(arr[0])を指す。p + 1 は次の要素 arr[1] のアドレス。*(p + 1) はその中身なので 20。

Q3. 同じく int arr[3] = {10, 20, 30}; int *p = arr; のとき、(*p + 1) の値はどれか。

解説を表示

正解:エ(11)

カッコが先頭にあるので、まず *p(arr[0] の中身=10)を取り出し、それに +1 する。よって 11。*(p+1)(=20)との違いに注意。

Q4. 次のコードの実行後、str の内容はどれか。

char str[] = "Hello";
char *p = str;
*(p + 1) = 'a';
printf("%s¥n", str);
解説を表示

正解:イ(Hallo)

p は str の先頭を指す。*(p + 1) は str の2文字目(index 1 = 'e')そのもの。そこへ 'a' を代入するので "Hallo" になる。

Q5. int a = 10; int *p = &a; int **pp = &p; のとき、**pp の値はどれか。

解説を表示

正解:ア(10)

pp は p のアドレス、*pp は p の中身(=a のアドレス)、**pp はそのまた中身(=a の値)。よって 10。* を外すごとに1段ずつ中身へ潜る。

Q6. 配列名 arrint arr[5];)が表すものはどれか。

解説を表示

正解:ウ(先頭アドレス)

「配列名は、その配列の先頭アドレス」。arr&arr[0] は同じ値になる。だから int *p = arr; のように、配列名をそのままポインタに代入できる。

Q7. int 型を4バイトとする。int *p; に対して p + 1 は、p から何バイト進んだアドレスになるか。

解説を表示

正解:ウ(4バイト)

ポインタの +1 は「指す型のサイズぶん」進む(自動計算)。int *(4バイト)なら +4 バイト、char *(1バイト)なら +1 バイト、double *(8バイト)なら +8 バイト。だから *(p+1) がちょうど隣の要素を指す。

Q8. 宣言 char *strings[] = {"apple", "banana", "cherry"}; の各要素 strings[0] が表すものはどれか。

解説を表示

正解:エ("apple" の先頭アドレス)

char *[]ポインタ配列で、各要素が「文字列の先頭アドレス(char *)」。strings[0] は "apple" の先頭を指すので printf("%s", strings[0]); で "apple" を表示できる。配列全体の先頭は char ** で受けられる。