谈内存必然离不开指针的概念,指针既是难点也是重点。
一、指针简介
Swift中也有专门的指针类型,这些都被定性为Unsafe
(不安全的),常见的有以下4种类型:
UnsafePointer<Pointee>
:类似于const Pointee *
(只读的泛型指针)UnsafeMutablePointer<Pointee>
:类似于Pointee *
(可读可写的泛型指针)UnsafeRawPointer
:类似于const void *
(只读的原始类型指针)UnsafeMutableRawPointer
:类似于void *
(可读可写的原始类型指针)
示例代码一:
1 | var age = 10 |
泛型指针可以通过指针变量属性pointee
读写内存。
原始指针通过load
实例方法读取内存数据,参数as
传入创建的实例类型。
原始指针通过storeBytes
实例方法写入数据,参数as
传入存储数据的类型,参数
of`传入数据。
二、获取指针
2.1. 获取指向某个变量的指针
示例代码:
1 | var age = 10 |
调用函数的时候传参&age
,意味着传入的是指针地址,通过这个地址可以直接获取age
的内存。能不能直接定义变量的时候就定义呢?
直接定义一个指针变量指向了age
:
1 | var ptr: UnsafePointer<Int> = &age |
明显不行,编译器直接报错了:
可以使用下面的方法获取指针变量:
1 | @inlinable public func withUnsafePointer<T, Result>(to value: inout T, _ body: (UnsafePointer<T>) throws -> Result) rethrows -> Result |
使用:
1 | var ptr = withUnsafePointer(to: &age) { $0 } |
发现ptr
就是Int
类型指针:
withUnsafePointer
函数的第一个参数是传入变量的地址,第二个参数是闭包,闭包的参数其实是函数的第一个参数,返回值是一个泛型(传参是什么类型,返回值就是什么类型)。
模仿实现withUnsafePointer
代码:
1 | func withUnsafePointer<Result, T>(to: UnsafePointer<T>, body: (UnsafePointer<T>) -> Result) -> Result { |
withUnsafePointer
和withUnsafeMutablePointer
返回的都是泛型指针,通过修改尾随闭包的返回值类型可以间接修改这两个函数的返回值类型。
UnsafeRawPointer
和UnsafeMutableRawPointer
都有各自的初始化器。
1 | var ptr = withUnsafePointer(to: &age) { UnsafeRawPointer($0) } |
证明获取的是指针:
1 | var age = 10 |
通过计算得出:0x100001d2e + 0xa612 = 0x10000C340
,和上面指针变量ptr
打印的地址值一样,也就是age
保存的地址。
2.2. 获得指向堆空间实例的指针
示例代码:
1 | class Person { |
思考:ptr
存储的是什么?存储的是变量person
的地址值还是堆空间Person
对象的地址值?
1 | print(ptr) // 输出:0x000000010000c4c8 |
很明显ptr
存储的是person
变量的地址值。其实从withUnsafePointer
的入参和返回值也能反映出ptr
存储的是person
变量的地址值,因为传入什么,返回值就是什么。ptr
本质就是person
。
获取堆空间的地址:
1 | var person = Person(age: 10) |
ptr1
保存的是person
地址值,所以ptr1.load
取的是person
保存的地址personObjAddress
(对象堆空间地址)。ptr2
指针装的就是对象堆空间地址,换句话说就是ptr2
指针指向了对象堆空间地址。
三、创建指针
示例代码一(通过malloc
创建):
1 | // 堆空间创建指针(16代表申请16个字节的内存,返回值类型是UnsafeMutableRawPointer可选类型) |
通过malloc
创建的指针一定要在结束后销毁。
示例代码二(通过UnsafeMutableRawPointer.allocate
创建):
1 | // 创建指针(返回值是UnsafeMutableRawPointer类型) |
注意advanced
返回的是偏移指定字节后的指针。
示例代码三(通过UnsafeMutablePointer.allocate
创建):
1 | // 创建指针 |
使用泛型指针initialize
初始化数据时,一定要使用deinitialize
反初始化(count
和initialize
次数要一致)。
注意:UnsafeMutableRawPointer.allocate
因为没有指定类型,所以需要传入指定类型和字节。
UnsafeMutablePointer.allocate
是泛型指针,所以只需要告诉系统创建多大容量的内存。
示例代码四:
1 | class Person { |
如果上面的代码不写deinitialize
,就不会有deinit
输出;如果deinitialize(count: 2)
,第三个initialize
就不会释放;所以一定要及时正确的使用deinitialize
,否则会有内存泄漏。
建议:在函数内部创建指针时,把指针释放的代码放到
defer
函数体内。
四、指针之间的转换
示例代码:
1 | // 创建指针 |
unsafeBitCast
是忽略数据类型的强制转换,不会因为数据类型的变化而改变原来的内存数据(可以认为是内存数据直接搬过去的,一般情况下的强制转换都会改变原来的内存数据形成新的内存数据存储)。
unsafeBitCast
也可以直接创建一个指针指向堆空间:
1 | class Person { } |
person
保存的是Person
对象的堆空间地址,unsafeBitCast
就是把person
保存的内存地址拿出来原封不动转换为UnsafeRawPointer
类型的指针给ptr
,所以ptr
保存的地址和person
保存的地址是一样的。
还有一种方式是把person
地址取出来,利用地址创建一个指针指向堆空间:
1 | var address = unsafeBitCast(person, to: UInt.self) |
注意:原始指针和泛型指针
ptr + 8
是有区别的。原始指针ptr + 8
指的是跳过8个字节,泛型指针指的是跳过8*类型占用字节
个字节。