[C言語] アスタリスク2つ付ける意味と使い方を徹底解説

C言語において、アスタリスク2つ**は「ポインタのポインタ」を表します。

これは、ポインタが指す先にさらに別のポインタがあることを意味します。

例えば、int **ptrは、整数型へのポインタを指すポインタです。

ポインタのポインタは、動的な2次元配列の管理や、関数内でポインタを変更したい場合に使われます。

関数にポインタを渡す際、ポインタ自体を変更したい場合は、ポインタのポインタを引数として渡すことで、関数内で元のポインタを変更することが可能です。

この記事でわかること
  • ポインタのポインタの基本的な概念とその宣言方法
  • 2次元配列やリンクリストなどのデータ構造におけるポインタのポインタの具体的な活用法
  • メモリリークを防ぐための注意点とNULLポインタの確認方法
  • ポインタのポインタを使った文字列操作やファイル操作の応用例
  • 関数内でポインタの指す先を変更する方法とその利点

目次から探す

ポインタの基礎知識

ポインタはC言語における重要な概念であり、メモリ管理や効率的なデータ操作に欠かせない要素です。

このセクションでは、ポインタの基本的な概念から宣言、初期化、操作方法について詳しく解説します。

ポインタとは何か

ポインタとは、メモリ上の特定のアドレスを指し示す変数のことです。

通常の変数がデータそのものを保持するのに対し、ポインタはそのデータが格納されているメモリのアドレスを保持します。

これにより、ポインタを使うことで、データの直接操作や関数間でのデータの受け渡しが効率的に行えます。

ポインタの宣言と初期化

ポインタを使用するためには、まずポインタ変数を宣言し、初期化する必要があります。

ポインタの宣言は、データ型の後にアスタリスク(*)を付けて行います。

初期化は、ポインタに有効なメモリアドレスを代入することで行います。

#include <stdio.h>
int main() {
    int value = 10; // 通常の整数変数
    int *ptr;       // 整数型のポインタ変数の宣言
    ptr = &value;   // ポインタの初期化(valueのアドレスを代入)
    printf("valueのアドレス: %p\n", (void*)ptr);
    printf("valueの値: %d\n", *ptr);
    return 0;
}
valueのアドレス: 0x7ffee3bff6ac
valueの値: 10

この例では、valueという整数変数のアドレスをptrというポインタに代入しています。

ptrを使ってvalueの値を間接的に参照しています。

ポインタの操作方法

ポインタを操作する際には、主に以下の方法があります。

  • アドレス演算子(&): 変数のアドレスを取得します。
  • 間接演算子(*): ポインタが指し示すアドレスの値を取得または設定します。

以下に、ポインタの操作方法を示す例を示します。

#include <stdio.h>
int main() {
    int value = 20;
    int *ptr = &value; // ポインタの宣言と初期化
    printf("初期のvalueの値: %d\n", *ptr);
    *ptr = 30; // ポインタを使ってvalueの値を変更
    printf("変更後のvalueの値: %d\n", value);
    return 0;
}
初期のvalueの値: 20
変更後のvalueの値: 30

この例では、ポインタptrを使ってvalueの値を変更しています。

ポインタを使うことで、変数の値を直接操作することが可能です。

ポインタのポインタとは

ポインタのポインタは、ポインタを指し示すポインタのことを指します。

これは、メモリのアドレスを持つポインタ自体のアドレスを保持するための構造です。

ポインタのポインタを使うことで、より複雑なデータ構造や多次元配列の操作が可能になります。

ポインタのポインタの概念

ポインタのポインタは、通常のポインタがメモリ上のデータのアドレスを指すのに対し、ポインタ自体のアドレスを指します。

これにより、ポインタを間接的に操作することが可能になります。

例えば、関数内でポインタを変更したい場合や、動的に多次元配列を扱いたい場合に有用です。

ポインタのポインタの宣言と初期化

ポインタのポインタを宣言するには、データ型の前にアスタリスクを2つ付けます。

初期化は、通常のポインタと同様に、ポインタのアドレスを代入することで行います。

#include <stdio.h>
int main() {
    int value = 50;
    int *ptr = &value;    // 通常のポインタ
    int **pptr = &ptr;    // ポインタのポインタ
    printf("valueの値: %d\n", **pptr);
    return 0;
}
valueの値: 50

