6日でできる 新C言語入門|ポインタのポインタ(ダブルポインタ)

ポインタを指すポインタを理解すると、C言語の見える世界が一段深くなる。
ダブルポインタの仕組みを学び、文字列配列や動的メモリ確保を扱える力を身につけよう。

C言語でポインタの基本を学ぶと、変数のアドレスをポインタ変数に保存できることが分かります。

たとえば、int型の変数scoreがあり、そのアドレスをint*型のポインタpScoreに保存すると、pScoreはscoreを指している状態になります。そして、*pScoreを使うと、pScoreが指しているscoreの値を読み書きできます。

ここからさらに一歩進むと、ポインタのポインタという考え方が登場します。これは、ダブルポインタや二重ポインタとも呼ばれます。

ダブルポインタは、普通の変数のアドレスではなく、ポインタ変数そのもののアドレスを保存するポインタです。

普通のポインタが変数を指すのに対して、ダブルポインタはポインタ変数を指します。つまり、値へたどり着くまでに、アドレスを2段階たどるイメージです。

最初は難しく見えますが、考え方を分けると整理できます。

変数は値を持ちます。
ポインタは変数のアドレスを持ちます。
ダブルポインタはポインタ変数のアドレスを持ちます。

ダブルポインタは、文字列の配列を関数に渡す場合や、関数の中で呼び出し元のポインタ変数を書き換えたい場合に使われます。特に、動的メモリ確保で確保したアドレスを関数の外へ返したい場合には、とても重要な考え方になります。

この記事では、ダブルポインタの基本、アドレスを2段階たどる仕組み、文字列配列での使い方、関数内でポインタ自体を変更する方法を、サンプルプログラムを使ってやさしく解説していきます。

ダブルポインタはポインタ変数のアドレスを保存する

通常の変数、ポインタ、ダブルポインタの関係を整理してみましょう。

種類保存するもの
通常の変数int score;整数の値
ポインタint* pScore;int型変数のアドレス
ダブルポインタint** ppScore;int*型ポインタ変数のアドレス

たとえば、次のような関係を考えます。

int score = 75;
int* pScore = &score;
int** ppScore = &pScore;

scoreは普通のint型変数です。75という値を持っています。

pScoreはint型変数を指すポインタです。scoreのアドレスを保存しています。

ppScoreはint*型のポインタを指すダブルポインタです。pScoreのアドレスを保存しています。

このように、ダブルポインタは普通の変数を直接指すのではなく、ポインタ変数を指します。

値へたどり着くまでの段階

ダブルポインタでは、*を使って段階的に中身をたどります。

意味
scorescoreの値
&scorescoreのアドレス
pScorescoreのアドレス
*pScorepScoreが指す先の値、つまりscoreの値
&pScorepScore自身のアドレス
ppScorepScoreのアドレス
*ppScoreppScoreが指す先の値、つまりpScoreの値
**ppScorepScoreが指す先の値、つまりscoreの値

ここで重要なのは、*を1つ使うと1段階たどるということです。

ppScoreはpScoreのアドレスを持っています。
*ppScoreはpScoreそのものを表します。
**ppScoreはpScoreが指しているscoreの値を表します。

図:変数・ポインタ・ダブルポインタの関係

この図から分かること

この図から分かるのは、ダブルポインタが普通の変数を直接指すのではなく、ポインタ変数を指しているということです。

pScoreはscoreのアドレスを保存しています。ppScoreはpScoreのアドレスを保存しています。そのため、ppScoreからscoreの値へたどるには、まずpScoreへ進み、さらにpScoreが指しているscoreへ進むという2段階の流れになります。

ダブルポインタの基本を確認するプログラム

次のプログラムでは、変数、ポインタ、ダブルポインタの値とアドレスを表示します。

プロジェクト/ファイル名: Lesson61_1/main.c

#include <stdio.h>

