言語ごとの特徴 · 34
ポインタ
値のある場所(アドレス)を持ち歩き、呼び出し側の変数を書き換えます。
この単元は C++ · C 向けです。
Python・JavaScript・TypeScript・Java・Craykon には生のアドレスを持つポインタがなく、オブジェクトには参照でたどり着きます(関数の単元のリストの渡し方)。
C# は unsafe コードの中でだけポインタを使えます。
ポインタは、値の代わりにその値があるメモリアドレスを持つ変数です。
下の例はアドレスを渡して 2 つの変数を入れ替え、配列をポインタで受け取って合計と最大の要素を見つけます。
#include <iostream> void swapPtr(int* a, int* b) { int t = *a; *a = *b; *b = t;} int sum(const int* p, int n) { // 배열은 첫 칸의 포인터로 넘어와요 int s = 0; for (const int* end = p + n; p < end; ++p) s += *p; return s;} int* findMax(int* p, int n) { if (n == 0) return nullptr; int* best = p; for (int i = 1; i < n; ++i) if (p[i] > *best) best = p + i; return best;} int main() { int a, b, n, arr[100]; std::cin >> a >> b; swapPtr(&a, &b); // 실제 코드라면 std::swap(a, b) std::cout << a << ' ' << b << '\n'; std::cin >> n; for (int i = 0; i < n; ++i) std::cin >> arr[i]; std::cout << sum(arr, n) << '\n'; int* m = findMax(arr, n); if (m != nullptr) std::cout << *m << ' ' << (m - arr) << '\n';}#include <stdio.h> void swap(int *a, int *b) { int t = *a; *a = *b; *b = t;} int sum(const int *p, int n) { /* 배열은 첫 칸의 포인터로 넘어와요 */ int s = 0; for (const int *end = p + n; p < end; p++) s += *p; return s;} int *find_max(int *p, int n) { if (n == 0) return NULL; int *best = p; for (int i = 1; i < n; i++) if (p[i] > *best) best = p + i; return best;} int main(void) { int a, b, n, arr[100]; scanf("%d %d", &a, &b); swap(&a, &b); printf("%d %d\n", a, b); scanf("%d", &n); for (int i = 0; i < n; i++) scanf("%d", arr + i); printf("%d\n", sum(arr, n)); int *m = find_max(arr, n); if (m != NULL) printf("%d %td\n", *m, m - arr); return 0;}3 8 4 5 1 7 2
8 3 15 7 2
ポイント
&xは x のアドレス、*p(間接参照)は p が指す場所の値です。int *p = &x;の後の*p = 5;はx = 5;と同じです。scanfに&nを渡したときから、もうアドレスを使っていました。- 数を渡すと値がコピーされるので、関数の中で変えても呼び出し側はそのままです。
呼び出し側の変数を変えるにはアドレスを渡します:swap(&a, &b)。 - 配列は関数に渡したり式で使ったりすると、先頭要素を指すポインタに変わります(decay)。
だからp[i]は*(p + i)とまったく同じです。 - ポインタ演算:
p + 1は 1 バイトではなく 1 要素(sizeof(int)、ふつう 4 バイト)先を指します。
同じ配列の中の 2 つのポインタを引くと(m - arr)要素数、つまり添字になります。 - 配列の末尾のすぐ次(
p + n)までは指してもかまいませんが、その場所を読んではいけません。 - 何も指さないポインタは NULL です。
例のfind_maxは要素が 0 個なら NULL を返し、呼び出し側は確かめてから読みます。
言語ごとの書き方
| C++ | ポインタは C と同じで、空のポインタは nullptr(C++11 から)— 呼び出し側を変えるなら参照 void swap(int& a, int& b) のほうが簡単で、実際には std::swap(a, b) を使います。参照は null にならず、別の対象に付け替えもできない別名なので、ポインタは本当に必要なときだけ使います |
|---|---|
| C | int *p = &x;、*p で読み書き — 空のポインタは NULL。呼び出し側を変える関数は void swap(int *a, int *b)、配列を受け取るときは int sum(const int *p, int n) のように長さも別に受け取ります |
よくある間違い
NULL を間接参照した
int *m = find_max(arr, 0);
printf("%d\n", *m);→ 未定義動作で、たいてい Segmentation fault で落ちます。Java の NullPointerException のように、どこで間違えたかは教えてくれません
if (m != NULL) で確かめてから *m を読みます。
ローカル変数のアドレスを返した
int *f(void) {
int x = 3;
return &x;
}→ x は関数が終わると消えるので、受け取ったポインタはもうない場所を指します(ダングリングポインタ)。GCC は function returns address of local variable と警告し、実行するとでたらめな値が出るか落ちます
結果を入れる場所は、呼び出し側が用意した配列か、次の単元の動的メモリにします。
引数の配列の長さを sizeof で測った
int count(int arr[]) {
return sizeof(arr) / sizeof(arr[0]);
}→ 64 ビットのコンピュータでは 8 / 4 = 2 です。引数の int arr[] は実は int *arr なので、長さの情報が消えます(main の中の int arr[5] なら 5)
C は長さ n を別に受け取り、C++ は長さも一緒に持つ std::vector や std::span(C++20)を渡します。
言語ごとの違い
ポインタを間接参照する前に、それがまだ生きている何かを指しているか必ず確かめましょう。
NULL、すでに終わった関数のローカル変数、配列の範囲外を指すポインタを間接参照すると、C・C++ はエラーメッセージなしにでたらめな値を出すか落ちます。
ポインタを間接参照する前に、それがまだ生きている何かを指しているか必ず確かめましょう。
NULL、すでに終わった関数のローカル変数、配列の範囲外を指すポインタを間接参照すると、C・C++ はエラーメッセージなしにでたらめな値を出すか落ちます。