この例では、pptrptrのアドレスを保持し、ptrvalueのアドレスを保持しています。

**pptrを使うことで、valueの値を取得しています。

ポインタのポインタのメモリ構造

ポインタのポインタを使うと、メモリ上でのデータの配置がより複雑になります。

以下の図は、ポインタのポインタのメモリ構造を示しています。

スクロールできます
メモリアドレス内容
0x1000valueの値
0x20000x1000(ptrの指すアドレス)
0x30000x2000(pptrの指すアドレス)

この構造により、pptrを使ってvalueの値にアクセスすることができます。

ポインタのポインタを使うことで、間接的にデータを操作することが可能になり、特に多次元配列や複雑なデータ構造の操作において強力な手段となります。

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

ポインタのポインタは、特に多次元配列の管理や関数でのポインタの操作、動的メモリ管理において非常に有用です。

このセクションでは、ポインタのポインタの具体的な使い方について解説します。

2次元配列の管理

2次元配列は、ポインタのポインタを使って動的に管理することができます。

これにより、配列のサイズを実行時に決定することが可能になります。

#include <stdio.h>
#include <stdlib.h>
int main() {
    int rows = 3;
    int cols = 4;
    int **array;
    // 2次元配列の動的メモリ確保
    array = (int **)malloc(rows * sizeof(int *));
    for (int i = 0; i < rows; i++) {
        array[i] = (int *)malloc(cols * sizeof(int));
    }
    // 配列に値を代入
    for (int i = 0; i < rows; i++) {
        for (int j = 0; j < cols; j++) {
            array[i][j] = i * cols + j;
        }
    }
    // 配列の内容を表示
    for (int i = 0; i < rows; i++) {
        for (int j = 0; j < cols; j++) {
            printf("%d ", array[i][j]);
        }
        printf("\n");
    }
    // メモリの解放
    for (int i = 0; i < rows; i++) {
        free(array[i]);
    }
    free(array);
    return 0;
}
0 1 2 3 
4 5 6 7 
8 9 10 11

この例では、ポインタのポインタを使って2次元配列を動的に確保し、値を代入しています。

最後に、確保したメモリを解放しています。

関数でのポインタの変更

ポインタのポインタを使うことで、関数内でポインタ自体を変更することができます。

これにより、関数を通じてポインタの指す先を変更することが可能です。

#include <stdio.h>
void changePointer(int **pptr) {
    static int newValue = 100;
    *pptr = &newValue; // ポインタの指す先を変更
}
int main() {
    int value = 50;
    int *ptr = &value;
    printf("変更前の値: %d\n", *ptr);
    changePointer(&ptr);
    printf("変更後の値: %d\n", *ptr);
    return 0;
}
変更前の値: 50
変更後の値: 100

この例では、changePointer関数を使って、ptrの指す先を変更しています。

関数内でポインタの指す先を変更することで、呼び出し元のポインタの指す先を変えることができます。

メモリの動的確保と解放

ポインタのポインタは、動的メモリ管理においても重要な役割を果たします。

特に、複数のポインタを管理する場合に便利です。

#include <stdio.h>
#include <stdlib.h>
int main() {
    int **ptrArray;
    int size = 5;
    // ポインタの配列の動的メモリ確保
    ptrArray = (int **)malloc(size * sizeof(int *));
    for (int i = 0; i < size; i++) {
        ptrArray[i] = (int *)malloc(sizeof(int));
        *ptrArray[i] = i * 10; // 値を代入
    }
    // 値の表示
    for (int i = 0; i < size; i++) {
        printf("ptrArray[%d]: %d\n", i, *ptrArray[i]);
    }
    // メモリの解放
    for (int i = 0; i < size; i++) {
        free(ptrArray[i]);
    }
    free(ptrArray);
    return 0;
}
ptrArray[0]: 0
ptrArray[1]: 10
ptrArray[2]: 20
ptrArray[3]: 30
ptrArray[4]: 40

この例では、ポインタの配列を動的に確保し、それぞれのポインタに値を代入しています。

最後に、確保したメモリをすべて解放しています。

ポインタのポインタを使うことで、複数のポインタを効率的に管理することができます。

ポインタのポインタを使った具体例

