0
点赞
收藏
分享

微信扫一扫

Swift指针的应用

迎月兮 2022-02-18 阅读 77


Swift与指针

由于​​Swift​​​本身是一门较为现代的语言,支持很多高级特性,所以对于程序员来说,大部分时候不需要用到指针这种更“底层”的特性。而​​Swift​​语言的设计者也在尽可能希望开发者能尽量少的使用指针。

但是,“慎用”不代表“不能用”,更不代表“没用”。相反,指针非常有用,在某些场景下还是必不可少的特性。尤其是开发工作和系统底层特性、内存处理、高性能需求息息相关时。

所以,​​Swift​​​通过在施加某种限制的前提下为开发者暴露了指针的使用接口,本篇文章重点介绍​​Swift​​使用指针的相关类型、函数,以及在实践应用中灵活使用指针解决问题的技巧。

类型限定的指针 ​​UnsafePointer​​

​Swift​​​通过​​UnsafePointer<T>​​​来指向一个类型为​​T​​的指针,该指针的内容是 ​只读​ 的,对于一个​​UnsafePointer<T>​​​变量来说,通过​​pointee​​​成员即可获得​​T​​的值。

func call(_ p: UnsafePointer<Int>) {

print("\(p.pointee)")

}

var a = 1234

call(&a) // 打印:1234

以上例子中函数​​call​​​接收一个​​UnsafePointer<Int>​​​类型作为参数,变量​​a​​​通过在变量名前面加上​​&​​​将其地址传给​​call​​​。函数​​call​​​直接打印指针的​​pointee​​​成员,该成员就是​​a​​​的值,所以最终打印结果为​​1234​​。


注1:​​&a​​​是​​swift​​提供的语法特性,用于传递指针,但它有严格的适用场景限制。



注2:注意示例中对于变量​​a​​​使用了​​var​​​声明,而事实上​​UnsafePointer​​​是“常量指针”,并不会修改​​a​​​的内容,即使是这样​​a​​​还是必须用​​var​​​声明,如果用​​let​​​会报错​​Cannot pass immutable value as inout argument: 'a' is a 'let' constant​​​。这是因为​​swift​​​规定​​UnsafePointer​​​作为参数只能接收​​inout​​​修饰的类型,而​​inout​​​修饰的类型必然是可写的,所以使用​​var​​在所难免。


内容可写的类型限定指针 ​​UnsafeMutablePointer​​

既然有 ​内容只读​ 指针,必须也得有 ​内容可读写​ 指针搭配才行,在​​Swift​​​中,内容可读写的类型限定指针为​​UnsafeMutablePointer<T>​​​类型,就和名字描述的那样,它和​​UnsafePointer​​最大的区别就是它指向的内容是可更改的,并且更改后指向的“数据源”也会被改动。

func modify(_ p: UnsafeMutablePointer<Int>) {

p.pointee = 5678

}

var a = 1234

modify(&a)

print("\(a)") // 打印:5678

在以上的例子中,指针​​p​​​指向的值被重新赋值为​​5678​​​,这也使得指针的“源”,即变量​​a​​​的值发生变化,最终打印​​a​​​的结果可以看出​​a​​​被修改为​​5678​​。

指针的辅助函数 ​​withUnsafePointer​​

通过函数​​withUnsafePointer​​,获得指定类型的对应指针。该函数原型如下:

func withUnsafePointer<T, Result>(to value: inout T, _ body: (UnsafePointer<T>) throws -> Result) rethrows -> Result

这个函数原型看似很复杂很长,其实只要理解它所需要的信息只有两个:


  1. 指针指向类型是什么。
  2. 想要返回的指针地址是什么。

var a = 1234

let p = withUnsafePointer(to: &a) { $0 }

print("\(p.pointee)") // 打印:1234

以上例子是​​withUnsafePointer​​​最精简的调用例子,我们定义了一个整形​​a​​​,而​​p​​​就是指向​​a​​​的整形指针,事实上它的类型会被自动转换为​​UnsafePointer<Int>​​​,第二个参数被简化为了​​{ $0 }​​​,它传入了一个代码块,代码块接收一个​​UnsafePointer<Int>​​​参数,该参数即是​​a​​​的地址,直接通过​​$0​​​将它返回,即得到了​​a​​​的指针,最终它被传给了​​p​​。

