読み込み中...

【C++】ポインタのポインタを使いこなす方法8選

C++のポインタのポインタを分かりやすく解説した記事のイメージ C++
この記事は約16分で読めます。

【サイト内のコードはご自由に個人利用・商用利用いただけます】

この記事では、プログラムの基礎知識を前提に話を進めています。

説明のためのコードや、サンプルコードもありますので、もちろん初心者でも理解できるように表現してあります。

本記事のサンプルコードを活用して機能追加、目的を達成できるように作ってありますので、是非ご活用ください。

※この記事は、一般的にプロフェッショナルの指標とされる『実務経験10,000時間以上』を満たす現役のプログラマチームによって監修されています。

※Japanシーモアは、常に解説内容のわかりやすさや記事の品質に注力しております。不具合、分かりにくい説明や不適切な表現、動かないコードなど気になることがございましたら、記事の品質向上の為にお問い合わせフォームにてご共有いただけますと幸いです。
(送信された情報は、プライバシーポリシーのもと、厳正に取扱い、処分させていただきます。)

はじめに

C++プログラミングを学ぶ上で避けて通れないのが、ポインタの理解です。

特に「ポインタのポインタ」は初心者にとって難解なコンセプトの一つですが、これをマスターすることで、C++の機能をより深く、効率的に活用することができるようになります。

この記事では、C++におけるポインタのポインタの基本から応用までを、初心者でも理解しやすいように段階的に解説していきます。

●C++のポインタのポインタとは

ポインタのポインタとは、文字通りポインタを指すポインタです。

C++では、変数はメモリ上に格納され、それぞれの変数はメモリアドレスを持っています。

ポインタはこのメモリアドレスを指し示すために用いられる変数です。

したがって、ポインタのポインタは、あるポインタのメモリアドレスを指し示す変数となります。

この概念は、複雑なデータ構造や動的なメモリ管理において非常に役立ちます。

○ポインタの基本概念の復習

ポインタの基本を理解することは、ポインタのポインタを理解する上で非常に重要です。

C++では、ポインタは変数のアドレスを保存するために使用されます。

例えば、int *ptrは、int型の変数を指し示すポインタです。ここで、*はポインタ変数を宣言するための演算子です。

また、ポインタを通して変数の値にアクセスするには、デリファレンス演算子*を使用します。

○ポインタのポインタの定義と重要性

ポインタのポインタを宣言するには、**を使用します。例えば、int **pptrは、int型のポインタを指し示すポインタを意味します。

ポインタのポインタは、複数のレベルで間接的にデータにアクセスする場合や、動的なデータ構造(例えば、リンクドリストやツリー構造)を扱う際に重要な役割を果たします。

また、関数の引数としてポインタを渡す際に、そのポインタ自体を変更する必要がある場合にもポインタのポインタが使用されます。

●ポインタのポインタの使い方

ポインタのポインタの使い方を理解するためには、まずは基本的な宣言方法から始めます。

ポインタのポインタは、通常のポインタと同じように宣言されますが、*の数が2つになります。

これは、ポインタを指し示すポインタであることを意味します。

ポインタのポインタは、多次元配列や動的なデータ構造、関数への引数としてのポインタの渡し方など、さまざまなシチュエーションで役立ちます。

○サンプルコード1:基本的なポインタのポインタの宣言と使用

ここでは、基本的なポインタのポインタの使用について考えてみましょう。

#include <iostream>
using namespace std;

int main() {
    int var = 23;     // 通常の変数
    int *ptr = &var;  // varを指すポインタ
    int **pptr = &ptr; // ptrを指すポインタのポインタ

    cout << "varの値: " << var << endl;
    cout << "ptrが指す値: " << *ptr << endl;
    cout << "pptrが指すポインタが指す値: " << **pptr << endl;

    return 0;
}

このコードでは、varという変数を宣言し、そのアドレスをptrが指し、さらにpptrptrのアドレスを指しています。

ここで**pptrを使用することで、varの値にアクセスすることができます。

