指针——C的灵魂
一、二级指针认知
不管是二级指针还是多级指针,考虑它们和一级指针都是一样的,一级指针保存的是普通变量的地址,而二级(多级)指针保存的是指针变量的地址,即地址的地址,指针的指针,二级指针的写法如下:
int **p;
为了引入二级指针,我们先看一段代码:
int data = 100;
int *p = &data;
printf("data的地址是%p\n",&data);
printf("p保存data的地址是%p,内容是%d\n",p,*p);
在这里,我们定义了一个变量 d a t a data data,并定义了一个指针 p p p 指向 d a t a data data ,然后用 d a t a data data 和 p p p 分别访问了 d a t a data data 的地址和值。那我们如何去保存指针 p p p 的值呢?在这里我们就用到了二级指针:
int **p2 = &p;
我们看一下完整的代码和运行结果:
#include<stdio.h>
int main()
{
int data = 100;
int *p = &data;
printf("data的地址是%p\n",&data);
printf("p保存data的地址是%p,内容是%d\n",p,*p);
int **p2;
p2 = &p;
printf("p2保存的是p的地址:%p\n",p2);
printf("*p2是%p\n",*p2);
printf("**p2来访问data:%d\n",**p2);
return 0;
}
运行结果:
我们分析以下,
p
2
p2
p2 是一个二级指针,它指向了指针
p
p
p ,所以它保存的是
p
p
p 的地址 000000000061FE08 ,因此
∗
p
2
*p2
∗p2 就是
p
p
p 的值,即
d
a
t
a
data
data 的地址:000000000061FE14 ,再对这个地址取值就是
∗
∗
p
2
**p2
∗∗p2 ,取出来的就是
d
a
t
a
data
data 的值:
100
100
100 。所以打印的结果如上图。地址关系可以参照下图:
关于三级指针,道理是一样的,这里不做详细讲解,用代码来做一下示范:
#include<stdio.h>
int main()
{
int data = 100;
int *p = &data;
printf("data的地址是%p\n",&data);
printf("p保存data的地址是%p,内容是%d\n",p,*p);
int **p2;
p2 = &p;
printf("p2保存的是p的地址:%p\n",p2);
printf("*p2是%p\n",*p2);
printf("**p2来访问data:%d\n",**p2);
int ***p3;
p3 = &p2;
printf("p3保存的是p2的地址:%p\n",p3);
printf("*p3是%p\n",*p3);
printf("***p3来访问data:%d\n",***p3);
return 0;
}
运行结果:
二、二级指针实战应用
题目:有a个学生,每个学生有b门课的成绩。要求在用户输入学生序号以后,输出该学生的全部成绩,用指针函数来实现。
#include<stdio.h>
int* getPpos(int pos, int (*pstu)[4]) //第二形参为一个数组指针
{
int *p;
p = *(pstu+pos); //由于pstu为面向父数组的指针,为了解决p和pstu类型不匹配,所以进行*取得面向子数组的指针
return p; //返回一个指向子数组的指针,即地址
}
int main()
{
int scores[3][4] = {
{11,22,33,44},
{55,66,77,88},
{34,54,57,87}
};
int *ppos;
int pos;
printf("请输入你想要查看学生成绩的号数:\n");
scanf("%d",&pos);
ppos = getPpos(pos, scores);
for(int i = 0;i<4;i++){
printf("%d ",*ppos++);
}
return 0;
}
以上的题目我们用指针函数和数组指针的方法将它实现,下面,我们用二级指针的形式讨论以下这道题目:
首先,我们以上的方法使用返回值为指针的函数将地址再函数中赋值再进行返回,我们能不能将指针传入,将需要改变的指针即它指向的地址在函数中改变以下呢?我们先看一种方法:
#include<stdio.h>
void getPpos(int pos, int (*pstu)[4], int *ppos) //第二形参为一个数组指针
{
ppos = *(pstu+pos);
}
int main()
{
int scores[3][4] = {
{11,22,33,44},
{55,66,77,88},
{34,54,57,87}
};
int *ppos;
int pos;
printf("请输入你想要查看学生成绩的号数:\n");
scanf("%d",&pos);
getPpos(pos, scores ,ppos);
for(int i = 0;i<4;i++){
printf("%d ",*ppos++);
}
return 0;
}
运行结果:
很明显,出现了段错误,为什么呢?因为这是因为在我们把指针变量传到函数中去的时候,函数会为函数中的变量在开辟一块内存空间,主函数中的指针变量和函数中的指针变量并不是同一个变量,而是两个独立的变量,函数中的指针指向了特定区域,而主函数中的指针是一个野指针。这就类似与我们之前讨论的为什么不能通过函数值传递去交换两个数,就是因为函数传参只是值拷贝,不是主函数自己的变量发生变化,想要真正改变主函数变量中的值,必须通过操纵变量地址的形式去更改其中的内容,这里由于我们的传进去的参数是指针,所以我们必须采用二级指针去指向指针的地址,然后再通过取一级指针的值去改变传进去的指针的指向。可以用这幅图去理解:
下面我们看代码:
#include<stdio.h>
void getPpos(int pos, int (*pstu)[4], int **ppos) //第二形参为一个数组指针
{
int *p;
*ppos = *(pstu+pos);
}
int main()
{
int scores[3][4] = {
{11,22,33,44},
{55,66,77,88},
{34,54,57,87}
};
int *ppos;
int pos;
printf("请输入你想要查看学生成绩的号数:\n");
scanf("%d",&pos);
getPpos(pos, scores ,&ppos);
for(int i = 0;i<4;i++){
printf("%d ",*ppos++);
}
return 0;
}
运行结果:
这样就实现了在函数中实现修改指针变量的指向。
三、二级指针和二维数组的避坑指南
关于二维数组,我们知道,它可以拆分成父数组和子数组,分别对应两层地址,而二级指针也包含两层地址,那么这两者可以简单的对应吗?答案是肯定没有那么简单,我们一起来验证一下:
#include<stdio.h>
int main()
{
int scores[3][4] = {
{11,22,33,44},
{55,66,77,88},
{34,54,57,87}
};
int **p;
p = scores;
printf("scores:%p\n",scores);
printf("p=%p\n",p);
printf("*p=%p\n",*p);//*p是一个野指针,不是我们认为的会变成列地址
printf("*scores=%p\n",*scores);
return 0;
}
运行结果:
不难发现,子数组的地址和二级指针的二级地址是不一样的,虽然说二级指针指向了二维数组,但是二级指针的二级地址并不是子数组的地址,二级指针的二级地址指向了不知道的区域,即 ∗ p *p ∗p 是一个野指针。
那么我们如何才能让两者对应上呢?我们可以这样:
#include<stdio.h>
int main()
{
int scores[3][4] = {
{11,22,33,44},
{55,66,77,88},
{34,54,57,87}
};
int (*p2)[4] = scores;
int **p = &p2;
**p = 100;
printf("%d\n",scores[0][0]);
return 0;
}
运行结果:
通过定义一个数组指针指向二维数组,这样就能将父数组和子数组分开,然后对父数组取
∗
*
∗ ,就能将子数组的地址取出来,这样就能用二级指针去访问二维数组。 当然,这样的用法在以后工作中并不会用到,这里将这个坑避开,避免大家不理解这个问题。