跳转至

导学案7 · 指针:地址这门手艺

前置要求:导学案6(会写函数,理解"值传递"改不到外面的变量) 参考课程:浙江大学 翁恺《C 语言程序设计》第 9 章(指针) 预计用时:1 ~ 1.5 小时(按需自取) 本节目标:理解指针与地址的对应关系;会用取地址 & 和解引用 *;会用指针让函数改到外面的变量;知道数组名本质是指针

先说清楚我们对指针的要求。 指针是 C 语言里名声最吓人的部分,但我们对它的要求不高:理解概念、会用即可。 本节不讲二级指针、多级指针,不讲 const 与指针的各种组合,也不讲复杂的指针运算——那些东西很容易把初学者绕晕。 你只需要带走三样东西:指针存的是地址、*p 能顺着地址摸到那个变量、把地址传给函数就能改到原件。


7.0 序

导学案2里我们说过一句话:变量 = 内存里一小片有名字的空间,地址是它的门牌号。 那时 &a 只是个"照做即可"的符号。这一节我们把它变成一件趁手的工具。

为什么绕不开它?因为单片机上的硬件,本质上就是一排排固定地址的格子(寄存器)。想让灯亮、让电机转,就是往某个地址写一个值。指针,就是 C 语言里"按地址操作内存"的唯一手段。

本节路线:地址与指针 → 指针在函数里怎么用 → 数组名本质是指针 → 练习


7.1 地址、指针、解引用

三个动作:

写法 名字 意思
&a 取地址 问出变量 a 住在几号门牌
int *p = &a; 定义指针 造一个专门存地址的变量 p,把 a 的门牌抄进去
*p 解引用 顺着 p 里存的门牌找过去,访问那个变量本身

int *p 表示 p 是一个指针,它指向一个 int。

#include <stdio.h>

int main(void)
{
    int a = 10;
    int *p = &a;             /* p 里存的是 a 的地址 */

    printf("a 的值      = %d\n", a);
    printf("a 的地址    = %p\n", (void *)&a);
    printf("p 里存的    = %p\n", (void *)p);
    printf("*p(解引用)= %d\n", *p);

    *p = 20;                 /* 顺着地址找到那个盒子,把值改掉 */
    printf("改完之后 a  = %d\n", a);

    return 0;
}

输出(地址每次运行、每台机器都不同,下面是其中一次):

a 的值      = 10
a 的地址    = 00000075679FFC44
p 里存的    = 00000075679FFC44
*p(解引用)= 10
改完之后 a  = 20

几点说明:

  • %p 是打印地址的占位符。输出格式因编译器而异(有的打 0x0061FE1C,有的打 0061FE1C),认得出"这是一串十六进制的门牌号"就够了,不要纠结长相。
  • 最后一行是关键:我们没有碰 a 这个名字,只通过 *p 就把 a 改成了 20。*p 和 a 是同一个盒子的两种叫法。
  • int *p 里的 * 只在定义时表示"这是个指针";定义之后,*p 才是解引用。同一个符号两个场合两种意思,和数组的 [5] 一样。

野指针警告:

/* 反面示例 */
int *p;          /* 没初始化,里面是个垃圾地址 */
*p = 5;          /* 往一个不知道是谁家的门牌里塞东西 */

这可能当场崩溃,也可能悄悄改掉别的变量。规矩只有一条:定义指针时就给它一个地址;暂时没有,就写 int *p = NULL;(NULL 表示"不指向任何东西")。


7.2 指针在函数里怎么用

7.2.1 终于能改到外面的变量了

回收导学案6 6.4 那个失败的 swap。这次传地址:

#include <stdio.h>

void swap(int *pa, int *pb)      /* 形参是两个指针:收到的是地址 */
{
    int t = *pa;
    *pa = *pb;
    *pb = t;
}

int main(void)
{
    int x = 1, y = 2;
    swap(&x, &y);                /* 把 x、y 的地址传进去 */
    printf("x = %d, y = %d\n", x, y);
    return 0;
}

输出:

x = 2, y = 1

这次真的换过来了。道理一句话:传值只能改副本,传地址就能改原件。 导学案2练习 5 的"三杯水",到这里才算真正成功。

7.2.2 一个函数"带回"多个值

return 一次只能带回一个值。想带回两个怎么办?用指针把结果"写"到调用者准备好的盒子里:

#include <stdio.h>

void minMax(int a, int b, int *pmin, int *pmax)
{
    if (a < b) {
        *pmin = a;
        *pmax = b;
    } else {
        *pmin = b;
        *pmax = a;
    }
}

int main(void)
{
    int lo = 0, hi = 0;

    minMax(7, 3, &lo, &hi);
    printf("小的是 %d,大的是 %d\n", lo, hi);

    return 0;
}

输出:

小的是 3,大的是 7

这个模式你在飞控代码里会反复见到:LZY_Scheduler(..., &ModeChangedFlag) 就是把变量的地址交出去,让别的模块往里写结果。

7.2.3 顺便解开一个旧谜

导学案2让你死记"scanf 里每个变量前面都要写 &"。现在你懂了:

scanf("%d", &a) 就是把 a 的地址交给 scanf,让它顺着地址把读到的值放进 a。 如果不给地址,scanf 就不知道往哪放。


7.3 数组名本质是指针

这是本节第二个必须带走的结论:

数组名就是"第一个元素的地址"。 即 a 等价于 &a[0]。 因此 a[i] 等价于 *(a + i):从首地址往后挪 i 个格子,再取里面的值。

#include <stdio.h>