○サンプルコード2:関数にポインタのポインタを渡す方法

ポインタのポインタを関数に渡すと、その関数内で元のポインタに影響を与えることができます。

この方法は、例えば関数内でメモリの割り当てを行い、その結果を呼び出し元のポインタに反映させたい場合などに有効です。

#include <iostream>
using namespace std;

void allocateMemory(int **ptr) {
    *ptr = new int; // 新しいint型メモリを割り当て
    **ptr = 5;      // 割り当てたメモリに値を設定
}

int main() {
    int *ptr = nullptr;
    allocateMemory(&ptr);

    cout << "割り当てたメモリの値: " << *ptr << endl;
    delete ptr; // メモリの解放

    return 0;
}

この例では、allocateMemory関数がポインタのポインタを受け取り、新しいメモリ領域を割り当て、値を設定しています。

呼び出し側では、この変更が反映されていることが確認できます。

○サンプルコード3:動的メモリ管理におけるポインタのポインタ

動的メモリ管理では、ポインタのポインタが非常に役立ちます。

これは、特に動的に割り当てられた配列や、柔軟に変更可能なデータ構造を扱う際に重要になります。

下記のサンプルコードでは、動的に2次元配列を割り当て、操作する方法を表しています。

#include <iostream>
using namespace std;

int main() {
    int rows = 2, cols = 3;
    int **array = new int*[rows]; // 行のためのメモリ割り当て

    // 各行に列のためのメモリを割り当て
    for(int i = 0; i < rows; ++i) {
        array[i] = new int[cols];
    }

    // 2次元配列の初期化と表示
    for(int i = 0; i < rows; i++) {
        for(int j = 0; j < cols; j++) {
            array[i][j] = i * cols + j; // 何らかの値で初期化
            cout << array[i][j] << " ";
        }
        cout << endl;
    }

    // 割り当てたメモリの解放
    for(int i = 0; i < rows; ++i) {
        delete[] array[i];
    }
    delete[] array;

    return 0;
}

このコードでは、まずint**型であるarrayに、行数に相当するメモリを割り当てます。

次に、各行に対して列数分のメモリを割り当てます。

こうして動的に生成された2次元配列に値を設定し、それを表示した後、使用したメモリを適切に解放します。

動的メモリ管理では、使用後のメモリ解放が非常に重要です。

メモリリークを防ぐため、割り当てたメモリは必ず解放する必要があります。

○サンプルコード4:配列とポインタのポインタの関係性

C++では、配列の名前はその配列の最初の要素を指すポインタとして機能します。

これは、ポインタと配列が密接に関連していることを表しています。

特に、多次元配列では、ポインタのポインタが非常に有効に使用されます。

下記のサンプルコードは、ポインタのポインタを使用して2次元配列にアクセスする方法を表しています。

#include <iostream>
using namespace std;

int main() {
    int array[3][2] = {{0, 1}, {2, 3}, {4, 5}};
    int (*ptr)[2] = array; // 2要素の整数配列を指すポインタ

    // ポインタを使って配列の要素にアクセス
    for(int i = 0; i < 3; ++i) {
        for(int j = 0; j < 2; ++j) {
            cout << ptr[i][j] << " ";
        }
        cout << endl;
    }

    return 0;
}

このコードでは、arrayという2次元配列があり、ptrというポインタがこの配列の最初の要素(ここでは1次元配列)を指しています。

このポインタを使用して、配列の各要素にアクセスしています。

○サンプルコード5:ポインタのポインタを使ったデータ構造の操作

ポインタのポインタは、動的にデータ構造を操作する際に特に役立ちます。

リンクドリストやツリーなどのデータ構造では、ノード間の接続をポインタで表します。

下記のサンプルコードは、簡単なリンクドリストを操作する例を表しています。

#include <iostream>
using namespace std;

struct Node {
    int data;
    Node* next;
};

void insertNode(Node** head, int newData) {
    Node* newNode = new Node(); // 新しいノードの作成
    newNode->data = newData;
    newNode->next = *head; // 新しいノードが古いヘッドを指すようにする
    *head = newNode; // ヘッドを新しいノードに更新
}

