ポインタの応用
ポインタは住所票のようなものです。ポインタへのポインタは、住所票に別の住所票の住所が書いてある状態です。複雑に聞こえますが、現実の「荷物を預けるロッカー番号」もまさにこの仕組みで、何重にも重なっています。
ポインタへのポインタ
ポインタ変数も変数であり、メモリ上に存在して自身のアドレスを持ちます。ポインタが別のポインタを指すと、「ポインタへのポインタ」になります。
int x = 42;
int *p = &x;
int **pp = &p;
**pp を通じて間接的に x の値を変更できます:
**pp = 100;
printf("%d\n", x);
100
二重ポインタの最も一般的な用途は、「関数内でポインタ自体を変更する」ことです。たとえば、関数にポインタのメモリを割り当てさせる場合です:
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;
}
hello
char *ptr を渡すだけでは、関数はコピーを変更するだけで、外側の元のポインタは変わりません。ポインタ自体を変更するには、そのアドレスを渡す必要があります。
ポインタ配列と配列ポインタ
この2つの概念は混同されやすいです。鍵となるのは * と [] の結合の優先順位です。
ポインタ配列
int *arr[4] — 先に [] と結合するため、要素が int * の配列になります。
int a = 10, b = 20, c = 30;
int *arr[3] = {&a, &b, &c};
for (int i = 0; i < 3; i++) {
printf("%d ", *arr[i]);
}
10 20 30
代表的な応用例:文字列の配列です。
const char *names[3] = {"Alice", "Bob", "Carol"};
for (int i = 0; i < 3; i++) {
printf("%s\n", names[i]);
}
Alice
Bob
Carol
配列ポインタ
int (*p)[4] — 先に * と結合するため、4つのintの配列を指すポインタになります。二次元配列を関数に渡す際によく使われます。
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]);
7
例
#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;
}
10 20 30 40
50 60 70 80
apple banana cherry
int *a[4] はポインタ配列(ポインタの配列)、int (*a)[4] は配列ポインタ(配列へのポインタ)です。
関数ポインタとコールバック
関数名はその関数の入口アドレスです。関数ポインタはこのアドレスを格納でき、「遅延呼び出し」や「コールバック」を実現します。
関数ポインタの宣言
int (*pf)(int, int);
これは、2つのintを引数に取りintを返す関数へのポインタとして pf を宣言します。
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;
}
8
6
コールバックの仕組み
関数ポインタを別の関数の引数として渡し、その関数が適切なタイミングで「呼び戻す」仕組みです。
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;
}
2 4 6 8
-2 -4 -6 -8
例
#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;
}
3 7 19 42 55
55 42 19 7 3
qsort は関数ポインタコールバックの古典的な応用例です。比較ルールを提供するだけで、ソートアルゴリズムが残りを処理してくれます。
constとポインタの3つの組み合わせ
const とポインタの組み合わせには3つの位置があり、それぞれ意味が異なります。
constへのポインタ
const int *p;
int const *p;
p を通じて指し先の値を変更することはできませんが、p 自体は別のアドレスを指すことができます。
int a = 10, b = 20;
const int *p = &a;
printf("%d\n", *p);
p = &b;
printf("%d\n", *p);
10
20
constポインタ
int * const p = &a;
p は指し先を変更できませんが、p を通じて値を変更することはできます。
int a = 10;
int * const p = &a;
*p = 99;
printf("%d\n", a);
99
constへのconstポインタ
const int * const p = &a;
ポインタの指し先も、指し先の値も変更できません。最も厳格な読み取り専用です。
const が * の左側にあればデータを修飾し、右側にあればポインタ自体を修飾します。
voidポインタ
void * は「汎用ポインタ」であり、任意の型を指せますが、使用前にキャストする必要があります。
int a = 42;
double b = 3.14;
void *p;
p = &a;
printf("%d\n", *(int *)p);
p = &b;
printf("%.2f\n", *(double *)p);
42
3.14
void * の代表的な用途:
mallocはvoid *を返し、任意のポインタ型に代入できますqsortの比較関数はconst void *パラメータを使用します- 汎用データ構造の実装(例:汎用連結リスト)
void * を逆参照することはできません。コンパイラは指し先のデータサイズを知らないためです。具体的なポインタ型にキャストしてから使用する必要があります。
❓ よくある質問
[] と結合すれば配列(ポインタ配列)、先に * と結合すればポインタ(配列ポインタ)です。括弧で優先順位を変更できます。const int *p と int const *p は同じですか?p+1 の歩幅は未定義です。具体的な型にキャストしてから演算する必要があります。📖 まとめ
- 二重ポインタ
int **ppは、関数内でポインタ自体を変更するために使用します - ポインタ配列
int *a[N]はポインタの配列、配列ポインタint (*p)[N]は配列へのポインタです - 関数ポインタ
int (*pf)(int,int)は関数のアドレスを格納し、コールバックと組み合わせて柔軟な設計を実現します constが*の左側にあればデータを修飾し、右側にあればポインタ自体を修飾しますvoid *は汎用ポインタであり、使用前にキャストする必要があります
📝 練習問題
- 関数
void swap_ptr(int a, int b)を作成し、2つのポインタの指し先を入れ替えてください。main関数で、入れ替え後の各ポインタが相手の元の値を指すことを確認してください - 関数ポインタ配列
int (*ops[4])(int,int)を宣言し、加算、減算、乗算、除算の関数を格納してください。ユーザーが入力したインデックスに基づいて対応する演算を呼び出してください - 汎用出力関数
void print_any(void *data, char type)を作成してください。typeが'i'ならint、'd'ならdouble、's'なら文字列として出力します



