C语言指针入门到理解:一篇文章系统梳理指针核心知识(1)
当我们初学C的时候,一看到指针就容易紧张。
其实指针难的地方,不在语法,而在于它把“变量”这层东西,进一步推进到了“内存”这一层。
这篇文章就带你从内存方面出发,一起梳理一下指针相关的核心知识点,包括一下内容:
- 指针到底是什么
&和*分别在做什么- 为什么指针要有类型
const修饰指针怎么区分- 什么是野指针,怎么规避
- 为什么函数想修改外部变量时要用指针
一、什么是指针
想理解指针,先要理解内存和地址。
程序运行时,变量都会放在内存中,而内存会被划分为一个个内存单元。每个内存单元都有编号,这个编号就是地址。CPU 正是通过地址去找到对应的数据。
所以从本质上说:
指针就是地址,指针变量就是专门用来存地址的变量。
你可以把内存理解成一栋宿舍楼:
- 每个房间就是一个内存单元
- 每个房间号就是地址
- 知道房间号,才能准确找到房间
在 C 语言里,这个“房间号”就对应指针。
二、取地址操作符&
定义一个变量时,本质上是在内存中申请空间,比如:
inta=10;如果我们想知道变量a在内存中的位置,就要使用取地址操作符&:
inta=10;printf("%p\n",&a);这里的&a表示“取出变量a的地址”。
需要注意的是,a可能占多个字节,但&a取到的是这块空间起始位置的地址,也就是较小地址那个字节的位置。
三、指针变量是什么
地址也是数据,既然是数据,就可以存起来。
存地址的变量,就是指针变量。
例如:
inta=10;int*pa=&a;这里pa就是一个指针变量,它里面存的是a的地址。指针变量本质上也是变量,只不过它存储的是地址。
这句代码可以拆开理解:
pa是变量名*表示pa是指针变量int表示pa指向的是一个int类型的对象
也就是说,int *pa的真实含义是:
pa是一个指向整型数据的指针。
四、解引用操作符*
有了地址之后,下一步就是:怎么通过地址访问对应的数据?
答案就是使用解引用操作符*。
inta=100;int*pa=&a;*pa=0;这里的*pa表示:根据pa中存放的地址,找到对应的那块内存空间。
因为pa里存的是a的地址,所以*pa实际上就是a本身。
因此*pa = 0;最终修改的就是a。
所以要分清:
pa是地址*pa是地址对应的内容
五、指针为什么要有类型
很多人会问:
既然指针里存的都是地址,那为什么还要区分int*、char*、double*?
原因很简单:
指针类型决定了编译器如何看待这块地址。
它主要影响两件事。
1. 决定解引用时访问几个字节
例如:
intn=0x11223344;int*pi=&n;char*pc=(char*)&n;*pi按int处理,通常一次访问 4 个字节*pc按char处理,一次只访问 1 个字节
这说明,指针类型决定了解引用权限。
2. 决定指针加减的步长
char*pc=(char*)&n;int*pi=&n;pc+1;// 向后移动1个字节pi+1;// 向后移动1个int大小所以:
char* + 1跳过 1 字节int* + 1通常跳过 4 字节
指针加 1,不是简单数值加 1,而是跨过一个对应类型的元素。
六、指针变量的大小
很多同学以为char*小一点,double*大一点,其实不是。(注意,这里是说的指针变脸本身的大小)
指针变量的大小,和它指向什么类型无关,只和平台有关。
一般来说:
- 32 位平台下,指针大小是 4 字节
- 64 位平台下,指针大小是 8 字节
所以在同一平台下,你如果运行下面的代码:
sizeof(char*)sizeof(int*)sizeof(double*)通常结果都是一样的。
七、void*指针
void*可以理解成“泛型指针”。
它的特点是:
- 可以接收任意类型的地址
- 常用于函数参数,增强通用性
- 但不能直接解引用
- 也不能直接进行指针加减运算
void*它适合在函数参数中接收不同类型的数据地址,从而实现一定程度的泛型效果,但本身缺少具体类型信息,因此不能直接参与解引用和常规指针运算。
例如:
void*p=&a;这样写可以,但不能直接写:
*p=10;p+1;真正使用前,需要先转换成具体类型。
比如说
void*p=&a;(int*)p=10;//强制类型转换为int* 类型的指针p+1;八、const修饰指针怎么理解
这是初学者特别容易混淆的内容。
1.const int *p
constint*p=&n;含义是:
p可以改指向- 但不能通过
p修改它指向的内容
也就是:
指向的内容不能改,指针本身能改。
2.int * const p
int*constp=&n;含义是:
p不能再指向别处- 但可以通过
p修改内容
也就是:
指针本身不能改,指向的内容能改。
3.const int * const p
两边都限制:
- 不能改指向
- 也不能通过它改内容
4. 一个简单记忆方法
看const限制谁:
const在*左边,限制的是“指向的内容”const在*右边,限制的是“指针本身”
九、指针运算
指针常见的运算主要有三种:
- 指针
+/-整数 - 指针 - 指针
- 指针关系运算
1. 指针+整数
常用于遍历数组:
intarr[5]={1,2,3,4,5};int*p=arr;for(inti=0;i<5;i++){printf("%d ",*(p+i));}这里p + i表示移动到第i个元素的位置。
2. 指针 - 指针
常用于计算两个位置之间相差多少个元素。
例如模拟strlen:
intmy_strlen(char*s){char*p=s;while(*p!='\0')p++;returnp-s;}这里p - s表示字符串长度。
3. 指针关系运算
例如:
while(p<arr+sz){printf("%d ",*p);p++;}通过比较指针位置来判断遍历是否结束。
十、什么是野指针
野指针是指针里最危险的问题之一。
野指针定义是:
野指针就是指针指向的位置不可知,或者是不正确、没有明确限制的位置。
简单理解就是:
这个指针已经不知道自己到底在指向哪里了,但你还试图通过它访问内存,那很容易造成各种各样的错误。
十一、野指针的常见成因
1. 指针未初始化
int*p;*p=20;p里是随机值,直接解引用非常危险。
2. 指针越界访问
intarr[10]={0};int*p=arr;for(inti=0;i<=11;i++){*(p++)=i;}一旦越过数组合法范围,p就成了野指针。
3. 返回局部变量地址
int*test(){intn=100;return&n;}函数结束后,局部变量生命周期结束,这个地址就失效了。
十二、如何规避野指针
1. 初始化指针
如果一开始不知道该指向谁,就先赋值为NULL:
int*p=NULL;NULL是一个值为 0 的标识符常量,表示空地址。
2. 不要越界访问
指针只能访问合法申请到的那块内存,不能超范围操作。
3. 不再使用时及时置空
p=NULL;这样做可以减少误用风险。
4. 使用前检查合法性
if(p!=NULL){// 再使用}这是非常常见的安全写法。
十三、assert断言的作用
在使用指针前,经常会先做合法性校验:
assert(p!=NULL);assert的作用是:如果条件为真,程序继续执行;如果条件为假,程序直接报错终止,并指出问题位置,便于调试。
例如:
intmy_strlen(constchar*str){assert(str);intcount=0;while(*str){count++;str++;}returncount;}这相当于在函数开头加了一道保护。
十四、指针最重要的应用:传址调用
为什么要学指针?
因为有些问题,单纯传值是解决不了的。
例如交换两个变量:
voidSwap1(intx,inty){inttmp=x;x=y;y=tmp;}这样写交换失败,因为这是传值调用,函数里改的是形参副本,不是原变量。
正确写法应该是传地址:
voidSwap2(int*px,int*py){inttmp=*px;*px=*py;*py=tmp;}调用时:
Swap2(&a,&b);这时传入的是a和b的地址,函数内部通过解引用直接修改原变量,所以交换成功。
这也是指针最核心的价值之一:
当函数需要修改主调函数中的变量时,就要用传址调用。
十五、总结
学指针,不能只背语法,主要是你得明白这样一条链路:
变量 -> 内存空间 -> 地址 -> 指针变量 -> 解引用访问
你把它理顺了,自然就会明白:
- 为什么要用
& - 为什么要有
* - 为什么指针类型很重要
- 为什么会有野指针
- 为什么传地址才能改外部变量
ps:各位读完文字后可以思考一下这几个问题哦,读者自证不难(其实答案我上面都算是写出来了)