Swiftでのポインタ、アドレスについて
Swiftのポインタというかアドレスについて調べる
普通のローカル変数のアドレス
func printAddress(_ message: String, _ ptr: UnsafePointer<Int>) {
var addr = String.init(format:"%016x", ptr)
print(message, addr)
}
func printAddress(_ message: String, _ ptr: UnsafeBufferPointer<Int>) {
var addr = String.init(format:"%016x", ptr.baseAddress!)
print(message, addr)
}
var value1 : Int = 3
printAddress("value1: ", &value1)
var value2 = value1
printAddress("value2: ", &value2)
value1: 000000000473c7b0
value2: 000000000473c7b8
value1
とvalue2
は8バイト違うアドレスですね。
Arrayのアドレス
var array1 = Array<Int>(repeating: 0, count: 1)
printAddress("array1: ", &array1)
var array2 = array1
printAddress("array2: ", &array2)
array1.withUnsafeBufferPointer( { ptr in
printAddress("buffer: ", ptr)
})
array1: 00000000011f0350
array2: 00000000011f0350
buffer: 00000000011f0350
array1
とarray2
は同じアドレスです。普通の変数とは違い、別の変数でもアドレスが同じです。copy-on-writeと呼ばれる最適化手法が使われているということなんですね。つまりこの場合、array1
かarray2
が更新されない限りarray1
とarray2
は同じメモリ領域を共有します。
ちなみに、&
で取れるポインタと、withUnsafeBufferPointer
で実行されるクロージャに渡ってくるポインタは同じなんですね。
array1[0] = 3
printAddress("array1: ", &array1)
printAddress("array2: ", &array2)
array1: 0000000001368aa0
array2: 00000000011f0350
array1
の中身を変更すると、array1
のアドレスが変わります。array2
はそのままなので、元々のarray1
のメモリ領域の所有権がarray2
に移ったかのような格好です(実際、そんな所有権管理のようなことはしてないと思いますが)。
print("array1.capacity: ", array1.capacity)
array1.capacity: 2
キャパシティは2なのでそれを超えるように、array1
に要素を足します。
printAddress("array1: ", &array1)
printAddress("array2: ", &array2)
var additional = [Int](repeating: 0, count: 10000)
array1.append(contentsOf: additional)
printAddress("array1: ", &array1)
printAddress("array2: ", &array2)
array1: 0000000001368aa0
array2: 00000000011f0350
array1: 0000000039e60020
array2: 00000000011f0350
要素を足す前と足した後で、アドレスが変わります。
この辺りは、C言語の感覚からすると違和感がありますが、C++のstd::vector
とかを考えるとまあそうなのかなという感じですね。
// ↓この書き方はエラーになる
//var ptr: UnsafePointer<Int> = &array
&
はアドレスを取得する演算子ではなくて、「inout式」と呼ばれるもの、ということですね。関数の引数にしか使えません。関数側は引数にinout
をつけるか、またはUnsafePointer
系引数を使う必要があります。
ローカル変数のアドレスを簡単に取得できないようにしておかないと、array1
の再配置のようなことがあったときに容易にバグにつながるから、ということでしょうね。
また、
var ptr = UnsafePointer<Int>(array1)
のような文を書くとInitialization of 'UnsafePointer<Int>' results in a dangling pointer
という警告が出ます。この文のあとにarray1
が変更された結果、アドレスが変わったとしてもptr
は古いアドレスを指し続けるよ、ということですね。
生のポインタのようなものを用意する
var buffer = UnsafeMutableRawBufferPointer.allocate(byteCount: MemoryLayout<Int16>.stride * 1024, alignment: MemoryLayout<Int16>.stride)
defer {
buffer.deallocate()
}
C言語で言うところのmalloc
のようなものですね。
このポインタを使ってmemset
とかできます。
// C/Objective-C/C++などのコードを呼び出す
memset(buffer.baseAddress!, 0, MemoryLayout<Int16>.stride * 1024)
// 4バイト目〜5バイト目にInt16型で30をセットする。
buffer.storeBytes(of: 30, toByteOffset: 4, as: Int16.self)
buffer
の中身はバイト列として直に見ていっても良いのですが、bindMemory
で、特定の型にキャストするようなことができます。
var arrayPtr = buffer.bindMemory(to: Int16.self)
var receivedData = [Int16](arrayPtr) // ここでポインタだけ渡して、要素数を指定してないことに注目
for i in (0 ..< 8) {
print(i, " ", receivedData[i])
}
特筆すべきは、[Int16](arrayPtr)
の部分ですね。ポインタだけ渡して、要素数を渡してないのに、ちゃんと配列を初期化できています。つまりUnsafeMutableRawBufferPointer
は、Unsafeを名乗るわりには自分の境界を把握しています。
ポインタの配列みたいなこともできるわりに、こういう境界を把握できてたりもするので、C/C++のメモリ配置を念頭においてプログラミングするとはまりそうな予感があります。