ポインタのポインタは、C言語における高度なプログラミングテクニックを実現するための強力なツールです。

このセクションでは、ポインタのポインタを使った具体的な例をいくつか紹介します。

2次元配列の動的メモリ確保

2次元配列を動的に確保することで、実行時に配列のサイズを柔軟に変更することができます。

ポインタのポインタを使うことで、各行のメモリを個別に確保し、管理することが可能です。

#include <stdio.h>
#include <stdlib.h>
void allocate2DArray(int ***array, int rows, int cols) {
    *array = (int **)malloc(rows * sizeof(int *));
    for (int i = 0; i < rows; i++) {
        (*array)[i] = (int *)malloc(cols * sizeof(int));
    }
}
void free2DArray(int **array, int rows) {
    for (int i = 0; i < rows; i++) {
        free(array[i]);
    }
    free(array);
}
int main() {
    int **array;
    int rows = 3, cols = 4;
    allocate2DArray(&array, rows, cols);
    // 配列に値を代入
    for (int i = 0; i < rows; i++) {
        for (int j = 0; j < cols; j++) {
            array[i][j] = i * cols + j;
        }
    }
    // 配列の内容を表示
    for (int i = 0; i < rows; i++) {
        for (int j = 0; j < cols; j++) {
            printf("%d ", array[i][j]);
        }
        printf("\n");
    }
    free2DArray(array, rows);
    return 0;
}
0 1 2 3 
4 5 6 7 
8 9 10 11

この例では、allocate2DArray関数を使って2次元配列を動的に確保し、free2DArray関数でメモリを解放しています。

ポインタのポインタを使うことで、関数を通じて配列のメモリを管理しています。

関数でのポインタの変更例

ポインタのポインタを使うことで、関数内でポインタの指す先を変更することができます。

これにより、関数を通じてポインタの指す先を動的に変更することが可能です。

#include <stdio.h>
void updatePointer(int **pptr) {
    static int newValue = 200;
    *pptr = &newValue; // ポインタの指す先を変更
}
int main() {
    int value = 100;
    int *ptr = &value;
    printf("変更前の値: %d\n", *ptr);
    updatePointer(&ptr);
    printf("変更後の値: %d\n", *ptr);
    return 0;
}
変更前の値: 100
変更後の値: 200

この例では、updatePointer関数を使って、ptrの指す先を変更しています。

関数内でポインタの指す先を変更することで、呼び出し元のポインタの指す先を変えることができます。

リンクリストの実装

リンクリストは、ポインタのポインタを使って実装することができます。

リンクリストは、動的に要素を追加・削除できるデータ構造で、ポインタのポインタを使うことで、リストの先頭を効率的に管理できます。

#include <stdio.h>
#include <stdlib.h>
typedef struct Node {
    int data;
    struct Node *next;
} Node;
void insertAtHead(Node **head, int data) {
    Node *newNode = (Node *)malloc(sizeof(Node));
    newNode->data = data;
    newNode->next = *head;
    *head = newNode;
}
void printList(Node *head) {
    Node *current = head;
    while (current != NULL) {
        printf("%d -> ", current->data);
        current = current->next;
    }
    printf("NULL\n");
}
void freeList(Node *head) {
    Node *current = head;
    Node *next;
    while (current != NULL) {
        next = current->next;
        free(current);
        current = next;
    }
}
int main() {
    Node *head = NULL;
    insertAtHead(&head, 10);
    insertAtHead(&head, 20);
    insertAtHead(&head, 30);
    printList(head);
    freeList(head);
    return 0;
}
30 -> 20 -> 10 -> NULL

この例では、insertAtHead関数を使ってリンクリストの先頭に新しいノードを追加しています。

ポインタのポインタを使うことで、リストの先頭を効率的に管理し、新しいノードを追加することができます。

ポインタのポインタを使う際の注意点

ポインタのポインタは強力なツールですが、使用する際にはいくつかの注意点があります。

これらの注意点を理解し、適切に対処することで、プログラムの安全性と効率性を向上させることができます。

メモリリークの防止

メモリリークは、動的に確保したメモリを解放しないままプログラムが終了することによって発生します。

ポインタのポインタを使う場合、特に多次元配列やリンクリストのようなデータ構造を扱う際には、メモリリークを防ぐために、確保したメモリを必ず解放することが重要です。

