前言
指针是 C 语言的核心特性,也是掌握 C 语言内存操作的关键。本文系统讲解指针的基础定义、类型、野指针、运算规则、与数组的关系、二级指针及指针数组,结合实战案例拆解核心逻辑,帮助读者规避常见错误。
一、指针是什么
(1)指针的定义:
在C语言中,指针(pointer)是存储“另一个变量的内存地址”的变量,同时也是一种数据类型。
简言之:
指针变量的值:是内存地址(一串十六进制数)
指针的核心作用:通过存储的地址间接访问 / 修改目标内存空间的数据。
举个例子:
把内存比作 “酒店房间”:
- 普通变量int a = 10:是 “房间号 001”,里面存放的 “客人” 是 10;
- 指针变量int *pa = &a:是 “房间号 002”,里面存放的不是数值,而是 “001”(目标房间的编号);
- 解引用*pa = 20:通过 “002 房间里的 001”,找到 001 房间并修改里面的客人为 20。
(2)图示
我们用一个简图来示例一下指针、变量。以32为操作系统为例:

#include <stdio.h>
int main()
{
int a = 10; // a占用4字节内存,存储值10
int *pa = &a; // &a获取a的首字节地址,赋值给指针变量pa
*pa = 20; // 解引用:通过pa的地址修改a的值
printf("a = %d\\n", a); // 输出20
return 0;
}
二、指针的类型
在同一个硬件平台中,所有指针的大小相同(32 位系统 4 字节,64 位系统 8 字节),但指针类型决定两个核心行为:
(1)指针类型决定了指针解引用的权限。指针解引用时,访问的内存字节数 = 指针指向类型的大小。
代码示例:
#include <stdio.h>
int main(void)
{
int a = 256; // 二进制:00000000 00000000 00000001 00000000
int* p_int = &a; // int*解引用访问4字节
char* p_char = (char*)&a; // 强制类型转换(避免编译警告)
printf("*p_int = %d\\n", *p_int); // 输出256(访问全部4字节)
printf("*p_char = %d\\n", *p_char); // 输出0(仅访问最低1字节)
return 0;
}
运行结果:
256
0
是因为char类型的指针再被解引用时,只能访问一个字节的空间,而256这个数字的存储空间已经超过了一个字节。
256 的最低 1 字节是00000000(十进制 0),因此char*解引用只能读到 0。
(2)指针的类型决定了指针的运算,即指针加减时的步长。
指针加减整数时,偏移的字节数 = 整数 × 指针指向类型的大小:
代码示例:
#include <stdio.h>
int main(void)
{
int arr[10] = { 0 };
int* p_int = arr; // int*类型指针,步长4字节
char* p_char = (char*)arr; // char*类型指针,步长1字节
printf("int型指针初始地址:%p\\n", p_int); // 示例:0x0061FE18
printf("char型指针初始地址:%p\\n", p_char); // 示例:0x0061FE18
printf("int型指针+1:%p\\n", p_int + 1); // 示例:0x0061FE1C(+4字节)
printf("char型指针+1:%p\\n", p_char + 1); // 示例:0x0061FE19(+1字节)
return 0;
}
运行结果:
int型指针初始地址: 010FFE18
char型指针初始地址: 010FFE18
int型指针+1: 010FFE1C
char型指针+1: 010FFE19
可以看出:
int类型的指针加1,地址向后移动了4个字节
char类型的指针加1,地址向后移动了1个字节
三、野指针
野指针是指向 “不可知 / 非法内存地址” 的指针(指向随机、已释放或越界的内存),直接操作会导致程序崩溃或数据异常。
(1)野指针的成因
1.指针定义时未初始化
2.指针访问范围越界
3.指针指向的内存空间被释放了,但是指针并没有被销毁或更新。
举例:
//1.指针定义时未初始化
int main()
{
int* p; //定义指针变量,但未初始化
*p = 10; //使用指针,这样就会产生野指针
return 0;
}
//2.指针访问范围越界
int main()
{
int arr[10] = { 0 };
int* p_arr = arr;
int i = 0;
for (i = 0; i <= 10; i++)
{
*p_arr = i;
p_arr++; //当第十一次进入循环时,指针访问数组就会发生越界,这时就会产生野指针
}
return 0;
}
//3.指针指向的内存空间被释放了,但是指针并没有被销毁或更新。
int* test()
{
int a = 10;
return &a; //函数将自己内部的一个局部变量的地址作为返回值,当这个函数执行完毕后,局部变量会被销毁,但是指针的值却被传递了出去,因此形成了野指针
}
int main()
{
int* p = test();//这里的p就是一个野指针,因为test函数的局部变量a已经被销毁了
*p = 20;
return 0;
}
(2)如何避免野指针
| 原则 | 具体做法 | 示例 |
| 初始化 | 未知指向时置NULL,已知指向时直接赋值 | int* p = NULL; / int* p = &a; |
| 防越界 | 手动检查数组 / 内存访问范围,避免超出边界 | 循环条件用i < 10而非i <= 10 |
| 及时置空 | 内存释放后立即将指针置NULL | free(ptr); ptr = NULL; |
| 有效性检查 | 使用指针前判断是否为NULL | if (p != NULL) { *p = 10; } |
四、指针的运算
指针的运算包括:
1.指针加减一个整数n的运算
指针加减一个整数的运算规则:指针向前或向后移动n个指针类型长度的距离
2.指针减去指针运算
-
- 两个指针相减的本质是
- 计算两个指针之间存在的数组元素的个数
- 相减的时候,首先要保证两个指针的类型相同
- 另外两个指针应该指向同一块空间,这样才有意义
3.指针的关系运算
指针的关系运算,是对指向同一段连续内存区域、且类型相同的指针,通过关系操作符(/<=/>=/==/!=)比较指针存储的内存地址大小,从而判断它们指向的元素在内存中的相对位置(如 “前 / 后”“是否指向同一元素”);其中 /<=/>= 可直接判断 “两个指针之间是否还有元素”,==/!= 仅用于判断 “两个指针是否指向同一个元素”。
代码示例
//1.指针加减一个整数的运算
int main()
{
int arr[10] = { 1,2,3,4,5,6,7,8,9,10 };
int* parr = arr; //将数组的首地址赋值给指针parr
printf("%d\\n", *(parr + 2)); //指针加上2,代表访问数组下标为2的元素
return 0;
}
//运行结果:3
//2.指针减去指针运算
int main()
{
int arr[10] = { 0 };
int* start = &arr[0]; //将数组的第一个元素的地址传递给指针start
int* end = &arr[9]; //将数组的最后一个元素的地址传递给指针end
printf("%d\\n", end – start);
return 0;
}
//运行结果:9
//3.指针的关系运算
int main()
{
int arr[10] = { 1,2,3,4,5,6,7,8,9,10 };
int* start = &arr[0]; //将数组的第一个元素的地址传递给指针start
int* end = &arr[9]; //将数组的最后一个元素的地址传递给指针end
while (start <= end) //两个指针比较大小
{
printf("%d\\n", *start);
start++;
}
return 0;
}
运行结果: 1 2 3 4 5 6 7 8 9 10
注意:
在C语言标准中规定:
允许指向数组元素的指针与指向数组最后一个元素后边的那个内存位置的指针比较,但是不允许与指向第一个元素之前的那个内存位置的指针进行比较。
因此下面的代码可能会在某些编译器内无法正常运行:
int arr[5]
for(parr = &arr[4]; parr >= &arr[0]; parr–)
{
*parr = 0;
}
但是反过来就可以:
int arr[5]
for(parr = &arr[0]; parr <= &arr[4]; parr++)
{
*parr = 0;
}
五、指针和数组
在前面,我们已经介绍过一维数组数组名的含义:
在以下两种情况:
sizeof(单独出现数组名)
&单独出现数组名
代表整个数组
其余所有情况,都代表了数组首元素的地址
基于上边的知识,我们在访问数组的时候,可以使用下标访问,也可以使用指针访问
示例:
int arr[10] = {1,2,3,4,5,6,7,8,9,10};
int* p = arr;
for(i = 0; i < 10; i++)
{
printf("%p %p\\n", &arr[i], p + i);
}
//以上代码打印出的两个地址是相同的
//首元素+i就是下标为i这个元素的地址
//因此利用指针访问数组的元素的一种方法:
//*(p + i)
扩展:大家觉得下面的代码中的 printf("%d\\n", 2[arr]);语法有问题吗?如果没问题,会输出什么结果呢?
int main()
{
int arr[10] = { 1,2,3,4,5,6,7,8,9,10};
int* p = arr;
printf("%d\\n", 2[arr]);
printf("%d\\n", arr[2]);
return 0;
}
答案是代码语法没问题
运行结果:
3
3
原因是:
C 语言标准明确规定:a[b] 等价于 *(a + b)—— 这是 [] 操作符的核心定义,而非 “语法糖”。
arr[2] → 展开为 *(arr + 2):
arr + 2:指针向后偏移 2 个int长度(找到arr[2]的地址);
*:解引用该地址,获取对应值(3)。
2[arr] → 展开为 *(2 + arr):
加法满足交换律,2 + arr 和 arr + 2 结果完全相同,因此解引用后的值也相同。
[] 是指针加法的语法糖,arr[2] 和 2[arr] 底层都是*(arr+2),但后者是‘反人类’写法,只需要理解原理,绝对不要在实际代码中使用。
六、二级指针
指针也是一种变量,也是需要存在内存中的,因此也会有地址,也可以有指针指向它。

int a = 10;
int* pa = &a; //pa是个指针变量,一级指针
int** ppa = &pa; //pa也是一个变量,&pa去除pa在内存中的地址。
//此时的ppa就是一个二级指针变量
//*ppa == pa
//*pa == a
//因此**ppa == a
//当然,也支持三级指针:int***pppa,不过应用很少
七、指针数组
指针数组还是一个数组,数组中存储的是指针变量。
int arr[10];//整型数组 – 存放的是整型的数据的数组
char ch[5];//字符数组 – 存放字符
int* pa[10];//存放整型指针的数组 – 整型指针数组
char* pch[5];//存放字符指针的数组 – 字符指针数组




