Cプログラマへの道 #5 - ポインタ

Last Edited: 7/29/2024

このブログ記事では、C言語における最も重要な概念の一つであるポインタを紹介します。

C Pointers

ポインタ

ポインタは、C言語で最も重要であり、習得が最も難しい概念の一つと言われています。ポインタを理解するためには、 変数がメモリにどのように格納されているかを理解する必要があります。例えば、int x = 5;のように変数を宣言して初期化すると、 実際にはその裏で、16進数で表現されるアドレスに値5が格納され、xという名前でそのアドレスを参照します。同様に、 int y[3] = {1, 2, 3};のように配列を格納する際には、値が連続したアドレスに格納され、配列の最初の要素のアドレスがy として参照されます。

RAMにおける格納方法
変数名アドレス
x0x16dc6f1105
y0x16dc6f1181,
2,
3

変数のアドレスを表示するには、フォーマット指定子として%p(pはポインタの略)を使用し、変数xのアドレスを表示するために&xを使います。 **ポインタは、他の変数のアドレスを格納する変数にすぎません。**これを定義するにはint *z = &x;のように書きます。int *はint型のポインタ を意味し、int型の変数のアドレスを格納します。ポインタを宣言して初期化した後、メモリは次のようになります。

RAMにおける格納方法
変数名アドレス
z0x16dc6f1080x16dc6f110
x0x16dc6f1105
y0x16dc6f1181,
2,
3

ポインタが指すアドレスに格納された値を取得したい場合、つまりポインタzから変数xの値を取得したい場合は、*zというデリファレンス演算子を使用します。

なぜポインタが重要なのか?

ポインタがなぜ有用なのか疑問に思うかもしれません。ここで、ポインタの使用が不可欠な例を見てみましょう。 例えば、main関数の外で変数の値を交換できる関数を作りたいとします。以下のような関数を思いつくかもしれません。

void swap (int x, int y) {
    int temp = x;
    x = y;
    y = temp;
}
 
int main () {
    int a = 5;
    int b = 10;
 
    swap(a, b);
 
    printf("a: %d, b: %d", a, b);
 
    return 0;
}

しかし、上記のコードを実行すると、a: 5, b: 10と表示されます。これは、C言語で変数を関数に渡すと、関数は変数の値を受け取り、 アドレスを受け取らないからです。したがって、swap(a, b)を呼び出すことは、swap(5, 10)を呼び出すことと同じです。 これを値渡し(call by value)と言います。代わりにポインタを使用することができます。

void swap (int *x, int *y) {
    int temp = *x;
    *x = *y;
    *y = temp;
}
 
int main () {
    int a = 5;
    int b = 10;
 
    swap(&a, &b);
 
    printf("a: %d, b: %d", a, b);
 
    return 0;
}

今度は、変数の代わりに、abのアドレスに設定されたポインタを渡しています。その結果、swap関数は適切にアドレスとそれに対応する値を使用して値を交換し、 a: 10, b: 5と正しく表示されます。これをポインタ渡し(または参照渡し)と呼び、これがC言語でポインタが非常に重要である理由の一つです。

配列とポインタ

上記でintに対してポインタがどのように機能するかを理解しましたが、charfloatdoubleについても同様に機能すると推測できます。では、 配列についてはどうでしょうか?配列に対するポインタの動作を理解するため、例を見てみましょう。

void addOne (int arr[], int size) {
    for (int i = 0; i < size; i++) {
        arr[i] += 1;
    }
}
 
int main () {
    int xs[3] = {1,2,3};
    int size = sizeof(xs)/sizeof(xs[0]);
 
    addOne(xs, size);
    printf("%d %d %d", xs[0], xs[1], xs[2]);
    
    return 0;
}

変数を関数に渡すと値渡しが行われてしまい、予測通りに動作しないことを見てきました。もしそれがここでも適用されるなら、 上記は機能せず、単に1 2 3を出力するでしょう。しかし、実際に上記を実行すると2 3 4と出力されます。なぜでしょうか?それは、 配列を関数に渡すと、配列がポインタに変形し、ポインタのように振る舞い始めるからです。ポインタも配列のように振る舞うことができます。

int xs[3] = {1,2,3};
int *p = xs;
 
printf("%d", p[2]); // 2

これは、arr[i]の表記が、配列の最初の要素のアドレスからiステップ離れたアドレスの値にアクセスするためのものだからです。 配列はその要素を隣接して格納するため、arr[i]は配列のi番目の要素に格納された値を取得できます。つまり、以下のコードも間違いではありません。

int main () {
    int x = 3;
    int *px = &x;
    printf("%d", px[1]);
    return 0;
}

上記のポインタはintを格納している変数xを指しているにもかかわらず、px[1]を使用して、xの隣にたまたま格納されている値にアクセスできます。 []と同じことをポインタの算術演算で達成することもできます。

int xs[3] = {1,2,3};
int *p = xs;
 
printf("%d", *(p + 1)); // 2
printf("%d", *(xs + 1)); // 2
 
p++;
xs+=2;
 
printf("%d", *(p)); // 2
printf("%d", *(xs)); // 3

ポインタに1を加算すると次のアドレスに移動し、*デリファレンス演算子を使用してその値にアクセスできます。配列はポインタに変形するため、配列も同様に動作します。 それらがそれほど似た振る舞いをするなら、本質的に両者は同じものなのでしょうか?配列がポインタに"変形する"という表現を使用した理由がここにあります。

int xs[3] = {1,2,3};
int ys[3] = {4,5,6};
int *p = xs;
 
// これは可能です
p = ys;
 
// これは不可能です
xs = ys;

ポインタはいつでも新しいアドレスに設定できますが、配列はそのように再割り当てできません。これは、配列とポインタが本質的には違うものであることを 意味しており、二つを扱う際に重要な区別です。配列と &array ポインタには、もう一つ重要な違いがあります。

int matrix[3][5] = {
    {1,2,3,4,5},
    {6,7,8,9,10},
    {11,12,13,14,15}
};
 
printf("matrix[1]+1: %d\n", *(matrix[1]+1));
printf("&matrix[1]+1: %d\n", *(&matrix[1]+1));
// matrix[1]+1: 7
// &matrix[1]+1: --12のアドレス--

配列matrix[1]+1によるポインタ算術の結果は、&array ポインタ&matrix[1]+1による結果とは異なります。&array ポインタをデリファレンスした後、 12を格納しているアドレスが返されます。その理由は、&array ポインタが配列の最初の要素ではなく、配列全体を指しているからです。このため、 &matrix[1]+1を行うと、次の要素に移動するのではなく、配列の次の行に移動します。

さらに、デリファレンスが機能しなかったのは、&array ポインタが配列の最初の要素へのポインタを格納しているためです。したがって、 *(*(&matrix[1]+1))のように再度デリファレンスすると、12が取得できます。また、&matrix[1]+1の型をintポインタとして int *p = (int *)(&matrix[1]+1)のようにキャストし、*pをデリファレンスすることでも12を取得できます。

クイズ

この記事では、学習した内容を確認するためのクイズを設けます。記事のメイン部分を読んだ後に、ぜひ自分で問題を解いてみることを強くお勧めします。各問題をクリックすると答えが表示されます。

リソース