#include <stdlib.h>
void free2DArray(int **array, int rows) {
    for (int i = 0; i < rows; i++) {
        free(array[i]); // 各行のメモリを解放
    }
    free(array); // 配列自体のメモリを解放
}

この例では、2次元配列の各行と配列自体のメモリを解放しています。

メモリを確保したら、必ず対応するfree関数を使って解放することを忘れないようにしましょう。

NULLポインタの確認

ポインタのポインタを使用する際には、NULLポインタの確認が重要です。

NULLポインタは、ポインタが有効なメモリアドレスを指していないことを示します。

NULLポインタをデリファレンスすると、プログラムがクラッシュする可能性があります。

#include <stdio.h>
void safeDereference(int **pptr) {
    if (pptr != NULL && *pptr != NULL) {
        printf("値: %d\n", **pptr);
    } else {
        printf("NULLポインタを参照しています\n");
    }
}

この例では、ポインタのポインタがNULLでないことを確認してからデリファレンスしています。

NULLポインタを参照する前に必ずチェックを行い、安全に操作することが重要です。

ポインタのポインタのデリファレンス

ポインタのポインタをデリファレンスする際には、間接演算子(*)を2回使用します。

デリファレンスの際には、ポインタが有効なメモリアドレスを指していることを確認する必要があります。

#include <stdio.h>
void printValue(int **pptr) {
    if (pptr != NULL && *pptr != NULL) {
        printf("デリファレンスした値: %d\n", **pptr);
    } else {
        printf("無効なポインタです\n");
    }
}
int main() {
    int value = 42;
    int *ptr = &value;
    int **pptr = &ptr;
    printValue(pptr);
    return 0;
}
デリファレンスした値: 42

この例では、pptrが有効なポインタであることを確認した上でデリファレンスを行っています。

ポインタのポインタをデリファレンスする際には、必ず有効性を確認し、安全に操作することが求められます。

応用例

ポインタのポインタは、さまざまな応用に利用できる強力なツールです。

このセクションでは、ポインタのポインタを使った具体的な応用例をいくつか紹介します。

ポインタのポインタを使った文字列操作

ポインタのポインタを使うことで、文字列の配列を効率的に管理することができます。

これは、複数の文字列を動的に扱う際に特に有用です。

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
void printStrings(char **strings, int count) {
    for (int i = 0; i < count; i++) {
        printf("%s\n", strings[i]);
    }
}
int main() {
    int count = 3;
    char **strings = (char **)malloc(count * sizeof(char *));
    
    strings[0] = strdup("こんにちは");
    strings[1] = strdup("世界");
    strings[2] = strdup("C言語");
    printStrings(strings, count);
    for (int i = 0; i < count; i++) {
        free(strings[i]);
    }
    free(strings);
    return 0;
}
こんにちは
世界
C言語

この例では、文字列の配列を動的に確保し、各文字列をstrdupでコピーしています。

ポインタのポインタを使うことで、文字列の配列を効率的に管理し、操作することができます。

ポインタのポインタを使ったデータ構造の実装

ポインタのポインタは、複雑なデータ構造の実装にも役立ちます。

例えば、ツリー構造やグラフ構造のノードを動的に管理する際に使用されます。

#include <stdio.h>
#include <stdlib.h>
typedef struct TreeNode {
    int data;
    struct TreeNode *left;
    struct TreeNode *right;
} TreeNode;
TreeNode* createNode(int data) {
    TreeNode *newNode = (TreeNode *)malloc(sizeof(TreeNode));
    newNode->data = data;
    newNode->left = NULL;
    newNode->right = NULL;
    return newNode;
}
void insertLeft(TreeNode **node, int data) {
    if (*node == NULL) {
        *node = createNode(data);
    } else {
        (*node)->left = createNode(data);
    }
}
void insertRight(TreeNode **node, int data) {
    if (*node == NULL) {
        *node = createNode(data);
    } else {
        (*node)->right = createNode(data);
    }
}
void printTree(TreeNode *node) {
    if (node != NULL) {
        printTree(node->left);
        printf("%d ", node->data);
        printTree(node->right);
    }
}
void freeTree(TreeNode *node) {
    if (node != NULL) {
        freeTree(node->left);
        freeTree(node->right);
        free(node);
    }
}
int main() {
    TreeNode *root = createNode(10);
    insertLeft(&root, 5);
    insertRight(&root, 15);
    printTree(root);
    printf("\n");
    freeTree(root);
    return 0;
}
5 10 15

