{ }codelper

学ぶ › C++

言語ごとの特徴 · 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';}
stdin
3 8
4
5 1 7 2
stdout
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 にならず、別の対象に付け替えもできない別名なので、ポインタは本当に必要なときだけ使います
Cint *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++ はエラーメッセージなしにでたらめな値を出すか落ちます。

練習問題