本文是《C语言期末速成》系列第 8 篇。如果说 C 语言有什么”灵魂”,那就是指针。普通变量放数值,指针变量放地址——就这么一句话,但延伸出 *pp、二级指针、指针数组、函数指针等一堆考点。本章从一个比喻出发,把这些概念一次打通。


上一篇C语言期末速成 7:数组

考点清单

  • 指针变量的本质是放地址,普通变量放数值
  • int *pp 是地址,*p 是 p 指向的那个变量(数值)
  • *p++地址移动(*p)++数值自增(口诀:取当前值再操作)
  • 二级指针 **q:存放一级指针的地址
  • 三名主义:数组名 = 首元素地址、函数名 = 函数入口地址、字符串常量名 = 首字符地址
  • 传值与传地址:传地址能改实参
  • 函数返回地址时,返回类型要写 *int *fun(...)
  • 指向哪个就等价哪个*p 出现的地方可以用它指向的变量等价替换
  • 指针数组:int *p[3](是数组);数组指针:int (*p)[3](是指针)

一、指针是什么:放地址的变量

内存是编好号的”储物柜”。普通变量 int a = 5 把 5 存进某个柜子;指针变量 int *p = &a 则是把”那个柜子的编号”存进 p。

int a = 5;
int *p = &a;   /* p 里放的是 a 的地址 */

printf("%d\n", a);   /* 5:直接读变量 */
printf("%p\n", p);   /* a 的地址 */
printf("%d\n", *p);  /* 5:顺着地址取 a 的数值 */

两个符号的分工:

写法含义
p地址(指向哪里)
*p数值(那里存的值),可以当变量用

*p 可以当变量用:*p = 20; 就是在修改 p 指向的变量,即 a = 20

判断口诀:* 表示”顺着地址取里面的值”。

指针的两种初始化方式

/* 方法一:定义时同时初始化 */
int a = 2;
int *p = &a;

/* 方法二:定义后再初始化 */
int b = 2;
int *q;
q = &b;

NULL 指针:野指针与悬空指针(加深)

指针有三个容易混的状态:

状态含义正确做法
NULL 指针指向地址 0,表示”不指向任何东西”初始化或清理时用 int *p = NULL;
野指针定义了却没初始化,里面是垃圾地址定义即初始化,别让它裸奔
悬空指针指向的内存已被释放(如 free 后)free(p) 后立刻 p = NULL;

判断指针是否为空,直接拿指针本身当条件用:

int *p = NULL;
if (p)      /* 非空才进来:p != NULL 才为真 */
    *p = 1;
if (!p)     /* 空才进来:p == NULL 才为真 */
    printf("p 还没有指向任何东西\n");

不要解引用 NULL 或野指针*p 访问的是”地址 0 处的内存”,程序会直接崩溃(空指针解引用是未定义行为)。这也是 malloc 后要检查返回值是否为 NULL 的原因(第 10 章)。

scanf 里的指针考点

第 4 章预告过:scanf 需要地址。

int x = 2, *p = &x;
scanf("%d", x);    /* 错:x 是数值 */
scanf("%d", p);    /* 对:p 就是 x 的地址 */
scanf("%d", &p);   /* 错:这是指针变量自己的地址 */
scanf("%d", *p);   /* 错:*p 是数值 */

凡是 scanf 第二个参数,都必须是”地址”。


二、指针的算术运算

2.1 *p++ vs (*p)++(必考)

这是改错题和概念题的高频点,关键看 ++ 作用在谁身上

  • *p++++ 作用在 p 上 → 地址移动(先取当前值,再移动指针);
  • (*p)++++ 作用在 (*p) 上 → 数值自增(指针不动,指向的值加 1)。
int a[] = {1, 3, 5, 7, 9};
int *p = a;

/* *p++:表达式值为 1(当前 *p);随后 p 移到 a[1] */
int v1 = *p++;          /* v1 = 1,现在 p 指向 3 */

