ポインタ

ポインタは、他の変数へのアクセスを提供する変数だ。ポインタの値は、それがアクセスする変数のアドレスだ。

ポインタは、あらゆる型の変数、オブジェクト、さらには他のポインタも指すことができる。この章では、これらすべてを単に"変数"と呼ぶことにする。

ポインタは、マイクロプロセッサの低レベル機能だ。これらは、システムプログラミングの重要な部分だ。

Dにおけるポインタの構文および意味論は、Cから直接継承されている。ポインタはCの機能の中で最も理解が難しいことで知られているが、Dではそれほど難しくないはずだ。これは、ポインタと意味論的に近いDの他の機能が、他の言語でポインタを使用しなければならない状況においてより有用であるためだ。ポインタの背後にある考え方がDの他の機能からすでに理解されていれば、ポインタも理解しやすいはずだ。

この章のほとんどで用いている短い例は、明らかに単純なものだ。章の最後にあるプログラムは、より現実的なものになっている。

この例で使用しているptr("ポインタ"の略)などの名前は、一般的に有用な名前とは考えないようにしよう。いつものように、実際のプログラムでは、より意味があり、説明的な名前を選択する必要がある。

参照の概念

以前の章で何度も参照に出会ってきたが、この概念をもう一度要約しよう。

foreachループにおけるref変数

foreachループの章で見たように、通常、ループ変数は要素のコピーである:

import std.stdio;

void main() {
    int[] numbers = [ 1, 11, 111 ];

    foreach (number; numbers) {
        number = 0;     // ← 要素ではなく、コピーが変更される
    }

    writeln("After the loop: ", numbers);
}
D
pointers.1

各ループで0に代入されるnumberは、配列の要素の1つのコピーである。そのコピーを変更しても、元の要素は変更されない:

ループ後[1, 11, 111]

実際の要素を変更する必要がある場合は、foreach変数をrefとして定義する必要がある:

foreach (ref number; numbers) {
    number = 0;     // ← 実際の要素が変更される
}
D

この場合、numberは配列内の実際の要素への参照になる:

ループ後[0, 0, 0]
ref関数パラメータ

関数パラメータの章で見たように、値型のパラメータは通常、引数のコピーである。

import std.stdio;

void addHalf(double value) {
    value += 0.5;        // ← mainの'値'には影響しない
}

void main() {
    double value = 1.5;

    addHalf(value);

    writeln("The value after calling the function: ", value);
}
D
pointers.2

関数パラメータはrefとして定義されていないため、関数内の代入は、その関数内のローカル変数にのみ影響する。main()内の変数は影響を受けない。

関数を呼び出した後の値1.5

refキーワードを使用すると、関数パラメータは引数への参照になる。

void addHalf(ref double value) {
    value += 0.5;
}
D

この場合、main()内の変数が変更される:

関数を呼び出した後の値2
参照型

一部の型は参照型である。このような型の変数は、個別の変数へのアクセスを提供する。

この区別は、値型と参照型の章で説明した。次の例は、2つのclass変数によって参照型を示している。

import std.stdio;

class Pen {
    double ink;

    this() {
        ink = 15;
    }

    void use(double amount) {
        ink -= amount;
    }
}

void main() {
    auto pen = new Pen;
    auto otherPen = pen;  // ← 両方の変数が
                          //   同じオブジェクトへのアクセスを提供する

    writefln("Before: %s %s", pen.ink, otherPen.ink);

    pen.use(1);          // ← 同じオブジェクトが使用される
    otherPen.use(2);     // ← 同じオブジェクトが使用される

    writefln("After : %s %s", pen.ink, otherPen.ink);
}
D
pointers.3

クラスは参照型であるため、クラス変数penotherPenは、同じPenオブジェクトへのアクセスを提供する。その結果、これらのクラス変数のいずれかを使用すると、同じオブジェクトに影響する。

pen.inkotherPen.ink
1515
1212

その単一のオブジェクトと2つのクラス変数は、メモリ内に以下の図のように配置される:

      (The Pen object)            pen        otherPen
 ───┬───────────────────┬───  ───┬───┬───  ───┬───┬───
    │        ink        │        │ o │        │ o │
 ───┴───────────────────┴───  ───┴─│─┴───  ───┴─│─┴───
              ▲                    │            │
              │                    │            │
              └────────────────────┴────────────┘

参照は、penotherPenのように、実際の変数を指す

プログラミング言語は、メモリ位置を指すためのマイクロプロセッサの特殊なレジスタを使用して、参照とポインタの概念を実装している。

Dの高レベルな概念(クラス変数、スライス、連想配列など)は、すべてポインタによって実装されている。これらの高レベルな機能はすでに効率的で便利なので、Dプログラミングではポインタはほとんど必要ない。それでも、Dプログラマはポインタをよく理解しておくことが重要だ。

構文

Dのポインタの構文は、ほとんどCと同じである。これは利点とも考えられるが、Cのポインタ構文の特殊性も必然的にDに引き継がれている。例えば、*文字の2つの意味は、混乱を招くかもしれない。

