文章目录
前言
指针是c中的精华,也是c/c++区分与其它语言的标志之一,希望我们能对这部分的知识理解的非常透彻。
一、内存与地址
计算机的内存有数以万计的位组成,每个位可以容纳一个0或者1,为了表示更大的数,我们往往将很多位组合在一起作为一个单位,从而可以存储更大的数。而现实中的计算机一般以字节(byte)看作一个单位,每个字节中有8个位,也就是说他可以存储0~255(2^8-1)范围内的数,而每个字节的位置通过地址
来标识。
但8个位的字节依旧无法满足我们现实中的需求,为了存储更大的数字,我们将两个甚至更多字节合在一起作为一个更大的单位。如我们所知道的整型(int
)就是由四个字节组成的。
需要注意的是,尽管我们将几个字节组成了一个更大的单位,但它仍然只有一个地址,或者说我们往往将一个单位看作一个整体,这个整体将会在计算机中拥有一个地址。而这个地址到底是最左边的字节位置还是最右边的字节位置,是由机器的种类决定的。
总之,我们需要知道的是:
- 内存中的每个位置都有一个独一无二的地址;
- 内存中的每个位置都包含一个值;
- 在将几个字节作为一个单位的时候,这时这个单位将会有它独一无二的地址作为标识。
所以如果我们记住了一个值的存储地址,就可以通过这个地址取得这个值。但是如果只能通过这种方式来读值实在是太不人性化了,所以我们所学习的高级语言通过编译器提供给我们可以使用变量的方式来访问内存中的信息,需要注意的是,在计算机内部硬件仍然通过地址访问内存中的信息。
二、指针基础
1.指针是一种特殊的变量
指针是一种特殊的变量,在其中存放的是地址。指针的初始化是由&
来完成的,该操作符用于产生操作数的内存地址。如下代码:
#include <stdio.h>
int main(){
int a = 0;
int* p = &a;
printf("%d\n",p);
}
上述需要注意的是我们定义的指针类型需要与我们存放的地址指向的操作数类型相同,如上,a是int
型,所以我们设置的存放a地址的指针p也是int
型指针。
我们需要区分指针的的值与指针本身的地址,运行代码如下:
#include <stdio.h>
int main(){
int a = 0;
int* p = &a;
printf("p = %d\n",p); //指针中的值
printf("&p = %d\n",&p); //指针变量p的地址
}
显示输出为:
2. 解引用(间接访问)
通过一个指针访问它所指向的地址的过程就称为解引用指针或间接访问,这个用于执行间接访问的操作符就是*
。我们可以使用如下代码加深理解:
#include <stdio.h>
int main(){
int a = 0;
int* p = &a;
printf("*p = %d\n",*p); //解引用指针p
}
所得的输出为:
需要特别注意的是,在对指针进行间接访问(解引用)的时候一定要确保该指针已经被初始化。
3. NULL指针
标准中定义的NULL指针表示不指向任何东西,想要使变量为NULL,可以向该指针赋零值。也就是说我们可以通过将指针与零值进行比较从而判断是否为NULL指针。之所以是零是由于一种源代码约定,就机器内部而言,NULL指针实际值可能并不是零。
NULL指针的概念是很有用的,它表示某个特定的指针目前并未指向任何东西。例如当我们想要查询某个数组中是否有特定值时返回一个指向查找元素的特定指针。如果该数组不包含这个特定值就返回一个NULL指针,这种方法允许返回值传达两个信息段:是否找到该元素?如果找到,是哪个元素?
NULL指针并未指向任何东西,所以它不能进行解引用。在对指针进行解引用时我们必须确保它不是NULL指针。如果程序对NULL指针进行了解引用,不同的机器会给出不同的反应,总体共分为两种情况,一种会进行报错,而另一种并不会妨碍程序修改该位置的值,第二种比第一种严重得多,往往使得程序员难以找到并修改这个错误。
4. 指针的指针
首先我们看一段代码:
#include <stdio.h>
int main()
{
int a = 25;
int *b = &a;
int **c = &b;
printf("*b = %d\n&b = %d\nc = %d\n*c = %d\n**c = %d\n",*b,b,c,*c,**c);
}
输出为:
可以看到我们初始化指针b指向整型变量a,指针c指向指针b,也就是说指针c中存放着b的地址,c是一个指针的指针。
除此以外我们再看看*
操作符,它具有从右往左的结合性,也就是说**c = *(*c)
,*c
先对c中存放的地址进行解引用,也就是b,*(*c)
其实就是*b
,b中存放着a的地址,对b进行解引用就是a,所以**c = *(*c) = *b = a = 25
。
三、指针运算
1. 左值与右值
我们需要先了解一下左值与右值的区别,左值就是能够出现在赋值符号左边的东西,右值就是出现在赋值符号右边的东西。
如a = b+25
,其中a
就是一个左值,b+25
是一个右值。
那么任何东西都可以做左值或者右值吗?并不是这样的,左值要求其可以标识一个可以存储结果值的地点,右值要求指定一个数值。
如b+25=a
,就是不合法的,a
可以用做右值,但b+25
却不可以作为左值,它的意义是将b所指定的值与25相加,但计算机在做完运算后并无法直接预测该结果处于哪个位置,也无法保证该表达式的值下次还存储在那个位置,所以它无法作为一个左值。同理,所有的字面值常量都无法作为左值。
2. 指针表达式
我们来看几个例子,分析一下当他们分别作为左值和右值是如何求值的。我们首先来看一些声明:
char a = 'a';
char* p = &a;
我们有了两个变量,其中指针p中为a的地址。
接下来我们分析一下p, &p, *p, ++p, *++p
的左值与右值。
左值 | 右值 | |
---|---|---|
p | p所处内存的位置 | p中所存的内容,也就是a的地址 |
&p | 非法 | 指针p的地址 |
*p | p中地址所指向的值的位置,也就是a的位置 | p中所指向的值 |
++p | 非法 | p中的地址值+1 |
*++p | p中地址+1后的位置 | p中地址+1后的位置中的值 |
理解这里的时候我们一定要明白地址值本身是个常量。只有两种方法可以标识存储值结果的位置,一种是使用变量,一种是使用"地址",这里的地址并不是地址值,而是采用解引用的方式指向该位置的值,毕竟地址值在计算机中本质上是作为一个常量存储在我们未知的区域。
3. 指针运算
1.算数运算
在上述中我们其实也涉及了一些指针的运算,*++p
这其实就涉及了指针和整数的运算,通过上述的分析我们也知道了指针加上一个整数可以看做是另外一个指针。问题是,它指向哪里?
当一个指针与整数量执行算数运算时,整数在执行加法运算之前始终会根据合适的大小进行调整,这个合适的大小就是指针所指向类型的大小,调整就是将将该整数与"合适大小相乘"。
如:定义一个指针int *p;
,进行指针运算p+2
,其实在计算机中加载指针的整型大小为2*sizeof(int) = 2*4 = 8
,将2与指针p相加其实是指将指针的值增加2个int的大小。
指针的运算只涉及两种,一种是指针和整数间的运算,一种是指针与指针之间的运算。
1. 指针与整数之间的运算
标准定义这种形式只能用于指向数组中的某个元素的指针(在c与指针这本书中,作者补充认为这种也适用于malloc
函数动态分配获得的内存)。数组中的元素存储在连续的内存位置中,后面元素的地址大于前面元素的地址。
在数组中,我们对指向数组中某元素的指针+1其实就是将其指向了下一个元素。换句话说,我们对该指针+5就是向右移动5个元素的位置,减去5就是向左移动5个位置。但是如果对指针进行整数间的加减法计算后,此时指针所指向的位置超过了该数组所在范围,那么此时对计算后的指针执行间接访问就会失败。
2. 指针 - 指针
这种运算只有当两个指针都指向同一个数组中的元素时才会被允许。它的结果类型是ptrdiff_t,是一种有符号整型,它的值是两个指针所指向的位置在内存中的距离(要注意,是以数组元素长度为单位)。
举个例子更好理解一下,如在整数型(int)数组中,起始位置为1000,第一个指针p1
所指向的位置为1008,第二个指针p2
所指向的位置为1028,但表达式p2-p1
的结果是5,而不是20。
2. 关系运算
同样的,指针间的关系运算要求,它们都指向同一个数组中的元素。指针的关系运算是有限制的,基本有以下四种:
< , <= ,> , >=
这种比较主要可以判断哪个指针指向数组更前或更后的位置。
除此以外,我们还可以对两个指针进行相等性或不等性比较,判断两个指针是否指向同一个位置。