ポインタを使った間接参照

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

今回から何回かに分けてポインタの解説を行います。
読者の中には「Cにはポインタがある」という話をどこかで聞いたことがあるという方もいらっしゃると思います。
ポインタそのものではありませんがPHPにも似たような概念はあるので、比較しながら解説することにしますね。

ポインタの用途

まずはじめに、Cポインタにはどんな用途があるのかを簡単に説明します。
ここを踏まえた上で個別の機能を見ていくことにします。

ポインタには大きく分けて次のような用途があります。

  • 間接参照
  • イテレータ
  • オブジェクトの所有

ちょっと難しい表現に感じられるかもしれませんけど、全部PHPにもある概念ばかりです。
おおまかな意味を理解していただくために、PHPでのそれぞれの書き方をおさらいしますね。

PHPにおける間接参照

PHPには可変変数や可変関数といった機能があります。
変数や関数の名前を頼りに間接参照するためのものですね。
たとえばこんな感じです。

<?php
$value = 123;
$name = 'value'; // 可変変数を使って間接参照させる。

echo $$name . PHP_EOL;

$$name = 456;
echo $$name . PHP_EOL;

PHPにおけるイテレータ

PHPのイテレータは反復処理に使うための機能です。
配列などの要素を順にたどっていくとき使います。

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

while ($it->valid())
{
  echo $it->current() . PHP_EOL;
  $it->next();
}

PHPにおけるオブジェクトの所有

PHPではクラスのオブジェクトをnewで構築しても、それを何かの変数に格納することがほとんどだと思います。
この場合、newで構築したオブジェクトをその変数が所有していると考えることができます。
変数のスコープから抜けるとそのオブジェクトは解放されることになります。

<?php
class A
{
  public function test()
  {
    echo 'test' .PHP_EOL;
  }
}

$obj = new A(); // オブジェクトの所有
$obj->test();

オブジェクトの間接参照

先ほどは、Cのポインタの用途に対応するPHPの機能をざっとおさらいしました。
ここからはCのポインタについて解説していきます。
一度にすべてを解説できませんので、今回は間接参照に絞って解説していくことにしますね。

ポインタによる間接参照はPHPの可変変数による間接参照とよく似ています。
PHPの間接参照は名前によって変数を指定しますが、Cのポインタはアドレスによってオブジェクトを指定します。

アドレスというのはメモリー上の特定の位置を表すための数値です。
ポインタはメモリー上の位置を表す数値であるアドレスとそこに格納されているオブジェクトの型を持っています。

それでは具体的な例を見てみましょう。

int x = 123;
int *p = &x;

上の例では、int型のオブジェクトxのアドレスを「&」演算子を使って取得しています。
そして取得したアドレスを使ってpというオブジェクトを初期化しています。
pの型は「int*」型です。
「int*」型はint型へのポインタ型という意味です。
原則として参照先の型名のあとに「*」を付ければポインタ型になります。

今度はこのpを使って間接参照先のアドレス先であるxにアクセスしてみましょう。
ポインタを使って間接参照するには「*」演算子を使います。

すぐに確認できるようにプログラム全体を掲載しておきますね。

#include <stdio.h>

int main(void)
{
  int x = 123;
  int *p = &x;

  printf("%d\n", *p);
  *p = 456;  // pを介してxを書き換える。
  printf("%d\n", *p);
  return 0;
}

コンパイルして実行すると、

123
456

と出力されたでしょうか?

この例ではint型へのポインタ型のオブジェクトpを介して、オブジェクトxの値を取得し、またxの値を456に書き換えています。

同じことをPHPの可変変数を使って行うと次のようになります。

<?php
$x = 123;
$p = 'x';

printf("%d\n", $$p);
$$p = 456;
printf("%d\n", $$p);

だいたいイメージをつかんでいただけたでしょうか?

関数の間接参照

PHPでは可変関数を使って関数の間接参照を行うこともできます。
コールバックなんかで多用しますので、もしかしたら可変変数より可変関数の方がよく使うのかもしれません(ごめんなさい、まだPHPの事情がよく分かっていません)。

Cでも、ポインタを使って関数を間接参照することができます。
まずは実際の例をご紹介します。

#include <stdio.h>

int func(int arg)
{
  return arg * 2;
}

int main(void)
{
  int (*p)(int) = &func;

  printf("%d\n", func(123));
  printf("%d\n", (*p)(123)); // pを介してfuncを呼び出す。
  return 0;
}

概念的にはオブジェクトの間接参照とまったく同じなんですけど、見た目の難しさで混乱する方が大勢いらっしゃるようです。
今回のポインタpの型は「int(*)(int)」型になります(関数へのポインタ型といいます)。
パッと見難しそうですよね。

この型を読み解く前にfunc関数の型について説明しておきます。
Cでは関数にも型があるんですよ。

func関数の型は「int(int)」型です。
関数の宣言から関数名を取り除いただけですね。

funcの型がint(int)なら、

int(int) *p = &func;

と書きたいと思われるかもしれませんが、Cではそのような書き方は許されていません。
じゃあ、

int *p(int) = &func;

