--- id: 指针 title: 指针 sidebar_position: 7 data: 2022年3月30日 --- 指针是 C 语言最重要的概念之一,也是最难理解的概念之一。 ## 简介 指针是什么?首先,它是一个值,这个值代表一个内存地址,因此指针相当于指向某个内存地址的路标。 字符`*`表示指针,通常跟在类型关键字的后面,表示指针指向的是什么类型的值。比如,`char*`表示一个指向字符的指针,`float*`表示一个指向`float`类型的值的指针。 ```c int* intPtr; ``` 上面示例声明了一个变量`intPtr`,它是一个指针,指向的内存地址存放的是一个整数。 星号`*`可以放在变量名与类型关键字之间的任何地方,下面的写法都是有效的。 ```c int *intPtr; int * intPtr; int* intPtr; ``` 本书使用星号紧跟在类型关键字后面的写法(即`int* intPtr;`),因为这样可以体现,指针变量就是一个普通变量,只不过它的值是内存地址而已。 这种写法有一个地方需要注意,如果同一行声明两个指针变量,那么需要写成下面这样。 ```c // 正确 int * foo, * bar; // 错误 int* foo, bar; ``` 上面示例中,第二行的执行结果是,`foo`是整数指针变量,而`bar`是整数变量,即`*`只对第一个变量生效。 一个指针指向的可能还是指针,这时就要用两个星号`**`表示。 ```c int** foo; ``` 上面示例表示变量`foo`是一个指针,指向的还是一个指针,第二个指针指向的则是一个整数。 ## * 运算符 `*`这个符号除了表示指针以外,还可以作为运算符,用来取出指针变量所指向的内存地址里面的值。 ```c void increment(int* p) { *p = *p + 1; } ``` 上面示例中,函数`increment()`的参数是一个整数指针`p`。函数体里面,`*p`就表示指针`p`所指向的那个值。对`*p`赋值,就表示改变指针所指向的那个地址里面的值。 上面函数的作用是将参数值加`1`。该函数没有返回值,因为传入的是地址,函数体内部对该地址包含的值的操作,会影响到函数外部,所以不需要返回值。事实上,函数内部通过指针,将值传到外部,是 C 语言的常用方法。 变量地址而不是变量值传入函数,还有一个好处。对于需要大量存储空间的大型变量,复制变量值传入函数,非常浪费时间和空间,不如传入指针来得高效。 ## & 运算符 `&`运算符用来取出一个变量所在的内存地址。 ```c int x = 1; printf("x's address is %p\n", &x); ``` 上面示例中,`x`是一个整数变量,`&x`就是`x`的值所在的内存地址。`printf()`的`%p`是内存地址的占位符,可以打印出内存地址。 上一小节中,参数变量加`1`的函数,可以像下面这样使用。 ```c void increment(int* p) { *p = *p + 1; } int x = 1; increment(&x); printf("%d\n", x); // 2 ``` 上面示例中,调用`increment()`函数以后,变量`x`的值就增加了1,原因就在于传入函数的是变量`x`的地址`&x`。 `&`运算符与`*`运算符互为逆运算,下面的表达式总是成立。 ```c int i = 5; if (i == *(&i)) // 正确 ``` ## 指针变量的初始化 声明指针变量之后,编译器会为指针变量本身分配一个内存空间,但是这个内存空间里面的值是随机的,也就是说,指针变量指向的值是随机的。这时一定不能去读写指针变量指向的地址,因为那个地址是随机地址,很可能会导致严重后果。 ```c int* p; *p = 1; // 错误 ``` 上面的代码是错的,因为`p`指向的那个地址是随机的,向这个随机地址里面写入`1`,会导致意想不到的结果。 正确做法是指针变量声明后,必须先让它指向一个分配好的地址,然后再进行读写,这叫做指针变量的初始化。 ```c int* p; int i; p = &i; *p = 13; ``` 上面示例中,`p`是指针变量,声明这个变量后,`p`会指向一个随机的内存地址。这时要将它指向一个已经分配好的内存地址,上例就是再声明一个整数变量`i`,编译器会为`i`分配内存地址,然后让`p`指向`i`的内存地址(`p = &i;`)。完成初始化之后,就可以对`p`指向的内存地址进行赋值了(`*p = 13;`)。 为了防止读写未初始化的指针变量,可以养成习惯,将未初始化的指针变量设为`NULL`。 ```c int* p = NULL; ``` `NULL`在 C 语言中是一个常量,表示地址为`0`的内存空间,这个地址是无法使用的,读写该地址会报错。 ## 指针的运算 指针本质上就是一个无符号整数,代表了内存地址。它可以进行运算,但是规则并不是整数运算的规则。 (1)指针与整数值的加减运算 指针与整数值的运算,表示指针的移动。 ```c short* j; j = (short*)0x1234; j = j + 1; // 0x1236 ``` 上面示例中,`j`是一个指针,指向内存地址`0x1234`。你可能以为`j + 1`等于`0x1235`,但正确答案是`0x1236`。原因是`j + 1`表示指针向内存地址的高位移动一个单位,而一个单位的`short`类型占据两个字节的宽度,所以相当于向高位移动两个字节。同样的,`j - 1`得到的结果是`0x1232`。 指针移动的单位,与指针指向的数据类型有关。数据类型占据多少个字节,每单位就移动多少个字节。 (2)指针与指针的加法运算 指针只能与整数值进行加减运算,两个指针进行加法是非法的。 ```c unsigned short* j; unsigned short* k; x = j + k; // 非法 ``` 上面示例是两个指针相加,这是非法的。 (3)指针与指针的减法 相同类型的指针允许进行减法运算,返回它们之间的距离,即相隔多少个数据单位。 高位地址减去低位地址,返回的是正值;低位地址减去高位地址,返回的是负值。 这时,减法返回的值属于`ptrdiff_t`类型,这是一个带符号的整数类型别名,具体类型根据系统不同而不同。这个类型的原型定义在头文件`stddef.h`里面。 ```c short* j1; short* j2; j1 = (short*)0x1234; j2 = (short*)0x1236; ptrdiff_t dist = j2 - j1; printf("%d\n", dist); // 1 ``` 上面示例中,`j1`和`j2`是两个指向 short 类型的指针,变量`dist`是它们之间的距离,类型为`ptrdiff_t`,值为`1`,因为相差2个字节正好存放一个 short 类型的值。 (4)指针与指针的比较运算 指针之间的比较运算,比较的是各自的内存地址哪一个更大,返回值是整数`1`(true)或`0`(false)。