voidポインタを除き、すべてのポインタは特定の型に関連付けられており、その特定の型の変数のみを指すことができる。例えば、intポインタは、int型の変数のみを指すことができる。

ポインタの定義構文は、関連付けられた型と*文字で構成される。

type_to_point_at * name_of_the_pointer_variable;
D

したがって、int型の変数を指すポインタ変数は次のように定義される:

int * myPointer;
D

この構文の*文字は"ポインタ"と発音する。したがって、上記のmyPointerの型は"intポインタ"となる。*文字の前後のスペースは省略可能だ。以下の構文もよく使われる。

int* myPointer;
int *myPointer;
D

int pointerのように、特にポインタ型を指す場合は、int*のように、型をスペースなしで書くのが一般的だ。

ポインタの値とアドレス演算子 &

変数自体がポインタであるため、ポインタにも値がある。ポインタのデフォルト値は、特別な値nullだ。これは、ポインタがまだどの変数も指していない(つまり、どの変数にもアクセスできない)ことを意味する。

ポインタが変数へのアクセスを提供するには、ポインタの値をその変数のアドレスに設定する必要がある。ポインタは、その特定のアドレスにある変数を指し始める。以下、その変数を"ポインタの被指す変数"と呼ぶ。

readfでこれまで何度も使用してきた&演算子は、値型と参照型の章でも簡単に説明した。この演算子は、その後に書かれた変数のアドレスを生成する。その値は、ポインタの初期化に使用できる。

int myVariable = 180;
int * myPointer = &myVariable;
D

myPointermyVariableのアドレスで初期化すると、myPointermyVariableを指すようになる。

ポインタの値は、myVariableのアドレスと同じになる。

writeln("The address of myVariable: ", &myVariable);
writeln("The value of myPointer   : ", myPointer);
D
myVariableのアドレス7FFF2CE73F10
myPointerの値7FFF2CE73F10

注釈:アドレスの値は、プログラムの起動ごとに異なる可能性が高い。

以下の図は、メモリ内のこれらの2つの変数の表現だ。

      myVariable at                myPointer at
  address 7FFF2CE73F10          some other address
───┬────────────────┬───     ───┬────────────────┬───
   │      180       │           │  7FFF2CE73F10  │
───┴────────────────┴───     ───┴────────│───────┴───
           ▲                             │
           │                             │
           └─────────────────────────────┘

myPointerの値はmyVariableのアドレスであり、概念的にはその位置にある変数を指している

ポインタも変数であるため、&演算子はポインタのアドレスも生成できる:

writeln("The address of myPointer : ", &myPointer);
D
myPointerのアドレス7FFF2CE73F18

上記の2つのアドレスの差は8であり、intが4バイトを占めることを考慮すると、myVariablemyPointerはメモリ上で4バイト離れていることがわかる。

ポインタを表す矢印を削除すると、これらのアドレス周辺のメモリの内容を次のようにイメージできる:

    7FFF2CE73F10     7FFF2CE73F14     7FFF2CE73F18
    :                :                :                :
 ───┬────────────────┬────────────────┬────────────────┬───
    │      180       │    (unused)    │  7FFF2CE73F10  │
 ───┴────────────────┴────────────────┴────────────────┴───

変数、関数、クラスなどの名前やキーワードは、Dのようなコンパイル言語のプログラムの一部ではない。プログラマがソースコードで定義した変数は、マイクロプロセッサのメモリまたはレジスタを占めるバイトに変換される。

注釈:デバッグを容易にするために、実際にはプログラムに名前(別名シンボル)が含まれている場合もあるが、これらの名前はプログラムの動作には影響しない。

アクセス演算子 *

上記で見たように、通常は乗算を表す*文字は、ポインタを定義する際にも使用される。ポインタの構文における難点の一つは、同じ文字が3つ目の意味を持つことだ。つまり、ポインタを介してポインタが指す変数(ポインタの指す先)にアクセスする際にも使用される。

ポインタの名前前に書かれると、ポインタが指す変数(つまりポインタが指す対象)を意味する:

writeln("The value that it is pointing at: ", *myPointer);
D
それが指している値180
ポインタのポインタの要素にアクセスするための.(ドット)演算子

C言語でポインタを学んだことがある場合、この演算子はC言語の->演算子と同じだ。

*演算子は、ポインタの指す値にアクセスするために使用されることを、これまで見てきた。これは、int*のような基本型のポインタには十分有用だ。基本型の値は、*myPointerと書くだけでアクセスできる。

しかし、ポインタが構造体またはクラスオブジェクトの場合、同じ構文では不便になる。その理由を確認するために、次の構造体を考えてみよう。

struct Coordinate {
    int x;
    int y;

    string toString() const {
        return format("(%s,%s)", x, y);
    }
}
D

次のコードは、オブジェクトとその型のポインタを定義している。

auto center = Coordinate(0, 0);
Coordinate * ptr = &center;    // ポインタの定義
writeln(*ptr);                 // オブジェクトへのアクセス
D

この構文は、Coordinateオブジェクト全体の値にアクセスする場合に便利だ。

