[C言語のはじめ方] Part16: 二重ポインタの使い方

Step 3: ポインタの理解 – 二重ポインタの使い方

ポインタのポインタ? 二重ポインタをマスターして、C言語の表現力をさらに広げよう!

このステップの目標:

  • 二重ポインタの概念を理解する。
  • 二重ポインタの宣言方法と使い方を習得する。
  • 二重ポインタがどのような場面で役立つかを知る。

これまでの学習で、ポインタの基本的な使い方(アドレス演算子 &、間接参照演算子 *、配列との関係、関数での利用)を学びましたね。今回は、その応用編とも言える「二重ポインタ」について掘り下げていきましょう!

1. 二重ポインタとは?

「二重ポインタ」と聞くと、少し難しそうに感じるかもしれません。でも、基本的な考え方はシンプルです。

通常のポインタは、「変数のアドレス」を格納するための変数でした。例えば、int 型の変数 num のアドレスを格納するポインタ ptr は次のように宣言しましたね。

int num = 10;
int *ptr = # // ptr は num のアドレスを指す

ここで、ポインタ変数 ptr 自体もメモリ上のどこかに存在しています。つまり、ポインタ変数 ptr にもアドレスがあるということです。

二重ポインタは、この「ポインタ変数のアドレス」を格納するための変数なのです。言い換えると、「ポインタを指すポインタ」ということになります。図でイメージできないのが残念ですが、段階的に考えてみましょう。

  1. 普通の変数(例: int num)があり、値とアドレスを持っています。
  2. その変数のアドレスを指すポインタ(例: int *ptr)があります。このポインタも変数なので、値(指しているアドレス)と自身のアドレスを持っています。
  3. そのポインタ変数のアドレスを指すのが二重ポインタ(例: int **ptr_ptr)です。

ポイント: 通常のポインタが「住所録」だとすると、二重ポインタは「住所録の保管場所を示したメモ」のようなイメージです。住所録(ポインタ)そのものがどこにあるかを指し示します。

2. 二重ポインタの宣言と初期化

二重ポインタを宣言するには、アスタリスク * を2つ付けます。

データ型 **変数名;

例えば、int 型のポインタ変数を指す二重ポインタ ptr_ptr は次のように宣言します。

int **ptr_ptr;

初期化は、ポインタ変数のアドレスを代入することで行います。

#include <stdio.h>

int main() {
    int num = 100;
    int *ptr = &num;       // ptr は num のアドレスを指す
    int **ptr_ptr = &ptr; // ptr_ptr は ptr のアドレスを指す

    printf("num の値: %d\n", num);
    printf("ptr が指すアドレス: %p\n", (void *)ptr);
    printf("ptr が指す値 (num の値): %d\n", *ptr);

    printf("--------------------\n");

    printf("ptr 自体のアドレス: %p\n", (void *)&ptr);
    printf("ptr_ptr が指すアドレス (ptrのアドレス): %p\n", (void *)ptr_ptr);
    printf("ptr_ptr が指すポインタ (ptr) が指す値 (numの値): %d\n", **ptr_ptr);

    return 0;
}

この例では、ptr_ptr はポインタ変数 ptr のアドレスを格納しています。ptr_ptr の値は ptr のアドレスであり、ptr_ptr が指す先は ptr というポインタ変数そのものです。

注意: ポインタのアドレスを出力する際は %p 書式指定子を使い、(void *) でキャストするのが一般的です。これは、異なるポインタ型の間で安全に変換するためのお作法のようなものです。

3. 二重ポインタを使った値へのアクセス

二重ポインタを使って、最終的に指し示されている変数の値にアクセスするには、間接参照演算子 * を2回使います。

  • *ptr_ptr : これは、ptr_ptr が指しているポインタ変数そのもの(上記の例では ptr)にアクセスします。つまり、*ptr_ptr の値は、ptr が格納しているアドレス(num のアドレス)になります。
  • **ptr_ptr : これは、*ptr_ptr (つまり ptr) が指している最終的な変数の値(上記の例では num の値)にアクセスします。
#include <stdio.h>

int main() {
    int num = 250;
    int *ptr = &num;
    int **ptr_ptr = &ptr;

    printf("num のアドレス: %p\n", (void *)&num);
    printf("ptr が格納している値 (numのアドレス): %p\n", (void *)ptr);
    printf("ptr_ptr が格納している値 (ptrのアドレス): %p\n", (void *)ptr_ptr);

    printf("--------------------\n");

    // *ptr_ptr は ptr と同じ意味になる
    printf("*ptr_ptr が示すアドレス (numのアドレス): %p\n", (void *)*ptr_ptr);
    printf("ptr と *ptr_ptr は同じか? -> %s\n", (ptr == *ptr_ptr) ? "Yes" : "No");

    // **ptr_ptr は *ptr と同じ意味になり、最終的な値 num にアクセスする
    printf("**ptr_ptr が示す値 (numの値): %d\n", **ptr_ptr);
    printf("*ptr が示す値 (numの値): %d\n", *ptr);
    printf("num と **ptr_ptr は同じか? -> %s\n", (num == **ptr_ptr) ? "Yes" : "No");

    // 二重ポインタ経由で値を変更することも可能
    **ptr_ptr = 500;
    printf("値を変更後の num: %d\n", num); // num の値が 500 に変わる

    return 0;
}

