配列とポインタ

こんにちは、めのんです!

前回に引き続きポインタの話題です。
Cのポインタはすごくシンプルな仕組みなんですが、その割には用途が幅広いんですよね。
その分解説にも時間がかかります。
ちょっと大変ですけど、頑張ってついてきてくださいね。

配列の要素への間接参照

Cのポインタとの比較対象として前回はPHPの可変変数や可変関数を取り上げました。
ですが、可変変数や可変関数はポインタそのものではありませんので違いはたくさんあります。
たとえば配列の要素を可変変数を使って間接参照を行うことはできません。

<?php
$a = [ 1, 2, 3 ];
$p = 'a[0]'; // こんなことはできない!

var_dump($$p);

ただし、PHPのもう一つの間接参照の方法であるリファレンスと組み合わせれば、配列の要素を間接参照することも可能になります。

<?php
$a = [ 1, 2, 3 ];
$r = &$a;
$p = 'r';  // これならOK!

var_dump($$p);

PHPのリファレンスは「&」を使うところなんかがCのシンタックスとよく似ていますが、ポインタとはずいぶん違うので可変変数の方がまだポインタのイメージには近いと私は思います。
それはそうと、いったん別の変数を介さないと配列の要素を間接参照できないのは手間もかかりますし、配列と一緒に中継する変数も管理しないといけないので現実的ではありません。

ではCではどうかというと、本当にあっさり配列要素への間接参照が実現できてしまいます。

int a[] = { 1, 2 , 3 };
int *p = &a[0];
printf("%d\n", *p);

普通のオブジェクトを参照するときと何も変わりませんね。
Cのポインタはアドレスという一種の数値を使っていますので、どんなオブジェクトだろうと何も変わらないのです。

配列の要素を順にアクセスする

次に、配列の要素を順番にアクセスする方法を考えてみましょう。
PHPではforeach文を使うのが一番簡単ですが、この方法では順に値を値を取り出すことはできても配列の要素を更新することはできません。

前回ご紹介したイテレータを使う方法でもそれは同じです。

<?php
$obj = new ArrayObject([ 1, 2, 3 ]);
$it = $obj->getIterator();  // イテレータを取得

while ($it->valid())
{
  $it->current() = 123;  // これは無理!
  $it->next();
}

ほかには、crrent関数とnext関数を使っても配列の要素を順に取り出せますが、やはり要素を更新することはできません。
PHP 7.2から非推奨になったeach関数を使ってもやはり同じです。

PHPでは配列の要素を更新するのであれば、私が知る限り添数を使ってアクセスするしか実用的な方法はないようです。

では、Cの場合はどうでしょうか。
ポインタを使えば簡単に実現することができます。

#include <stdio.h>

int main(void)
{
  int a[] = { 1, 2, 3 };

  for (int *p = &a[0]; p != &a[3]; p++)
    *p = *p * 2;

  for (int i = 0; i < 3; i++)
    printf("%d\n", a[i]);
  return 0;
}

上の例では、配列の要素をポインタを使って順に2倍にしていっています。

それでは順に解説していきますね。

配列の要素のアドレス

先ほどのサンプルコードの7行目にある最初のfor文では、ポインタpを配列aの先頭要素へのアドレスで初期化しています。

for文の継続条件として毎回a[3]のアドレスと比較しています。
配列aには3要素しか無いので有効な添数は0~2なのですが、それより1多いところへのアドレスまでなら有効です。
ただし、a[3]のアドレスに対して読み書きすることはできませんので、あくまで比較用のアドレスを取得できるだけです。

ポインタどうしの比較

ポインタどうしの比較ももちろんできます。
同じオブジェクトを指すポインタであれば比較すれば等しくなります。
そうでなければ等しくありません。

同じ配列の要素を指すポインタどうしであれば大小を比較することもできます。
ポインタの大小関係を比較すると、前の要素ほど小さく、うしろの要素ほど大きくなります。
大小比較ができるのは同じ配列内の要素を指すポインタどうしに限られるので注意してくださいね。
全然関係無いポインタどうしを比較してもコンパイルはできてしまいますが、結果に意味はありません。

ポインタのインクリメントとデクリメント

for文が1周するごとにpをインクリメントしています。
PHPの可変変数は文字列でしたのでインクリメントするなどあり得ませんでしたが、Cのポインタの値はアドレスという数値ですからインクリメントすることができます。

ポインタをインクリメントすれば次の要素のアドレスに移動することができます。
インクリメントができるということはデクリメントももちろんできます。
ポインタをデクリメントすれば1つ前の要素のアドレスに移動することになりますね。

いろいろなポインタの演算

先ほどはポインタの比較やインクリメント・デクリメントを紹介しました。
実はCのポインタはもっといろいろな演算を行うことができるんです。
順番に見ていきましょう。

ポインタと整数値の加減算

ポインタに格納されているアドレスは数値に過ぎませんので、整数値を足したり引いたりすることができます。
具体例を見ていくことにしましょう。