/* (*p)++:表达式值为 3(当前 *p);随后把 a[1] 变成 4 */
int v2 = (*p)++;        /* v2 = 3,a[1] 变成 4,p 还在原地 */

口诀*p++ 取完值地址后移;(*p)++ 取完值数值加 1。区分方法:看括号,括号让 ++ 先跟 *p(数值)结合,没有括号则 ++p(地址)结合。

2.2 指针加减整数

指针加 k = 地址前进 k 个”所指向类型的大小”:

int a[5] = {10, 20, 30, 40, 50};
int *p = a;
p = p + 2;        /* p 指向 a[2] */
printf("%d\n", *p);  /* 30 */

p 每加 1 跳过一个 int(4 字节)。指针算术的意义在数组上最明显——用指针遍历数组

int a[5] = {10, 20, 30, 40, 50};
for (int *p = a; p < a + 5; p++)
    printf("%d ", *p);   /* 10 20 30 40 50 */

三、二级指针:存”指针的地址”

一级指针 *p 存变量的地址;二级指针 **q一级指针的地址

int x = 7;
int *p = &x;      /* p 存 x 的地址 */
int **q = &p;     /* q 存 p 的地址 */

printf("%d\n", *p);   /* 7:p 指向 x */
printf("%p\n", *q);   /* x 的地址:*q 就是 p */
printf("%d\n", **q);  /* 7:**q 就是 *p 就是 x */

硬规则:二级指针只能存放一级指针的地址。

int x = 7;
int *p = &x;
int **q = &p;     /* 对 */
int **r = &x;     /* 错!&x 是一级指针的地址,类型不匹配 */

为什么要二级指针?最常见场景是”在函数里修改调用者传入的指针本身”(比如改变指针的指向),一级指针参数改不了指针变量的值,得用二级指针。


四、三名主义:三个”首地址”

记住三个”名”都代表地址,且都不能自增赋值

名字含义等价
数组名 a首元素地址&a[0]
函数名 add函数入口地址&add
字符串常量名 "abc"首字符地址&"abc"[0]
char *s = "hello";   /* "hello" 的首字符地址放进 s */

数组名、函数名、字符串常量名都是地址常量——a++add++ 之类的写法非法。


五、移动指针遍历字符串(加深)

用指针遍历一串字符,遇到 \0 停止:

char *s = "meikanshu";
while (*s) {              /* *s 非 0(没到 '\0')就继续 */
    printf("%c", *s);
    s++;                  /* 地址移动 */
}
/* 打印 meikanshu */

while (*s) 的妙处:字符串以 \0(ASCII 0)结尾,*s 恰好用”值是否为 0”充当循环条件。这是”指针 + 字符指针 + 循环”的经典组合,第 9 章还会用它手写字符串函数。


六、传值 vs 传地址(指针版再复习)

第 6 章已学过,这里从指针视角重申——要不要加 &* 取决于你想不想改实参

void no_change(int a, int b) { int t = a; a = b; b = t; }      /* 传值,没用 */
void do_change(int *a, int *b) { int t = *a; *a = *b; *b = t; } /* 传地址,有效 */

int x = 1, y = 3;
no_change(x, y);       /* 1 3 */
do_change(&x, &y);     /* 3 1 */

“指向哪个就等价哪个”*a 指向 x,凡是 *a 出现的地方,都可以看作 x 本身。所以 *a = *b 就是 x = y

int a = 2, *p = &a;
*p = *p + 2;    /* 等价于 a = a + 2 → a = 4 */

七、函数返回地址:返回值带 *

函数返回值可以是地址,前提是返回类型写成 指针

int *getMax(int *a, int *b) {   /* 返回 int*,即返回地址 */
    return *a > *b ? a : b;
}

int main(void) {
    int x = 7, y = 8;
    int *max = getMax(&x, &y);  /* max 接收返回的地址 */
    printf("%d\n", *max);       /* 8 */
    return 0;
}

注意:函数返回的是”某个变量的地址”,因此该变量的生命周期要能覆盖使用它的时刻——不要返回局部变量的地址(函数返回后局部变量空间就销毁了)。返回全局变量、static 变量、或外部传入的地址才安全。