int main(void) {
    int score = 75;
    int* pScore = &score;
    int** ppScore = &pScore;

    // 通常の変数の値とアドレスを表示する
    printf("scoreの値: %d\n", score);
    printf("scoreのアドレス: %p\n", (void*)&score);

    // ポインタpScoreの内容を表示する
    printf("pScoreの値、つまりscoreのアドレス: %p\n", (void*)pScore);
    printf("pScoreが指す先の値: %d\n", *pScore);
    printf("pScore自身のアドレス: %p\n", (void*)&pScore);

    // ダブルポインタppScoreの内容を表示する
    printf("ppScoreの値、つまりpScoreのアドレス: %p\n", (void*)ppScore);
    printf("ppScoreが指す先の値、つまりpScoreの値: %p\n", (void*)*ppScore);
    printf("ppScoreから2段階たどった値: %d\n", **ppScore);

    return 0;
}

実行例

scoreの値: 75
scoreのアドレス: 000000A95E6FF7A4
pScoreの値、つまりscoreのアドレス: 000000A95E6FF7A4
pScoreが指す先の値: 75
pScore自身のアドレス: 000000A95E6FF7C8
ppScoreの値、つまりpScoreのアドレス: 000000A95E6FF7C8
ppScoreが指す先の値、つまりpScoreの値: 000000A95E6FF7A4
ppScoreから2段階たどった値: 75

アドレスの値は、実行環境や実行するタイミングによって変わります。そのため、実行例と同じアドレスにならなくても問題ありません。

大切なのは、pScoreの値がscoreのアドレスになっていること、ppScoreの値がpScoreのアドレスになっていることを確認することです。

*ppScoreと**ppScoreの違い

ダブルポインタを読むときは、*がいくつ付いているかに注目します。

*ppScore

これは、ppScoreが指している先の値を表します。ppScoreはpScoreのアドレスを持っているので、*ppScoreはpScoreの値になります。つまり、scoreのアドレスです。

**ppScore

これは、ppScoreから2段階たどった値です。
1段階目でpScoreへたどり、2段階目でpScoreが指すscoreの値へたどります。

整理すると、次のようになります。

たどる段階結果
ppScore0段階pScoreのアドレス
*ppScore1段階pScoreの値、つまりscoreのアドレス
**ppScore2段階scoreの値

この関係を落ち着いて読めるようになると、ダブルポインタの理解がかなり進みます。

ダブルポインタは文字列配列でもよく使われる

ダブルポインタは、文字列の配列を扱うときにもよく登場します。

C言語の文字列は、文字の並びを表す配列として扱われます。そして、文字列を指すポインタはchar*型で表せます。

複数の文字列を並べる場合、char*の配列を使うことがあります。

char* menu[] = { "カレー", "うどん", "そば" };

menuは、文字列へのポインタを複数持つ配列です。

このような配列を関数に渡すと、関数側ではchar**として受け取ることができます。

void showMenu(char** menu, int count)

char**は、char*を指すポインタとして考えることができます。

つまり、文字列へのポインタが並んでいる先頭を受け取る形になります。

図:文字列配列とダブルポインタの関係

この図から分かること

この図から分かるのは、文字列配列が文字列そのものを直接並べているというより、文字列へのポインタを並べているように考えられるということです。

menu[0]、menu[1]、menu[2]は、それぞれ別の文字列を指しています。このようなchar*の並びを関数に渡すと、関数側ではchar**として受け取ることができます。

複数の文字列を関数に渡すプログラム

次のプログラムでは、複数の商品名を文字列配列として用意し、それを関数に渡して一覧表示します。

プロジェクト/ファイル名: Lesson61_2/main.c

#include <stdio.h>

// 文字列の配列を受け取り、一覧表示する関数
void showMenu(char** menu, int count) {
    int i;

    printf("メニュー一覧:\n");

    for (i = 0; i < count; i++) {
        printf("%d: %s\n", i + 1, menu[i]);
    }
}

int main(void) {
    char* menuList[] = { "カレー", "うどん", "そば", "ラーメン" };

    // 文字列配列を関数に渡す
    showMenu(menuList, 4);

    return 0;
}

実行結果

メニュー一覧:
1: カレー
2: うどん
3: そば
4: ラーメン

このプログラムでは、menuListという配列を用意しています。

char* menuList[] = { "カレー", "うどん", "そば", "ラーメン" };

menuListは、char*型の要素を複数持つ配列です。各要素は文字列を指しています。

showMenu関数では、引数としてchar** menuを受け取っています。

void showMenu(char** menu, int count)

関数の中では、menu[i]で各文字列にアクセスできます。

printf("%d: %s\n", i + 1, menu[i]);