Coordinate.xCoordinate.y
00

しかし、ポインタと*演算子を通じてオブジェクトのメンバにアクセスする場合、コードが複雑になる:

// x座標を調整する
(*ptr).x += 10;
D

この式は、centerオブジェクトのxメンバーの値を変更する。この式の左側は、次の手順で説明できる。

Dではポインタ構文の複雑さを軽減するため、.(ドット)演算子はポインタが指すオブジェクトに移され、そのメンバーへのアクセスを提供する。(この規則の例外は、このセクションの最後に記載されている。)

したがって、前の式は通常次のように記述する。

ptr.x += 10;
D

ポインタ自体に"x"という名前のメンバーが存在しないため、.xはポインタ先に対して適用され、centerxメンバーが変更される:

Coordinate.xCoordinate.y
100

これは、クラスで.(ドット) 演算子を使用した場合と同じであることに注意。.(ドット) 演算子がクラス変数に適用されると、クラスオブジェクトのメンバにアクセスできるようになる。

class ClassType {
    int member;
}

// ...

    // 左側に変数、右側にオブジェクト
    ClassType variable = new ClassType;

    // 変数に適用されるが、オブジェクトのメンバに
    // アクセスしている
    variable.member = 42;
D

クラス章で学んだように、クラスオブジェクトは右辺のnewキーワードによって構築される。variableは、それへのアクセスを提供するクラス変数だ。

これがポインタと同じであることに気づくことは、クラス変数とポインタがコンパイラによって同様の実装されていることを示している。

この規則には、クラス変数とポインタの両方に例外がある。.sizeofのような型プロパティは、ポインタの型ではなく、ポインタの指す型の型に適用される。

char c;
char * p = &c;

    writeln(p.sizeof);  // ポインタのサイズ、ポインタが指すもののサイズではない
D

.sizeofは、cのサイズではなく、char*のサイズであるpを生成する。charは、ポインタの型である。64ビットシステムでは、ポインタは8バイトの長さだ。

8
ポインタの値の変更

ポインタの値は、加算または減算することができ、加算および減算に使用することができる。

++ptr;
--ptr;
ptr += 2;
ptr -= 2;
writeln(ptr + 3);
writeln(ptr - 3);
D

算術演算とは異なり、これらの演算は、指定した量だけ実際の値を変更するわけではない。むしろ、ポインタの値が変更され、現在の変数から指定した数だけ先の変数を指すようになる。加算または減算の量は、ポインタが指す変数の移動量を指定する。

例えば、ポインタの値を加算すると、ポインタは次の変数を指すようになる。

++ptr;  // 古い変数のメモリの次に位置する
        // 変数を指し始める
D

これが正しく機能するには、ポインタの実際の値が変数のサイズだけ加算されている必要がある。例えば、intのサイズは4なので、型int*のポインタを加算すると、その値は4だけ変わる。プログラマは、この詳細に注意する必要はない。ポインタの値は、自動的に正しい量だけ変更される。

警告: プログラムに属する有効なバイトではない場所を指すことは、未定義の動作だ。たとえその場所で変数にアクセスするために実際に使用されていなくても、存在しない変数をポインタで指すことは無効だ。(この規則の唯一の例外は、配列の末尾の1つ後の架空の要素を指すことは有効だということだ。これについては、後で説明する。)

例えば、myVariableを指すポインタを加算することは無効である。myVariableは単一のintとして定義されているためだ。

++myPointer;       // ← 未定義の動作
D

未定義の動作とは、その操作後のプログラムの動作が予測できないことを意味する。そのポインタを加算すると、プログラムがクラッシュするシステムもある。しかし、最近のほとんどのシステムでは、ポインタは、前の図でmyVariablemyPointerの間に示した、使用されていないメモリ位置を指す可能性が高い。

そのため、ポインタの値は、新しい位置に有効なオブジェクトがある場合にのみ、加算または減算する必要がある(後述のように、配列の末尾の次の要素を指すことも有効だ)。配列(およびスライス)には、この特性がある。配列の要素は、メモリ内で隣り合って配置される。

スライスの要素を指すポインタは、スライスの末尾を超える要素にアクセスするために使用されない限り、安全に加算することができる。このようなポインタを++演算子で加算すると、次の要素を指すようになる。

import std.stdio;
import std.string;
import std.conv;

enum Color { red, yellow, blue }

struct Crayon {
    Color color;
    double length;

    string toString() const {
        return format("%scm %s crayon", length, color);
    }
}

void main() {
    writefln("Crayon objects are %s bytes each.", Crayon.sizeof);

    Crayon[] crayons = [ Crayon(Color.red, 11),
                         Crayon(Color.yellow, 12),
                         Crayon(Color.blue, 13) ];

    Crayon * ptr = &crayons[0];                   // (1)

    for (int i = 0; i != crayons.length; ++i) {
        writeln("Pointer value: ", ptr);          // (2)

        writeln("Crayon: ", *ptr);                // (3)
        ++ptr;                                    // (4)
    }
}
D
pointers.4
  1. 定義:ポインタは最初の要素のアドレスで初期化される。
  2. その値の使用:ポインタの値は、それが指している要素のアドレスだ。
  3. 指している要素へのアクセス。
  4. 次の要素を指す。