int main(void)
{
    int a[5] = {10, 20, 30, 40, 50};
    int *p = a;                  /* 数组名直接赋给指针,不用写 & */

    printf("a[0] = %d, *p = %d\n", a[0], *p);
    printf("a[2] = %d, *(p + 2) = %d, p[2] = %d\n", a[2], *(p + 2), p[2]);
    printf("a == &a[0] 吗? %d\n", a == &a[0]);

    return 0;
}

输出:

a[0] = 10, *p = 10
a[2] = 30, *(p + 2) = 30, p[2] = 30
a == &a[0] 吗? 1

这个结论顺手解释了两件旧事:

  1. 为什么 scanf("%s", name) 不写 &:name 本身就是地址,再写 & 反而不对。
  2. 为什么数组传给函数不用复制整个数组:传一个地址就够了,又快又省。

7.3.1 数组作函数参数

#include <stdio.h>

void printArr(int a[], int len)        /* 也可以写成 int *a,两种写法完全等价 */
{
    for (int i = 0; i < len; i++) {
        printf("%d ", a[i]);
    }
    printf("\n");
}

void doubleAll(int a[], int len)
{
    for (int i = 0; i < len; i++) {
        a[i] *= 2;                     /* 传进来的是地址,所以能改到原数组 */
    }
}

int main(void)
{
    int score[5] = {90, 85, 77, 68, 99};
    int len = (int)(sizeof(score) / sizeof(score[0]));

    printArr(score, len);
    doubleAll(score, len);
    printArr(score, len);

    return 0;
}

输出:

90 85 77 68 99
180 170 154 136 198

注意第二行:doubleAll 里改的就是 main 里的那个数组。数组作参数时是"传地址",不是"值传递",这是它和普通 int 参数最大的区别。

一个必须记住的坑:长度要单独传。 函数里的 a 只是一个指针,sizeof(a) 得到的是指针本身的大小(4 或 8 字节),不是数组的大小。所以导学案5说"sizeof(a)/sizeof(a[0]) 这个套路只在数组定义的地方有效"——原因就在这里。


7.4 综合练习

本节练习用于自练与自查,不强制完成、不需要提交;需要提交的是实验室统一布置的作业(作业一 / 作业二)。 参考代码统一放在本节末尾,做完之前不要翻。

  1. 写指针版 void swap(double *pa, double *pb),主函数读入两个小数,交换后输出。
  2. 写 void minMaxArr(int a[], int len, int *pmin, int *pmax),求数组的最小值和最大值。主函数定义一个 6 个元素的数组并打印结果。
  3. 写 void zeroEven(int a[], int len),把数组里所有偶数改成 0。主函数打印修改前后的数组,验证"函数里真的改到了原数组"。 4.(*)用指针遍历数组:int *p = a;,然后循环里 printf("%d ", *p); p++;。体会 p++ 的含义是"往后挪一个格子",挪的步长由指针的类型决定(int * 一次挪 4 字节)。

7.5 本节小结与自查

学完本节,你应该能做到:

  • 说清 &a、p、*p 三者的关系,知道 int *p 怎么读
  • 定义指针时就初始化,知道野指针为什么危险、NULL 表示什么
  • 用指针让函数改到外面的变量(成功的 swap)
  • 知道函数要"带回"多个值时用指针参数
  • 说清 a 与 &a[0]、a[i] 与 *(a + i) 的关系
  • 知道数组传给函数时传的是地址:必须同时传长度,且函数里能改到原数组
  • 解释 scanf("%d", &a[i]) 要写 &、而 scanf("%s", name) 不写的原因

下一节:导学案8 · 结构体。把不同类型的数据捆成一个"自定义类型",飞控里每个传感器数据包都长这样。

7.6 练习参考代码(做完再看)

练习 1 参考代码

#include <stdio.h>

void swap(double *pa, double *pb)
{
    double t = *pa;
    *pa = *pb;
    *pb = t;
}

int main(void)
{
    double x = 0.0, y = 0.0;

    printf("请输入两个小数:");
    scanf("%lf %lf", &x, &y);

    swap(&x, &y);
    printf("交换后:x = %.1f, y = %.1f\n", x, y);

    return 0;
}

练习 2 参考代码

#include <stdio.h>

void minMaxArr(int a[], int len, int *pmin, int *pmax)
{
    *pmin = a[0];
    *pmax = a[0];

    for (int i = 1; i < len; i++) {
        if (a[i] < *pmin) {
            *pmin = a[i];
        }
        if (a[i] > *pmax) {
            *pmax = a[i];
        }
    }
}

int main(void)
{
    int a[6] = {5, 2, 9, 1, 7, 3};
    int len = (int)(sizeof(a) / sizeof(a[0]));
    int lo = 0, hi = 0;

    minMaxArr(a, len, &lo, &hi);
    printf("最小 = %d,最大 = %d\n", lo, hi);

    return 0;
}

输出:最小 = 1,最大 = 9

练习 3 参考代码

#include <stdio.h>

void printArr(int a[], int len)
{
    for (int i = 0; i < len; i++) {
        printf("%d ", a[i]);
    }
    printf("\n");
}

void zeroEven(int a[], int len)
{
    for (int i = 0; i < len; i++) {
        if (a[i] % 2 == 0) {
            a[i] = 0;
        }
    }
}

int main(void)
{
    int a[6] = {4, 7, 2, 9, 6, 1};
    int len = (int)(sizeof(a) / sizeof(a[0]));

    printArr(a, len);
    zeroEven(a, len);
    printArr(a, len);

    return 0;
}

输出:

4 7 2 9 6 1
0 7 0 9 0 1

指针不可怕,可怕的是"不知道指针里存的是什么"。

以后每写一行带 * 的代码,先问自己:这个指针现在指向谁?

作者:Mike-lin300

最后更新:2026-09-25

更新说明:无(新编写)