ポインタの応用

ポインタは住所票のようなものです。ポインタへのポインタは、住所票に別の住所票の住所が書いてある状態です。複雑に聞こえますが、現実の「荷物を預けるロッカー番号」もまさにこの仕組みで、何重にも重なっています。

ポインタへのポインタ

ポインタ変数も変数であり、メモリ上に存在して自身のアドレスを持ちます。ポインタが別のポインタを指すと、「ポインタへのポインタ」になります。

C
int x = 42;
int *p = &x;
int **pp = &p;

**pp を通じて間接的に x の値を変更できます:

C
**pp = 100;
printf("%d\n", x);
TEXT
100

二重ポインタの最も一般的な用途は、「関数内でポインタ自体を変更する」ことです。たとえば、関数にポインタのメモリを割り当てさせる場合です:

C
void alloc_buf(char **ptr, int size) {
    *ptr = (char *)malloc(size);
}

int main(void) {
    char *buf = NULL;
    alloc_buf(&buf, 128);
    if (buf) {
        strcpy(buf, "hello");
        printf("%s\n", buf);
        free(buf);
    }
    return 0;
}
TEXT
hello
💡 ヒント: シングルポインタ char *ptr を渡すだけでは、関数はコピーを変更するだけで、外側の元のポインタは変わりません。ポインタ自体を変更するには、そのアドレスを渡す必要があります。

ポインタ配列と配列ポインタ

この2つの概念は混同されやすいです。鍵となるのは *[] の結合の優先順位です。

ポインタ配列

int *arr[4] — 先に [] と結合するため、要素が int * の配列になります。

C
int a = 10, b = 20, c = 30;
int *arr[3] = {&a, &b, &c};
for (int i = 0; i < 3; i++) {
    printf("%d ", *arr[i]);
}
TEXT
10 20 30

代表的な応用例:文字列の配列です。

C
const char *names[3] = {"Alice", "Bob", "Carol"};
for (int i = 0; i < 3; i++) {
    printf("%s\n", names[i]);
}
TEXT
Alice
Bob
Carol

配列ポインタ

int (*p)[4] — 先に * と結合するため、4つのintの配列を指すポインタになります。二次元配列を関数に渡す際によく使われます。

C
int matrix[3][4] = {
    {1, 2, 3, 4},
    {5, 6, 7, 8},
    {9, 10, 11, 12}
};
int (*p)[4] = matrix;
printf("%d\n", p[1][2]);
TEXT
7

C
#include <stdio.h>

void print_matrix(int (*m)[4], int rows) {
    for (int i = 0; i < rows; i++) {
        for (int j = 0; j < 4; j++) {
            printf("%3d", m[i][j]);
        }
        printf("\n");
    }
}

int main(void) {
    int matrix[2][4] = {
        {10, 20, 30, 40},
        {50, 60, 70, 80}
    };
    print_matrix(matrix, 2);

    const char *fruits[3] = {"apple", "banana", "cherry"};
    for (int i = 0; i < 3; i++) {
        printf("%s ", fruits[i]);
    }
    printf("\n");
    return 0;
}
▶ 試してみよう
TEXT
 10 20 30 40
 50 60 70 80
apple banana cherry
⚠️ 注意: 覚え方:int *a[4] はポインタ配列(ポインタの配列)、int (*a)[4] は配列ポインタ(配列へのポインタ)です。

関数ポインタとコールバック

関数名はその関数の入口アドレスです。関数ポインタはこのアドレスを格納でき、「遅延呼び出し」や「コールバック」を実現します。

関数ポインタの宣言

C
int (*pf)(int, int);

これは、2つのintを引数に取りintを返す関数へのポインタとして pf を宣言します。

C
int add(int a, int b) { return a + b; }
int sub(int a, int b) { return a - b; }

int main(void) {
    int (*pf)(int, int) = add;
    printf("%d\n", pf(3, 5));
    pf = sub;
    printf("%d\n", pf(10, 4));
    return 0;
}
TEXT
8
6

コールバックの仕組み

関数ポインタを別の関数の引数として渡し、その関数が適切なタイミングで「呼び戻す」仕組みです。

C
void process(int *arr, int len, int (*transform)(int)) {
    for (int i = 0; i < len; i++) {
        arr[i] = transform(arr[i]);
    }
}

int double_it(int n) { return n * 2; }
int negate(int n) { return -n; }

