12. 指针(一):指针的概念、指针变量的定义与初始化、指针的运算、指针与数组
指针,是C语言的灵魂。很多初学者觉得指针难,其实说白了,指针就是一个地址。你想想看,我们平时用变量存数据,那这个变量本身住在内存的哪个房间?指针就是那个房间的门牌号。
我个人习惯把指针比作“遥控器”。你不需要把电视机搬过来,按一下遥控器,电视就换台了。指针也是这样,它不直接存数据,它告诉你数据在哪,然后通过它去操作数据。
核心一句话:指针就是地址,指针变量就是存地址的变量。
12.1 指针的概念
内存是一排连续的存储单元,每个单元都有一个唯一的编号,这个编号就是地址。我们定义一个变量 int a = 10;,编译器会给 a 分配一个内存地址(比如 0x1000),a 的值 10 就存在这个地址里。
指针变量存的就是这种地址。比如 int *p = &a;,p 里面存的是 0x1000,而不是 10。通过 p 找到 a 的过程,叫“解引用”,用 *p 来访问 a 的值。
我在项目中遇到过一个问题:有个同事把指针和普通变量搞混了,直接拿指针做算术运算,结果把内存写坏了。嗯,这里要记住:指针存的是地址,不是普通数值。
避坑指南:我曾经在嵌入式开发中,因为没初始化指针就直接解引用,导致程序跑飞。指针必须指向一个合法的内存地址,否则就是野指针,非常危险。
12.2 指针变量的定义与初始化
定义指针变量的语法很简单:
类型名 *指针变量名;
比如:
int *p; // p 是一个指向 int 类型变量的指针
char *cp; // cp 是一个指向 char 类型变量的指针
double *dp; // dp 是一个指向 double 类型变量的指针
这里的 * 只是告诉编译器:这个变量是指针,不是普通变量。它跟乘法运算符是两码事,别搞混了。
初始化指针有两种方式:
- 取地址初始化:用 & 运算符取一个变量的地址赋给指针。
- 赋值为 NULL:表示指针不指向任何地方。
int a = 100;
int *p1 = &a; // p1 指向 a
int *p2 = NULL; // p2 不指向任何地方,安全
小技巧:我建议每次定义指针时都初始化,要么给一个合法地址,要么给 NULL。这样能避免很多莫名其妙的 bug。
为什么一定要初始化?因为局部变量如果不初始化,它的值是随机的。一个随机地址指向的内存,你根本不知道里面存了什么。解引用这种指针,轻则数据错乱,重则程序崩溃。
12.3 指针的运算
指针可以做的运算不多,但每个都很关键。主要有三种:
12.3.1 取地址运算 & 和解引用运算 *
& 取变量的地址,* 通过地址访问变量。这两个是互逆操作。
int a = 10;
int *p = &a; // & 取 a 的地址
printf("%d\n", *p); // * 解引用,输出 10
*p = 20; // 通过指针修改 a 的值
printf("%d\n", a); // 输出 20
12.3.2 指针的算术运算
指针可以加、减一个整数。但注意:指针加1,不是地址加1,而是加一个“单位”。这个单位的大小就是指针指向的类型的大小。
int arr[3] = {1, 2, 3};
int *p = arr; // p 指向 arr[0]
printf("%p\n", p); // 比如 0x1000
p++; // p 指向 arr[1],地址变成 0x1004(int 占4字节)
printf("%p\n", p); // 0x1004
为什么会这样?因为指针的类型决定了它“一步跨多远”。int * 一步跨4字节,char * 一步跨1字节,double * 一步跨8字节。
重要:指针的加减运算,实际地址变化 = 加减的数值 × sizeof(指向的类型)。
指针还可以相减,得到两个指针之间相差的元素个数。比如:
int arr[5];
int *p1 = &arr[0];
int *p2 = &arr[3];
int diff = p2 - p1; // diff = 3,表示相差3个元素
我在项目中用指针相减来算数组长度,比用 sizeof 更灵活,尤其是函数传参时数组退化成指针,sizeof 就失效了。
12.3.3 指针的关系运算
两个指向同一数组的指针,可以用 >、<、== 等比较。比如判断 p 是否指向数组的某个位置之前或之后。
int arr[5];
int *p = &arr[2];
if (p > arr) {
printf("p 指向 arr[0] 之后\n");
}
注意:指向不同数组的指针做关系运算,结果是未定义的。别这么干。
12.4 指针与数组
数组和指针的关系非常密切。数组名在大多数情况下会被当作指向数组首元素的指针。
int arr[3] = {10, 20, 30};
int *p = arr; // 等价于 int *p = &arr[0];
printf("%d\n", *p); // 输出 10
printf("%d\n", *(p+1)); // 输出 20
printf("%d\n", *(p+2)); // 输出 30
你看,用指针访问数组元素,跟用下标访问完全等价:arr[i] 等价于 *(arr + i)。编译器内部就是这么干的。
但有一个关键区别:数组名是常量指针,不能修改。你不能写 arr++,但可以写 p++。
int arr[3];
// arr++; // 错误!数组名不是左值
int *p = arr;
p++; // 正确,p 指向 arr[1]
我记得有一次调试代码,发现数组越界访问,就是因为有人把数组名当指针用了,还做了自增操作。嗯,编译器直接报错,省了不少事。
用指针遍历数组,效率往往比下标高一点点(虽然现代编译器优化后差别不大),但代码更灵活。比如:
int arr[5] = {1, 2, 3, 4, 5};
int *p;
for (p = arr; p < arr + 5; p++) {
printf("%d ", *p);
}
// 输出:1 2 3 4 5
这里 p < arr + 5 是判断指针是否越界,p++ 让指针一步步往后挪。这种写法在嵌入式编程中很常见,因为操作硬件寄存器时经常要用指针直接访问内存地址。
个人经验:我建议初学者先用下标方式写数组操作,等熟悉了再改用指针。指针写起来很爽,但调试起来也容易晕。尤其是多级指针,画个内存图会清晰很多。
12.5 本章知识体系
下面这张图帮你理清指针的核心脉络:
这张图把指针的核心知识点串起来了。从概念出发,到定义初始化,再到运算,最后落到指针与数组的关系。你顺着这个脉络学,思路会清晰很多。
指针这东西,刚开始觉得绕,等你用熟了,会发现它真的很强大。尤其是操作内存、处理字符串、实现动态数据结构时,指针几乎是不可替代的。
本章总结:
- 指针就是地址,指针变量存地址。
- 定义指针要初始化,避免野指针。
- 指针加减运算的步长由指向的类型决定。
- 数组名是常量指针,下标访问本质是指针运算。
公众号:蓝海资料掘金营,微信deep3321