void printList(Node* node) {
    while(node != nullptr) {
        cout << node->data << " ";
        node = node->next;
    }
    cout << endl;
}

int main() {
    Node* head = nullptr;

    insertNode(&head, 3);
    insertNode(&head, 2);
    insertNode(&head, 1);

    printList(head);

    return 0;
}

このコードでは、insertNode関数がリンクドリストの先頭に新しいノードを挿入します。

ポインタのポインタを使用することで、リンクドリストのヘッドを効率的に更新できます。

●ポインタのポインタでよくあるエラーと対処法

C++におけるポインタのポインタの使用においては、特有のエラーが生じることがあります。

これらのエラーを正しく理解し、適切に対処することは重要です。

○エラーの種類とその原因

ポインタのポインタを使用する際に起こり得る一般的なエラーには、下記のようなものがあります。

ポインタが無効なメモリ領域を指すことによる不正なアクセス、動的に割り当てたメモリの適切な解放を行わないことによるメモリリーク、またはポインタが意図しないメモリ領域を参照してしまうことによるセグメンテーション違反です。

これらは、プログラムが予期せぬ挙動を示す原因となり、場合によってはクラッシュを引き起こす可能性もあります。

○対処法の詳細と例

これらのエラーに対処するためには、下記のような手法が有効です。

ポインタを使用する前には、必ず初期化を行い、使用後は適切にメモリを解放することが重要です。

また、ポインタが有効なメモリ領域を指していることを確認するために、検査を行うことも重要です。

例えば、ポインタをnullptrで初期化し、new演算子でメモリを割り当てた後は、delete演算子でメモリを解放します。スマートポインタの使用も、メモリリークを防ぐための効果的な方法です。

セグメンテーション違反を避けるためには、ポインタが指す範囲内でのみアクセスするように注意が必要です。

これらの対策を適切に実施することで、ポインタのポインタを安全に使用することができます。

●ポインタのポインタの応用例

C++でのポインタのポインタの使用方法は多岐にわたり、様々な応用例が考えられます。

特に、データ構造やアルゴリズムの実装において、ポインタのポインタは非常に有用です。

○サンプルコード6:多次元配列の動的確保と操作

多次元配列は、ポインタのポインタを使って動的に確保し操作することができます。

下記のコードは、動的に2次元配列を確保し、その要素にアクセスする方法を表しています。

#include <iostream>
using namespace std;

int main() {
    int rows = 3, cols = 4;
    int** matrix = new int*[rows];

    for(int i = 0; i < rows; ++i) {
        matrix[i] = new int[cols];
    }

    // 配列の値を設定
    for(int i = 0; i < rows; ++i) {
        for(int j = 0; j < cols; ++j) {
            matrix[i][j] = i * cols + j;
        }
    }

    // 配列の値を表示
    for(int i = 0; i < rows; ++i) {
        for(int j = 0; j < cols; ++j) {
            cout << matrix[i][j] << " ";
        }
        cout << endl;
    }

    // メモリの解放
    for(int i = 0; i < rows; ++i) {
        delete[] matrix[i];
    }
    delete[] matrix;

    return 0;
}

このコードは、まず行に対応するポインタの配列を確保し、各行に対してさらに列に対応するポインタの配列を確保します。

こうして確保した2次元配列に値を設定し、表示しています。

使用後は確保したメモリを適切に解放することが重要です。

○サンプルコード7:リンクドリストとポインタのポインタ

リンクドリストの操作においても、ポインタのポインタは非常に役立ちます。

リンクドリストのノードの挿入や削除を行う際、ポインタのポインタを用いることで、より簡潔で効率的なコードが書けます。

ここでは、リンクドリストへのノードの挿入を行う例を紹介します。

#include <iostream>
using namespace std;

struct Node {
    int data;
    Node* next;
};

void insertAtBeginning(Node** head, int newData) {
    Node* newNode = new Node();
    newNode->data = newData;
    newNode->next = *head;
    *head = newNode;
}