int main(void) {
    int data[4] = {1, 2, 3, 4};
    process(data, 4, double_it);
    for (int i = 0; i < 4; i++) printf("%d ", data[i]);

    printf("\n");
    process(data, 4, negate);
    for (int i = 0; i < 4; i++) printf("%d ", data[i]);
    return 0;
}
TEXT
2 4 6 8
-2 -4 -6 -8

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

int cmp_asc(const void *a, const void *b) {
    return *(int *)a - *(int *)b;
}

int cmp_desc(const void *a, const void *b) {
    return *(int *)b - *(int *)a;
}

void sort_and_print(int *arr, int n, int (*cmp)(const void *, const void *)) {
    qsort(arr, n, sizeof(int), cmp);
    for (int i = 0; i < n; i++) {
        printf("%d ", arr[i]);
    }
    printf("\n");
}

int main(void) {
    int nums[5] = {42, 7, 19, 3, 55};
    sort_and_print(nums, 5, cmp_asc);

    int nums2[5] = {42, 7, 19, 3, 55};
    sort_and_print(nums2, 5, cmp_desc);
    return 0;
}
▶ 試してみよう
TEXT
3 7 19 42 55
55 42 19 7 3
🔥 よくある間違い: 標準ライブラリの qsort は関数ポインタコールバックの古典的な応用例です。比較ルールを提供するだけで、ソートアルゴリズムが残りを処理してくれます。

constとポインタの3つの組み合わせ

const とポインタの組み合わせには3つの位置があり、それぞれ意味が異なります。

constへのポインタ

C
const int *p;
int const *p;

p を通じて指し先の値を変更することはできませんが、p 自体は別のアドレスを指すことができます。

C
int a = 10, b = 20;
const int *p = &a;
printf("%d\n", *p);
p = &b;
printf("%d\n", *p);
TEXT
10
20

constポインタ

C
int * const p = &a;

p は指し先を変更できませんが、p を通じて値を変更することはできます。

C
int a = 10;
int * const p = &a;
*p = 99;
printf("%d\n", a);
TEXT
99

constへのconstポインタ

C
const int * const p = &a;

ポインタの指し先も、指し先の値も変更できません。最も厳格な読み取り専用です。

💡 ヒント: 早見法:const* の左側にあればデータを修飾し、右側にあればポインタ自体を修飾します。

voidポインタ

void * は「汎用ポインタ」であり、任意の型を指せますが、使用前にキャストする必要があります。

C
int a = 42;
double b = 3.14;
void *p;

p = &a;
printf("%d\n", *(int *)p);

p = &b;
printf("%.2f\n", *(double *)p);
TEXT
42
3.14

void * の代表的な用途:

⚠️ 注意: void * を逆参照することはできません。コンパイラは指し先のデータサイズを知らないためです。具体的なポインタ型にキャストしてから使用する必要があります。

❓ よくある質問

Q ポインタ配列と配列ポインタを素早く見分けるにはどうすればよいですか?
A 変数名が先に何と結合するかを見ます。先に [] と結合すれば配列(ポインタ配列)、先に * と結合すればポインタ(配列ポインタ)です。括弧で優先順位を変更できます。
Q 関数ポインタと関数名の違いは何ですか?
A 関数名は関数のアドレスであり、定数です。関数ポインタは変数であり、異なる関数のアドレスを格納でき、実行時に呼び出し対象を切り替えられます。
Q const int *pint const *p は同じですか?
A はい、全く同じです。どちらもconst intへのポインタを意味し、pを通じてデータを変更することはできませんが、p自体は別のアドレスを指せます。
Q voidポインタでポインタ演算はできますか?
A いいえ。コンパイラはvoidポインタが参照するデータのサイズを知らないため、p+1 の歩幅は未定義です。具体的な型にキャストしてから演算する必要があります。

📖 まとめ

📝 練習問題

  1. 関数 void swap_ptr(int a, int b) を作成し、2つのポインタの指し先を入れ替えてください。main関数で、入れ替え後の各ポインタが相手の元の値を指すことを確認してください
  2. 関数ポインタ配列 int (*ops[4])(int,int) を宣言し、加算、減算、乗算、除算の関数を格納してください。ユーザーが入力したインデックスに基づいて対応する演算を呼び出してください
  3. 汎用出力関数 void print_any(void *data, char type) を作成してください。typeが'i'ならint、'd'ならdouble、's'なら文字列として出力します
100%