出力:

クレヨン
長さポインタの値
11cm赤いクレヨン7F37AC9E6FC0
12cm黄色いクレヨン7F37AC9E6FD0
13cm青いクレヨン7F37AC9E6FE0

上記のループは、ポインタが常に有効な要素へのアクセスに使用されるように、合計crayons.length回繰り返されることに注意。

ポインタは危険である

コンパイラとDランタイム環境は、ポインタが常に正しく使用されることを保証できない。ポインタがnullであるか、有効なメモリ位置(変数、配列の要素など)を指していることを確認するのは、プログラマーの責任だ。

このため、ポインタを使用する前に、Dのより高水準な機能を検討する方が常に望ましい。

配列の末尾の次の要素

配列の末尾の次の仮想的な要素を指すことは有効だ。

これは、数値範囲に似た便利なイディオムだ。数値範囲でスライスを定義する場合、2番目のインデックスはスライスの要素の1つ後の位置を指す:

int[] values = [ 0, 1, 2, 3 ];
writeln(values[1 .. 3]);   // 1と2を含み、3は除く
D

このイディオムはポインタでも使用できる。CおよびC++では、関数パラメータの1つが最初の要素を指し、もう1つが最後の要素の次の要素を指すという関数設計がよくある。

import std.stdio;

void tenTimes(int * begin, int * end) {
    while (begin != end) {
        *begin *= 10;
        ++begin;
    }
}

void main() {
    int[] values = [ 0, 1, 2, 3 ];

    // 2番目の要素のアドレス:
    int * begin = &values[1];

    // その要素の2つ先の要素のアドレス
    tenTimes(begin, begin + 2);

    writeln(values);
}
D
pointers.5

begin + 2は、beginが指している要素の2つ後の要素(つまり、インデックス3の要素)を意味する。

tenTimes()関数は、2つのポインタパラメータを取る。この関数は、最初のポインタが指す要素を使用するが、2番目のポインタが指す要素にはアクセスしない。その結果、インデックス1と2の要素だけが変更される。

[0, 10, 20, 3]

このような関数は、forループでも実装できる。

for ( ; begin != end; ++begin) {
    *begin *= 10;
}
D

範囲を定義する2つのポインタは、foreachループでも使用できる:

foreach (ptr; begin .. end) {
    *ptr *= 10;
}
D

これらのメソッドをスライスのすべての要素に適用するためには、2つ目のポインタは必ず最後の要素の後に指している必要がある:

// 2番目のポインタは、配列の末尾を越えた
// 仮想的な要素を指している:
tenTimes(begin, begin + values.length);
D

これが、配列の最後の要素の次の仮想要素を指すことが合法である理由である。

配列インデックス演算子とのポインタの使用[]

Dでは必ずしも必要ではないが、ポインタを使用して、インデックス値によって配列の要素に直接アクセスすることができる。

double[] floats = [ 0.0, 1.1, 2.2, 3.3, 4.4 ];

double * ptr = &floats[2];

*ptr = -100;      // それが指すものに直接アクセスする
ptr[1] = -200;    // インデックスによるアクセス

writeln(floats);
D

出力:

[0, 1.1, -100, -200, 4.4]

この構文では、ポインタが指す要素は仮想スライスの最初の要素とみなされる。[]演算子は、そのスライスの指定された要素にアクセスする。上記のptrは、元のfloatsスライスのインデックス2の要素を指す。ptr[1]は、ptrから始まる仮想スライスの要素1への参照(つまり、元のスライスのインデックス3)だ。

この動作は複雑に見えるかもしれないが、その構文の背後には非常に単純な変換がある。 実際には、コンパイラはpointer[index]構文を*(pointer + index)式に変換している。

ptr[1] = -200;      // スライス構文
*(ptr + 1) = -200;  // 前の行と同等
D

前述したように、コンパイラは、この式が有効な要素を参照していることを保証することはできない。Dのスライスは、より安全な代替手段なので、代わりに検討すべきだ。

double[] slice = floats[2 .. 4];
slice[0] = -100;
slice[1] = -200;
D

通常、インデックス値は実行時にスライスに対してチェックされる。

slice[2] = -300;  // ランタイムエラー: スライスの外部にアクセスしている
D

上記のスライスはインデックス2に要素がないため、実行時に例外がスローされる (-releaseコンパイラスイッチでコンパイルされている場合を除く)。

core.exception.RangeError@deneme(8391): 範囲違反
Undefined
ポインタからスライスを生成する

ポインタは、スライスインデックス演算子とともに使用することはできるが、要素の有効範囲を認識しないため、スライスほど安全でも便利でもない。

ただし、有効な要素の数が既知の場合、ポインタを使用してスライスを作成できる。

以下のmakeObjects()関数がCライブラリ内にあるとしよう。makeObjectsは、指定された数のStructオブジェクトを作成し、それらのオブジェクトの最初のオブジェクトへのポインタを返すとしよう。

