指针和引用的区别
Updated:
从概念理解和用法上来讲,C/C++ 中的指针和 Java 等语言中的引用非常类似。它们和被指向的内容分开存储(一般存储在栈上),并且持有被指向的对象的地址。通过指针/引用,我们可以方便的操作实际的对象。
当然,区别还是有的。C 系的指针更加透明,换句话说指针其实就是一个整数,只不过这个整数恰好表示了被指向的变量的地址。这是一种非常简单的,直来直去的表示方法。而 Java 等语言的引用就像是一个黑盒了。Java 的规范并没有要求引用的值一定就是内存地址,在实现的时候,它有可能有一个中间层进行映射,但最终还是指针的模型,也就是说依然会持有地址,只不过从使用者的角度来看,地址的概念经过封装已经不存在了。具体的做法我不太清楚,据说是为了提高垃圾回收算法的效率。
指针可以进行四则运算
一个是透明的数字,表示内存地址。一个是不透明的中间层,带来的直接区别就是指针可以进行四则运算。指针四则运算在提供方便的同时,也相当危险,比如这段代码:
1
2
3
4
5
6
7
8
9
|
int main(int argc, const char * argv[]) {
int array[4] = {9,10,11,12};
int array2[8] = {1,2,3,4,5,6,7,8};
for (int i = 0; i < 12; ++i) {
printf("%d\n", array2[i]);
}
return 0;
}
|
在这个十二次的 for 循环中,前八次的输出结果毫无疑问是依次输出 array2
数组的每个元素。但之后的四次循环并不会发送数组越界,而是相当“正常的”输出了 array
数组的元素。在大部分机器上,这段代码的运行结果应该是依次输出 1 到 12 这 12 个数字。
实际上,这里的输出结果完全是由 array
和 array2
两个数组变量在内存中的布局决定的,如果增加或减少一点 array
数组的长度,恐怕就没有那么幸运了。
可见,透明的指针带来的问题在于,开发者知道了太多他们本来不该知道的东西(比如开发者竟然可以拿到变量的真实地址,还可以不借助变量名就访问一个变量,比如这里的 array
)。在某些精心构造的场合下,攻击者甚至可以通过修改字符串的值来控制程序的执行逻辑,只要计算得当,他们可以调用原本根本不会被调用的函数。
在引用的概念中,引用就是引用,它什么也不是,更不可能是数字,也就谈不上什么四则运算了。因此想要通过 A 对象的引用来访问 B 对象是完全不可能的。
指针无法检查类型
指针的灵活性除了体现在四则运算外,还包括它对类型的弱检查。实际上,因为指针仅仅是数字,它根本没有检查类型的可能。C 的 reinterpret_cast
方法可以将任何一个类型的指针转化为其它任何一个类型的指针,这种做法可以通过编译,但会在运行时报错。
1
2
|
int value = 21;
Person p = reinterpret_cast
|
比如我们可以把一个整数的指针转换成对象类型,然后到处拿去使用,最终将会得到一个 EXC_BAD_ACCESS
的错误。
绝大多数时候,reinterpret_cast
既危险,也鸡肋。倒是在计算对象的哈希值时,把指针类型转换成整数类型会有助于计算:
1
2
3
4
|
unsigned short Hash( void *p ) {
unsigned int val = reinterpret_cast<unsigned int>( p );
return ( unsigned short )( val ^ (val >> 16));
}
|
而引用由于具备了中间层,完全可以在编译期进行类型检查,确保被转换的类型之间存在继承关系,从而确保安全性:
1
2
3
4
|
Father father = new Son(); // 正常
Father father = new Father();
Son son = (Son) father; // ClassCastException
|
至于在运行时进行类型检测,这已经是另一码事了。单就编译期而言,引用对类型的控制能力远超过指针。
引用是未来的趋势
很多人通过分析 Swift 的优点来解释为什么 Swift 会取代 Objective-C。他们说的都对,因为这些确实是 Swift 的优点;但说的也都错,因为这些都不是取代 Objective-C 的理由。
因为 Swift 的创造者自己已经解释过了,OC 是基于 C 语言的,使用的是指针的模型,这就注定了 OC 不是一门安全的语言。而 Swift 采用了引用的模型,仅通过 UnsafePointer
开放了微弱的指针能力,它的命名也时刻提醒使用者,指针的操作是不安全的。
而那些所谓的 Swift 的优点,没有一个是替换 Objective-C 的理由,因为它们要么可以在 Objective-C 上引入,要么并不见得优于 Objective-C。
如果站在架构或者工程的角度来看问题的话,框架的提供者应该为使用者提供尽可能简洁的操作,一方面降低使用成本,最重要的则是减少出错概率。可能出现的错误往往会随着可以使用的操作的线性增加而几何式的增加。如何提供一些简单的,正交的基础操作,让使用者在有限、可控的操作下完成全部任务,是设计的艺术,也是对设计者的挑战。