八、指针数组 vs 数组指针(易混淆,必考)

区别在谁先跟谁结合,看括号和类型的位置:

写法本质含义
int *p[3]数组(数组,里面存 3 个 int*指针数组
int (*p)[3]指针(指向含 3 个 int 的数组)数组指针
int *p[3] vs int (*p)[3]括号改变结合方向——

记忆*p[3][] 优先级比 * 高,所以 p[3] 先结合,p 是数组;加了括号 (*p)[3]*p 先结合,p 是指针。

int a[3] = {1, 2, 3};
int *arrp[3];        /* 指针数组:三个指针元素 */
int (*ap)[3] = &a;   /* 数组指针:指向 int[3] 这种类型的指针 */

arrp[0] = &a[0];     /* 指针数组里放地址 */
printf("%d\n", (*ap)[1]);  /* 2:*ap 得到 a,再取下标 */

二维数组里的行指针 a(第 7 章)本质就是指向一行的数组指针a + 1 跳一行,正好和 int (*p)[3]p + 1 跳 3 个 int 对应。


九、函数指针:把函数当变量存

函数名是函数入口地址,可以存进”函数指针”,然后通过它调用函数:

int add(int x, int y) { return x + y; }

int main(void) {
    int (*f)(int, int);   /* f 是函数指针:指向"返回 int、两个 int 参数"的函数 */
    f = add;              /* 函数名 add 就是地址,直接赋值 */

    printf("%d\n", add(2, 3));    /* 方式一:直接调 */
    printf("%d\n", f(2, 3));      /* 方式二:通过指针调 */
    printf("%d\n", (*f)(2, 3));   /* 方式三:解引用后再调(等价) */
    return 0;
}

三种调用方式等价add(2,3)f(2,3)(*f)(2,3)。函数指针最常见的用途是把函数作为参数传给另一个函数(回调),例如排序时传入比较函数。

声明语法拆解:int (*f)(int,int)(*f) 表示 f 是指针,后面 (int,int) 表示它指向的函数带两个 int 参数,最前 int 是函数返回类型。


易错点 / 陷阱

  1. *p++(*p)++ 混用:前者地址移动、后者数值自增。
  2. int *p[3] 当数组指针[] 优先,它是指针数组(数组);int (*p)[3] 才是数组指针。
  3. 二级指针存一级变量的地址int **q = &x 非法,必须是 int **q = &p(p 是一级指针)。
  4. 对数组名 a++:a 是地址常量,不能自增。
  5. 返回局部变量的地址:函数返回后局部变量销毁,返回它的地址是悬空指针。
  6. scanf("%d", *p) 当正确:*p 是数值不是地址。
  7. *(p+2) 算成”第 2 个元素”p+2 指向下标 2,即第 3 个元素。
  8. 函数指针写成 int *f(...):那是”返回指针的函数”,不是函数指针;函数指针要 int (*f)(...)

自测小练习

第 1 题int x=10, *p=&x;*pp&x 之间的关系。

第 2 题int a[]={1,3,5,7}; int *p=a; 执行 int v = *p++; 后,v 是多少?p 指向哪?

第 3 题int x=7; int *p=&x; int **q=&p;*p*q**q

第 4 题:解释 int *p[3]int (*p)[3] 的区别。

第 5 题int add(int,int); 写出它的函数指针声明,并列出三种等价调用。

参考答案
  1. p 里存的就是 &x,所以 p == &x*p == x == 10
  2. *p++ 先取当前值 1 给 v,再移动指针 → v=1p 指向 a[1](3)。
  3. *p = 7(x 的值);*q = p(一级指针的值,即 x 的地址);**q = 7
  4. int *p[3] 是数组(3 个 int 指针);int (*p)[3] 是指针(指向含 3 个 int 的数组)。
  5. 声明 int (*f)(int, int);,赋值 f = add;,三种调用 add(2,3)f(2,3)(*f)(2,3)

下一篇C语言期末速成 9:字符串

评论区