#include <stdio.h>

int main(void)
{
  int a[] = { 1, 2, 3 };

  int *p = &a[0];
  int *q = p + 2;

  printf("%d\n", *p); // a[0]を参照
  printf("%d\n", *q); // a[2]を参照
  return 0;
}

上の例では、ポインタpに2を足した結果をポインタqに格納しています。
ポインタに整数値を足したり引いたりした結果は同じポインタ型になります。

ポインタqはpに2を足しています。
pはa[0]を指していますから、それに2を足したポインタであるqはa[2]を指すことになります。

上の例では、いったんqという別のポインタにp + 2の結果を格納しましたが、p + 2という式を使っていきなり間接参照することもできます。
こんな感じです。

printf("%d\n", *(p + 2));

次はポインタから整数値を引いてみます。

int *r = q - 1;
printf("%d\n", *r);  // a[1]を参照

ポインタと整数値の加算では、ポインタと整数値のどちらが先でも結果は同じです。
つまり、p + 2でも2 + pでも同じ結果になります。
p - 2と-2 + pも同じです。

ポインタの差

ポインタどうしを引き算することもできます。
これも具体的に見ていきますね。

#include <stdio.h>

int main(void)
{
  int a[] = { 1, 2, 3 };

  int *p = &a[0];
  int *q = &a[2];
  int  d = q - p;

  printf("%d\n", d);  // 2を出力
  return 0;
}

上の例のように、同じ配列内の要素を指すポインタどうしであれば、引き算することでその差というか距離を求めることができます。
pはa[0]を指していますし、qはa[2]を指していますから、q - pの結果は2になります。
これはイメージ通りではないでしょうか?

ちなみにポインタどうしの足し算というのはできません。
そんなことをしても結果が何を意味するのかわかりませんよね。

ところで、先ほどはポインタどうしの引き算の結果をint型のオブジェクトdに格納しましたが、本当はポインタどうしを引き算した結果は「ptrdiff_t」型になります。
引き算するだけなら特別なヘッダをインクルードする必要はありませんが、「ptrdiff_t」という名前を使うには「stddef.h」というヘッダをインクルードしてください。

#include <stdio.h>
#include <stddef.h>  // ptrdiff_t型のために必要

int main(void)
{
  int a[] = { 1, 2, 3 };

  int *p = &a[0];
  int *q = &a[2];
  ptrdiff_t d = q - p;

  printf("%td\n", d);  // 書式には「%td」を使用
  return 0;
}

上の例ですでに使ってしまいましたが、ptrdiff_t型の値をprintf関数で書式化するには「%td」のように「t」を使います。
うっかり%dを使ってしまうと未定義の動作になることがあるのでご注意ください(ptrdiff_t型が実際どんな型かは処理系定義なんです)。

ポインタと添字演算子

ポインタを使って間接参照するにはこれまで「*」演算子を使ってきました。
それはそれでいいのですが、実はもうひとつ間接参照するための演算子があります。
それが添字演算子です。

添字演算子というと配列の添字を指定するための[]のことです。
ポインタに対してもこの添字演算子を使うことができるんです。

#include <stdio.h>

int main(void)
{
  int a[] = { 1, 2, 3 };
  int *p = &a[0];

  printf("%d\n", p[0]);  // a[0]を参照
  printf("%d\n", p[2]);  // a[2]を参照
  return 0;
}

先ほどは「ポインタに対してもこの添字演算子を使うことができる」といいましたが、実はCの添字演算子のオペランドは配列型ではなくポインタ型が必要になります。

つまり、添字演算子は
<配列型> [ <整数型> ]
ではなく、
<ポインタ型> [ <整数型> ]
が本来の形なんです。

どうしてそんなことになるのか、その理由をこのあとすぐ解説しますね。

配列からポインタへの暗黙的な型変換

今まで配列型に対して使っていたと思っていた添字演算子[]は、実はポインタに対して使っていたんです。
でも実際にオペランドとして渡していたのは配列型でしたよね。
これはどういうことでしょうか?

実は、Cの配列型はほとんどの演算でポインタ型に暗黙的に型変換されてしまうのです。
配列がポインタに暗黙に型変換されたとき、そのポインタは配列の先頭要素を指します。

そうした事情があって、配列型に直接添字演算子を使っても普通に要素をアクセすることができるんです。

Cでは、添字演算子を使った式
p[n]
というのは、実は
*(p + n)
を書きやすく、また読みやすくしただけで、同じ意味なんです。

このように、Cのポインタは本当にいろんな演算ができます。
いろんな演算ができることで、Cのポインタは本当に何役もこなしてくれるパワフルな機能なんです。


今回の解説は以上となります。
ポインタに関してはまだまだ解説しないといけないことがあります。
たくさん覚えないといけないことがあって大変ですが、ブログを読むだけじゃなくてご自身で実際に手を動かしながらひとつひとつ確認していってくださいね。