对于第二个参数,或许有人会产生疑问,它似乎是没有意义的参数,大部分时候我们不是直接返回​​a​​的地址吗,为什么要多此一举通过代码块返回一次?这个疑问是合理的,绝大多数时候确实第二个参数显得有些多余,当然了,有时候可以通过第二个参数提供指针偏移的灵活性,如下例子可以提供一个案例。

var a = [1234, 5678]

let p = withUnsafePointer(to: &a[0]) { $0 + 1 }

print("\(p.pointee)") // 打印:5678

以上例子中,通过在第二个参数中对地址施加偏移,可以原来指向数组首个元素的地址偏移到第二个地址中。

另外,由于​​withUnsafePointer​​带着两个泛型参数,这意味着第二个参数可以是不同的类型。

var a = 1234

let p = withUnsafePointer(to: &a) { $0.debugDescription }

print("\(p)")

以上例子中,​​withUnsafePointer​​​返回的并不是​​UnsafePointer<Int>​​​类型,甚至不是指针,而是一个字符串,字符串保存着​​a​​​对应指针的​​debug​​信息。


注1:同样的,和​​withUnsafePointer​​​相对应的,还有​​withUnsafeMutablePointer​​,一样是只读和可读写的区别。读者可以自行测试用法。



注2:基本上​​Swift​​​指针操作的​​with​​系列函数都提供了第二个参数用来灵活的提供函数的返回类型。


获取指针并进行字节级操作 ​​withUnsafeBytes​​

有时候,我们需要对某块内存进行字节级编程。比如我们用一个​​32​​​位整形来表示一个​​32​​位的内存块,对内存中的每个字节进行读写操作。

通过​​withUnsafeBytes​​,可以得到某个类型的数据的字节指针,从而可以对它们进行字节级编程。

var a: UInt32 = 0x12345678

let p = withUnsafeBytes(of: &a) { $0 }

var log = ""

for item in p {

let hex = NSString(format: "%x", item)

log += "\(hex)"

}

print("\(p.count)") // 打印:4

print("\(log)") // 对于小端机器会打印:78563412

在以上例子中,​​withUnsafeBytes​​​返回了一个类型​​UnsafeRawBufferPointer​​​,该类型代表着一个字节级的内存块,并提供了等价于数组操作,所以你可以通过下标索引、​​for​​循环的方式来处理返回的对象。

例子中的​​a​​​是一个​​32​​​位整形,所以​​p​​​指针的​​count​​​返回的是​​4​​,单位为字节。

在本例中,对内存块​​p​​​从低到高逐字节的打印每个字节的​​16进制​​值。

具体打印出来的结果因运行的机器而异,在大端机器上,打印的结果是​​12345678​​​,而在小端机器上打印结果则是​​78563412​​。


注:大端和小端决定了一个基础数据单元在内存中是如何按序存放的,例如小端机器会将基本数据单元的低位放在内存的低位,由低到高排列,而大端机器则相反。具体相关知识可查阅维基百科。大部分情况下,同一台机器采用的字节序列是一致的,某些​​CPU​​可以配置大小端的切换。


指向连续内存的指针 ​​UnsafeBufferPointer​​

​Swift​​​的数组提供了函数​​withUnsafeBufferPointer​​,通过它我们可以方便的用指针来处理数组。如下例子:

let a: [Int32] = [1, 2, -1, -2, 5, 6]

let p = a.withUnsafeBufferPointer { $0 }

print("\(p.count)") // 打印:6

print("\(p[3])") // 打印:-2

在该例子中,通过​​withUnsafeBufferPointer​​​,可以获得变量​​p​​​,​​p​​​的类型为​​UnsafeBufferPointer<Int32>​​,它代表着一整块的连续内存,我们可以像看待数组一样看待它,并且它也支持大部分数组操作。

指针的类型转换

介绍了那么多​​Swift​​中的指针类型,每一种都有各自的用途,但是在实际开发中,很可能我们需要将一个指针类型转换为特定的指针类型。

以下例子提供了几个类型指针之间的转换

let a: [Int32] = [1, 2, -1, -2, 5, 6]

// 类型 p: UnsafeBufferPointer<Int32>

let p = a.withUnsafeBufferPointer { $0 }

// 类型 p2: UnsafePointer<UInt32>

let p2 = p.baseAddress!.withMemoryRebound(to: UInt32.self, capacity: p.count) { $0 }

