swfit进阶-05-指针

169 阅读3分钟

「这是我参与2022首次更文挑战的第6天,活动详情查看:2022首次更文挑战」。

  • 本文主要介绍swift中的指针 指针:我们通常通过指针访问指针指向的地址信息。在oc中我们通常使用*表示指针 swift中指针主要分为2种,指定数据类型指针(type Pointer),未指定数据类型指针(raw Pointer)。
SwiftObject-C描述
unsafePointer < T >const*T指针及指针指向的内容不可变
unsafeMutablePointer*T指针及指针指向的内容可变
unsafeRawPointerconst void*指针指向的内存区域未定
unsafeMutableRawPointervoid*指针指向的内存区域未定
  • 指针为什么不安全 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字节对齐。 image.png

我们运行上面的代码,发现值和我们存储的对应不上,我们存储的时候没有进行指针偏移,一直存储在第一个8字节的位置。

image.png

  • advanced(by n: Int)
for i in 0..<4{

    p.advanced(by: 8*i).storeBytes(of: i+1, as: Int.self)//存储

}

我们通过移动当前指针的位置,在对应的位置进行存储

image.png

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类型对齐后的指针大小空间,通过移动指针在连续空间的位置进行存储读取对象。

读取越界的话,访问不到,这里应该系统做了优化处理

image.png

存储的话越界会导致崩溃

image.png

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()//销毁

image.png

但是有的时候是会奔溃的,

image.png

  • 这里的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))

运行多次没啥问题。 image.png

  • 因此我们知道类型的时候可以直接移动指针即可
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()//销毁

image.png

3.3 successor接替者

查找当前指针存储的下一个实例对象

image.png

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()

image.png

4. 内存的绑定

内存的绑定或者重新绑定有3种方式

4.1 assumingMemoryBound(to:)不绑定内存类型

有的时候我们处理代码的过程中,只有原始指针(没有保留指针类型),但此刻对于处理代码的我们来说,明确知道指针的类型,我们就可以使⽤ assumingMemoryBound(to:) 来告诉编译器预期的类型。 (注意:这⾥只是让编译器绕过类型检查,并没有发⽣实际类型的转换)。

这里类型报错 image.png

但是实际是元组中的类型也是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 临时绑定内存类型

  • 如果方法的类型与传入参数的类型不一致,会报错

image.png 这个时候我们就可以使⽤ 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临时改变内存绑定类型,出了作用域后还原)