从零学习Swift 14: Swift 中指针的使用

一: 内存访问冲突

如果两个操作同时满足下面几个条件,就会产生内存访问冲突:

  1. 至少一个是写入操作
  2. 访问的是同一块内存
  3. 访问的时间重叠(比如在同一个方法内)

比如说像下面这种操作就会产生内存访问冲突:

内存访问冲突

test(a: &num1, b: &num1)传入同一个参数.就会同时读取和写入同一块内存,产生内存访问冲突.

那么如何解决内存访问冲突?只要让这两个操作不是同一块内存即可:

解决内存访问冲突

内存访问冲突典型:

元组内存访问冲突
结构体内存访问冲突

以上两种情况都会产生内存访问冲突,因为元组和结构体的成员都在同一块连续的内存中.虽然访问的是不同的成员,但本质仍然是同一块内存.

指针

swift中也有专门的指针类型,这些都被定性为不安全的unsafe,常用的有以下四种类型:

  1. UnsafePointer<T> : 带泛型的不可修改指针,比如说UnsafePointer<Int> 等价于 OC 的 const Int*

  2. UnsafeMutablePointer<T> : 带泛型的可修改的指针,比如说UnsafeMutablePointer<Int>等价于 OC 的Int*

  3. UnsafeRawPointer : 不带泛型的不可修改指针,也就是普通指针,相当于 OC 中的 const void*

  4. UnsafeMutableRawPointer : 不带泛型的可修改指针,相当于 OC 中的void*

指针的读取和写入

泛型指针和非泛型指针的读取和写入操作是不一样的,我们先看一下泛型指针的读取和写入操作:

泛型指针的读取和写入

再来看看非泛型指针的读取和写入:

非泛型指针的读取和写入

指针的应用

在 OC 的 NSArray中,系统提供了一个遍历数组元素的方法,它里面就用到了指针类型:

OC 中的的指针类型

我们在Swift开发中难免会用到OC中的NSArray,如果我们想在swift语言的开发中使用 OC 那种遍历方法呢?

swift 中的 enumerateObjects 方法

可以看到,OC 中的 Bool *swift中变成了UnsafeMutablePointer<ObjCBool>.正是我们上面讲的可变泛型指针.

还有另一种遍历方法,不需要用到指针:

.

获得指向某个变量的指针

如果我们想获取指向某个变量的指针,应该怎么做到呢?swift也提供了相关的API:


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


@inlinable public func withUnsafeMutablePointer<T, Result>(to value: inout T, _ body: (UnsafeMutablePointer<T>) throws -> Result) rethrows -> Result

这两个API要求传入两个参数:第一个参数是想要获得哪个变量指针的地址;第二个参数是传入一个闭包,这个闭包要求传入一个参数,并且有一个返回值.

咱们仔细看这两个API,会发现这个API的返回值就是第二个闭包参数的返回值.

返回值

闭包的第一个参数可以理解为是系统设置的,指向age变量的指针:

闭包第一个参数

下面我们来看看这两个API是怎么用的:

因为闭包的第一个参数恰恰也是闭包的返回值,而闭包的返回值恰恰又是函数的返回值,所以简写如下:


var age = 10

var ptr = withUnsafePointer(to: &age) { $0 }

print(ptr.pointee)

获取一个指向某个变量的可变类型的指针跟上面一样,我们直接贴出代码:


var age = 10

var ptr = withUnsafeMutablePointer(to: &age) { $0 }
ptr.pointee = 20

print(ptr.pointee)

上面获取的指向某个变量的指针是泛型的,如果我们想要非泛型的指针应该怎么做呢?

还是同样的API,只不过在闭包内把返回值改为非泛型的.

非泛型的指针类型有一个初始化方法:传入一个泛型指针,初始化为非泛型指针:

泛型指针转非泛型指针

所以我们只需在闭包体内转换即可:


var age = 10

var ptr1 = withUnsafePointer(to: &age) { UnsafeMutableRawPointer(mutating: $0)}
ptr1.storeBytes(of: 20, as: Int.self)

var ptr2 = withUnsafePointer(to: &age) { (p) -> UnsafeRawPointer in
    UnsafeRawPointer(p)
}

print(ptr2.load(as: Int.self))

获得指向堆空间实例的指针

如果我们想获取指向堆空间实例的指针,应该怎么做到呢?还是用上面的方法试一下.


class Person{
   var name: String
   var age: Int
   init(name: String, age: Int) {
       self.name = name
       self.age = age
   }
}


var p1 = Person(name: "Jack Ma", age: 56)

var ptr1 = withUnsafePointer(to: &p1) { $0 }

如上代码,ptr1指针指向的是Person对象在堆空间的内存吗?我们通过汇编窥探一下:

