Posts 从指针说起
Post
Cancel

从指针说起

指针

作为一个使用Objective-C的iOS开发者,指针对很多人来说却是最熟悉的陌生人–虽然每天都在用,但是却对它了解的不是很多。那么指针到底是什么呢?

指针即指针变量,是一个存储变量内存地址的变量。

这句话可能有点绕,我们举个例子来说一下:

1
2
int a = 0;
int *p = &a;

上面的代码首先声明了一个int类型的变量a,它的值为0。然后又声明了一个指针类型的变量pp这个变量存储的值就是a变量的内存地址。

C++中的引用

引用是变量的别名。

我们同样举个例子来说一下:

1
2
int a = 0;
int &b = a;

首先声明了一个int类型的变量a,它的值为0,然后又声明了一个a的引用bb只是a的一个别名而已,并没有占用内存空间。实际上他们是同一个同西,在内存中占用同样的一个存储单元。

指针和引用的区别

除了在定义上的区别,他们还有很多不同:

  1. const指针,没有const引用。
  2. 指针可以有很多级,而引用只有一级(比如int **p是合法的,而int &&b是不合法的)。
  3. 指针可以为空,而引用不能为空,定义的时候必须对其进行初始化。
  4. 指针的值可以在初始化之后改变,而引用在初始化之后就不能变了。
  5. sizeof(引用)得到的是引用所表示的变量的大小,而sizeof(指针)得到的是指针本身的大小。

令人迷惑的*和&

*有两种含义,表示乘法和指针。&有点复杂,它既表示位运算的,又表示取地址,还表示引用,两个&拼在一起&&又表示逻辑运算的(天哪,为什么它的事情这么多!)。我们来看一段代码压压惊:

1
2
3
4
5
int a = 0;
int *p = &a;
*p = 1;
int &b = b;
b = 2;

第一行没啥说的,我们来看第二行:int *p = &a;表示的是声明一个int类型的名叫p的指针变量并把a的地址赋给它,所以这行代码其实应该写成这样int* p = &a。但是为什么我们一般要写成第一种形式呢?其实是为了防止引起歧义。比如int* p, p1;,这里的p1到底是指针类型呢还是int类型?这样写的话很容易让人感觉p1是指针类型的,而实际上它是int类型的。所以为了防止产生歧义,要声明两个指针我们就要写成这样int *p, *p1;

说的好像很有道理的样子,那么第二行又是什么鬼?*p不是表示指针吗?为什么会给它赋值1?好吧,其实p才是指针,这里的*解引用的意思,为了不跟上面说的引用产生歧义,你可以把它理解成解指针。所以*p指的是解引用,也就是根据p中所存储的内存地址来拿到p所指向的内存空间然后写入整数1

所以同样的*号在变量声明的时候其实只是为了告诉编译器你要声明的变量是一个指针类型的变量,仅此而已。而在使用的时候*号表示的是引用的意思。

&号在声明的时候也同样只是为了告诉编译器你要创建一个变量的别名,与*号不同的是在使用的时候并不需要解引用之类的操作,直接使用引用名即可(如代码第五行所示)。需要注意的是如果在使用时候在变量名前加上&符号就表示取这个变量的地址(如代码第二行所示)。

值传递、指针传递、引用传递

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
#include <iostream>

void increase(int a) {
    a++;
}

void increase1(int *p) {
    (*p)++;
}

void increase2(int &b) {
    b++;
}

int main(int argc, const char * argv[]) {
    int a = 0;
    increase(a);
    printf("%d\n", a);
    int *p = &a;
    increase1(p);
    printf("%d\n", a);
    int &b = a;
    increase2(b);
    printf("%d\n", a);
    return 0;
}

上面这段代码中的三个函数分别对应值传递指针传递引用传递,输出结果为0 1 2。说明第一个函数并没有改变a的值,是因为在值传递的过程中会对传递的变量进行一次拷贝increase函数改变的不是a而是a的拷贝,并不会对a造成影响。既然值传递会进行拷贝,那么指针也是一个变量,指针传递的过程中应该也会对指针进行拷贝。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
#include <iostream>

void increase1(int *p) {
    printf("%X\n", p);
    printf("%X\n", &p);
    (*p)++;
}

int main(int argc, const char * argv[]) {
    int a = 0;
    int *p = &a;
    printf("%X\n", p);
    printf("%X\n", &p);
    increase1(p);
    return 0;
}

上面的代码输出结果为5FBFF74C 5FBFF740 5FBFF74C 5FBFF718,我们发现传递前和传递后指针p的值没有发生变化,但是p的地址却发生了变化。这说明指针传递的过程中也会发生拷贝,不过拷贝的是指针,而指针所指向的内存地址不会发生改变,这也是变量a的值能够被修改的原因。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
#include <iostream>

void increase2(int &b) {
    b++;
    printf("%d\n", b);
    printf("%X\n", &b);
}

int main(int argc, const char * argv[]) {
    int a = 0;
    printf("%d\n", a);
    printf("%X\n", &a);
    increase2(a);
    return 0;
}

上面这段代码的输出结果为0 5FBFF74C 1 5FBFF74C,这说明引用传递的过程中并没有发生值拷贝,传过去的引用和原来的变量是同一个东西,所以对它的修改就是对a的修改。

关于NSError

1
2
3
4
5
NSError *error;
[[NSFileManager defaultManager] moveItemAtPath:srcPath toPath:dstPath error:&error];
if (error) {
    // handle error.
}

开发过程中经常会用到类似上面的代码,大家都知道NSError是这么用的,但是很多初学者对于为什么这样写却说不出个所以然。我们首先来看一下这个方法的声明- (BOOL)moveItemAtPath:(NSString *)srcPath toPath:(NSString *)dstPath error:(NSError **)error;,通过这个方法的声明我们可以知道这里其实需要的是一个NSError**类型的参数。所以这段代码的意思是先创建一个NSError类型的指针变量error(并没有创建NSError对象),然后把它的地址传递过去,当NSFileManager对象在执行的过程中发生了错误就会创建一个NSError对象并把它的地址写入我们之前创建的error指针的内存空间,也就是让error指针指向新创建的NSError对象。

有人可能会问这里为什么要用二级指针NSError**,而不是用用一级指针NSError*?我觉得有以下两点原因:

  1. 节省内存。如果使用一级指针,无论错误发生与否,我们势必要在传参之前创建NSError对象,这样如果执行过程中没有错误发生,我们创建的NSError对象就没有起到任何作用。
  2. 优雅。如果使用一级指针,我们就需要让使用者对我们创建的NSError对象的属性进行修改来写入错误信息。我们在检验错误是否发生的时候也就不能简单的通过判空来实现。

Java中的引用

看了上面的一堆东西可能你已经头晕目眩了,也可能你正高兴终于搞懂了。不管怎么样,接下里的路还的走!

Java中的引用乍一听感觉跟C++中的引用比较像,其实它们只是名字像而已,真正跟它比较像的是指针。某种意义上来说Java中的引用其实就是C++中的指针,都是某种结构的变量,变量中保存了所指向的对象的内存地址,只不过受到了一些限制,不能像C++一样灵活的进行各种指针运算、指向任意的内存。

参考文章:Java中的对象类型像引用还是指针,谁是谁非?

This post is licensed under CC BY 4.0 by the author.

为Git设置代理

GPS坐标纠偏和加偏

Comments powered by Disqus.