Struct * ptr = makeObjects(10);
D

ポインタからスライスを生成する構文は次の通りだ。

/* ... */ slice = pointer[0 .. count];
D

したがって、makeObjects()が返す10個のオブジェクトのスライスを、以下のコードで構築できる:

Struct[] slice = ptr[0 .. 10];
D

この定義の後、sliceは、他のスライスと同じようにプログラム内で安全に使用できるようになる。

writeln(slice[1]);    // 2番目の要素を表示
D
void*は、あらゆる型を指すことができる

Dではほとんど必要になることはないが、Cの特別なポインタ型void*はDでも使用できる。void*は、あらゆる型を指すことができる。

int number = 42;
double otherNumber = 1.25;
void * canPointAtAnything;

canPointAtAnything = &number;
canPointAtAnything = &otherNumber;
D

上記のvoid*は、2つの異なる型、intdoubleの変数を指すことができる。

void*ポインタは機能が制限されている。その柔軟性のため、ポインタはポインタが指す変数にアクセスすることはできない。実際の型が不明の場合、そのサイズも不明になる。

*canPointAtAnything = 43;     // ← コンパイルエラー
D

その代わりに、その値はまず正しい型のポインタに変換する必要がある。

int number = 42;                                  // (1)
void * canPointAtAnything = &number;              // (2)

// ...

int * intPointer = cast(int*)canPointAtAnything;  // (3)
*intPointer = 43;                                 // (4)
D
  1. 実際の変数
  2. 変数のアドレスをvoid*
  3. そのアドレスを正しい型のポインタに代入する
  4. 新しいポインタを介して変数を変更する

void*ポインタの値は、加算または減算することができる。その場合、その値は、ubyteのような1バイト型のポインタであるかのように変更される。

++canPointAtAnything;    // 1ずつ増加する
D

void*Cは、インターフェイス、クラス、テンプレートなどの高レベル機能を持っていないため、Cライブラリはvoid*型に依存している。

論理式でのポインタの使用

ポインタは、自動的にboolに変換される。値nullを持つポインタはfalseになり、その他のポインタはtrueになる。つまり、変数を指していないポインタはfalseになる。

オブジェクトを標準出力に表示する関数を考えてみよう。この関数は、出力したバイト数も提供するよう設計しよう。ただし、この情報は、特に要求があった場合にのみ出力するようにしよう。

ポインタの値がnullであるかどうかを調べることで、この動作をオプションにすることができる。

void print(Crayon crayon, size_t * numberOfBytes) {
    immutable info = format("Crayon: %s", crayon);
    writeln(info);

    if (numberOfBytes) {
        *numberOfBytes = info.length;
    }
}
D

呼び出し元がこの特別な情報を必要としない場合、引数としてnullを指定できる:

print(Crayon(Color.yellow, 7), null);
D

バイト数が実際に重要な場合は、null以外のポインタ値を渡す必要がある。

size_t numberOfBytes;
print(Crayon(Color.blue, 8), &numberOfBytes);
writefln("%s bytes written to the output", numberOfBytes);
D

これは単なる例であることに注意。そうでなければ、print()のような関数は、無条件にバイト数を返すほうがよいだろう。

size_t print(Crayon crayon) {
    immutable info = format("Crayon: %s", crayon);
    writeln(info);

    return info.length;
}
D
new 一部の型に対するポインタを返す

newは、クラスオブジェクトの構築にのみ使用してきたが、構造体、配列、および基本型などの他の型にも使用できる。newによって構築される変数は、動的変数と呼ばれる。

new まず、変数用のメモリ領域を割り当て、その領域に変数を構築する。変数自体はコンパイルされたプログラム内でシンボリック名を持たず、newが返す参照を通じてアクセスされる。

newが返す参照は、変数の型に応じて異なる。

この区別は、左側に型が明示されていない場合、通常、明らかではない。

auto classVariable = new Class;
auto structPointer = new Struct;
auto intPointer = new int;
auto slice = new int[100];
D

次のプログラムは、さまざまな種類の変数に対するnewの戻り値の型を出力する。

import std.stdio;

struct Struct {
}

class Class {
}

void main() {
    writeln(typeof(new int   ).stringof);
    writeln(typeof(new int[5]).stringof);
    writeln(typeof(new Struct).stringof);
    writeln(typeof(new Class ).stringof);
}
D
pointers.6

new 構造体および基本型の場合はポインタを返す。

int*
int[]
Struct*
Class

new値型の動的変数の構築に使用される場合、その変数の有効期間は、プログラム内にそのオブジェクトへの参照(ポインタなど)が存在する限り延長される。(これは、参照型の場合のデフォルトの状況だ。)

配列の.ptrプロパティ

配列およびスライスの.ptrプロパティは、最初の要素のアドレスだ。この値の型は、要素の型へのポインタだ。

int[] numbers = [ 7, 12 ];

int * addressOfFirstElement = numbers.ptr;
writeln("First element: ", *addressOfFirstElement);
D