と書けるかというとこれもダメです。
これだと、int型の引数を受け取ってint*型(int型へのポインタ型)を返す関数の意味になってしまいます。
そういう事情があって、すごくわかりにくいですが、

int (*p)(int) = &func;

のように丸括弧で*pを囲まないといけません。

関数へのポインタを使ってもとの関数を呼び出す場合も同じ考え方の延長になります。
ポインタpの型はint(*)(int)型ですから、pに「*」演算子を適用すれば間接参照することができます。
ただし、*p(123)のようにしてしまうと、p(123)だけで関数呼び出しになってしまい、「*」はp(123)が返したポインタに「*」演算子を適用するという意味になってしまいます。
ですので、ここでも丸括弧を使って(*p)のように囲んでいます。

参考までにPHPの可変関数を使って同じことをやる例も掲載しておきますね。

<?php
function func(int $arg): int
{
  return $arg * 2;
}

$p = 'func';
printf("%d\n", func(123));
printf("%d\n", $p(123));

ここでもしかしたら気付かれた方もいらっしゃるかもしれませんが、PHPの可変関数では「(\$p)(123)」ではなく「\$p(123)」と書けています。
これはなぜかというと、PHPでは関数が返り値が可変変数や可変関数だった場合でも、直接それらを使って間接参照することができないためです。
具体的にはこういうことです。

<?php
$x = 123;

function func()
{
  return 'x';
}

printf("%d\n", $x);
printf("%d\n", $func());  // エラー! こういう使い方はできない。

PHPではできなかったことがCではできます。

#include <stdio.h>

int x = 123;

int *func(void)
{
  return &x;
}

int main(void)
{
  printf("%d\n", x);
  printf("%d\n", *func()); // xの値を出力。
  return 0;
}

Cではこのような書き方ができる代わりに、というよりできるが故に、ポインタのオブジェクトを宣言するときの書き方が難しくなっています。
一長一短だと思いますが、宣言の書き方は慣れてしまえば問題ないので、機能的にはCのポインタの方がPHPの可変変数や可変関数より優れていると思います。

ポインタを使う上での注意点

ポインタは便利ですが、注意しないと深刻な不具合につながります。
注意点はいろいろあるんですけど、ここでは本当に基本的なものだけをご紹介します。

ポインタは必ず初期化すること!

ポインタ型のオブジェクトも普通のオブジェクトと同じで、明示的に初期化しなければ中にはゴミが入っています。
意味的にはゴミだったとしてもそれはどこかのアドレスを指している可能性が高いんです。
ですから、そんなゴミのアドレスを使って間接参照してしまうと簡単にシステムをクラッシュさせてしまいます。

もし、有効なアドレスを指していないポインタが欲しい場合は「空ポインタ」を使います。
空ポインタを格納しておけば、ポインタが保持しているアドレスが空ポインタかどうかを比較できますが、ゴミだとそれが正しいアドレスかどうかを簡単に判別する方法がありません。

空ポインタを使うには「NULL」という定数を使います。
これはマクロといって多くの標準ライブラリのヘッダの中で定義されています(マクロそのものについては別の機会に詳しく解説します)。

PHPにもNULLがあるので、どういう使い方をするかイメージできると思います。
ただし、PHPでは小文字のnullでも有効ですが、Cでは必ず大文字でNULLと書きます。
また、is_null関数のようなものはありませんので普通に「==」演算子などで比較してください。

自動記憶域期間を持つオブジェクトへのポインタを関数から戻ったあとで使わない!

自動記憶域期間を持つオブジェクトというのは関数の中でstatic指定子を付けずに宣言したオブジェクトのことです。
そうしたオブジェクトは関数から戻った時点でもう存在していません。
メモリ自体はずっとありますが、すぐに別の用途に使われます。

具体的にダメな例を挙げますね。

#include <stdio.h>

int *func(void)
{
  int x = 123;  // xは自動記憶域期間
  return &x;
}

int main(void)
{
  printf("%d\n", x);
  printf("%d\n", *func()); // func関数の中のxはすでに存在しない。
  return 0;
}

先ほどの例とそっくりですが、1カ所だけ違っています。
xの宣言がfunc関数の中で行われていることです。
これだとfunc関数から戻った時点でxはもう存在していませんので、func関数が返したポインタは単なるゴミになります。

PHPでも同じようなことをすると期待しない結果になりますよね。

<?php
function func()
{
 $x = 123;
  return 'x';
}

printf("%d\n", $x);
$p = func();  // func関数の中のxはすでに存在しない。
printf("%d\n", $p);

こんな感じでポインタを使う際には十分注意しないといけません。
プラットフォームによってはハードウェアを破壊してしまうこともありますし、場合によってはそれ以上の事故につながることもあります(PCだとほとんどの場合、プログラムがクラッシュする程度で済むとは思います)。
注意して使うためにはポインタについて正確に理解しておく必要があります。


それでは今回の解説は以上です。
今回ポインタについて解説したのは間接参照だけですので、ほかの用途(イテレータとオブジェクトの所有)については次回以降に解説することにします。
イテレータとしての用途についてはたぶん次回に解説すると思います。
どうぞご期待ください!