このように、* を一つ付けるごとに、ポインタの階層を一つ深くたどっていくイメージです。

4. 二重ポインタの主な使い道

二重ポインタは少し複雑に見えますが、特定の状況で非常に役立ちます。主な使い道を2つ紹介します。

4.1. 動的に確保されたポインタの配列 (文字列の配列など)

プログラム実行時にサイズが決まるような「ポインタの配列」を扱いたい場合、二重ポインタがよく使われます。特に、複数の文字列(文字列は char * 型のポインタで扱われることが多い)をまとめて管理する際に便利です。

例えば、複数のユーザー名を格納する配列を考えてみましょう。各ユーザー名は文字列なので char * 型です。そして、それらの char * 型ポインタを格納するための配列が必要になります。この「char * 型ポインタの配列」の先頭アドレスを指すのが、char ** 型の二重ポインタになります。

#include <stdio.h>
#include <stdlib.h> // malloc, free を使うため

int main() {
    int num_users = 3;
    // char* 型のポインタを num_users 個格納できるメモリ領域を確保
    // usernames は、確保された領域の先頭 (char* 型ポインタ) を指すポインタ、つまり char** 型
    char **usernames = (char **)malloc(num_users * sizeof(char *));

    if (usernames == NULL) {
        fprintf(stderr, "メモリ確保に失敗しました。\n");
        return 1;
    }

    // 各要素 (char*) に文字列リテラルのアドレスを代入
    // 文字列リテラルは変更不可なメモリ領域に格納されることが多い
    usernames[0] = "Alice";
    usernames[1] = "Bob";
    usernames[2] = "Charlie";

    printf("ユーザーリスト:\n");
    for (int i = 0; i  num_users; i++) {
        // usernames[i] は i番目の char* ポインタ
        // usernames[i] は i番目の文字列の先頭アドレスを指す
        printf("- %s\n", usernames[i]);
    }

    // 重要: ポインタ配列自体のメモリを解放する
    // この例では文字列リテラルを指しているので、個々の文字列の解放は不要
    free(usernames);
    usernames = NULL; // 解放後は NULL を代入する習慣をつける (ダングリングポインタ防止)

    return 0;
}

この例では、まず malloc を使って「char * 型のポインタを格納するための領域」を確保しています。malloc は確保したメモリの先頭アドレス(void * 型)を返すので、それを char ** 型にキャストして usernames に代入します。

usernames[i]i 番目の char * ポインタにアクセスし、printf%s はそのポインタが指す文字列を表示します。

注意: この例では文字列リテラル("Alice" など)のアドレスを直接代入しています。文字列リテラルは通常、読み取り専用のメモリ領域に配置されるため、free する必要はありません。もし malloc などで動的に各文字列のメモリを確保した場合は、それらのメモリも個別に free する必要があります。メモリ管理については後のステップで詳しく学びます。

コマンドライン引数 argv も、実は char ** 型(またはそれに類する型)として扱われています。argv は文字列 (char *) の配列の先頭を指すポインタです。

4.2. 関数でポインタ変数の値(アドレス)を変更する

通常の関数呼び出しでは、引数に渡された変数のコピーが関数内で使われます(値渡し)。ポインタを引数に渡した場合(参照渡し)、関数内でそのポインタが指す先のを変更することはできます。

#include <stdio.h>

void change_value(int *p) {
    printf("change_value 内 (変更前): p が指す値 = %d\n", *p);
    *p = 200; // ポインタ p が指す先の値を変更
    printf("change_value 内 (変更後): p が指す値 = %d\n", *p);
}

int main() {
    int value = 100;
    int *ptr_value = &value;

    printf("main 内 (呼び出し前): value = %d\n", value);
    printf("main 内 (呼び出し前): ptr_value が指すアドレス = %p\n", (void *)ptr_value);

    change_value(ptr_value); // ポインタを渡す

    printf("main 内 (呼び出し後): value = %d\n", value); // 値が 200 に変わっている
    printf("main 内 (呼び出し後): ptr_value が指すアドレス = %p\n", (void *)ptr_value); // アドレスは変わらない

    return 0;
}

しかし、関数内でポインタ変数自体が指すアドレスを変更したい場合はどうでしょうか? 例えば、関数内で新しくメモリを確保し、そのアドレスを呼び出し元のポインタ変数に設定したい場合などです。

値渡しでは、関数に渡されるのはポインタ変数のコピーなので、関数内でそのコピー(仮引数)が指すアドレスを変更しても、呼び出し元のポインタ変数(実引数)には影響しません。