このプロパティは、Cライブラリとやり取りする場合に特に便利だ。C関数の中には、メモリ内の連続した要素の最初の要素のアドレスを取るものがある。

文字列も配列であることを覚えておくと、.ptrプロパティは文字列にも使用できる。ただし、文字列の最初の要素は、文字列の最初の文字である必要はなく、その文字の最初のUnicodeコードユニットであることに注意。例えば、文字éは、charの文字列では2つのコードユニットとして格納される。

.ptrプロパティを介してアクセスすると、文字列のコード単位に個別にアクセスできる。これについては、以下の例で説明する。

連想配列のin演算子

実際、連想配列の章で、ポインタをすでに使用している。その章では、in演算子の正確な型については意図的に言及せず、論理式でのみ使用した。

if ("purple" in colorCodes) {
    // キー"purple"の要素がある

} else {
    // キー"purple"の要素がない
}
D

実際、in演算子は、指定したキーに対応する要素がある場合はその要素のアドレスを返し、ない場合はnullを返す。上記のif文は、実際にはポインタの値がboolに自動的に変換されることに依存している。

inの戻り値がポインタに格納されると、そのポインタを介して要素に効率的にアクセスできる。

import std.stdio;

void main() {
    string[int] numbers =
        [ 0 : "zero", 1 : "one", 2 : "two", 3 : "three" ];

    int number = 2;
    auto element = number in numbers;             // (1)

    if (element) {                                // (2)
        writefln("I know: %s.", *element);        // (3)

    } else {
        writefln("I don't know the spelling of %s.", number);
    }
}
D
pointers.7

ポインタ変数elementは、in演算子の値(1)で初期化され、その値は論理式(2)で使用される。要素の値は、ポインタがnullでない場合にのみ、そのポインタ(3)を通じてアクセスされる。

上記のelementの実際の型は、連想配列の要素(つまり値)と同じ型のポインタである。上記のnumbersの要素はstring型であるため、instring*を返す。したがって、型は明示的に記述することも可能だった。

string * element = number in numbers;
D
ポインタを使用するタイミング

Dではポインタはほとんど使われない。標準入力からの読み込みの章で見たように、readfは実際には明示的なポインタを使わずに使用できる。

ライブラリで必要とされる場合

ポインタは、CおよびC++ライブラリバインディングで出現する。例えば、GtkDライブラリの次の関数はポインタを受け取る。

GdkGeometry geometry;
// ... 'geometry'のメンバーを設定する ...

window.setGeometryHints(/* ... */, &geometry, /* ... */);
D
値型の変数を参照する場合

ポインタはローカル変数を参照するために使用できる。次のプログラムは、コインを投げた結果をカウントする。2つのローカル変数のうち1つを参照する際にポインタを利用している:

import std.stdio;
import std.random;

void main() {
    size_t headsCount = 0;
    size_t tailsCount = 0;

    foreach (i; 0 .. 100) {
        size_t * theCounter = (uniform(0, 2) == 1)
                               ? &headsCount
                               : &tailsCount;
        ++(*theCounter);
    }

    writefln("heads: %s  tails: %s", headsCount, tailsCount);
}
D
pointers.8

もちろん、同じことを実現する方法は他にもある。例えば、三項演算子を別の方法で使用する。

uniform(0, 2) ? ++headsCount : ++tailsCount;
D

if文を使用すると、次のように記述できる。

	if (uniform(0, 2)) {
        ++headsCount;

    } else {
        ++tailsCount;
    }
D
データ構造のメンバ変数として

ポインタは、多くのデータ構造を実装する際に不可欠である。

配列の要素はメモリ上で隣り合っているのに対し、多くの他のデータ構造の要素は離れている。このようなデータ構造は、その要素が他の要素を指すという概念に基づいている。

例えば、リンクリストの各ノードは次のノードを指している。同様に、二分木の各ノードは、そのノードの下にある左と右の分岐を指している。ポインタは、他のほとんどのデータ構造でも使用される。

Dの参照型を利用することも可能だが、場合によってはポインタの方がより自然で効率的である場合もある。

以下では、ポインタメンバーの例を見ていく。

メモリに直接アクセスする場合

ポインタは、低レベルのマイクロプロセッサ機能であり、メモリ位置へのバイトレベルのアクセスを提供する。このような位置は、依然として有効な変数に属している必要があることに注意。ランダムなメモリ位置にアクセスしようとすると、未定義の動作になる。

シンプルな連結リスト

連結リストの要素はノードに格納される。連結リストの概念は、各ノードが次のノードを指すことに基づいている。最後のノードは指す他のノードがないため、nullに設定される:

   first node           next node                 last node
 ┌─────────┬───┐     ┌─────────┬───┐          ┌─────────┬──────┐
 │ element │ o────▶  │ element │ o────▶  ...  │ element │ null │
 └─────────┴───┘     └─────────┴───┘          └─────────┴──────┘

上記の図は誤解を招く可能性がある:実際には、ノードはメモリ上で隣り合って配置されていない。各ノードは次のノードを指すが、次のノードは完全に異なる位置にある可能性がある。

