因为我并不是计算机专业出身,所以下面对C语言和指针的理解都来自于很久很久上过的C课程和工作以后接触的oc以及swift。说的不对的地方欢迎指正。
首先需要说明的概念是比特(bit),理论上,这是一个计算系统最小的单位,不是0就是1,可以简单理解成为,通电和断电。八个比特组成一个字节(byte),因为现代家用和商用的操作系统一般是32位或者64位系统。那么这是什么意思呢?不严谨的理解就是操作系统指挥cpu一次性拾取这么多位数据。也就是说32位系统一次从内存读取4个字节到32位cpu而64位系统一次读取8个字节到cpu。读到这里,作为一个工程师,你肯定会问以下两个问题:
1. 那我要是一个数据占不满这么多位怎么办啊?
2. 我要是数据超出了cpu怎么解决啊?
如果不是工程师,那么有可能会觉得问这样问题的人是不是有病?没满就没满啊,满了就再开一个不就好了啊?事实上,这涉及到一个最简单的工程思维,很多“常识性”的浪费其实并不是真正浪费。比如有一个叫做对齐的概念。懂一点计算机常识的人或多或少都听过这个概念,比如ssd的4k对齐。那么是内存对齐呢?其实就是利用占位符(也就是0)来填满没有被使用的内存空间,如果你要储存一个int 10在内存中,实际上是10 0 0 0 0 0 0 0 0,当然,这里只是一个示例,显然不可能是10存储在内存中。表面上看起来这是一种巨大的浪费,为什么我要占位而不能接着利用剩下来的空间呢?因为在cpu读取的时候,对齐之后的数据可以大大降低读写次数,比如你要存储“10个人”这三个字,如果我只想提取10出来,那么我只需要找到指向integer的指针然后直接读取所有步长就可以了,这会很大程度上减少非必要的读取次数。当然除此以外,还有很多其他的原因比如原子性之类的原因,具体的可以参考操作系统设计之类的资料。那么当我们说读取的时候是一个什么样的概念呢?其实有点像送信,每个房屋都会在邮政有一个代码,邮递员就是根据这个代码把邮件快递送到你家的,每个位也有一个在操作系统注册的代码,操作系统就是依赖这个代码来让cpu去找到具体的数据的。
说了这么多,所以到底什么是指针呢?指针就是一个整型数据,储存这个某一个数据在内存的中的地址。一般人看到这里就会懵逼了,既然指针是指向数据的一个整数,那么整数本身不也是一种数据吗?你是不是在蒙我?这怎么听起来哪里不对?所以指针可以指向另外一个指针?答案就是这么粗暴,是这样的。那你妹的不是太坑爹了吗?所以你给了我一个整数,我怎么知道这是一个数据还是一个指向数据的指针还是一个指向指针的指针?是的,你又猜对了,你不知道,至少对于swift来说,你不知道。很好,那怎么解决这个问题呢?swift对c指针进行了一些改造,给了你八种指针:
1. UnsafeMutablePointer<T>
2. UnsafePointer<T>
3. UnsafeMutableBufferPointer<T>
4. UnsafeBufferPointer<T>
5. UnsafeMutableRawPointer
6. UnsafeMutableRawBufferPointer
7. UnsafeRawBufferPointer
又是一脸懵逼,这都什么玩意?c 语言里面就一个指针,swift什么鬼?给我这么多名字比我太奶奶裹脚布太长的东西?冷静一下,仔细看看这些东西,其实还是很有规律的,pointer当然表示这些都是指针,unsafe表示不安全,use on your own risk。这句话的意思是,swift本身是一门安全的语言,你们可劲作,崩了算我输。编译器负责开创,管理,销毁内存中的数据,所以像我这样非计算机专业出身的智障开发者,要用什么开什么就好了,完全没有任何技术含量,不需要任何计算机常识。但是,这个的前提是编译器来管理内存而不是开发者,如果你就是这么任性,不听不听,飞鞋点金。那么,swift能怎么办?只能贴个免责申明,对吧,人之常情,你也经常看到什么“仅供研究学习,不得用于商业”之类的东西,unsafe就是这个意思。作为ios开发者,mutable还是可以看得懂的,generic也是看得懂的,所以只剩下raw和buffer了。Raw表示这个指针直接指向数据而不是指向其他指针!!!c语言常识告诉我们,所有变量名,函数名,struct名都是指针。所以这就是为什么指向这些东西的指针实质上是指向指针的指针。Buffer是什么意思呢?其实就是这个单词本身的意思,你可以理解为一个array,表示这个一个指针群,可能包含不止一个指针。这样一来,我们已基本能够通过名字知道该如何使用一个指针了。
还记得C语言是如何创建一个整数的吗? int x = 8. 这行代码说明了什么呢?说明了int这个东西是一个raw value,所以呢,如果想要创建一个指向int的指针,我们应该使用UnsafeMutableRawPointer:
let intPoint = UnsafeMutableRawPointer.allocate(bytes: bytes, alignedTo: alignment)
通过系统的Documentaion,我们可以找到上面这个方法来初始化一个指针,那么这个函数名很直白的告诉你,这个函数的作用是在heap区allocate一个空间,空间包含有bytes位数据,按照alignment方法对齐。那么我们再按照documentation可以很清楚的得知如何去开创一个内存空间:
/*
MemoryLayout<Int>
MemoryLayout是一个抽象类,使用的时候需要告诉这个类具体的type
你可以使用你自己的class或者struct,MemoryLayout<MyClass>来获取相关内存的信息。
对于MemoryLayout,我们有三个很重要的属性:size,alignment,stride。
当你开创了空间以后系统会自动给这三个属性赋值,会告诉你,内存块大小是多少,对齐方式是怎么样的,递进步长是多少。
如果我们要创建一个指向一个Int类型数据的指针,那么首先,Int是不会超过一个byte的,对齐方式是64位对齐。
*/
let bytes = MemoryLayout<Int>.stride
let alignment = MemoryLayout<Int>.alignment
let intPoint = UnsafeMutableRawPointer.allocate(bytes: bytes, alignedTo: alignment)
如果你说,这他么有什么用?好,那你听过数组吗?我们通过指针来创建一个指定大小swift数组Array<UInt>:
class IntArray {
private let count:Int
private var index = -1
private var first = true
private var start: UnsafeMutableRawPointer!
private var bufferPointer: UnsafeRawBufferPointer!
private let stride = MemoryLayout<Int>.stride
private let alignment = MemoryLayout<Int>.alignment
private let q = DispatchQueue(label: "ReservedQ")
private var byteCount:Int {
get{return stride * count}
}
init(count:Int) {
self.count = count
start = UnsafeMutableRawPointer.allocate(bytes: byteCount, alignedTo: alignment)
}
func append(_ newElement:UInt) {
q.sync {
guard index < count else {return}
index += 1
if first {
first = false
start.storeBytes(of: newElement, as: UInt.self)
}else {
start.advanced(by: stride * index).storeBytes(of: newElement, as: UInt.self)
}
bufferPointer = UnsafeRawBufferPointer(start: start, count: byteCount)
for (i, byte) in bufferPointer.enumerated() {
guard i < (index+1)*8 else {break}
if byte != 0 && byte != 255 {
print(byte)
}else if i%8 == 0 && byte == 0 {
print(0)
}
}
print("--------------------")
}
}
deinit {
//这个函数是不可缺少的,这是unsafe的本质,you are on your own!你必须自己管理内存了。
start.deallocate(bytes: byteCount, alignedTo: alignment)
}
}
运行一下:
let arr = IntArray(count: 3)
arr.append(40)
arr.append(3)
arr.append(0)
结果是:
40
--------------------
40
3
--------------------
40
3
0
--------------------
当然直接说这个就是一个Array实在是太夸张了,但是,通过这么一个例子,我们可以更好的去理解如何在实际工作中去使用指针。毕竟对于iOS开发来说,指针这种东西一般也就是用来写一写C函数的wrapper的。比如给keychain写一个wrapper,给一些第三方的SDK写一写Wrapper(可能在今天正常的公司都会给个最起码是objective c的SDK,但是公司内部的很多硬件SDK还是只有C和C++接口 T_T)。但是这并不表示指针这样一样强力工具对于iOS开发就是不需要掌握的,因为懂指针可以让你用很简单的几行代码完成一些以前很难完成的工作。我们来看一个来自stackoverflow的例子:遍历整个enum的所有case,如果你不会指针,那么你写出来的方案就会是:
enum ProductCategory : String {
case Washers = "washers", Dryers = "dryers", Toasters = "toasters"
static let allValues = [Washers, Dryers, Toasters]
}
for category in ProductCategory.allValues{
}
虽然看上去还可以,但是设想一下,如果你有十几个case的话,那么不仅很烦,而且还需要保证allValues array里面都是正确的。通过指针你就可以很轻松的使用一个static方法来完成:
enum ProductCategory : String {
case Washers = "washers", Dryers = "dryers", Toasters = "toasters"
static func iterateEnum() -> AnyIterator<ProductCategory> {
var i = 0
return AnyIterator {
let next = withUnsafeBytes(of: &i) { $0.load(as: self) }
if next.hashValue != i { return nil }
i += 1
return next
}
}
}
for category in ProductCategory.allValues{
}