// これは意図通りに動作しない例
#include <stdio.h>
#include <stdlib.h>

void allocate_memory_wrong(int *p) {
    p = (int *)malloc(sizeof(int)); // p は main の ptr のコピーなので、この変更は main に伝わらない
    if (p != NULL) {
        *p = 50;
        printf("allocate_memory_wrong 内: 新しいメモリを確保し %d を設定しました (アドレス: %p)\n", *p, (void*)p);
    } else {
        printf("allocate_memory_wrong 内: メモリ確保失敗\n");
    }
}

int main() {
    int *ptr = NULL;
    printf("main 内 (呼び出し前): ptr = %p\n", (void *)ptr);

    allocate_memory_wrong(ptr); // ptr の値 (NULL) がコピーされて渡される

    printf("main 内 (呼び出し後): ptr = %p\n", (void *)ptr); // ptr は NULL のまま!

    // もし ptr が NULL でなければ解放するべきだが、この例では NULL のまま
    // if (ptr != NULL) {
    //     free(ptr);
    // }

    return 0;
}

このような場合に、二重ポインタを使います。関数にポインタ変数のアドレス(つまり、ポインタへのポインタ)を渡すことで、関数内で呼び出し元のポインタ変数が指すアドレスを変更できるようになります。

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

// ポインタ変数のアドレス (ポインタへのポインタ) を受け取る
void allocate_memory_correct(int **pp) {
    // *pp は main の ptr を指す
    *pp = (int *)malloc(sizeof(int)); // main の ptr が指すアドレスを変更する
    if (*pp != NULL) {
        **pp = 50; // 新しく確保したメモリ領域に値を設定
        printf("allocate_memory_correct 内: 新しいメモリを確保し %d を設定しました (アドレス: %p)\n", **pp, (void*)*pp);
    } else {
        printf("allocate_memory_correct 内: メモリ確保失敗\n");
    }
}

int main() {
    int *ptr = NULL;
    printf("main 内 (呼び出し前): ptr = %p\n", (void *)ptr);

    allocate_memory_correct(&ptr); // ptr のアドレス (&ptr) を渡す

    printf("main 内 (呼び出し後): ptr = %p\n", (void *)ptr); // ptr に新しいアドレスが設定されている!

    if (ptr != NULL) {
        printf("main 内 (呼び出し後): ptr が指す値 = %d\n", *ptr);
        free(ptr); // 確保したメモリを解放
        ptr = NULL;
    }

    return 0;
}

この正しい例では、allocate_memory_correct 関数は int **pp 型の引数を取ります。main 関数から呼び出す際には、ポインタ変数 ptr のアドレス &ptr を渡します。

関数内では、*ppmain 関数の ptr 変数そのものを指すことになります。そのため、*pp = ... という代入は、main 関数の ptr 変数の値を直接変更することになり、malloc で確保した新しいメモリのアドレスが ptr に設定されます。

5. 注意点

  • NULLポインタチェック: 二重ポインタを使う際は、ptr_ptr 自体が NULL でないか、そして *ptr_ptr(それが指すポインタ)が NULL でないか、状況に応じて両方のチェックが必要になることがあります。
  • メモリ解放: 動的にメモリを確保した場合(特にポインタの配列など)、確保した順番と逆の順番で解放するのが基本です。例えば、文字列の配列を動的に確保した場合、まず各文字列のメモリを解放し、次にポインタ配列自体のメモリを解放します。メモリ管理は慎重に行いましょう。
  • 複雑さ: ポインタの階層が増えると、コードが複雑になり、バグを生みやすくなります。本当に二重ポインタが必要な場面かよく考え、使う場合は分かりやすい変数名をつけたり、コメントを適切に残したりするなどの工夫が大切です。

6. まとめ

今回は、ポインタの応用である「二重ポインタ」について学びました。

  • 二重ポインタは「ポインタ変数のアドレス」を格納する変数(ポインタへのポインタ)。
  • データ型 **変数名; のように宣言し、ポインタ変数のアドレス &ポインタ変数 で初期化する。
  • 間接参照演算子 * を2回使う(**ptr_ptr)ことで、最終的に指されている値にアクセスできる。
  • 主な使い道は、動的に確保されたポインタの配列(文字列配列など)の管理や、関数内でポインタ変数が指すアドレス自体を変更したい場合。
  • NULLチェックやメモリ管理には十分注意が必要。

二重ポインタは、C言語のメモリ操作の柔軟性を高める強力なツールですが、同時に複雑さも増します。今回の内容をしっかり理解し、サンプルコードを実際に動かしてみることで、より深く理解できるはずです。焦らず、一歩ずつ進んでいきましょう!

これで Step 3「ポインタの理解」は完了です! ポインタはC言語の肝とも言える部分なので、しっかり復習しておきましょう。次は Step 4「構造体・共用体・列挙体」に進み、より複雑なデータ構造を扱えるようになりましょう!

コメントを残す

メールアドレスが公開されることはありません。 が付いている欄は必須項目です