以下のstructは、このようなintの連結リストのノードを表すために使用できる:

struct Node {
    int element;
    Node * next;

    // ...
}
D

注釈: Nodeは、それ自体と同じ型への参照を含むため、の再帰型である。

リスト全体は、最初のノードを指す単一のポインタで表すことができ、このポインタは一般的に"ヘッド"と呼ばれる:

struct List {
    Node * head;

    // ...
}
D

例を短くするために、リストの先頭に要素を追加する関数を1つだけ定義しよう。

struct List {
    Node * head;

    void insertAtHead(int element) {
        head = new Node(element, head);
    }

    // ...
}
D

insertAtHead()内の行は、リストの先頭に新しいノードを追加することで、ノード間のリンクを維持している。(リストの最後に追加する関数の方がより自然で便利だろう。その関数は、後の問題で説明する。)

この行の右側の式は、Nodeオブジェクトを構築する。この新しいオブジェクトが構築されると、そのnextメンバーは、リストの現在の先頭によって初期化される。リストのheadメンバーが、この新しくリンクされたノードに割り当てられると、新しい要素が最初の要素になる。

次のプログラムは、この2つの構造体をテストする。

import std.stdio;
import std.conv;
import std.string;

struct Node {
    int element;
    Node * next;

    string toString() const {
        string result = to!string(element);

        if (next) {
            result ~= " -> " ~ to!string(*next);
        }

        return result;
    }
}

struct List {
    Node * head;

    void insertAtHead(int element) {
        head = new Node(element, head);
    }

    string toString() const {
        return format("(%s)", head ? to!string(*head) : "");
    }
}

void main() {
    List numbers;

    writeln("before: ", numbers);

    foreach (number; 0 .. 10) {
        numbers.insertAtHead(number);
    }

    writeln("after : ", numbers);
}
D
pointers.9

出力:

()
(9 -> 8 -> 7 -> 6 -> 5 -> 4 -> 3 -> 2 -> 1 -> 0)
メモリの内容を確認するubyte*

各メモリアドレスに格納されているデータは1バイトである。すべての変数は、その変数の型のサイズと同じバイト数で構成されるメモリ領域上に構築される。

メモリ位置の内容を確認するには、ubyte*というポインタ型が適している。変数のアドレスがubyteポインタに割り当てられると、そのポインタを加算することで、その変数のすべてのバイトを確認することができる。

メモリ内のバイトの配置をわかりやすくするために、16進表記で初期化された次の整数を考えてみよう。

int variable = 0x01_02_03_04;
D

その変数を指すポインタは次のように定義できる:

int * address = &variable;
D

その変数の値は、cast演算子によってubyte指し針に割り当てることができる。

ubyte * bytePointer = cast(ubyte*)address;
D

このようなポインタを使用すると、int変数の4つのバイトを個別にアクセスできる:

writeln(bytePointer[0]);
writeln(bytePointer[1]);
writeln(bytePointer[2]);
writeln(bytePointer[3]);
D

私のマイクロプロセッサのようにリトルエンディアンの場合、0x01_02_03_04の値のバイトは逆の順序で表示されるはずだ。

4
3
2
1

この考え方を、あらゆる型の変数のバイトを観察するときに役立つ関数で使ってみよう。

import std.stdio;

void printBytes(T)(ref T variable) {
    const ubyte * begin = cast(ubyte*)&variable;    // (1)

    writefln("type   : %s", T.stringof);
    writefln("value  : %s", variable);
    writefln("address: %s", begin);                 // (2)
    writef  ("bytes  : ");

    writefln("%(%02x %)", begin[0 .. T.sizeof]);    // (3)

    writeln();
}
D
pointers.10
  1. 変数のアドレスをubyteポインタに代入する。
  2. ポインタの値を出力する。
  3. .sizeofで型のサイズを取得し、変数のバイトを出力する。(beginポインタからスライスが生成され、そのスライスがwritefln()で直接出力されていることに注意。)

バイトを出力する別の方法は、*演算子を個別に適用することだ:

foreach (bytePointer; begin .. begin + T.sizeof) {
    writef("%02x ", *bytePointer);
}
D

bytePointerの値は、beginからbegin + T.sizeofに変化し、変数のすべてのバイトを訪問する。値begin + T.sizeofは範囲外であり、アクセスされることはないことに注意。

次のプログラムは、さまざまな型の変数でprintBytes()を呼び出す。

struct Struct {
    int first;
    int second;
}

class Class {
    int i;
    int j;
    int k;

    this(int i, int j, int k) {
        this.i = i;
        this.j = j;
        this.k = k;
    }
}

void main() {
    int integerVariable = 0x11223344;
    printBytes(integerVariable);

    double doubleVariable = double.nan;
    printBytes(doubleVariable);

    string slice = "a bright and charming façade";
    printBytes(slice);

    int[3] array = [ 1, 2, 3 ];
    printBytes(array);

    auto structObject = Struct(0xaa, 0xbb);
    printBytes(structObject);

    auto classVariable = new Class(1, 2, 3);
    printBytes(classVariable);
}
D

