Contents

  • ポインタとは(復習)
    • アドレスってなんやねん!
    • アドレスとポインタってなにがどうやねん!
    • 変数のアドレスってどこやねん!
  • ポインタの使い方
    • ポインタと配列とインデックス演算子とポインタへの加算
    • ポインタと左辺値
    • ポインタの初期値とゼロ値
    • 特別なポインタvoid*とスライスvoid[]
  • おわりに

ポインタとは(復習)

これまでの記事でポインタは何回も出てきたので、ここまで読み進めた方にはわかると思いますが、ポインタとは「メモリ上でのアドレス」です。

アドレスってなんやねん!

メモリには1バイトごとにアドレスが振り分けられています。 たとえば、32bitのシステムですと、アドレスが0x00000000 ~ 0xFFFFFFFF2^^32個しかないので、1バイトごとにアドレスを割り振れば2^^32バイト(大体4GB, 丁度4GiB)のメモリが使用可能です。 これが64bitのシステムだと、アドレスが0x0000000000000000 ~ 0xFFFFFFFFFFFFFFFF2^^64個もあるので、1バイトごとだと2^^64バイト(大体16EB, 丁度16EiB)もメモリ空間が広がっています。

アドレスがある理由は、よく配達業に例えられるのですが、配達したいものがあっても配達先の住所がなければ配達できない、ということです。IPアドレスとか、URLとかと存在理由は同じですね。

アドレスとポインタってなにがどうやねん!

「ポインタ型の値」に格納されている値が「メモリ上でのアドレス」です。 (正確に言えば、ポインタ変数の用途によるので、「ポインタ型の値に格納されている値は、メモリ上でのアドレスと期待しても良い」が正しいのでしょう)

D言語では、ポインタの値はアドレス値に等しいので、ポインタとメモリ上でのアドレスは等しいと考えても構いません。

変数のアドレスってどこやねん!

変数(左辺値)のポインタは&aで取得できますし、配列の先頭要素へのポインタはarr.ptrもしくは&a[0]で得れますね。 連想配列ではkey in aaとすることで、連想配列にキーがあるかどうか調べることと、そのキーへのポインタを返すことができました。

さて、変数のアドレス、つまりメモリ上での位置はどのあたりでしょうか? 正確に言えることは、「nullではないが、実行してみない限り分からない」ことです。 ここで重要なのは、実体(instance)を指すポインタは必ずnullではなく、逆に実態のないものを指すポインタは必ずnullですし、nullにしなければいけません。

つまり、ポインタがnullであるかどうか調べることで、ポインタが参照する先に実体があるかどうか確認できます。 この仕組みを使ったのが、連想配列でのkey in aaなのです。

ポインタの使い方

この章では今までより一歩踏み込んで、ポインタに対する演算や、ポインタの使い方について説明します。 ただし、D言語的な書き方をしている限りは、ポインタに出くわすことはないので、参考程度に捉えてください。

ポインタと配列とインデックス演算子とポインタへの加算

配列の先頭要素へのポインタはarr.ptrであり、n要素目へのポインタは&(arr[n])と書けます。 同様に、arr.ptr + nn要素目へのポインタですし、&(arr.ptr[n])n要素目へのポインタになります。

つまり、ポインタpと整数nに対して、p + nは、p[n]へのポインタを示します。 このように、ポインタは加算や減算はもちろん、インクリメントとデクリメントもできます。

加算や減算の場合、p + nはポインタpn * typeof(*p).sizeofだけ進めます。 つまり、int* pに対してp + 3はpから12バイト(3 * 4バイト)先のintを指すことになります。 よって、p[n] == *(p + n)なのです。

ポインタと左辺値

左辺値からは&lvalueによってポインタを得ることができますし、*pとすればポインタから左辺値が得られます。 D言語では、C++のint&というような左辺値を参照する変数を定義できないので、代わりに、ポインタやクラスを使うことになります。 (関数の引数については例外で、参照で受け取ることが可能)

int a = 2,
    b = 3;

int* p = a < b ? &a : &b;

*p = 10;                // a = 10と同じ

writeln(a);             // 10
writeln(b);             // 3

ポインタの初期値とゼロ値

ポインタの初期値(T*).initnullという値です。 nullの性質として、nulltruefalseかでいうと、falseになり、nullでないポインタはtrueです。 また、nullなポインタを通してアクセスしようとすると、例外(いわゆる「ぬるぽ」)が発生します。

int* p;
writeln(p);             // null

if(!p)                  // !p は true
    writeln("ぬるぽ");

*p = 10;                // 例外(いわゆる「ぬるぽ」)

/*  以下は例外の内容
object.Error: Access Violation
----------------
0x00402021 in _imp__LeaveCriticalSection
0x0040947C in _NULL_IMPORT_DESCRIPTOR
0x004094B7 in _NULL_IMPORT_DESCRIPTOR
0x004090B5 in _NULL_IMPORT_DESCRIPTOR
*/

特別なポインタvoid*とスライスvoid[]

すべてのポインタはvoid*というポインタ型に暗黙変換可能です。 void*は特別なポインタであり、*pp[0]という風に参照先の値にアクセスできません。 void*型は、アドレスを指定しているだけで、実際にどのような型の値を取得するかわからないからです。 よって、アクセスする際にはキャストで違うポインタへ変換する必要があります。

スライスvoid[]も全てのスライスから暗黙変換可能で、長さはスライスが所有しているバイト数になります。

int[] a = [1, 2, 3];

void* p = a.ptr;
//writeln(*p);                  Error: expression *p is void and has no value
//writeln(p[0]);                Error: (略)

void[] slice = a;
//writeln(slice[0]);            Error: (略)
writeln(slice);                 // [1, 0, 0, 0, 2, 0, 0, 0, 3, 0, 0, 0]
                                // リトルエンディアン

writeln(*cast(int*)p);          // 1
writeln((cast(int[])slice));    // [1, 2, 3]

おわりに

さて、ポインタが終わってしまいました。 D言語ではポインタはそれほど出てこないので、重要度はかなり低い方です。 ただ、クラスやデリゲート、関数ポインタ、連想配列、配列はポインタのすごいバージョンとも捉えられますし、ポインタを知っていると参照型は理解しやすいかと思います。