指针基本介绍

一个典型的计算机RAM,每一个字节会对应一个地址。现在我们在C语言中声明一个变量a,当程序执行时,计算机会为这个变量a分配空间(现代计算机系统中每一个进程都访问虚拟地址而非直接访问物理地址,这个不在讨论范围内),而分配的空间大小取决该变量的数据类型,例如int类型,会占用4个字节。同时还取决于编译器,因为不同的编译器对于数据类型空间的分配可能略微有差距。

指针是一个变量,它存放着另外一个变量的地址。

int a = 5;
int *p;

p = &a;

现在假设,变量a的地址为204(即内存中的起始位置),指针p的位置为64,声明结束后,我们进行了一次赋值p=&a;即将a的地址赋值给p,那么此时便有:

p -> 204
&a -> 204
&p -> 64
*p -> 5

变量前&为取当前变量的地址,*为取当前变量的值

其中为了获取a的值而使用*p,被称为解引用

对于指针的声明,存在以下两种方式

int *p;
int* p;

同时可以在声明的同时就赋值,因为指针就是存放地址的变量

int *p = &a;

指针的算数运算与类型

指针的算数运算

看下面一段代码:

int main(){
    int a = 10;		//假设a的地址为2002
    int *p = &a;
    
    printf("%d\n",p);
    printf("%d\n",p+1);
}

结果输出:

2002
2006

为什么第二次结果是2006,这是因为a为占用4字节的Int类型,因此当p+1时,会得到下一个int型地址(起始位置),即2002+4=2006

现在,我们在其中插入以下一段代码:

printf("%d\n",*(p+1));

输出结果:

-858993460

这里显示的数字无意义,这是因为首先计算的是p+1,即获取到了下一个int类型地址,是一个未初始化的地址,此时使用*取其值自然是获得了无意义的数字。

指针类型

指针是强类型的,这就意味着需要一个特定类型的指针变量来存储特定类型的变量地址,即与存储地址的对象变量类型保持一致。如int类型变量需要int类型的指针变量来存储它的地址。

那么,为什么强类型?为什么不为所有指针指定一个通用类型呢?这是因为,指针不仅仅是存储着对象变量的地址,同时指针变量也解引用那些地址的内容。

现在,假设我们有一个以下变量:

int a = 1025;

int类型占用4字节,一字节为8位(X86架构计算机),而1025换算成二进制数为10000000001,那么变量a在内存中的布局即可看成如下所示:

 byte3    byte2    byte1    byte0
00000000 00000000 00000100 00000001
   203      202      201      200

其中我们假定byte0~byte3这四个字节的位置分别是200~203,那么此时我们声明一个指针并将a地址赋值给它,即int *p = &a,那么p的值即为a的地址,即200

顺带一提,当整数在内存中表示时,最左边的比特,被称为符号位,用来指定整数的正负性,0表示正,1表时负,剩下的31位用来存储数字,因此不难推断出,int类型的取值范围为-2^31~2^31-1惭愧!直到今天才真正明白为什么int型的取值范围是-231~231-1_WaiterXiaoYY的博客-CSDN博客)

现在再继续看以下代码:

int a = 1025;	//假设其位置为200
int *p = &a;
printf('%d\n',p);
printf('%d\n',*p);

char *n = (char*)p;		//强制类型转换
printf('%d\n',n);
printf('%d\n',*n);

输出结果:

200
1025
200
1

可以看到,n同样存储a的地址,但是解引用后值却是1,我们回看a在内存中的布局即可明白,因为char类型只有一字节,因此当解引用*n时,计算机只会根据它char类型来寻找一字节的内容,因此只有byte0被解引用,byte0中存储的二进制数转换为十进制后值即为1。同理如下代码:

printf('%d\n',n+1);
printf('%d\n',*(n+1));

输出结果:

201
4

结果并未出乎意料,因为n指向byte0,那么n+1自然便跳到下一个char201,其中201存储的二进制数为转换为十进制后结果为4

void类型指针

有一种无类型的指针,它不针对任何类型,这种指针被称为void类型指针。

我们继续接上一代码片段再声明一个void类型指针:

void *p0;
p0 = p;
printf("%d\n",p0);

输出结果:

200

我们在这里并没有进行解引用以及类似p+1的算数运算 操作,这是因为void类型指针没有映射到任何类型,因此不能解引用,同时也能进行算数运算操作,因为这些都必须基于一个特定的数据类型。

同时也注意到,我们在将p赋值给p0时,并没有像上面赋值给char类型指针时进行类型转换,正是因为void类型指针无特定类型,因此它可以指向任何类型的数据,即 任何类型指针都可以直接赋值给void类型指针

但是,这不意味着void类型指针就可以直接赋值给任何类型指针,如果你尝试以下代码,便会引发错误:

int *a;
void *p;
...
a = p;	//这会引发错误

因此若要将void类型指针赋值给其它类型指针时,必须进行强制转换:

a = (int*)p;

void类型指针作为一个无类型指针,可以指向任意类型,因此当我们声明一个函数时,其参数可以是任意类型指针时,都应该将其声明为void*,void类型指针可以看成一种通用类型指针。

指针的指针

如下所示:

int a = 5;
int *p = &a;
int **p0 = &p;

printf("%d\n",p);
printf("%d\n",*p0);
printf("%d\n",*(*p0));

输出结果:

200
200
5

其实不难理解**p0*p的关系,同理甚至可以出现***p1这样的嵌套甚至一直嵌套下去。

其中*(*p0)可以写成**p0,但仍建议写成*(*p0),这是一个好习惯。

函数传值与传引用

如下所示一段代码:

void increment(int a){
    printf('%d\n',&a);
}
int main(){
    int a = 5;
    increment(a);
    printf('%d\n',&a);
}

输出结果:

200
300

可以看到两个a根本不是一个变量

对于这样的函数需求,可以写成以下形式

void increment(int *p){
    *p = (*p) +1;
}
int main(){
    int a = 5;
    increment(a);
    printf('%d\n',&a);
}

输出结果:

6

这样被称为传引用,可以很好地节约内存