プログラムの出力は参考になる:

型   : int
値  : 287454020
アドレス: 7FFF19A83FB0
バイト  : 44 33 22 11                             ← (1)

型   : double
値  : nan
アドレス: 7FFF19A83FB8
バイト  : 00 00 00 00 00 00 f8 7f                 ← (2)

型   : string
値  : a bright and charming façade
アドレス: 7FFF19A83FC0
バイト  : 1d 00 00 00 00 00 00 00 e0 68 48 00 00 00 00 00
                                                 ← (3)
型   : int[3LU]
値  : [1, 2, 3]
アドレス: 7FFF19A83FD0
バイト  : 01 00 00 00 02 00 00 00 03 00 00 00     ← (1)

型   : Struct
値  : Struct(170, 187)
アドレス: 7FFF19A83FE8
バイト  : aa 00 00 00 bb 00 00 00                 ← (1)

型   : Class
値  : deneme.Class
アドレス: 7FFF19A83FF0
バイト  : 80 df 79 d5 97 7f 00 00                 ← (4)

観察事項:

  1. リトルエンディアンシステムでは逆の順序になるが、一部の型のバイトは予想どおりになっている。int、固定長配列 (int[3])、および構造体オブジェクトのバイトは、メモリ内に隣り合って配置されている。
  2. double.nanの特別な値のバイトもメモリ内で逆の順序になっていることを考えると、これは特別なビットパターン0x7ff8000000000000で表現されていることがわかる。
  3. stringは16バイトから構成されていると報告されているが、文字を "a bright and charming façade"これほど少ないバイト数では文字をすべて収めることは不可能だ。これは、stringが実際には構造体として実装されているためだ。コンパイラが使用する内部型であることを強調するために、その名前に__を付加すると、その構造体は次のようなものになる。
    struct __string {
        size_t length;
        char * ptr;    // 実際の文字
    }
    D

    この事実の証拠は、上記のstringの出力バイトに隠されている。çは2つのUTF-8コードユニットで構成されているため、文字列の28文字は "a bright and charming façade"は合計29バイトで構成されていることに注意。上記の出力の文字列の最初の8バイトの値0x000000000000001dも29。これは、文字列が実際に上記の構造体のようにメモリにレイアウトされていることを強く示している。

  4. 同様に、クラスオブジェクトの3つのintメンバーを8バイトに収めることはできない。上記の出力は、裏でクラス変数が実際のクラスオブジェクトを指す単一のポインタとして実装されている可能性を示唆している:
    struct __Class_VariableType {
        __Class_ActualObjecType * object;
    }
    D

ここでは、より柔軟な関数について考えてみよう。変数のバイトを出力する代わりに、指定した位置に指定したバイト数を出力する関数を定義しよう。

import std.stdio;
import std.ascii;

void printMemory(T)(T * location, size_t length) {
    const ubyte * begin = cast(ubyte*)location;

    foreach (address; begin .. begin + length) {
        char c = (isPrintable(*address) ? *address : '.');

        writefln("%s:  %02x  %s", address, *address, c);
    }
}
D
pointers.11

UTF-8コードユニットの一部はターミナルの制御文字に対応し、出力を妨げる可能性があるため、std.ascii.isPrintable()で個々にチェックして、表示可能な文字のみを表示する。表示できない文字はドットで表示する。

この関数を使用して、string.ptrプロパティを通じて、UTF-8コード単位を出力することができる。

import std.stdio;

void main() {
    string s = "a bright and charming façade";
    printMemory(s.ptr, s.length);
}
D

出力からわかるように、文字"ç"は2バイトで構成されている:

アドレスバイナリ値文字
47B4F061a
47B4F120
47B4F262b
47B4F372r
47B4F469i
47B4F567g
47B4F668h
47B4F774t
47B4F820
47B4F961a
47B4FA6en
47B4FB64d
47B4FC20
47B4FD63c
47B4FE68h
47B4FF61a
47B50072r
47B5016dm
47B50269i
47B5036en
47B50467g
47B50520
47B50666f
47B50761a
47B508c3.
47B509a7.
47B50A61a
47B50B64d
47B50C65e
演習
  1. 引数に渡される値の交換を行うように、次の関数を修正しよう。この演習では、パラメータをrefとして指定せず、ポインタとして受け取るようにしよう。
    void swap(int lhs, int rhs) {
        int temp = lhs;
        lhs = rhs;
        rhs = temp;
    }
    
    void main() {
        int i = 1;
        int j = 2;
    
        swap(i, j);
    
        // これらの値は入れ替える必要がある
        assert(i == 2);
        assert(j == 1);
    }
    D
    pointers.12

    プログラムを実行すると、assertのチェックが現在失敗していることに気付くはずだ。

  2. 上記で定義したリンクリストを、任意の型の要素を格納できるようにテンプレートに変換しよう。
  3. 要素をリンクリストの末尾に追加する方が自然だ。Listを修正して、末尾にも要素を追加できるようにしよう。

    この演習では、最後の要素を指す追加のポインタメンバー変数が役立つ。