C语言进阶:深入探讨指针(一)
我们知道,在C语言中,指针是用于存储内存地址的变量。从底层看,任何被定义的实体(如变量、函数、数组、字符串等)在程序运行时都会占用一块确定的内存空间,就像酒店房间一样,有唯一编号,这些内存块也通过唯一的地址来标识。编译器/运行时系统会为这些实体分配内存,而指针的作用就是记录这些地址,从而实现精准访问。
一、简单的指针
示例:
int a = 8;
// 定义一个整型指针并赋值
int *p = &a; // a 必须是一个整型变量// 定义一个浮点型指针
float a = 2.5f;
int *p = &a; // a是一个浮点型变量这类指针相对简单直观,容易理解。
假设:a是一个整型变量,那么指向它的指针类型就是 int *。同理:
- int * ———— 一般整型指针,指向int类型数据
- float * ———— 单精度浮点型指针,指向float类型数据
- char * ———— 字符类型指针,指向字符类型数据
- unsigned int * ———— 无符号整型类型指针,指向无符号整型数据
其指向结构示意图如下:

如上图所示,指针 p 存储了变量 a 的内存地址(假设为0x44ff0066)。由于 p 指向 a 所在的内存空间,我们可以通过解引用指针p(即 *p)来间接访问或修改变量 a 的值。例如:
int a = 10;
int *p = &a;
*p = 50;
二、多级指针
从指针的定义出发,我们可以进一步思考:指针本身也是一个变量,既然它能存储其他变量的内存地址,那么指针自身的内存地址是否也存在? 答案是肯定的——指针变量同样存储在内存中,因此它也有自己的内存地址。
既然指针变量也有地址,那么我们就可以定义另一个指针(即“指针的指针”)来存储这个地址。
例如:
int a = 10;
int *p = &a; // 一级指针
int **p1 = &p; // 二级指针
int ***p2 = &p1; // 三级指针
int ****p3 = &p2; // 四级指针
// 你可以无限套娃这样形成 p3 ——> p2 ——> p1 ——> p ——> a 的4级间接访问
三、数组中指针的移动
在 C 中,指针存储的是内存地址(通常为十六进制数值),而指针运算的本质是 地址的算术偏移。其核心规则是:指针 ± 整数 = 原地址 ± (整数 × 数据类型字节数)
3.1、运算的意义
- 合法运算:如 int* p加 1,实际地址会 向后偏移 4 字节(假设 int占 4 字节),指向下一个整数单元。
- 非法运算:类似 date + date(日期相加无意义),指针间的直接相加(如 p + q)也无意义,编译器可能报错
因此只有:同一数据类型且指向同一内存区域 的指针才能安全运算。
代码示例:
char arr[11] = {65, 66, 67, 68, 69, 70, 71, 72, 73, 74, 75};
// 变量arr 就是一个指针它存储的是首元素的地址,即65的地址char* p = arr; // p 指向数组首地址
p++; // 地址 +1 → 指向 arr[1]
p -= 2; // 地址 -2 → 指向 arr[-1](危险!越界访问)其示意内存图如下所示:

指针减去指针,它可以得到两个相同类型指针之间的距离(元素个数)。类似于日期减去日期可以的到两个日期之间相差的天数。例如:
int* q = &arr[3]; // 假设arr是一个整型数组
ptrdiff_t dist = q - p; // 结果为 2(非字节差)3.2、指针运算注意事项
void * 类型的指针不能直接进行运算,因为计算机不知到它指向的数据是什么类型,就无法精确得知需要偏移几个字节
需要避免越界情况。
例如:
int arr[3] = {1,2,3};
int *p = arr;
p = p - 1; // 可以计算,但是越界了上述代码就是一个错误示例,如果这时对 *p 进行解引用操作,是非常危险的。因为你不知到 p 的指向哪里,具体有以下三种情况:
- 未分配的内存
- 其他变量或数据(但你不应该随意访问)
- 操作系统保留区域(访问它可能会导致崩溃)
需要避免出现野指针(没有被正确初始化 或者 指向了无效内存区域 的指针)
四、指针数组
顾名思义,指针数组是一个用来存储内存地址的数组。
例如:
int a = 0; int b = 0;
int *parr[2] = {&a, &b}; // 指针数组的定义和初始化 最为典型的是它经常用来存储多个字符串:
#include <stdio.h>int main() {char *names[] = { // 指针数组,每个元素是 char *"Alice","Bob","Charlie"};int num = sizeof(names) / sizeof(names[0]); // 数组的长度 即 数组大小 / 每个元素的大小for (int i = 0; i < num; i++) {printf("Name %d: %s\n", i + 1, names[i]);}return 0;
}此外,通过它我们可以模拟二维数组:
int arr[5] = {1,2,3,4,5};
int arr1[5] = {2,4,6,8,10};
int *p_arr[2] = {arr, arr1}; // 达成模拟二维数组的效果