ポインタ
ポインタは、他の変数へのアクセスを提供する変数だ。ポインタの値は、それがアクセスする変数のアドレスだ。
ポインタは、あらゆる型の変数、オブジェクト、さらには他のポインタも指すことができる。この章では、これらすべてを単に"変数"と呼ぶことにする。
ポインタは、マイクロプロセッサの低レベル機能だ。これらは、システムプログラミングの重要な部分だ。
Dにおけるポインタの構文および意味論は、Cから直接継承されている。ポインタはCの機能の中で最も理解が難しいことで知られているが、Dではそれほど難しくないはずだ。これは、ポインタと意味論的に近いDの他の機能が、他の言語でポインタを使用しなければならない状況においてより有用であるためだ。ポインタの背後にある考え方がDの他の機能からすでに理解されていれば、ポインタも理解しやすいはずだ。
この章のほとんどで用いている短い例は、明らかに単純なものだ。章の最後にあるプログラムは、より現実的なものになっている。
この例で使用しているptr
("ポインタ"の略)などの名前は、一般的に有用な名前とは考えないようにしよう。いつものように、実際のプログラムでは、より意味があり、説明的な名前を選択する必要がある。
参照の概念
以前の章で何度も参照に出会ってきたが、この概念をもう一度要約しよう。
foreach
ループにおけるref
変数
foreach
ループの章で見たように、通常、ループ変数は要素のコピーである:
各ループで0に代入されるnumber
は、配列の要素の1つのコピーである。そのコピーを変更しても、元の要素は変更されない:
ループ後 | [1, 11, 111] |
---|
実際の要素を変更する必要がある場合は、foreach
変数をref
として定義する必要がある:
この場合、number
は配列内の実際の要素への参照になる:
ループ後 | [0, 0, 0] |
---|
ref
関数パラメータ
関数パラメータの章で見たように、値型のパラメータは通常、引数のコピーである。
関数パラメータはref
として定義されていないため、関数内の代入は、その関数内のローカル変数にのみ影響する。main()
内の変数は影響を受けない。
関数を呼び出した後の値 | 1.5 |
---|
ref
キーワードを使用すると、関数パラメータは引数への参照になる。
この場合、main()
内の変数が変更される:
関数を呼び出した後の値 | 2 |
---|
参照型
一部の型は参照型である。このような型の変数は、個別の変数へのアクセスを提供する。
- クラス変数
- スライス
- 連想配列
この区別は、値型と参照型の章で説明した。次の例は、2つのclass
変数によって参照型を示している。
クラスは参照型であるため、クラス変数pen
とotherPen
は、同じPen
オブジェクトへのアクセスを提供する。その結果、これらのクラス変数のいずれかを使用すると、同じオブジェクトに影響する。
pen.ink | otherPen.ink | |
---|---|---|
前 | 15 | 15 |
後 | 12 | 12 |
その単一のオブジェクトと2つのクラス変数は、メモリ内に以下の図のように配置される:
(The Pen object) pen otherPen ───┬───────────────────┬─── ───┬───┬─── ───┬───┬─── │ ink │ │ o │ │ o │ ───┴───────────────────┴─── ───┴─│─┴─── ───┴─│─┴─── ▲ │ │ │ │ │ └────────────────────┴────────────┘
参照は、pen
とotherPen
のように、実際の変数を指す。
プログラミング言語は、メモリ位置を指すためのマイクロプロセッサの特殊なレジスタを使用して、参照とポインタの概念を実装している。
Dの高レベルな概念(クラス変数、スライス、連想配列など)は、すべてポインタによって実装されている。これらの高レベルな機能はすでに効率的で便利なので、Dプログラミングではポインタはほとんど必要ない。それでも、Dプログラマはポインタをよく理解しておくことが重要だ。
構文
Dのポインタの構文は、ほとんどCと同じである。これは利点とも考えられるが、Cのポインタ構文の特殊性も必然的にDに引き継がれている。例えば、*
文字の2つの意味は、混乱を招くかもしれない。
void
ポインタを除き、すべてのポインタは特定の型に関連付けられており、その特定の型の変数のみを指すことができる。例えば、int
ポインタは、int
型の変数のみを指すことができる。
ポインタの定義構文は、関連付けられた型と*
文字で構成される。
したがって、int
型の変数を指すポインタ変数は次のように定義される:
この構文の*
文字は"ポインタ"と発音する。したがって、上記のmyPointer
の型は"intポインタ"となる。*
文字の前後のスペースは省略可能だ。以下の構文もよく使われる。
int pointerのように、特にポインタ型を指す場合は、int*
のように、型をスペースなしで書くのが一般的だ。
ポインタの値とアドレス演算子 &
変数自体がポインタであるため、ポインタにも値がある。ポインタのデフォルト値は、特別な値null
だ。これは、ポインタがまだどの変数も指していない(つまり、どの変数にもアクセスできない)ことを意味する。
ポインタが変数へのアクセスを提供するには、ポインタの値をその変数のアドレスに設定する必要がある。ポインタは、その特定のアドレスにある変数を指し始める。以下、その変数を"ポインタの被指す変数"と呼ぶ。
readf
でこれまで何度も使用してきた&
演算子は、値型と参照型の章でも簡単に説明した。この演算子は、その後に書かれた変数のアドレスを生成する。その値は、ポインタの初期化に使用できる。
myPointer
をmyVariable
のアドレスで初期化すると、myPointer
はmyVariable
を指すようになる。
ポインタの値は、myVariable
のアドレスと同じになる。
myVariableのアドレス | 7FFF2CE73F10 |
---|---|
myPointerの値 | 7FFF2CE73F10 |
注釈:アドレスの値は、プログラムの起動ごとに異なる可能性が高い。
以下の図は、メモリ内のこれらの2つの変数の表現だ。
myVariable at myPointer at address 7FFF2CE73F10 some other address ───┬────────────────┬─── ───┬────────────────┬─── │ 180 │ │ 7FFF2CE73F10 │ ───┴────────────────┴─── ───┴────────│───────┴─── ▲ │ │ │ └─────────────────────────────┘
myPointer
の値はmyVariable
のアドレスであり、概念的にはその位置にある変数を指している。
ポインタも変数であるため、&
演算子はポインタのアドレスも生成できる:
myPointerのアドレス | 7FFF2CE73F18 |
---|
上記の2つのアドレスの差は8であり、int
が4バイトを占めることを考慮すると、myVariable
とmyPointer
はメモリ上で4バイト離れていることがわかる。
ポインタを表す矢印を削除すると、これらのアドレス周辺のメモリの内容を次のようにイメージできる:
7FFF2CE73F10 7FFF2CE73F14 7FFF2CE73F18 : : : : ───┬────────────────┬────────────────┬────────────────┬─── │ 180 │ (unused) │ 7FFF2CE73F10 │ ───┴────────────────┴────────────────┴────────────────┴───
変数、関数、クラスなどの名前やキーワードは、Dのようなコンパイル言語のプログラムの一部ではない。プログラマがソースコードで定義した変数は、マイクロプロセッサのメモリまたはレジスタを占めるバイトに変換される。
注釈:デバッグを容易にするために、実際にはプログラムに名前(別名シンボル)が含まれている場合もあるが、これらの名前はプログラムの動作には影響しない。
アクセス演算子 *
上記で見たように、通常は乗算を表す*
文字は、ポインタを定義する際にも使用される。ポインタの構文における難点の一つは、同じ文字が3つ目の意味を持つことだ。つまり、ポインタを介してポインタが指す変数(ポインタの指す先)にアクセスする際にも使用される。
ポインタの名前前に書かれると、ポインタが指す変数(つまりポインタが指す対象)を意味する:
それが指している値 | 180 |
---|
ポインタのポインタの要素にアクセスするための.
(ドット)演算子
C言語でポインタを学んだことがある場合、この演算子はC言語の->
演算子と同じだ。
*
演算子は、ポインタの指す値にアクセスするために使用されることを、これまで見てきた。これは、int*
のような基本型のポインタには十分有用だ。基本型の値は、*myPointer
と書くだけでアクセスできる。
しかし、ポインタが構造体またはクラスオブジェクトの場合、同じ構文では不便になる。その理由を確認するために、次の構造体を考えてみよう。
次のコードは、オブジェクトとその型のポインタを定義している。
この構文は、Coordinate
オブジェクト全体の値にアクセスする場合に便利だ。
Coordinate.x | Coordinate.y |
---|---|
0 | 0 |
しかし、ポインタと*
演算子を通じてオブジェクトのメンバにアクセスする場合、コードが複雑になる:
この式は、center
オブジェクトのx
メンバーの値を変更する。この式の左側は、次の手順で説明できる。
ptr
:center
*ptr
: オブジェクト(つまりcenter
自体)にアクセスする(*ptr)
:.
(ドット)演算子がオブジェクトに適用され、ポインタに適用されないようにするための括弧(*ptr).x
:ptr
が指すオブジェクトのx
メンバー
Dではポインタ構文の複雑さを軽減するため、.
(ドット)演算子はポインタが指すオブジェクトに移され、そのメンバーへのアクセスを提供する。(この規則の例外は、このセクションの最後に記載されている。)
したがって、前の式は通常次のように記述する。
ポインタ自体に"x
"という名前のメンバーが存在しないため、.x
はポインタ先に対して適用され、center
のx
メンバーが変更される:
Coordinate.x | Coordinate.y |
---|---|
10 | 0 |
これは、クラスで.
(ドット) 演算子を使用した場合と同じであることに注意。.
(ドット) 演算子がクラス変数に適用されると、クラスオブジェクトのメンバにアクセスできるようになる。
クラス章で学んだように、クラスオブジェクトは右辺のnew
キーワードによって構築される。variable
は、それへのアクセスを提供するクラス変数だ。
これがポインタと同じであることに気づくことは、クラス変数とポインタがコンパイラによって同様の実装されていることを示している。
この規則には、クラス変数とポインタの両方に例外がある。.sizeof
のような型プロパティは、ポインタの型ではなく、ポインタの指す型の型に適用される。
.sizeof
は、c
のサイズではなく、char*
のサイズであるp
を生成する。char
は、ポインタの型である。64ビットシステムでは、ポインタは8バイトの長さだ。
8
ポインタの値の変更
ポインタの値は、加算または減算することができ、加算および減算に使用することができる。
算術演算とは異なり、これらの演算は、指定した量だけ実際の値を変更するわけではない。むしろ、ポインタの値が変更され、現在の変数から指定した数だけ先の変数を指すようになる。加算または減算の量は、ポインタが指す変数の移動量を指定する。
例えば、ポインタの値を加算すると、ポインタは次の変数を指すようになる。
これが正しく機能するには、ポインタの実際の値が変数のサイズだけ加算されている必要がある。例えば、int
のサイズは4なので、型int*
のポインタを加算すると、その値は4だけ変わる。プログラマは、この詳細に注意する必要はない。ポインタの値は、自動的に正しい量だけ変更される。
警告: プログラムに属する有効なバイトではない場所を指すことは、未定義の動作だ。たとえその場所で変数にアクセスするために実際に使用されていなくても、存在しない変数をポインタで指すことは無効だ。(この規則の唯一の例外は、配列の末尾の1つ後の架空の要素を指すことは有効だということだ。これについては、後で説明する。)
例えば、myVariable
を指すポインタを加算することは無効である。myVariable
は単一のint
として定義されているためだ。
未定義の動作とは、その操作後のプログラムの動作が予測できないことを意味する。そのポインタを加算すると、プログラムがクラッシュするシステムもある。しかし、最近のほとんどのシステムでは、ポインタは、前の図でmyVariable
とmyPointer
の間に示した、使用されていないメモリ位置を指す可能性が高い。
そのため、ポインタの値は、新しい位置に有効なオブジェクトがある場合にのみ、加算または減算する必要がある(後述のように、配列の末尾の次の要素を指すことも有効だ)。配列(およびスライス)には、この特性がある。配列の要素は、メモリ内で隣り合って配置される。
スライスの要素を指すポインタは、スライスの末尾を超える要素にアクセスするために使用されない限り、安全に加算することができる。このようなポインタを++
演算子で加算すると、次の要素を指すようになる。
- 定義:ポインタは最初の要素のアドレスで初期化される。
- その値の使用:ポインタの値は、それが指している要素のアドレスだ。
- 指している要素へのアクセス。
- 次の要素を指す。
出力:
長さ | 色 | ポインタの値 |
---|---|---|
11cm | 赤いクレヨン | 7F37AC9E6FC0 |
12cm | 黄色いクレヨン | 7F37AC9E6FD0 |
13cm | 青いクレヨン | 7F37AC9E6FE0 |
上記のループは、ポインタが常に有効な要素へのアクセスに使用されるように、合計crayons.length
回繰り返されることに注意。
ポインタは危険である
コンパイラとDランタイム環境は、ポインタが常に正しく使用されることを保証できない。ポインタがnull
であるか、有効なメモリ位置(変数、配列の要素など)を指していることを確認するのは、プログラマーの責任だ。
このため、ポインタを使用する前に、Dのより高水準な機能を検討する方が常に望ましい。
配列の末尾の次の要素
配列の末尾の次の仮想的な要素を指すことは有効だ。
これは、数値範囲に似た便利なイディオムだ。数値範囲でスライスを定義する場合、2番目のインデックスはスライスの要素の1つ後の位置を指す:
このイディオムはポインタでも使用できる。CおよびC++では、関数パラメータの1つが最初の要素を指し、もう1つが最後の要素の次の要素を指すという関数設計がよくある。
値begin + 2
は、begin
が指している要素の2つ後の要素(つまり、インデックス3の要素)を意味する。
tenTimes()
関数は、2つのポインタパラメータを取る。この関数は、最初のポインタが指す要素を使用するが、2番目のポインタが指す要素にはアクセスしない。その結果、インデックス1と2の要素だけが変更される。
[0, 10, 20, 3]
このような関数は、for
ループでも実装できる。
範囲を定義する2つのポインタは、foreach
ループでも使用できる:
これらのメソッドをスライスのすべての要素に適用するためには、2つ目のポインタは必ず最後の要素の後に指している必要がある:
これが、配列の最後の要素の次の仮想要素を指すことが合法である理由である。
配列インデックス演算子とのポインタの使用[]
Dでは必ずしも必要ではないが、ポインタを使用して、インデックス値によって配列の要素に直接アクセスすることができる。
出力:
[0, 1.1, -100, -200, 4.4]
この構文では、ポインタが指す要素は仮想スライスの最初の要素とみなされる。[]
演算子は、そのスライスの指定された要素にアクセスする。上記のptr
は、元のfloats
スライスのインデックス2の要素を指す。ptr[1]
は、ptr
から始まる仮想スライスの要素1への参照(つまり、元のスライスのインデックス3)だ。
この動作は複雑に見えるかもしれないが、その構文の背後には非常に単純な変換がある。 実際には、コンパイラはpointer[index]
構文を*(pointer + index)
式に変換している。
前述したように、コンパイラは、この式が有効な要素を参照していることを保証することはできない。Dのスライスは、より安全な代替手段なので、代わりに検討すべきだ。
通常、インデックス値は実行時にスライスに対してチェックされる。
上記のスライスはインデックス2に要素がないため、実行時に例外がスローされる (-release
コンパイラスイッチでコンパイルされている場合を除く)。
ポインタからスライスを生成する
ポインタは、スライスインデックス演算子とともに使用することはできるが、要素の有効範囲を認識しないため、スライスほど安全でも便利でもない。
ただし、有効な要素の数が既知の場合、ポインタを使用してスライスを作成できる。
以下のmakeObjects()
関数がCライブラリ内にあるとしよう。makeObjects
は、指定された数のStruct
オブジェクトを作成し、それらのオブジェクトの最初のオブジェクトへのポインタを返すとしよう。
ポインタからスライスを生成する構文は次の通りだ。
したがって、makeObjects()
が返す10個のオブジェクトのスライスを、以下のコードで構築できる:
この定義の後、slice
は、他のスライスと同じようにプログラム内で安全に使用できるようになる。
void*
は、あらゆる型を指すことができる
Dではほとんど必要になることはないが、Cの特別なポインタ型void*
はDでも使用できる。void*
は、あらゆる型を指すことができる。
上記のvoid*
は、2つの異なる型、int
とdouble
の変数を指すことができる。
void*
ポインタは機能が制限されている。その柔軟性のため、ポインタはポインタが指す変数にアクセスすることはできない。実際の型が不明の場合、そのサイズも不明になる。
その代わりに、その値はまず正しい型のポインタに変換する必要がある。
- 実際の変数
- 変数のアドレスを
void*
- そのアドレスを正しい型のポインタに代入する
- 新しいポインタを介して変数を変更する
void*
ポインタの値は、加算または減算することができる。その場合、その値は、ubyte
のような1バイト型のポインタであるかのように変更される。
void*
Cは、インターフェイス、クラス、テンプレートなどの高レベル機能を持っていないため、Cライブラリはvoid*
型に依存している。
論理式でのポインタの使用
ポインタは、自動的にbool
に変換される。値null
を持つポインタはfalse
になり、その他のポインタはtrue
になる。つまり、変数を指していないポインタはfalse
になる。
オブジェクトを標準出力に表示する関数を考えてみよう。この関数は、出力したバイト数も提供するよう設計しよう。ただし、この情報は、特に要求があった場合にのみ出力するようにしよう。
ポインタの値がnull
であるかどうかを調べることで、この動作をオプションにすることができる。
呼び出し元がこの特別な情報を必要としない場合、引数としてnull
を指定できる:
バイト数が実際に重要な場合は、null
以外のポインタ値を渡す必要がある。
これは単なる例であることに注意。そうでなければ、print()
のような関数は、無条件にバイト数を返すほうがよいだろう。
new
一部の型に対するポインタを返す
new
は、クラスオブジェクトの構築にのみ使用してきたが、構造体、配列、および基本型などの他の型にも使用できる。new
によって構築される変数は、動的変数と呼ばれる。
new
まず、変数用のメモリ領域を割り当て、その領域に変数を構築する。変数自体はコンパイルされたプログラム内でシンボリック名を持たず、new
が返す参照を通じてアクセスされる。
new
が返す参照は、変数の型に応じて異なる。
- クラスオブジェクトの場合、それはクラス変数である:
- 構造体オブジェクトおよび基本型の変数の場合、それはポインタである。
- 配列の場合、それはスライスである:
この区別は、左側に型が明示されていない場合、通常、明らかではない。
次のプログラムは、さまざまな種類の変数に対するnew
の戻り値の型を出力する。
new
構造体および基本型の場合はポインタを返す。
int*
int[]
Struct*
Class
new
が値型の動的変数の構築に使用される場合、その変数の有効期間は、プログラム内にそのオブジェクトへの参照(ポインタなど)が存在する限り延長される。(これは、参照型の場合のデフォルトの状況だ。)
配列の.ptr
プロパティ
配列およびスライスの.ptr
プロパティは、最初の要素のアドレスだ。この値の型は、要素の型へのポインタだ。
このプロパティは、Cライブラリとやり取りする場合に特に便利だ。C関数の中には、メモリ内の連続した要素の最初の要素のアドレスを取るものがある。
文字列も配列であることを覚えておくと、.ptr
プロパティは文字列にも使用できる。ただし、文字列の最初の要素は、文字列の最初の文字である必要はなく、その文字の最初のUnicodeコードユニットであることに注意。例えば、文字éは、char
の文字列では2つのコードユニットとして格納される。
.ptr
プロパティを介してアクセスすると、文字列のコード単位に個別にアクセスできる。これについては、以下の例で説明する。
連想配列のin
演算子
実際、連想配列の章で、ポインタをすでに使用している。その章では、in
演算子の正確な型については意図的に言及せず、論理式でのみ使用した。
実際、in
演算子は、指定したキーに対応する要素がある場合はその要素のアドレスを返し、ない場合はnull
を返す。上記のif
文は、実際にはポインタの値がbool
に自動的に変換されることに依存している。
in
の戻り値がポインタに格納されると、そのポインタを介して要素に効率的にアクセスできる。
ポインタ変数element
は、in
演算子の値(1)で初期化され、その値は論理式(2)で使用される。要素の値は、ポインタがnull
でない場合にのみ、そのポインタ(3)を通じてアクセスされる。
上記のelement
の実際の型は、連想配列の要素(つまり値)と同じ型のポインタである。上記のnumbers
の要素はstring
型であるため、in
はstring*
を返す。したがって、型は明示的に記述することも可能だった。
ポインタを使用するタイミング
Dではポインタはほとんど使われない。標準入力からの読み込みの章で見たように、readf
は実際には明示的なポインタを使わずに使用できる。
ライブラリで必要とされる場合
ポインタは、CおよびC++ライブラリバインディングで出現する。例えば、GtkDライブラリの次の関数はポインタを受け取る。
値型の変数を参照する場合
ポインタはローカル変数を参照するために使用できる。次のプログラムは、コインを投げた結果をカウントする。2つのローカル変数のうち1つを参照する際にポインタを利用している:
もちろん、同じことを実現する方法は他にもある。例えば、三項演算子を別の方法で使用する。
if
文を使用すると、次のように記述できる。
データ構造のメンバ変数として
ポインタは、多くのデータ構造を実装する際に不可欠である。
配列の要素はメモリ上で隣り合っているのに対し、多くの他のデータ構造の要素は離れている。このようなデータ構造は、その要素が他の要素を指すという概念に基づいている。
例えば、リンクリストの各ノードは次のノードを指している。同様に、二分木の各ノードは、そのノードの下にある左と右の分岐を指している。ポインタは、他のほとんどのデータ構造でも使用される。
Dの参照型を利用することも可能だが、場合によってはポインタの方がより自然で効率的である場合もある。
以下では、ポインタメンバーの例を見ていく。
メモリに直接アクセスする場合
ポインタは、低レベルのマイクロプロセッサ機能であり、メモリ位置へのバイトレベルのアクセスを提供する。このような位置は、依然として有効な変数に属している必要があることに注意。ランダムなメモリ位置にアクセスしようとすると、未定義の動作になる。
例
シンプルな連結リスト
連結リストの要素はノードに格納される。連結リストの概念は、各ノードが次のノードを指すことに基づいている。最後のノードは指す他のノードがないため、null
に設定される:
first node next node last node ┌─────────┬───┐ ┌─────────┬───┐ ┌─────────┬──────┐ │ element │ o────▶ │ element │ o────▶ ... │ element │ null │ └─────────┴───┘ └─────────┴───┘ └─────────┴──────┘
上記の図は誤解を招く可能性がある:実際には、ノードはメモリ上で隣り合って配置されていない。各ノードは次のノードを指すが、次のノードは完全に異なる位置にある可能性がある。
以下のstruct
は、このようなint
の連結リストのノードを表すために使用できる:
注釈: Node
は、それ自体と同じ型への参照を含むため、の再帰型である。
リスト全体は、最初のノードを指す単一のポインタで表すことができ、このポインタは一般的に"ヘッド"と呼ばれる:
例を短くするために、リストの先頭に要素を追加する関数を1つだけ定義しよう。
insertAtHead()
内の行は、リストの先頭に新しいノードを追加することで、ノード間のリンクを維持している。(リストの最後に追加する関数の方がより自然で便利だろう。その関数は、後の問題で説明する。)
この行の右側の式は、Node
オブジェクトを構築する。この新しいオブジェクトが構築されると、そのnext
メンバーは、リストの現在の先頭によって初期化される。リストのhead
メンバーが、この新しくリンクされたノードに割り当てられると、新しい要素が最初の要素になる。
次のプログラムは、この2つの構造体をテストする。
出力:
前 | () |
---|---|
後 | (9 -> 8 -> 7 -> 6 -> 5 -> 4 -> 3 -> 2 -> 1 -> 0) |
メモリの内容を確認するubyte*
各メモリアドレスに格納されているデータは1バイトである。すべての変数は、その変数の型のサイズと同じバイト数で構成されるメモリ領域上に構築される。
メモリ位置の内容を確認するには、ubyte*
というポインタ型が適している。変数のアドレスがubyte
ポインタに割り当てられると、そのポインタを加算することで、その変数のすべてのバイトを確認することができる。
メモリ内のバイトの配置をわかりやすくするために、16進表記で初期化された次の整数を考えてみよう。
その変数を指すポインタは次のように定義できる:
その変数の値は、cast
演算子によってubyte
指し針に割り当てることができる。
このようなポインタを使用すると、int
変数の4つのバイトを個別にアクセスできる:
私のマイクロプロセッサのようにリトルエンディアンの場合、0x01_02_03_04
の値のバイトは逆の順序で表示されるはずだ。
4
3
2
1
この考え方を、あらゆる型の変数のバイトを観察するときに役立つ関数で使ってみよう。
- 変数のアドレスを
ubyte
ポインタに代入する。 - ポインタの値を出力する。
.sizeof
で型のサイズを取得し、変数のバイトを出力する。(begin
ポインタからスライスが生成され、そのスライスがwritefln()
で直接出力されていることに注意。)
バイトを出力する別の方法は、*
演算子を個別に適用することだ:
bytePointer
の値は、begin
からbegin + T.sizeof
に変化し、変数のすべてのバイトを訪問する。値begin + T.sizeof
は範囲外であり、アクセスされることはないことに注意。
次のプログラムは、さまざまな型の変数でprintBytes()
を呼び出す。
プログラムの出力は参考になる:
型 : 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)
観察事項:
- リトルエンディアンシステムでは逆の順序になるが、一部の型のバイトは予想どおりになっている。
int
、固定長配列 (int[3]
)、および構造体オブジェクトのバイトは、メモリ内に隣り合って配置されている。 double.nan
の特別な値のバイトもメモリ内で逆の順序になっていることを考えると、これは特別なビットパターン0x7ff8000000000000で表現されていることがわかる。string
は16バイトから構成されていると報告されているが、文字を"a bright and charming façade"
これほど少ないバイト数では文字をすべて収めることは不可能だ。これは、string
が実際には構造体として実装されているためだ。コンパイラが使用する内部型であることを強調するために、その名前に__
を付加すると、その構造体は次のようなものになる。この事実の証拠は、上記の
string
の出力バイトに隠されている。çは2つのUTF-8コードユニットで構成されているため、文字列の28文字は"a bright and charming façade"
は合計29バイトで構成されていることに注意。上記の出力の文字列の最初の8バイトの値0x000000000000001dも29。これは、文字列が実際に上記の構造体のようにメモリにレイアウトされていることを強く示している。- 同様に、クラスオブジェクトの3つの
int
メンバーを8バイトに収めることはできない。上記の出力は、裏でクラス変数が実際のクラスオブジェクトを指す単一のポインタとして実装されている可能性を示唆している:
ここでは、より柔軟な関数について考えてみよう。変数のバイトを出力する代わりに、指定した位置に指定したバイト数を出力する関数を定義しよう。
UTF-8コードユニットの一部はターミナルの制御文字に対応し、出力を妨げる可能性があるため、std.ascii.isPrintable()
で個々にチェックして、表示可能な文字のみを表示する。表示できない文字はドットで表示する。
この関数を使用して、string
の.ptr
プロパティを通じて、UTF-8コード単位を出力することができる。
出力からわかるように、文字"ç"は2バイトで構成されている:
アドレス | バイナリ値 | 文字 |
---|---|---|
47B4F0 | 61 | a |
47B4F1 | 20 | |
47B4F2 | 62 | b |
47B4F3 | 72 | r |
47B4F4 | 69 | i |
47B4F5 | 67 | g |
47B4F6 | 68 | h |
47B4F7 | 74 | t |
47B4F8 | 20 | |
47B4F9 | 61 | a |
47B4FA | 6e | n |
47B4FB | 64 | d |
47B4FC | 20 | |
47B4FD | 63 | c |
47B4FE | 68 | h |
47B4FF | 61 | a |
47B500 | 72 | r |
47B501 | 6d | m |
47B502 | 69 | i |
47B503 | 6e | n |
47B504 | 67 | g |
47B505 | 20 | |
47B506 | 66 | f |
47B507 | 61 | a |
47B508 | c3 | . |
47B509 | a7 | . |
47B50A | 61 | a |
47B50B | 64 | d |
47B50C | 65 | e |
演習
- 引数に渡される値の交換を行うように、次の関数を修正しよう。この演習では、パラメータを
ref
として指定せず、ポインタとして受け取るようにしよう。プログラムを実行すると、
assert
のチェックが現在失敗していることに気付くはずだ。 - 上記で定義したリンクリストを、任意の型の要素を格納できるようにテンプレートに変換しよう。
- 要素をリンクリストの末尾に追加する方が自然だ。
List
を修正して、末尾にも要素を追加できるようにしよう。この演習では、最後の要素を指す追加のポインタメンバー変数が役立つ。