文字列配列を関数に渡すときにchar**が出てくる理由は、char*が複数並んだものを受け取るためです。

char*の配列とchar**の関係

char*の配列は、文字列へのポインタが並んでいる形です。

char* menuList[] = { "カレー", "うどん", "そば" };

このmenuListを関数に渡すと、先頭要素へのポインタとして扱われます。

menuListの各要素はcharです。
そのchar
の並びの先頭を指すので、関数側ではchar**として受け取れるわけです。

書き方意味
char*文字列を指すポインタ
char* menuList[]文字列へのポインタの配列
char** menuchar*の並びを受け取るポインタ

この関係は、最初は少し複雑に見えます。まずは、char*が文字列を指し、char**が文字列ポインタの並びを受け取る、と考えると分かりやすいです。

関数の中でポインタ自体を変更したい場合

ダブルポインタは、関数の中で呼び出し元のポインタ変数そのものを変更したい場合にも使います。

通常、C言語の関数では、引数は値渡しです。

つまり、関数に変数を渡しても、その変数のコピーが関数側に渡されます。ポインタ変数を渡した場合も、ポインタの値、つまりアドレスのコピーが渡されます。

そのため、関数内でポインタ変数に新しいアドレスを代入しても、呼び出し元のポインタ変数自体は変わりません。

呼び出し元のポインタ変数そのものを書き換えたい場合は、そのポインタ変数のアドレスを渡す必要があります。

そこで、ダブルポインタを使います。

やりたいこと使う引数
ポインタが指す先の値を変更するint*
ポインタ変数そのものを変更するint**

たとえば、関数の中でメモリを確保し、そのアドレスを呼び出し元のポインタにセットしたい場合、int**を使います。

関数内で配列用メモリを確保するプログラム

次のプログラムでは、関数の中でint型配列のメモリを確保し、呼び出し元のポインタにそのアドレスをセットします。

プロジェクト/ファイル名: Lesson61_3/main.c

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

// 点数を保存する配列を作成する関数
int createScores(int** pScores, int count) {
    int i;

    // count個分のint型領域を確保する
    *pScores = (int*)malloc(sizeof(int) * count);

    if (*pScores == NULL) {
        printf("メモリの確保に失敗しました。\n");
        return 0;
    }

    // 確保した配列に初期値を入れる
    for (i = 0; i < count; i++) {
        (*pScores)[i] = (i + 1) * 10;
    }

    return 1;
}

int main(void) {
    int* scores = NULL;
    int count = 5;
    int i;
    int result;

    // scores自体を書き換えてもらうために、scoresのアドレスを渡す
    result = createScores(&scores, count);

    if (result) {
        printf("作成した点数配列:\n");

        for (i = 0; i < count; i++) {
            printf("%d番目: %d\n", i + 1, scores[i]);
        }

        // 確保したメモリを解放する
        free(scores);
        scores = NULL;
    }

    return 0;
}

実行結果

作成した点数配列:
1番目: 10
2番目: 20
3番目: 30
4番目: 40
5番目: 50

このプログラムでは、main関数側にscoresというポインタ変数があります。

int* scores = NULL;

最初はどこも指していない状態です。

createScores関数では、int** pScoresを引数として受け取ります。

int createScores(int** pScores, int count)

main関数からは、scoresのアドレスを渡しています。

result = createScores(&scores, count);

これにより、createScores関数は呼び出し元のscoresそのものを変更できます。

関数内では、mallocで確保したメモリのアドレスを*pScoresに代入しています。

*pScores = (int*)malloc(sizeof(int) * count);

pScoresはscoresのアドレスを持っています。
そのため、*pScoresは呼び出し元のscoresを表します。

つまり、*pScoresに代入することで、main関数側のscoresに新しいアドレスが入ります。

(*pScores)[i]の意味

createScores関数の中では、次のような書き方をしています。

(*pScores)[i] = (i + 1) * 10;

これは、確保した配列のi番目に値を代入する処理です。

ここで丸かっこが重要です。

*pScoresは、呼び出し元のscoresを表します。
(*pScores)[i]は、scoresが指している配列のi番目を表します。

もし丸かっこを付けないと、意味が変わってしまいます。

書き方意味
*pScores呼び出し元のscores
(*pScores)[i]scoresが指す配列のi番目
*pScores[i]解釈が異なり、意図しない参照になりやすい

