「这是我参与2022首次更文挑战的第6天,活动详情查看:2022首次更文挑战」。
- 本文主要介绍swift中的
指针指针:我们通常通过指针访问指针指向的地址信息。在oc中我们通常使用*表示指针 swift中指针主要分为2种,指定数据类型指针(type Pointer),未指定数据类型指针(raw Pointer)。
| Swift | Object-C | 描述 |
|---|---|---|
| unsafePointer < T > | const*T | 指针及指针指向的内容不可变 |
| unsafeMutablePointer | *T | 指针及指针指向的内容可变 |
| unsafeRawPointer | const void* | 指针指向的内存区域未定 |
| unsafeMutableRawPointer | void* | 指针指向的内存区域未定 |
- 指针为什么不安全
1.
野指针
简单的说就是我们指针指向的内存地址销毁了,但是我们的指针还没有销毁,这个时候通过指针访问不存在的内存地址,我们当前的行为就变成了未定义行为俗称野指针。
2.访问越界
我们在开辟内存空间的时候是有一定大小的,如果超过这个边界就会造成错误,比如我们定义的数组对象个数为3,通过指针偏移访问index为4就会造成越界。
3.类型不匹配
系统分配的内存空间都是有一定类型的,指针的类型可能和内存空间值的类型不一致,这也是不安全 ,比如 一个Int8 类型的指针 指向了一个 Int类型的数据,就可能会精度缺失
1. 原生指针访问
我们定义一个 可变的原生指针,大小为32字节,对齐方式为8字节,存储4个Int类型的值。
//定义一个原生指针raw指针
let p = UnsafeMutableRawPointer.allocate(byteCount: 32, alignment: 8)
for i in 0..<4{
p.storeBytes(of: i+1, as: Int.self)
}
//读取
for i in 0..<4{
let value = p.load(fromByteOffset: i*8, as: Int.self)
print("index:\(i)value is\(value)")
}
//销毁
p.deallocate()
这里说明下一些概念
MemoryLayout< T >.size计算当前类型的大小,实际大小MemoryLayout< T >.stride计算当前类型的步幅大小,可以理解为当前类型内存对齐后的大小MemoryLayout< T >.alignment对齐的字节大小,通常8字节对齐。
我们运行上面的代码,发现值和我们存储的对应不上,我们存储的时候没有进行指针偏移,一直存储在第一个8字节的位置。
advanced(by n: Int)
for i in 0..<4{
p.advanced(by: 8*i).storeBytes(of: i+1, as: Int.self)//存储
}
我们通过移动当前指针的位置,在对应的位置进行存储
2. 类型指针访问
泛型指针相对于原生指针来说就是,指定了当前指针已经绑定的类型。
我么对泛型指针进行访问的时候并不使用load读取,store存储操作。我们使用当前泛型指针内部的变量pointee来访问
泛型指针的访问有2种形式
withUnsafePointer
withUnsafePointer(to: <#T##T#>, <#T##body: (UnsafePointer<T>) throws -> Result##(UnsafePointer<T>) throws -> Result#>)
查看withUnsafePointer(to:的定义中,第二个参数传入的是闭包表达式,然后通过rethrows重新抛出Result(即闭包表达式产生的结果)
//通过withUnsafePointer访问age的指针
withUnsafePointer(to: &age) { ptr in
print(ptr)
}
//所以可以将闭包表达式进行简写(简写参数、返回值),其中`$0`表示第一个参数,`$1`表示第二个参数,以此类推
let p = withUnsafePointer(to: &age) { $0 }
print(p)
类似我们oc中的浅拷贝,相当于指针拷贝
var age = 10
withUnsafeMutablePointer(to: &age ){
ptr in
ptr.pointee += 10
}
print(age)//打印结果为20
allocate创建内存空间
var age = 10
//开辟一个Int类型的内存空间,但是还没有初始化
let intPtr = UnsafeMutablePointer<Int>.allocate(capacity:1)
//初始化分配内存空间
intPtr.initialize(to: age)
//访问当前内存空间的值,直接通过pointee的属性来进行访问
print(intPtr.pointee)
相当于我们oc中的深拷贝,验证下
var age = 10
//开辟一个Int类型的内存空间,但是还没有初始化
let intPtr = UnsafeMutablePointer<Int>.allocate(capacity:1)
//初始化分配内存空间
intPtr.initialize(to: age)
intPtr.deinitialize(count: 1)//结束分配
//访问当前内存空间的值,直接通过pointee的属性来进行访问
intPtr.pointee += 20
print(intPtr.pointee)//打印结果为30
print(age)//打印结果为10
intPtr.deallocate()//销毁
- 修改age的值
上面我们知道了获取指针的2种方式,那么我们怎么修改age呢
1.
间接修改:获取age的指针,直接修改指针指向的值pointee
var age = 10
age = withUnsafePointer(to: &age){
ptr in
return ptr.pointee + 10
}
print(age)
2.间接修改:相当于指针拷贝,创建一个新的指针通过withUnsafeMutablePointer
var age = 10
withUnsafeMutablePointer(to: &age ){
ptr in
ptr.pointee += 10
}
print(age)//打印结果为20
3. 访问实例对象指针
定义一个结构体对象
struct Person {
var name = "jack"
var age = 10
}
var p = Person()
通过UnsafeMutablePointer访问结构体实例对象有3种方式
3.1 通过下标
struct Person {
var name = "jack"
var age = 10
}
let p = UnsafeMutablePointer<Person>.allocate(capacity: 2)//分配2个
//
p[0] = Person(name: "hello", age: 11)
p[1] = Person(name: "sun", age: 13)
print(p[0])//**Person(name: "hello", age: 11)**
print(p[1])//打印**Person(name: "sun", age: 13)**
p.deinitialize(count: 2)//和开辟成对出现
p.deallocate()//销毁
我们通过指针的下标进行偏移访问我们存储的值,我的理解是我们开辟一个2个Person类型对齐后的指针大小空间,通过移动指针在连续空间的位置进行存储读取对象。
读取越界的话,访问不到,这里应该系统做了优化处理
存储的话越界会导致崩溃
3.2 通过移动步长
struct Person {
var name:String = "jack"
var age:Int = 10
}
//
let p = UnsafeMutablePointer<Person>.allocate(capacity: 2)//分配2个
p.initialize(to: Person())//初始化第一个
p.advanced(by:MemoryLayout<Person>.stride).initialize(to: Person(name: "ss", age: 20))
//
print(p.advanced(by: 0).pointee)
print(p.advanced(by: MemoryLayout<Person>.stride).pointee)
p.deinitialize(count: 2)
p.deallocate()//销毁
但是有的时候是会奔溃的,
- 这里的
p如果使用advanced(by: MemoryLayout<Person>.stride)即24字节大小,此时获取的结果是有问题的,由于这里知道具体的类型,所以只需要标识指针前进 几步即可,即advanced(by: 1)
我们不知道类型的时候
let ptr = UnsafeMutableRawPointer.allocate(byteCount: 24*2, alignment: 8)
ptr.storeBytes(of: Person(), as: Person.self)
ptr.advanced(by: MemoryLayout<Person>.stride).storeBytes(of: Person(name: "lucy", age: 20), as: Person.self)
print(ptr.load(as: Person.self))
print(ptr.load(fromByteOffset: MemoryLayout<Person>.stride, as: Person.self))
运行多次没啥问题。
- 因此我们知道类型的时候可以
直接移动指针即可
let p = UnsafeMutablePointer<Person>.allocate(capacity: 2)//分配2个
p.initialize(to: Person())//初始化第一个
//p.advanced(by:MemoryLayout<Person>.stride).initialize(to: Person(name: "ss", age: 20))
p.advanced(by: 1).initialize(to: Person(name: "ss", age: 20))
//
print(p.advanced(by: 0).pointee)
print(p.advanced(by: 1).pointee)
p.deinitialize(count: 2)
p.deallocate()//销毁
3.3 successor接替者
查找当前指针存储的下一个实例对象
let p = UnsafeMutablePointer<Person>.allocate(capacity: 2)//分配2个
p.initialize(to: Person())//初始化第一个
p.successor().initialize(to: Person(name: "successor", age: 12))
print(p.pointee)
print(p.successor().pointee)
p.deinitialize(count: 2)
p.deallocate()
4. 内存的绑定
内存的绑定或者重新绑定有3种方式
4.1 assumingMemoryBound(to:)不绑定内存类型
有的时候我们处理代码的过程中,只有原始指针(没有保留指针类型),但此刻对于处理代码的我们来说,明确知道指针的类型,我们就可以使⽤ assumingMemoryBound(to:) 来告诉编译器预期的类型。 (注意:这⾥只是让编译器绕过类型检查,并没有发⽣实际类型的转换)。
这里类型报错
但是实际是元组中的类型也是Int,存储的也是Int类型的数据,这个时候我们使用assumingMemoryBound告诉编译器已经绑定了Int类型了,编译器就不会报错了
func testPointerMethod(_ pointer:UnsafePointer<Int>) {
print(pointer[0])
print(pointer[1])
}
let tuple = (10,20)
withUnsafePointer(to: tuple) {(tuplePtr: UnsafePointer<(Int,Int)>) in
//转换为原生指针后,告诉编译器已经绑定了Int类型了,编译器就不会报错了
testPointerMethod(UnsafeRawPointer(tuplePtr).assumingMemoryBound(to: Int.self))
}
4.2. bindMemory(to: capacity:)永久绑定内存类型
⽤于更改内存绑定的类型,如果当前内存还没有类型绑定,则将⾸次绑定为该类型;否则重新绑定该类型,并且内存中所有的值都会变成该类型。
func testPointerMethod(_ pointer:UnsafePointer<Int>) {
print(pointer[0])
print(pointer[1])
}
let tuple = (10,20)
withUnsafePointer(to: tuple) { (tuplePtr: UnsafePointer<(Int, Int)>) in
testPointerMethod(UnsafeRawPointer(tuplePtr).bindMemory(to: Int.self, capacity: 2))
}
4.3 withMemoryRebound 临时绑定内存类型
- 如果方法的类型与传入参数的类型
不一致,会报错
这个时候我们就可以使⽤
withMemoryRebound(to: capacity: body:) 来临时更改内存绑定类型。
func testPointerMethod(_ pointer:UnsafePointer<Int64>) {
print(pointer[0])
}
var age = 10
let ptr = withUnsafePointer(to: &age) {$0}
ptr.withMemoryRebound(to: Int64.self, capacity: 1) { (ptr: UnsafePointer<Int64>) in
testPointerMethod(ptr)
}
5. 总结
swift中指针通常有2种大的类型原生指针rawPointer 和指定了类型的typePointer。指针访问实例对象通常有3种方式:下标Index,移动步长通过advanced,以及sucessor(下一个指针地址)。内存绑定通常有3种方式,明确是相同类型连续的时候使用assumingMemoryBound(绕过编译器,不改变内存绑定的类型),bindMemory(永久的改变内存的绑定类型),withMemoryRebound(临时改变内存绑定类型,出了作用域后还原)