Swift 指针的应用
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
这个函数原型看似很复杂很长,其实只要理解它所需要的信息只有两个:
- 指针指向类型是什么。
- 想要返回的指针地址是什么。
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
以上例子中,我们获得了以下三个指针类型
UnsafeBufferPointer<Int32>
类型的指针p
。UnsafePointer<UInt32>
类型的指针p2
。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
在该例子中我们通过空指针完成了如下操作:
UnsafeRawPointer
通过构造函数接收了p
的“基地址”构造了一个空指针类型。- 由于构造的是空指针类型,我们需要对它进行类型转换,通过
assumingMemoryBound
把它转换成新的数据类型UnsafePointer<UInt32>
。 - 通过
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
,请参见地址:github.com/FengHaiTong… 。
作者:风海铜锣
链接:https://juejin.cn/post/7030789069915291661
来源:稀土掘金