// 类型 p3: UnsafeBufferPointer<UInt32>

let p3 = UnsafeBufferPointer(start: p2, count: p.count)

print("\(p3.count)") // 打印:6

print("\(p3[3])") // 打印:4294967294

以上例子中,我们获得了以下三个指针类型


  1. ​UnsafeBufferPointer<Int32>​​类型的指针​​p​​。
  2. ​UnsafePointer<UInt32>​​类型的指针​​p2​​。
  3. ​UnsafeBufferPointer<UInt32>​​类型的指针​​p3​​。

该例子有部分细节必须讲明,首先是​​baseAddress​​​,通过该成员得到​​UnsafeBufferPointer​​​的​​基地址​​​,获得的数据类型是​​UnsafePointer<>​​。

由于​​a​​​指向的元数据类型是​​Int32​​​,所以其​​baseAddress​​​类型即是​​UnsafePointer<Int32>​​。

在本例中,我们将元数据类型由​​Int32​​​改为​​UInt32​​​,这里用到了​​UnsafePointer​​​的成员函数​​withMemoryRebound​​​,通过它将​​UnsafePointer<Int32>​​​转换为​​UnsafePointer<UInt32>​​。

最后一部分,我们创建了一个新的指针​​UnsafeBufferPointer​​​,通过其构造函数,我们让该指针的起始位置设定为​​p2​​​,元素个数设定为​​p​​​的元素个数,这样就成功得到了一个​​UnsafeBufferPointer<UInt32>​​类型。

接下来的打印语句,我们可以看到​​p3​​​类型的​​count​​​成员依然是​​6​​​,而​​p3[3]​​​打印的结果却是​​4294967294​​​,而不是数组​​a​​​对应元素的​​-2​​​,这是因为从​​p3​​​的角度来看,它是用​​UInt32​​​类型来“看待”原先的​​Int32​​数据元素。

回调函数的实用性

前面讨论​​withUnsafePointer​​时我曾经提过第二个回调参数似乎略显鸡肋,事实上它非常有用,通过回调函数,我们可以对上一段代码进行“优化”。

let a: [Int32] = [1, 2, -1, -2, 5, 6]

// 类型 p: UnsafeBufferPointer<Int32>

let p = a.withUnsafeBufferPointer { $0 }

// 类型 p3: UnsafeBufferPointer<UInt32>

let p3 = p.baseAddress!.withMemoryRebound(to: UInt32.self, capacity: p.count) {

UnsafeBufferPointer(start: $0, count: p.count)

}

print("\(p3.count)") // 打印:6

print("\(p3[3])") // 打印:4294967294

可以看到利用回调函数,我们把原先的​​p2​​​和​​p3​​​代码合并了,这样可以让​​withMemoryRebound​​​立刻返回​​UnsafeBufferPointer<UInt32>​​类型。


注:事实上该回调还可以不断“套娃”,也就是说可以直接把​​p3​​​部分的代码和​​p​​也进行合并,但是出于可读性考虑,开发者应自己根据需要选择性进行嵌套。


​​Swift​​中的空指针:UnsafeRawPointer

就像​​C​​​语言有​​void*​​​(即空指针)一样,​​Swift​​​也有自己的空指针,它通过类型​​UnsafeRawPointer​​来获得,我们知道,空指针没有指向特定的类型,又“可以”指向任何类型,灵活性极高,也需要程序员自己能够理解和处理好对应的细节。

同样是将​​UnsafeBufferPointer<Int32>​​​转换为​​UnsafeBufferPointer<UInt32>​​​,以下代码通过​​UnsafeRawPointer​​来实现。

let a: [Int32] = [1, 2, -1, -2, 5, 6]

let p = a.withUnsafeBufferPointer { $0 }

let p2 = UnsafeRawPointer(p.baseAddress!).assumingMemoryBound(to: UInt32.self)

let p3 = UnsafeBufferPointer(start: p2, count: p.count)

print("\(p3.count)") // 打印:6

print("\(p3[3])") // 打印:4294967294

在该例子中我们通过空指针完成了如下操作:


  1. ​UnsafeRawPointer​​通过构造函数接收了​​p​​的“基地址”构造了一个空指针类型。
  2. 由于构造的是空指针类型,我们需要对它进行类型转换,通过​​assumingMemoryBound​​把它转换成新的数据类型​​UnsafePointer<UInt32>​​。
  3. 通过​​UnsafeBufferPointer​​构造函数重新构造了一个新的指针​​UnsafeBufferPointer<UInt32>​​。

