指针(上):指针的概念与定义、取地址与解引用、指针与数组、指针运算

指针,是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。我曾经在调试一个串口驱动时,发现数据总是对不上,查了半天,原来是指针解引用时类型写错了,导致读出来的数据被截断。嗯,指针的类型匹配,真的不能马虎。

注意: 解引用一个未初始化或非法的指针,会导致段错误(Segmentation Fault)。我曾经在项目里见过同事直接 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 很难查,因为它不是每次都崩,而是偶尔出现奇怪的值。我曾经在通信协议解析里吃过这个亏,从那以后,遍历数组时我都会格外小心边界条件。

避坑指南: 指针运算时,一定要确保指针指向的是同一块连续内存(比如同一个数组)。两个不同数组的指针相减,结果是未定义的,别这么干。

知识体系总览

下面这张图,把本章的核心知识点串起来了。你可以把它当作一个思维导图来看。

指针(上) 概念与定义 存放地址的变量 类型决定“视野” 取地址 & 与解引用 * & 获取门牌号 * 通过门牌号进屋 指针与数组 数组名 = 首元素地址 arr[i] ⇔ *(arr+i) 指针运算 以 sizeof(类型) 为单位 p+n、p-n、p1-p2、++/-- 核心:指针是地址的载体,运算以元素为单位

指针这部分内容,是C语言的基石。你把它搞透了,后面学动态内存、函数指针、链表这些,就会轻松很多。我个人觉得,学指针最好的方法就是多写、多调试。光看书是学不会的,得亲手敲代码,亲眼看看地址是怎么变的。

好了,这一章就到这里。记住:指针不可怕,可怕的是不初始化就乱用。下一章我们继续聊指针的更多玩法。


公众号:蓝海资料掘金营,微信 deep3321