void printList(Node* node) {
    while(node != nullptr) {
        cout << node->data << " ";
        node = node->next;
    }
    cout << endl;
}

int main() {
    Node* head = nullptr;
    insertAtBeginning(&head, 10);
    insertAtBeginning(&head, 20);
    insertAtBeginning(&head, 30);

    printList(head);

    return 0;
}

この例では、リンクドリストの先頭に新しいノードを挿入するために、ポインタのポインタを使用しています。

insertAtBeginning関数は、リンクドリストのヘッドへのポインタのポインタを引数として受け取り、新しいノードをリストの先頭に挿入します。

○サンプルコード8:グラフ構造とポインタのポインタ

グラフ構造の操作においても、ポインタのポインタは重要な役割を果たします。

特に動的なグラフ構造を扱う際に、ノード間の接続をポインタのポインタを使って効率的に表現できます。

#include <iostream>
using namespace std;

struct Node {
    int data;
    int neighborCount;
    Node **neighbors; // 隣接ノードへのポインタの配列

    Node(int data, int nCount) : data(data), neighborCount(nCount) {
        neighbors = new Node*[nCount];
        for(int i = 0; i < nCount; ++i) {
            neighbors[i] = nullptr;
        }
    }

    ~Node() {
        delete[] neighbors;
    }
};

int main() {
    Node *node1 = new Node(1, 2); // ノード1
    Node *node2 = new Node(2, 0); // ノード2
    Node *node3 = new Node(3, 1); // ノード3

    // 隣接関係の構築
    node1->neighbors[0] = node2;
    node1->neighbors[1] = node3;
    node3->neighbors[0] = node2;

    // グラフ構造を操作するコード

    // メモリ解放
    delete node1;
    delete node2;
    delete node3;

    return 0;
}

このサンプルコードでは、Node構造体にデータと隣接ノードへのポインタの配列neighborsを持たせています。

この配列を通して、グラフのノード間の接続関係を表現しています。

各ノードは動的にメモリが割り当てられ、隣接関係が設定された後、不要になった際に適切にメモリを解放することが重要です。

●C++プログラミングにおけるポインタのポインタの豆知識

ポインタのポインタは、C++プログラミングにおいて多くのシチュエーションで便利であり、より深い知識と理解が求められます。

ここでは、ポインタのポインタに関する幾つかの豆知識を紹介し、それらがC++プログラミングにおいてどのように役立つかを探ります。

○豆知識1:ポインタのポインタとオブジェクト指向

C++では、オブジェクト指向プログラミングのパラダイムをサポートしています。

ポインタのポインタを使用することで、オブジェクトの配列や複雑なデータ構造の管理が容易になります。

例えば、オブジェクトのポインタの配列を動的に管理したり、ポインタを介してオブジェクトのメソッドにアクセスしたりすることが可能です。

これにより、プログラムの柔軟性が増し、より複雑な構造を効果的に扱えるようになります。

○豆知識2:ポインタのポインタと最適化テクニック

C++プログラミングにおいて、パフォーマンスの最適化は重要な側面の一つです。

ポインタのポインタを利用することで、メモリの使用効率を改善したり、アルゴリズムの実行速度を向上させたりすることができます。

例えば、大きなデータセットを扱う際、ポインタのポインタを用いて間接的にデータにアクセスすることで、データのコピーに要するコストを削減できます。

また、動的プログラミングやメモリキャッシュの最適化など、多くのアルゴリズムにおいてポインタのポインタが役立ちます。

まとめ

C++におけるポインタのポインタは、プログラミングの様々な側面で重要な役割を果たします。

この記事では、基本的な使い方から動的メモリ管理、データ構造の操作、そしてよくあるエラーとその対処法に至るまで、幅広く解説しました。

ポインタのポインタを適切に理解し使用することで、C++プログラミングの可能性を大きく広げることができます。

これにより、プログラムの効率性、柔軟性、信頼性が向上し、より複雑な問題への対応が可能になるでしょう。