2009.12.01
yna

「アドレス、ポインタ、リファレンス」

Joelは曰く、「Java」は、実装言語としては、悪い言語ではない。しかし、「Java」はすぐれたプログラマと凡庸なプログラマを見分けることができるほど、難しい言語ではない。
うん、大いに同感(笑)

本当は今日は「Joel on Software」の続刊、「Mode Joel on Software」を紹介しようかなと思っていました。その中の8章「Javaスクールの危険」という記事で、最近の学生は、ポインタを理解できていないという話があったので、それをネタに、ちょっとコンピュータの内部の世界を紹介しようと思います。

コンピュータには、CPUとメモリと(それにI/O)というので出来ているということは、知っていると思う。そして、メモリには、アドレスと言う番号が付いていることも、まあ、プログラマであれば、知っている人も多いと思う。
アドレスというのは、メモリのなかの位置を示しているのですが、それぞれのデータは1byte単位で(現在のPCで使われるほとんどのCPU)で、そのデータがどんなデータなのか分かりません。
デバッガというツールでメモリの中を表示すると、こんなように表示されます。


00123660 41 42 43 44 45 00 00 00-E8 03 00 00 10 27 00 00
00123670 FE 00 56 00 4E 00 56 00-E4 05 00 00 10 3C 00 00
.....

左側がアドレスで、その右側に並んでいるのが、そのアドレスの内容です。慣例として16進数で表示されることが多いです。一般に32ビットCPUと呼ばれるCPUでも、データそのものは、バイト単位でアクセスできます。これだけ見ただけでは、このデータが文字列を示しているか、数値を示しているかは判りません。それは、プログラムを作る人が管理することになっています。

このままではあまりに使いづらいので、指されているアドレスがどんなデータなのかを示したのが、ポインタです。ポインタとは型の付いたアドレスともいえます。

[C]

int aryTest[10] = { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10};

int     intsum( int *ptr, int nSize )
{
int     ret = 0;
    while(nSize -- > 0) ret += *ptr ++;     # ここ
    return ret;
}

ポインタは、アドレスに比べ、示している位置に入っているデータの種類をちゃんと保持していると意味では、アドレスに比べ抽象性が上がっています。ptr ++すると、次の位置を示します。(示されている変数のサイズ分アドレスを進めます)
間違った場所を示すこともできますし、お行儀の悪いことも平然とできる仕組みなっています。たとえば、C で書かれたこのコードは、どんなことが起きるでしょうか?(VC++では、とりあえず警告は表示されますが、コンパイルされます)

[C]
unsigned int    *func_a()
{
    unsigned int        x = 1;
    return &x;
}

void    func_b(unsigned int *p)
{
    *p++ = 200;
    *p++ = 200;
    *p++ = 200;
}

main()
{
    unsigned int    *p = func_a();
    func_b(p);
}

コンパイラによっては、正常に動作したように見えることもあるでしょう。でも多くの場合は、重大なエラーを起こすはずです。(VC++では実際エラーが起きます。)

さて、この扱いづらいポインタをもっと大人しくして、抽象化したのが、リファレンスです。
たとえば、Perlではこんな風に使うことができます。

[Perl]
    $a = 100;
    $p = \$a;
    $$p ++ ;
    print $a;

$aという変数の参照を$pに入れて、$$pという形で元の$aにアクセスしています。
これを実行すると、$aを+1した101を返してきます。

    101

しかしリファレンスは、ポインタと異なって、示している位置を移動することができません。以下のように、配列の要素のリファレンスを使って、次の要素にアクセスしようとするとどうなるでしょう?。perlの以下のプログラムでは、3行の目の$p++で、リファレンスから、ただの数値に変化してしまい、4行目でエラーになります。

[Perl]
    @a = (1,2,3,4,5);
    $p = \$a[0];
    $p ++;
    $$p = 10;           # ここで、エラーが発生します。
    print $a[1];

PHP になると、Perlのリファンレスでは持っていたポインタの色合いが無くなってしまいます。(こうなると、リファレンスそのものを操作することができません。)

[PHP]
    $a = 100;
    $p = &$a ;
    $p++;
    print $a;

実際PHP のマニュアルには、以下のように書かれています。

PHP において、リファレンスとは同じ変数の内容を異なった名前で コールすることを意味します。これは C のポインタとは異なります。リファレンスを使ってポインタの演算をすることはできませんし、リファレンスは実メモリのアドレスでもありません。

PHPやPerlでは、一旦確保された変数が何らかのリファレンスで参照されている限りは、解放されません。そのため、先ほどC ではエラーになったような、コードでも問題なく動きます。

[Perl]
sub     func_a()
{
    my      $x = 1;
    return \$x;
}
    $p = func_a();
    print "\$p = $p \$\$p = $$p\n";

 実行すると、こんな実行結果が返ってきます。

    $p = SCALAR(0x182fa60) $$p = 1

自由度が減る代わりに、安全性はかなり増します。

PS.
今回のネタとはあまり関係がないけど、この2冊は必読です。
「Joel on Software」
https://www.amazon.co.jp/Joel-Software-Spolsky/dp/4274066304
「Mode Joel on Software」
https://www.amazon.co.jp/More-Joel-Software-Spolsky/dp/4798118923/

yna

一覧に戻る