ダブルポインタと配列の添え字を組み合わせるときは、丸かっこを使って、どの部分を先に評価したいのかをはっきりさせることが大切です。

違いを表で整理

書き方実際の解釈意味
(*pScores)[i](*pScores)[i]pScores が指すポインタを取り出し、その配列の i 番目にアクセスする
*pScores[i]*(pScores[i])pScores[i] を先に取り出し、そのポインタが指す値にアクセスする

今回のように、関数内で malloc した配列に値を入れる場合は、(*pScores)[i] を使うと覚えると分かりやすいです。

mallocで確保したメモリはfreeで解放する

Lesson61_3では、mallocを使ってメモリを確保しています。

*pScores = (int*)malloc(sizeof(int) * count);

mallocで確保したメモリは、使い終わったらfreeで解放します。

free(scores);
scores = NULL;

freeを忘れると、使い終わったメモリが解放されず、メモリリークの原因になります。

また、freeしたあとのポインタには、以前のアドレスが残っています。そのまま使うと危険なので、NULLを代入しておくと安全です。

処理目的
malloc必要なメモリを確保する
NULLチェックメモリ確保に失敗していないか確認する
free確保したメモリを解放する
NULL代入解放後に誤って使う危険を減らす

動的メモリ確保とダブルポインタは、C言語の応用でよく一緒に登場します。

ダブルポインタで注意したいこと

ダブルポインタは便利ですが、扱いを間違えると分かりにくいバグにつながります。

初期化されていないポインタを使わない

ポインタ変数は、使う前にNULLで初期化するか、有効なアドレスを代入してから使います。

int* scores = NULL;

何段階たどっているかを意識する

ダブルポインタでは、*を1つ使うと1段階、**を使うと2段階たどります。

意味
ppScoreポインタ変数のアドレス
*ppScoreポインタ変数の値
**ppScore最終的に指しているデータの値

実体とアドレスを混同しない

ポインタ変数を関数の中で変更したい場合は、そのポインタ変数のアドレスを渡します。

createScores(&scores, count);

この&scoresは、scoresが指している先ではなく、scores自身のアドレスです。

mallocの結果を確認する

mallocは、メモリの確保に失敗するとNULLを返すことがあります。必ず確認してから使いましょう。

if (*pScores == NULL) {
    printf("メモリの確保に失敗しました。\n");
    return 0;
}

ダブルポインタの使いどころを整理する

ダブルポインタは、どのような場面で使うのでしょうか。

使いどころ内容
ポインタ変数のアドレスを扱うポインタそのものを指す
関数内でポインタを変更する呼び出し元のポインタへ新しいアドレスを代入する
文字列配列を扱うchar*の配列をchar**として受け取る
動的メモリ確保と組み合わせる関数内で確保したメモリを呼び出し元へ渡す
多段階のデータ構造を扱う配列の配列やリスト構造などにつながる

ダブルポインタは、単に難しい文法というより、ポインタ変数そのものを操作したいときに必要になる仕組みです。

特に、関数の中でポインタの指す先を変えたいときに使う、と考えると実用的に理解しやすくなります。

ダブルポインタを理解するとポインタの使い方が深まる

ダブルポインタは、ポインタのアドレスを保存するためのポインタです。

普通のポインタは変数のアドレスを保存します。ダブルポインタは、そのポインタ変数のアドレスを保存します。そのため、値にたどり着くには2段階の参照が必要になります。

ppScoreはpScoreのアドレスを持ちます。
*ppScoreはpScoreの値を表します。
**ppScoreはpScoreが指しているscoreの値を表します。

また、char*の配列を関数に渡すときには、char**として受け取る場面があります。文字列配列を扱うときに、ダブルポインタの考え方が役立ちます。

さらに、関数の中で呼び出し元のポインタ変数を書き換えたい場合にも、ダブルポインタが必要です。mallocで確保したメモリのアドレスを呼び出し元へセットするような処理では、int**のような形が使われます。

ダブルポインタは、最初は複雑に見えます。しかし、何を指しているのか、*をいくつ使って何段階たどっているのかを丁寧に追えば、少しずつ理解できるようになります。ポインタをより実践的に使うための大切なステップとして、変数、ポインタ、ダブルポインタの関係をしっかり押さえておきましょう。