指针(上):指针的概念与定义、取地址与解引用、指针与数组、指针运算
指针,是C语言的灵魂。这话不是我说的,但做了十几年嵌入式开发,我越来越觉得这话一点不夸张。很多初学者觉得指针难,其实说白了,它就是一个存放地址的变量。你想想看,我们平时用变量存数字、存字符,指针呢?它存的是别人在内存里的“门牌号”。
我个人习惯把指针比作“遥控器”。你拿着遥控器,不用亲自走到电视机跟前,就能换台、调音量。指针也是这样,通过它,你可以间接操作内存中的数据。嗯,这个比喻虽然不完美,但用来理解指针的“间接性”,挺管用的。
1. 指针的概念与定义
指针变量,就是用来存放内存地址的变量。它的类型决定了它“看”的是什么类型的数据。
定义指针的语法很简单:
int *p; // p 是一个指向 int 类型数据的指针
char *cp; // cp 是一个指向 char 类型数据的指针
float *fp; // fp 是一个指向 float 类型数据的指针
这里的 * 是类型说明符,告诉编译器:我定义的是一个指针变量,不是普通变量。我在项目中遇到过不少新手,把 int* p, q; 误以为 p 和 q 都是指针。其实只有 p 是指针,q 是普通 int 变量。正确的写法是 int *p, *q;,或者干脆分开写,更清晰。
* 紧贴变量名写,比如 int *p;。这样读起来更自然——“int 类型的指针 p”。
2. 取地址与解引用
指针有两个核心操作:取地址(&)和解引用(*)。
- 取地址(&):获取变量在内存中的起始地址。
- 解引用(*):通过指针访问它指向的那个变量。
看个例子:
int a = 10;
int *p = &a; // p 保存了 a 的地址
printf("a 的值:%d\n", a); // 直接访问
printf("a 的地址:%p\n", &a); // 取地址
printf("p 的值:%p\n", p); // p 里存的就是 a 的地址
printf("通过 p 访问 a:%d\n", *p); // 解引用,得到 10
*p = 20; // 通过指针修改 a 的值
printf("修改后 a 的值:%d\n", a); // 输出 20
为什么会这样?因为 *p 就相当于 a 本身。你操作 *p,就是在操作 a。我曾经在调试一个串口驱动时,发现数据总是对不上,查了半天,原来是指针解引用时类型写错了,导致读出来的数据被截断。嗯,指针的类型匹配,真的不能马虎。
int *p; *p = 100;,程序跑起来就崩。指针一定要先赋值,再使用。
3. 指针与数组
数组名,本质上就是指向数组第一个元素的指针常量。这句话我当年学的时候也绕了半天,后来写代码多了,才真正理解。
int arr[5] = {1, 2, 3, 4, 5};
int *p = arr; // 等价于 int *p = &arr[0];
printf("arr[0] = %d\n", arr[0]); // 1
printf("*p = %d\n", *p); // 1
printf("*(p+1) = %d\n", *(p+1)); // 2
你看,arr 和 &arr[0] 是同一个地址。用指针访问数组元素,其实就是做指针运算。数组下标 arr[i],编译器会把它翻译成 *(arr + i)。所以,arr[i] 和 *(arr + i) 是完全等价的。
但有一点要记住:数组名是常量,不能自增自减。你不能写 arr++,但可以写 p++。因为 p 是指针变量,可以改变指向。
4. 指针运算
指针运算不是普通的算术运算,它是以“元素大小”为单位的。说白了,p + 1 不是地址加1,而是加 sizeof(类型) 个字节。
| 运算 | 含义 | 示例(int *p,假设 p=0x1000) |
|---|---|---|
| p + n | 指针向后移动 n 个元素 | p+1 → 0x1004(int 占4字节) |
| p - n | 指针向前移动 n 个元素 | p-1 → 0x0FFC |
| p1 - p2 | 两个指针之间相差的元素个数 | 若 p1=0x1008, p2=0x1000,差值为2 |
| ++p / p++ | 指针自增,指向下一个元素 | p++ 后 p=0x1004 |
| --p / p-- | 指针自减,指向上一个元素 | p-- 后 p=0x0FFC |
我在项目中用指针运算遍历数组时,特别喜欢用 p++ 这种写法,简洁高效。但要注意,不要越界。C语言不会检查指针是否越界,全靠程序员自己把控。
int arr[5] = {10, 20, 30, 40, 50};
int *p = arr;
for (int i = 0; i < 5; i++) {
printf("%d ", *p);
p++; // 指向下一个元素
}
// 输出:10 20 30 40 50
你想想看,如果循环条件写成 i < 10,程序不会报错,但会读到数组后面的垃圾数据。嗯,这种 bug 很难查,因为它不是每次都崩,而是偶尔出现奇怪的值。我曾经在通信协议解析里吃过这个亏,从那以后,遍历数组时我都会格外小心边界条件。
知识体系总览
下面这张图,把本章的核心知识点串起来了。你可以把它当作一个思维导图来看。
指针这部分内容,是C语言的基石。你把它搞透了,后面学动态内存、函数指针、链表这些,就会轻松很多。我个人觉得,学指针最好的方法就是多写、多调试。光看书是学不会的,得亲手敲代码,亲眼看看地址是怎么变的。
好了,这一章就到这里。记住:指针不可怕,可怕的是不初始化就乱用。下一章我们继续聊指针的更多玩法。
公众号:蓝海资料掘金营,微信 deep3321