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 类型变量的指针

这里的 * 只是告诉编译器:这个变量是指针,不是普通变量。它跟乘法运算符是两码事,别搞混了。

初始化指针有两种方式:

  1. 取地址初始化:用 & 运算符取一个变量的地址赋给指针。
  2. 赋值为 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 本章知识体系

下面这张图帮你理清指针的核心脉络:

指针 概念:地址 定义与初始化 运算 内存地址 vs 变量值 指针变量存地址 解引用 * 访问数据 类型 *变量名; 取地址 & 初始化 赋 NULL 避免野指针 & 和 * 互逆 加减:步长 = sizeof(类型) 关系运算:同数组内比较 指针与数组:arr[i] ≡ *(arr+i)

这张图把指针的核心知识点串起来了。从概念出发,到定义初始化,再到运算,最后落到指针与数组的关系。你顺着这个脉络学,思路会清晰很多。

指针这东西,刚开始觉得绕,等你用熟了,会发现它真的很强大。尤其是操作内存、处理字符串、实现动态数据结构时,指针几乎是不可替代的。

本章总结:

  • 指针就是地址,指针变量存地址。
  • 定义指针要初始化,避免野指针。
  • 指针加减运算的步长由指向的类型决定。
  • 数组名是常量指针,下标访问本质是指针运算。

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