再来看看指针ptr1中存储的是p1变量的地址,还是Person实例在堆空间的地址:

通过打印可以发现,ptr1指向的是全局变量p1的地址,并不是Person实例的堆空间地址.

怎样获取一个指向堆空间实例地址的指针呢?

要想获得一个指向堆空间实例地址的指针需要用到另一个函数:


UnsafeRawPointer(bitPattern: <#T##Int#>)

UnsafeRawPointer(bitPattern: Int)传入一个内存地址,返回一个指向此内存地址的指针.

所以现在我们的思路就要先获取一个指向堆空间实例对象的内存地址,怎么获取堆空间实例对象的内存地址呢?


class Person{
    var name: String
    var age: Int
    init(name: String, age: Int) {
        self.name = name
        self.age = age
    }
}


var p1 = Person(name: "Jack Ma", age: 56)

//获取指向 p1 的非泛型指针
var ptr1 = withUnsafePointer(to: &p1) { UnsafeRawPointer($0) }

//取出 p1 指向的内存地址
//取出 p1 的前 8 个 字节,也就是实例对象在堆空间的内存地址
var personAdress = ptr1.load(as: UInt.self)

//获取指向堆空间实例对象的指针
var heapAdress = UnsafeRawPointer(bitPattern: personAdress)

上面代码就实现了我们的目的.heapAdress就是一个指向堆空间实例内存地址的指针.

创建指针

我们也可以自己向堆空间申请一块内存,让一个指针变量指向这块内存.

第一种做法是使用malloc向堆空间申请一块内存,它会返回一个UnSafeMutableRawPointer类型的指针.需要注意的是malloc需要导入Foundation框架:

malloc

import Foundation

//向堆空间申请16个字节的内存
var ptr1 = malloc(16)

//把 20 放到前 8 个字节
ptr1?.storeBytes(of: 20, as: Int.self)

上面代码我们就向堆空间申请了16个字节的内存,并把20存储在了前8个字节中.我们如何使用后8个字节的内存呢?

系统提供了一个带偏移量的方法,可以访问其他内存:ptr1?.storeBytes(of: T, toByteOffset: Int, as: T.Type):


import Foundation

//向堆空间申请16个字节的内存
var ptr1 = malloc(16)

//把 20 放到前 8 个字节
ptr1?.storeBytes(of: 20, as: Int.self)

//把 30 放到后 8 个字节
ptr1?.storeBytes(of: 30, toByteOffset: 8, as: Int.self)

//读取前8个字节
print(ptr1?.load(fromByteOffset: 0, as: Int.self) ?? 0)
//读取后8个字节
print(ptr1?.load(fromByteOffset: 8, as: Int.self) ?? 0)

第二种方法是使用UnsafeMutableRawPointer.allocate (byteCount: alignment:)方法.

示例:


//像堆空间申请24个字节
var ptr1 = UnsafeMutableRawPointer.allocate(byteCount: 24, alignment: 1)

//存储前8个字节
ptr1.storeBytes(of: 10, as: Int.self)
//存储中间8个字节
ptr1.storeBytes(of: 20, toByteOffset: 8, as: Int.self)

我们要访问其他字节的内存空间,除了使用上面说的偏移量方法外,还有一种方法:advanced(by:),这个方法会传入一个Int类型的字节数,然后直接返回指向这个字节处的指针:

advanced 方法

示例:


//像堆空间申请16个字节
var ptr1 = UnsafeMutableRawPointer.allocate(byteCount: 24, alignment: 1)

//存储前8个字节
ptr1.storeBytes(of: 10, as: Int.self)
//存储中间8个字节
ptr1.storeBytes(of: 20, toByteOffset: 8, as: Int.self)

//存储最后8个字节
ptr1.advanced(by: 16).storeBytes(of: 30, as: Int.self)


print(ptr1.load(as: Int.self))
print(ptr1.advanced(by: 8).load(as: Int.self))
print(ptr1.advanced(by: 16).load(as: Int.self))

上面介绍的都是非泛型的指针类型,其实泛型的指针类型也是可以自己创建的:


var ptr1 = UnsafeMutablePointer<Int>.allocate(capacity: 3)

可以看到,在创建泛型类型的指针类型时,传入的参数并不是字节数,而是容量.因为泛型已经确定了具体的类型,所以只要确定了容量就确定了字节数.

示例:


var ptr1 = UnsafeMutablePointer<Int>.allocate(capacity: 3)
//初始化前8个字节
ptr1.initialize(to: 10)
print(ptr1.pointee)
//释放
ptr1.deinitialize(count: 1)
ptr1.deallocate()

上面的代码我们就创建了一个可以存放3个Int类型的指针.并且前8个字节存放了10.后面16个字节我们怎么访问呢?

我们可以使用successor()来访问下一个容量单位的内存:

示例:


var ptr1 = UnsafeMutablePointer<Int>.allocate(capacity: 3)

//初始化
ptr1.initialize(to: 10)
ptr1.successor().initialize(to: 20) //等价于 (ptr1 + 1).initialize(to: 20)
ptr1.successor().successor().initialize(to: 30) //等价于 (ptr1 + 2).initialize(to: 30)


//读取方法一
print(ptr1.pointee)
print(ptr1.successor().pointee)
print(ptr1.successor().successor().pointee)

//读取方法二
print(ptr1.pointee)
print((ptr1 + 1).pointee)
print((ptr1 + 2).pointee)


//读取方法三
print(ptr1[0])
print(ptr1[1])
print(ptr1[2])

//反初始化
ptr1.deinitialize(count: 3)
//释放
ptr1.deallocate()

由于是泛型指针类型,它的操作不是以字节数为单位,而是以单个容量为单位,所以可以使用ptr + 1ptr1[0]来直接操作容量块.

需要注意的是,我们在使用initialize()初始化内存后,一定要使用deinitialize()反初始化释放内存,不然会引起内存泄露.

指针之间的切换

通过上面我们自己创建指针类型可以发现,在操作泛型指针类型时明显要比操作非泛型指针类型要方便的多.所以,为了简便,我们有时可能会需要将非泛型指针转换为泛型指针.

但是我们查看泛型指针的初始化方法,没有发现传入非泛型指针,初始化为一个泛型指针的初始化方法:

所以我们要找其他方法来转换,系统提供了这么一个方法,可以将非泛型指针转换为泛型指针assumingMemoryBound(to: ):

实例:


//创建一个非泛型指针
var ptr1 = UnsafeMutableRawPointer.allocate(byteCount: 16, alignment: 1)

//前8个字节转泛型Int操作
ptr1.assumingMemoryBound(to: Int.self).pointee = 10
//后8个字节转泛型Double操作
(ptr1 + 8).assumingMemoryBound(to: Double.self).pointee = 20.0

//读取后8个字节
print((ptr1 + 8).assumingMemoryBound(to: Double.self).pointee)

swift中还有个专门用来进行类型转换的函数unsafeBitCast(x: T , to: U.Type).它的意思是传入一个变量,转换为另一种类型.

所以我们也可以直接使用这种转换方法:

unsafeBitCast(ptr1, to: UnsafeMutablePointer<Int>.self).pointee = 30

unsafeBitCast(x: T , to: U.Type)是一种忽略数据类型的转换,它不会因为数据类型的变化而改变原来的内存数据.

什么意思呢?我么来看看下面代码:


var num1 = 10
var num2 = Double(10)

print(num1)
print(num2)

上面代码也是一种类型转化,把10转换成了Double类型的10.0,我们来看看num1num2内存中存储的是什么数据:

num1 和 num2存储的数据

可以看到,10转换为Double类型的10.0后,底层的数据结构变成了0x4024000000000000,这就是浮点数10.0在内存中的格式,这是因为浮点数在内存中的存储很复杂,浮点数的存储采用的是:符号位+阶码+尾数的形式.

像上面通过初始化的方法把10转换为浮点数10.0的方法是安全的,因为它会根据数据类型的不同,通过一定的计算改变内存数据.

而通过unsafeBitCast(x: T , to: U.Type)转换是不安全的,因为它不会根据数据类型的不同改变原来的内存数据.原来的内存数据是什么,它就直接拿过来.

我们通过下面代码试验一下:


var num1 = 10
var num2 = unsafeBitCast(num1, to: Double.self)

转换后num2的内存数据如下:

可以看到,num1通过unsafeBitCast转换为Double类型后,num2的内存数据还是0x000000000000000a.并不是0x4024000000000000.

我们打印看看,转换后num2是什么:

num2的数据

可以看到num2的数据为5e-323,毫无疑问是不正确的.

既然unsafeBitCast是不安全的,那我们通过它把非泛型指针类型转换为泛型指针类型,会不会也不安全呢?

不会的,因为不管是非泛型指针类型还是泛型指针类型,他们的本质就是地址.他们在内存中就是一串0x0x1005b1a7d这样的地址.所以通过unsafeBitCast 转换后还是同样的内存地址.

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 159,117评论 4 362
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,328评论 1 293
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 108,839评论 0 243
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 44,007评论 0 206
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,384评论 3 287
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,629评论 1 219
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,880评论 2 313
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,593评论 0 198
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,313评论 1 243
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,575评论 2 246
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,066评论 1 260
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,392评论 2 253
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,052评论 3 236
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,082评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,844评论 0 195
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,662评论 2 274
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,575评论 2 270