通过指针动态创建、销毁内存

有时候我们需要动态开辟和管理一块内存,最后释放它,​​Swift​​​提供了​​UnsafeMutablePointer​​​的成员函数​​allocate​​来处理该工作。

let p = UnsafeMutablePointer<Int32>.allocate(capacity: 1)

p.initialize(to: 0) // 初始化

p.pointee = 32

print("\(p.pointee)") // 打印:32

p.deinitialize(count: 1) // 反初始化

p.deallocate()

以上例子中我们提供了一个存放32位整形的内存块,容量为​​1​​(即其容量为1个32位整形,实际就是 ​4​ 个字节)。

接下来的代码示例较为简单,即开辟内存,初始化、赋值、反初始化,释放内存的流程。

​​Swift​​指针类型和​​C​​指针类型的对应关系

​Swift​​​的指针类型看似繁多,事实上只是对​​C​​指针类型进行了封装和类别整理,并增加了一定程度上的安全性。

下表提供了​​Swift​​​和​​C​​部分指针类型和函数的大致等价关系。

| Swift | C | 描述 |

| — | — | — |

| UnsafeMutableRawPointer | void* | 空指针 |

| UnsafeMutablePointer | T* | 类型指针 |

| UnsafeRawPointer | const void* | 常量空指针 |

| UnsafePointer | const T* | 常量类型指针 |

| UnsafeMutablePointer.allocate | (int32_t*)malloc | 分配内存 |

可以看出​​Swift​​​的指针并不神秘,它只是映射了​​C​​语言指针的对应操作(只是乍看一下更复杂)。

进阶实践:​​C​​标准库函数的映射调用

​Swift​​​提供了大量的​​C​​​标准库的桥接调用,也就是说,我们可以像调用​​C​​​语言库函数一样调用​​Swift​​​函数。这其中包括很多有用的函数,如​​memcpy​​​,​​strcpy​​等。

下面通过一段示例程序来展现这类函数的调用。

var n = 10086

// malloc

let p = malloc(MemoryLayout<Int32>.size)!

// memcpy

memcpy(p, &n, MemoryLayout<Int32>.size)

let p2 = p.assumingMemoryBound(to: Int32.self)

print("\(p2.pointee)") // 打印:10086

// strcpy

let str = "abc".cString(using: .ascii)!

if str.count != MemoryLayout<Int32>.size {

return

}

let pstr = p.assumingMemoryBound(to: CChar.self)

strcpy(pstr, str)

print("\(String(cString: pstr))") // 打印:abc

// strlen

print("\(strlen(pstr))") // 打印: 3

// memset

memset(p, 0, MemoryLayout<Int32>.size)

print("\(p2.pointee)") // 打印:0

// strcat

strcat(pstr, "h".cString(using: .ascii)!)

strcat(pstr, "i".cString(using: .ascii)!)

print("\(String(cString: pstr))") // 打印:hi

// strstr

let s = strstr(pstr, "i")!

print("\(String(cString: s))") // 打印:i

// strcmp

print("\(strcmp(pstr, "hi".cString(using: .ascii)!))") // 打印:0

// free

free(p)

以上demo提供了如​​memset​​​,​​strcpy​​​等​​C​​​库函数原型的调用方式。通过该例子可以看出指针操作的灵活性,对于开辟的一块4个字节的内存,我们既可以把它看做一个32位整形,又可以把它看做4个​​ascii​​​字符,当把它看做4个字符时,我们可以用它存放​​abc​​​三个字符,并在最后一个字节用​​\0​​作为终止符。

总结

指针可以让我们用更底层的视角来看待程序和数据,在某些场景下,通过指针我们有机会开发出更高性能的代码。但同时指针的使用有时也是极复杂易出错的。如何使用好这把双刃剑,全看开发者自身的能力和态度。本文仅仅是抛砖引玉的提供了​​Swift​​指针的基本框架和使用技巧,大量细节因为篇幅原因并未提及,还需要读者自行不断研究和学习。

本文的样例代码已上传至我的​​github​​,请参见地址:https://github.com/FengHaiTongLuo/Swift-Study/blob/main/swift_and_c_pointer.swift 。



举报

相关推荐

0 条评论