この例では、二分木を実装しています。

ポインタのポインタを使うことで、ノードの挿入や削除を効率的に行うことができます。

ポインタのポインタを使ったファイル操作

ポインタのポインタは、ファイル操作においても役立ちます。

特に、ファイルの内容を動的に読み込む際に使用されます。

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
void readFileLines(const char *filename, char ***lines, int *lineCount) {
    FILE *file = fopen(filename, "r");
    if (!file) {
        perror("ファイルを開けません");
        return;
    }
    size_t bufferSize = 256;
    char buffer[bufferSize];
    *lineCount = 0;
    *lines = NULL;
    while (fgets(buffer, bufferSize, file)) {
        *lines = (char **)realloc(*lines, (*lineCount + 1) * sizeof(char *));
        (*lines)[*lineCount] = strdup(buffer);
        (*lineCount)++;
    }
    fclose(file);
}
void freeLines(char **lines, int lineCount) {
    for (int i = 0; i < lineCount; i++) {
        free(lines[i]);
    }
    free(lines);
}
int main() {
    char **lines;
    int lineCount;
    readFileLines("example.txt", &lines, &lineCount);
    for (int i = 0; i < lineCount; i++) {
        printf("%s", lines[i]);
    }
    freeLines(lines, lineCount);
    return 0;
}

この例では、example.txtというファイルの内容を行ごとに読み込み、動的にメモリを確保して保存しています。

ポインタのポインタを使うことで、ファイルの内容を効率的に管理し、操作することができます。

よくある質問

ポインタのポインタはどのようにデバッグすれば良いですか?

ポインタのポインタをデバッグする際には、以下のポイントに注意すると良いでしょう。

  • アドレスの確認: ポインタのポインタが指しているアドレスを確認し、期待通りのメモリアドレスを指しているかをチェックします。

例:printf("アドレス: %p\n", (void*)pptr);

  • NULLチェック: ポインタのポインタやその指すポインタがNULLでないことを確認します。

NULLポインタをデリファレンスするとクラッシュの原因になります。

  • メモリリークの確認: 動的に確保したメモリが適切に解放されているかを確認します。

メモリリークは、プログラムのパフォーマンスを低下させる原因となります。

ポインタのポインタを使うときのパフォーマンスへの影響は?

ポインタのポインタを使うことによるパフォーマンスへの影響は、主に以下の点に関連します。

  • 間接参照のオーバーヘッド: ポインタのポインタをデリファレンスする際には、通常のポインタよりも間接参照が増えるため、若干のオーバーヘッドが発生します。

ただし、現代のコンパイラはこのオーバーヘッドを最小限に抑える最適化を行うため、通常の使用では大きな問題にはなりません。

  • メモリ管理のコスト: 動的メモリを頻繁に確保・解放する場合、メモリ管理のコストが増加します。

これにより、プログラムのパフォーマンスが低下する可能性があります。

ポインタのポインタを使うべき場面はどんなときですか?

ポインタのポインタを使うべき場面は、以下のようなケースです。

  • 多次元配列の動的管理: 2次元以上の配列を動的に管理する際に、ポインタのポインタを使うことで柔軟なメモリ管理が可能になります。
  • 関数でのポインタの変更: 関数内でポインタの指す先を変更したい場合に、ポインタのポインタを使うことで、呼び出し元のポインタを変更することができます。
  • 複雑なデータ構造の実装: リンクリストやツリー構造など、動的に要素を追加・削除するデータ構造を実装する際に、ポインタのポインタを使うことで効率的に管理できます。

まとめ

この記事では、C言語におけるポインタのポインタの基本から具体的な使い方、注意点、応用例までを詳しく解説しました。

ポインタのポインタを活用することで、より柔軟で効率的なプログラムを作成するための基盤を築くことができます。

これを機に、実際のプログラムでポインタのポインタを活用し、より高度なプログラミングに挑戦してみてはいかがでしょうか。

  • URLをコピーしました!
目次から探す