前言
很多初学者学C语言都会卡在指针:分不清数组指针和指针数组、搞不懂 sizeof 和 strlen 、二维数组传参一头雾水、函数指针看不懂、笔试题一做全错。
指针本质只有一句话:指针 = 内存单元编号 = 地址。本文结合全套课件知识点,由浅入深逐层拆解,配套可运行代码+高频笔试题,看完彻底告别指针困惑。
一、指针底层基础:内存与地址
1.1内存编址原理
计算机内存被切分成一个个1字节内存单元,每个单元分配唯一编号,这个编号就是地址(也就是指针)。
32位系统:32根地址总线,地址占4字节;
64位系统:地址占8字节。所有指针变量大小只由系统位数决定,和指向类型无关。
1.2两大核心操作符
1.取地址操作符(&)
&a就可以取出地址,拿的是字节较小的那个地址。int*pa=&a(pa可以用来存放地址(指针),所以pa叫指针变量)。这个int*pa叫做整型指针变量。*是在说明指针变量,int是告诉我们pa指向的变量是int类型。pa类型是int*但是理解时int和*都有意义。
2.取地址操作符(间接访问操作符)
*pa:*——解引用操作符,通过pa里存的地址找到a
1.3指针类型的意义
1.指针的解引用一次访问几个字节:比如int*访问4个字节、char*访问1个字节、double*访问8个字节、short*访问2个字节。
2.
由上两图可知,p所存放的地址往后跳了四个字节。这是因为p的类型是int*,p认为自己指向的是int类型数据,pi+1就应该跳过1个int类型的数据也就是4个字节。由此可知,指针类型决定了向前或向后跳过的距离有多大。
3.void*指针(理解为无具体类型的指针)
不能直接进行解引用和+或-整数的操作。一般就是使用在函数参数的部分,用来接受不同类型数据的地址
二、指针进阶:const修饰指针、野指针与传址调用
2.1const修饰指针
(1)const修饰变量
比如const int n = 10 //常变量,是变量但是具有常属性(不变的属性)。那么const修饰指针时也有异曲同工之妙。
(2)const修饰指针
1)const放在*的左边 const int * p = &n---------------在这种情况下,不让改*p但是可以通过修改p=&n这种方式修改n的值。这种写法限制的是指针变量指向的内容。
2)const放在*的右边 int * const p = &n----------------在这种情况下,不让修改p=&n但是可以通过修改*p这种方式改变n的大小。这种写法限制的是p这个指针变量。
3)const放在*的左右两边:p和*p指向内容都不能修改了。
2.2野指针成因与规避
1. 野指针:它是指针但是指向的位置是不可知的。
2.成因:(1)指针未初始化
(2)指针越界访问:指针指向的范围超出arr(数组)的范围时,p就是野指针
(3)指针指向的空间释放。比如说当n是局部变量时,如果在主函数中再用p来保存n的地址这时p就是野指针。
3.规避:(1)指针初始化:如果明确知道指针指向哪里就直接赋值地址,如果不知道指针应该指向哪里,可以给指针赋值NULL。int *p = NULL
(2)小心指针越界
(3)避免返回局部变量地址
2.3传值调用和传址调用
1.传值:函数创建临时副本,无法修改外部实参;
2.传址:传递变量地址,函数内可直接修改外部变量;
交换数字实例:
三、数组与指针核心规则
3.1数组名两大例外
1.sizeof(数组名):数组名代表整个数组,计算数组总字节;
2.&数组名:取出整个数组的地址,类型为数组指针;
3。其余场景:数组名=数组首元素地址。
区分arr与&arr:int arr[10]={0}
arr + 1;//跳过1个int,+4字节
&arr + 1;//跳过整个数组,+40字节
3.2sizeof和strlen
| 特性 | sizeof | strlen |
| 本质 | C语言操作符 | string.h函数 |
| 计算内容 | 占用内存总字节 | \0前有效字节个数 |
| 参数 | 类型、变量、数组、指针均可 | 仅接收字符指针,必须有\0终止 |
| 风险 | 无越界问题 | 无\0会持续越界查找,返回随机值 |
3.3字符数组和字符串常量指针
char arr[] = "abcdef";//栈上数组,可修改元素
char* p = “abcdef”;//指向常量字符串,只读,“p”修改会崩溃,首字符a的地址放到了p里
常量字符串会存入只读数据段,多个同字面量指针指向同一块内存;字符数组每次开辟独立栈空间。
3.4指针数组和数组指针
1.指针数组:int*p[10]:数组,每个元素是int*指针;
2.数组指针:int(*p)[10]:指针,专门存放数组指针。
3.5二位数组与数组指针
二维数组的本质:存放一维数组的数组,数组名arr是第一行一维数组的地址,类型为int(*)[列数]。
访问元素:*(*(p+i)+j) == p[i][j]
四、二级指针、指针运算与经典笔试题
4.1二级指针(char**或int**)
存储一级指针变量的地址,两大用途:
(1)函数内修饰一级指针本身;
(2)接收指针数组数组名;
int a = 10;
int* p = &a;
int**pp = &p;
**pp = 100;//等价a = 100
*pp = NULL;//等价p = NULL
4.2三种指针运算
1.指针+或-整数:按指向类型步长偏移;
2.指针-指针:仅同一块连续内存有效,返回中间元素个数;
3.指针关系运算:< > == 比较地址高低,仅数组内合法。
4.3笔试例题解析(X86 32位,指针4字节)
例1
例2
五、函数指针、回调函数、qsort快速排序
5.1函数指针基础
函数名=函数地址,&函数名等价函数名;定义格式:返回类型(*指针名)(参数列表)
例如:int Add(int x, int y) { return x+y; }
int (*pf)(int, int) = Add;
printf("%d", pf(2,3)); // 等价(*pf)(2,3)
5.2回调函数定义
将函数指针作为参数传给另一个函数,由对方在内部调用,被调用的函数即为回调函数。
5.3qsort快速排序(回调函数核心应用)
函数原型:void qsort(void*base,//数组首地址
size_t num,//base指向的数组的元素个数
size_t size,//每个元素的大小,单位是字节
int(*compare)(const void*p1,const void* p2)//函数指针,指向的函数可以比较数组中的两个元素
);//p1指向的元素比p2指向的元素小,返回小于0的数字;一样大返回0;p1比p2大返回大于0的数字。
例子:测试qsort来排序整型数组
六、指针避坑总结
1. 数组传参退化为指针,函数内部无法用 sizeof 求数组长度;
2. strlen 参数必须带 \0 ,字符数组无结束符会越界;
3. 常量字符串 char* p = "xxx" 只读,禁止解引用修改;
4. 区分 int (*p)[N] 数组指针和 int* p[N] 指针数组;
5. 函数返回禁止返回局部栈变量地址;
6. void* 不能直接解引用、不能±整数;
7. &数组名 是整个数组地址,+1跳过全部数组元素;
8. 二级指针只能接收一级指针的地址,不能直接赋值普通变量地址。