云计算百科
云计算领域专业知识百科平台

深入理解指针4

深入理解指针(四):字符指针、数组指针、函数指针与转移表

数组和指针的关系,本质上就是“地址 + 类型 + 步长”的关系。 前面我们已经讨论了数组名、指针访问、传参退化、二级指针、指针数组和二维模拟。这一篇继续沿着这条主线,把字符指针、数组指针、二维数组传参、函数指针、函数指针数组和转移表一次讲透。


在这里插入图片描述

目录

  • 本节目标
  • 字符指针变量
  • 数组指针变量
  • 二维数组传参的本质
  • 函数指针变量
  • 函数指针数组
  • 转移表
  • 常见陷阱与调试建议
  • 总结
  • 本节目标

  • 理解 char *、字符数组和字符串字面量的区别。
  • 掌握数组指针 int (*p)[N] 的定义、初始化和步长。
  • 明白二维数组传参为什么不能写成 int **。
  • 掌握函数指针的定义、调用和作为参数使用。
  • 掌握函数指针数组的定义与初始化。
  • 能用转移表替代冗长的 switch,写出更优雅的计算器。

  • 一、字符指针变量

    1.1 字符指针的两种常见用法

    字符指针的类型是 char *。它既可以指向单个字符,也可以指向字符串。


    #include <stdio.h>

    int main(void)
    {
    char ch = 'w';
    char *pc = &ch;

    printf("ch = %c\\n", ch);
    printf("*pc = %c\\n", *pc);

    *pc = 'W';
    printf("修改后 ch = %c\\n", ch);

    return 0;
    }

    在这里插入图片描述

    输出示例:

    ch = w
    *pc = w
    修改后 ch = W

    另一种常见写法:

    #include <stdio.h>

    int main(void)
    {
    const char *pstr = "hello bit.";
    printf("%s\\n", pstr);
    return 0;
    }

    这里并不是把整个字符串“放进”了 pstr,而是把字符串首字符 'h' 的地址存入了 pstr。pstr 指向常量字符串,通常位于只读区域,因此不建议通过 pstr 修改内容。

    1.2 字符数组 vs 字符指针

    #include <stdio.h>

    int main(void)
    {
    char arr[] = "hello bit."; // 字符数组:内容可修改
    const char *p = "hello bit."; // 指向字符串字面量:内容不可修改

    arr[0] = 'H';
    printf("arr = %s\\n", arr);
    printf("p = %s\\n", p);

    printf("sizeof(arr) = %zu\\n", sizeof(arr));
    printf("sizeof(p) = %zu\\n", sizeof(p));

    // p[0] = 'H'; // 错误:修改字符串字面量是未定义行为

    return 0;
    }

    在这里插入图片描述

    输出示例:

    arr = Hello bit.
    p = hello bit.
    sizeof(arr) = 12
    sizeof(p) = 8

    结论:

    • char arr[] = "hello bit."; 是字符数组,有独立存储空间,可以修改。
    • const char *p = "hello bit."; 是指针,指向字符串字面量,不能通过 p 修改内容。
    • sizeof(arr) 是整个数组大小,sizeof(p) 是指针大小。
    对比项字符数组 char arr[]字符指针 const char *p
    本质 数组 指针变量
    存储内容 字符本身 地址
    可修改内容 可以 不可以(指向字面量时)
    sizeof 整个数组大小 指针大小
    strlen 字符串长度 字符串长度
    整体赋值 不可以 可以重新指向
    自增 不可以 可以
    传参 退化为 char * 本身就是 char *
    典型用途 可修改字符串缓冲区 只读字符串 / 遍历
    操作字符数组 char arr[]字符指针 const char *p
    修改元素 arr[0] = 'H'; 合法 p[0] = 'H'; 未定义行为
    整体赋值 arr = "world"; 错误 p = "world"; 合法
    复制内容 strcpy(dst, arr); strcpy(dst, p);
    自增 arr++; 错误 p++; 合法
    返回局部 不能返回局部数组地址 不能返回局部数组地址

    二、数组指针变量

    2.1 数组指针是什么?

    数组指针:指向数组的指针。

    int (*p)[10];

    读法:p 是一个指针,指向一个“有 10 个 int 元素的数组”。

    对比:

    int *p1[10]; // 指针数组:p1 是数组,有 10 个元素,每个元素是 int *
    int (*p2)[10]; // 数组指针:p2 是指针,指向 int[10]

    因为 [] 的优先级高于 *,所以 int *p1[10] 先看 p1[10],说明 p1 是数组;而 int (*p2)[10] 中 p2 先和 * 结合,说明 p2 是指针。

    2.2 数组指针的初始化

    数组指针用来存放数组的地址。数组的地址可以通过 &数组名 获得。

    #include <stdio.h>

    int main(void)
    {
    int arr[10] = {0, 1, 2, 3, 4, 5, 6, 7, 8, 9};
    int (*p)[10] = &arr;

    printf("arr = %p\\n", (void *)arr);
    printf("&arr = %p\\n", (void *)&arr);
    printf("p = %p\\n", (void *)p);
    printf("p + 1 = %p\\n", (void *)(p + 1));

    printf("sizeof(arr) = %zu\\n", sizeof(arr));
    printf("sizeof(p) = %zu\\n", sizeof(p));
    printf("sizeof(*p) = %zu\\n", sizeof(*p));

    return 0;
    }

    在这里插入图片描述

    输出示例:

    arr = 000000000061FE00
    &arr = 000000000061FE00
    p = 000000000061FE00
    p + 1 = 000000000061FE28
    sizeof(arr) = 40
    sizeof(p) = 8
    sizeof(*p) = 40

    结论:

    • p + 1 跳过整个数组,也就是 10 * sizeof(int) = 40 字节。
    • *p 就是整个数组,sizeof(*p) 等于数组大小。
    • 数组指针的步长由它指向的数组类型决定。
    核心点说明
    本质 数组指针是指针,不是数组;指向一个数组
    类型写法 int (*p)[N]; 表示 p 指向 int[N]
    与指针数组区别 int *p[N]; 是指针数组;int (*p)[N]; 是数组指针
    优先级 [] 高于 *,必须加括号 (*p) 保证 p 先与 * 结合
    步长 p + 1 跳过整个数组,步长为 N * sizeof(元素类型)
    解引用 *p 得到整个数组;p[i] 得到第 i 个数组
    初始化 用 &arr 或二维数组名 arr 初始化
    传参 二维数组传参本质是传递首行地址,形参写 int (*p)[N]
    用途 二维数组传参、行指针、动态二维数组、模拟二维数组

    2.3 数组指针类型解析

    int (*p)[10];

    拆开看:

    • p:变量名。
    • *p:说明 p 是指针。
    • (*p)[10]:说明 p 指向一个含有 10 个元素的数组。
    • int (*p)[10]:说明数组的每个元素是 int。
    2.3.1 从内到外读类型

    读复杂指针类型时,从变量名开始,先看它和谁结合:

  • p 先和 * 结合 → p 是指针。
  • 再和 [10] 结合 → 指向一个有 10 个元素的数组。
  • 最后看元素类型 int → 数组的每个元素是 int。
  • 所以 p 的类型是:指向 int[10] 的指针,即 int (*)[10]。

    2.3.2 对比指针数组
    写法含义读法
    int *p[10]; 指针数组 p 是数组,有 10 个元素,每个元素是 int *
    int (*p)[10]; 数组指针 p 是指针,指向一个 int[10] 数组

    区别在于括号:int *p[10] 中 [] 优先级高,p 先和 [] 结合;int (*p)[10] 中 p 先和 * 结合。

    2.3.3 优先级与括号

    [] 的优先级高于 *。 如果写成 int *p[10];,编译器会理解为 int *(p[10]);,即 p 是数组。 要得到“指针”,必须加括号:int (*p)[10];。

    2.3.4 步长与解引用

    int arr[5] = {0};
    int (*p)[5] = &arr;

    printf("p = %p\\n", (void *)p);
    printf("p + 1 = %p\\n", (void *)(p + 1));
    printf("sizeof(*p) = %zu\\n", sizeof(*p));

    • p + 1 跳过整个数组,步长是 5 * sizeof(int)。
    • *p 得到整个数组,sizeof(*p) 等于 sizeof(arr)。
    • (*p)[i] 访问数组第 i 个元素,等价于 arr[i]。
    2.3.5 用 typedef 简化

    typedef int (*parr_t)[10]; // parr_t 是“指向 int[10] 的指针”类型
    parr_t p = &arr;

    新的类型名 parr_t 必须写在 * 的右边。

    2.3.6 常见错误

    int *p1[10]; // 指针数组:数组,元素是 int *
    int (*p2)[10]; // 数组指针:指针,指向 int[10]

    // int (*p3)[10] = arr; // 如果 arr 是 int[10],应该写 &arr
    // int (*p4)[10] = &arr; // 正确

    • 忘记括号:int *p[10]; 变成指针数组。
    • 初始化时漏掉 &:数组名 arr 退化为 int *,不能直接赋给 int (*)[10]。
    • 把二维数组传参写成 int **:类型不匹配。

    三、二维数组传参的本质

    3.1 二维数组可以看作“数组的数组”

    对于 int arr[3][5];

    • arr 是二维数组,有 3 行,每行 5 个 int。
    • arr 的首元素是第一行,类型是 int[5]。
    • 数组名 arr 在大多数表达式中退化为首元素地址,也就是第一行的地址,类型是 int (*)[5]。
    • arr + 1 跳过一整行。

    3.2 形参写成数组形式

    #include <stdio.h>

    void print_matrix(int a[3][5], int rows, int cols)
    {
    for (int i = 0; i < rows; ++i) {
    for (int j = 0; j < cols; ++j) {
    printf("%d ", a[i][j]);
    }
    printf("\\n");
    }
    }

    int main(void)
    {
    int arr[3][5] = {
    {1, 2, 3, 4, 5},
    {2, 3, 4, 5, 6},
    {3, 4, 5, 6, 7}
    };

    print_matrix(arr, 3, 5);
    return 0;
    }

    在这里插入图片描述

    等价写法

    下面三种形参完全等价:

    void f(int a[3][5], int rows, int cols);
    void f(int a[][5], int rows, int cols);
    void f(int (*a)[5], int rows, int cols);

    • 第一维可省,第二维不能省。
    • 编译器按 int (*a)[5] 处理。
    用宏或 typedef 简化

    #define COLS 5

    void f(int a[][COLS], int rows);

    typedef int Row[COLS];
    void f(Row a[], int rows);

    常见错误

    void f(int a[][], int rows, int cols); // 错误:第二维不能省
    void f(int **a, int rows, int cols); // 类型不匹配
    int rows = sizeof(a) / sizeof(a[0]); // 错误:得到指针除法

    总结
    写法是否合法等价类型
    int a[3][5] 合法 int (*a)[5]
    int a[][5] 合法 int (*a)[5]
    int a[5][] 非法 第二维不能省
    int **a 类型不匹配 用于指针数组或动态二维数组
    int (*a)[5] 合法 本质形式

    二维数组传参:第一维可省,第二维不能省;行数必须传,列数可推出。

    3.3 形参写成指针形式

    #include <stdio.h>

    void print_matrix(int (*p)[5], int rows, int cols)
    {
    for (int i = 0; i < rows; ++i) {
    for (int j = 0; j < cols; ++j) {
    printf("%d ", *(*(p + i) + j));
    }
    printf("\\n");
    }
    }

    int main(void)
    {
    int arr[3][5] = {
    {1, 2, 3, 4, 5},
    {2, 3, 4, 5, 6},
    {3, 4, 5, 6, 7}
    };

    print_matrix(arr, 3, 5);
    return 0;
    }

    在这里插入图片描述

    这两种写法本质等价,编译器都按 int (*)[5] 处理。

    指针形式解析
    • int (*p)[5]:p 是指针,指向 int[5]。
    • p + i:跳过 i 行,指向第 i 行。
    • *(p + i):得到第 i 行数组,类型为 int[5]。
    • *(p + i) + j:第 i 行第 j 个元素的地址。
    • *(*(p + i) + j):第 i 行第 j 个元素的值。
    等价写法

    p[i][j] // 下标形式
    *(*(p + i) + j) // 指针形式

    两者完全等价。

    形参写成指针形式,更清楚地表达“二维数组传参传递的是行指针”。

    3.4 为什么不能写成 int **?

    真正二维数组 int arr[3][5] 的内存是连续的,arr 退化为 int (*)[5],不是 int **。

    内存布局对比

    int arr[3][5] int **p
    +—-+—-+—-+—-+ +—-+ +—-+—-+—-+—-+
    | 1 | 2 | 3 | 4 | | * | —> | 1 | 2 | 3 | 4 |
    +—-+—-+—-+—-+ +—-+ +—-+—-+—-+—-+
    | 5 | 6 | 7 | 8 | | * | —> | 5 | 6 | 7 | 8 |
    +—-+—-+—-+—-+ +—-+ +—-+—-+—-+—-+
    | 9 | 10 | 11 | 12 | | * | —> | 9 | 10 | 11 | 12 |
    +—-+—-+—-+—-+ +—-+ +—-+—-+—-+—-+
    连续一整块 指针数组 + 每行独立分配

    • 真正二维数组:一整块连续内存,arr + 1 跳过一行。
    • int **:先有一个指针数组,每个指针再指向一块独立内存,行与行不一定连续。
    类型对比
    写法类型含义
    int arr[3][5] int (*)[5] 指向一行(int[5])的指针
    int **p int ** 指向 int * 的指针
    int *p[3] int *[3] 指针数组,元素是 int *
    典型误区

    void f(int **p, int rows, int cols); // 错误匹配真正二维数组

    int arr[3][5];
    f(arr, 3, 5); // 类型不匹配:int (*)[5] 不能传给 int **

    int ** 的正确用途

    // 用途一:指针数组
    int a = 1, b = 2, c = 3;
    int *p[3] = {&a, &b, &c}; // p 是 int *[3]

    // 用途二:动态二维数组
    int **m = malloc(rows * sizeof(int *));
    for (int i = 0; i < rows; ++i)
    m[i] = malloc(cols * sizeof(int));

    四、函数指针变量

    4.1 函数也有地址

    函数名在大多数表达式中会转换为函数地址。


    #include <stdio.h>

    void test(void)
    {
    printf("hehe\\n");
    }

    int main(void)
    {
    printf("test = %p\\n", (void *)test);
    printf("&test = %p\\n", (void *)&test);
    return 0;
    }

    在这里插入图片描述

    通常 test 和 &test 输出相同。

    4.2 定义函数指针

    int (*pf)(int, int);

    读法:pf 是一个指针,指向“参数为 (int, int),返回值为 int”的函数。

    完整示例:

    #include <stdio.h>

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

    int Sub(int x, int y)
    {
    return x – y;
    }

    int main(void)
    {
    int (*pf)(int, int) = Add;

    printf("Add(2, 3) = %d\\n", Add(2, 3));
    printf("pf(2, 3) = %d\\n", pf(2, 3));
    printf("(*pf)(2, 3) = %d\\n", (*pf)(2, 3));

    pf = Sub;
    printf("pf(5, 3) = %d\\n", pf(5, 3));

    return 0;
    }

    在这里插入图片描述

    输出示例:

    Add(2, 3) = 5
    pf(2, 3) = 5
    (*pf)(2, 3) = 5
    pf(5, 3) = 2

    结论:

    • pf(2, 3) 和 (*pf)(2, 3) 都合法。
    • 函数指针可以像变量一样重新赋值,指向不同函数。
    4.2.1 初始化方式

    函数指针可以用函数名初始化,也可以用 &函数名 初始化:

    int (*pf1)(int, int) = Add;
    int (*pf2)(int, int) = &Add;

    两种写法等价,因为函数名在表达式中会转换为函数地址。

    4.2.2 调用方式

    下面两种调用完全等价:

    pf(2, 3);
    (*pf)(2, 3);

    • pf(2, 3):直接通过函数指针调用。
    • (*pf)(2, 3):先解引用得到函数,再调用。

    C 语言允许省略 *,所以两种写法都合法。

    4.2.3 可以重新赋值

    函数指针是变量,可以重新指向其他函数,只要签名一致:

    pf = Sub;
    printf("%d\\n", pf(5, 3)); // 输出 2

    4.2.4 常见错误
    错误写法问题
    int *pf(int, int); 声明了函数,不是函数指针
    int (*pf)(int, int) = Add(); Add() 是调用结果,不是函数地址
    pf = Add(2, 3); 把返回值赋给函数指针,类型不匹配
    调用时写成 *pf(2, 3); 优先级问题,等价于 *(pf(2, 3)),不是调用函数指针

    4.3 函数指针作为参数

    函数指针最常见的用途之一是“回调函数”。

    #include <stdio.h>

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

    int Sub(int x, int y)
    {
    return x – y;
    }

    int Calc(int (*op)(int, int), int x, int y)
    {
    return op(x, y);
    }

    int main(void)
    {
    printf("Calc(Add, 10, 3) = %d\\n", Calc(Add, 10, 3));
    printf("Calc(Sub, 10, 3) = %d\\n", Calc(Sub, 10, 3));

    return 0;
    }

    在这里插入图片描述

    输出示例:

    Calc(Add, 10, 3) = 13
    Calc(Sub, 10, 3) = 7

    4.4 两段有趣的代码

    《C陷阱和缺陷》中有两段经典代码:

    (*(void (*)())0)();

    void (*signal(int, void (*)(int)))(int);

    第一段代码:把 0 强制转换为“无参数、返回 void 的函数指针”,然后调用它。 第二段代码:signal 是一个函数,参数为 int 和 void (*)(int),返回类型是 void (*)(int)。

    用 typedef 可以简化:

    typedef void (*pfun_t)(int);
    pfun_t signal(int, pfun_t);

    typedef 用来给类型起别名。对于函数指针:

    typedef void (*pfun_t)(int);

    新的类型名 pfun_t 必须在 * 的右边。


    五、函数指针数组

    5.1 定义函数指针数组

    函数指针数组:数组的每个元素都是函数指针。

    int (*parr[3])(int, int);

    从内到外:

    • parr 先和 [] 结合 → parr 是数组。
    • 数组有 3 个元素。
    • 每个元素的类型是 int (*)(int, int),即函数指针。

    所以 parr 是一个“存放函数指针的数组”。

    类型拆解

    int (*parr[3])(int, int);

    部分含义
    parr 变量名
    parr[3] 数组,有 3 个元素
    *parr[3] 每个元素是指针
    (*parr[3])(int, int) 每个指针指向参数为 (int, int) 的函数
    int (*parr[3])(int, int) 每个函数返回 int
    常见错误写法

    int *parr2[3](int, int); // 错误:parr2 是数组,元素是返回 int* 的函数?
    int (*parr3[3])(int, int); // 正确

    int *parr2[3](int, int); 会被解析为“数组,每个元素是函数声明”,语法不合法或语义错误。 函数指针数组必须把 * 和数组名用括号括起来:(*parr[3])。

    5.2 函数指针数组示例

    #include <stdio.h>

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

    int Sub(int x, int y)
    {
    return x – y;
    }

    int Mul(int x, int y)
    {
    return x * y;
    }

    int main(void)
    {
    int (*ops[3])(int, int) = { Add, Sub, Mul };

    int a = 10;
    int b = 5;

    for (int i = 0; i < 3; ++i) {
    printf("ops[%d](%d, %d) = %d\\n", i, a, b, ops[i](a, b));
    }

    return 0;
    }

    在这里插入图片描述

    输出示例:

    ops[0](10, 5) = 15
    ops[1](10, 5) = 5
    ops[2](10, 5) = 50


    六、转移表

    6.1 什么是转移表?

    转移表本质就是“函数指针数组”。 它可以把冗长的 switch-case 改成按索引调用函数。

    例如计算器:

    • 1:加法
    • 2:减法
    • 3:乘法
    • 4:除法
    • 0:退出

    用转移表写:

    int (*table[5])(int, int) = { NULL, Add, Sub, Mul, Divide };

    调用:

    table[choice](a, b);

    6.1 什么是转移表?

    转移表(jump table)本质上就是一个函数指针数组。 它把“根据编号选择函数”的逻辑,从冗长的 switch-case 改成按索引查表调用。

    例如计算器:

    选择功能
    1 加法
    2 减法
    3 乘法
    4 除法
    0 退出

    用转移表写:

    int (*table[5])(int, int) = { NULL, Add, Sub, Mul, Divide };

    调用:

    table[choice](a, b);

    为什么叫“转移表”?

    程序原本要经过一连串 if 或 switch 判断,才能决定调用哪个函数。 使用转移表后,直接用下标从数组中取出函数地址并调用,控制流“转移”到目标函数,所以叫转移表。

    6.2 普通 switch 版计算器

    #include <stdio.h>

    int Add(int a, int b)
    {
    return a + b;
    }

    int Sub(int a, int b)
    {
    return a – b;
    }

    int Mul(int a, int b)
    {
    return a * b;
    }

    int Divide(int a, int b)
    {
    if (b == 0) {
    printf("除数不能为 0\\n");
    return 0;
    }
    return a / b;
    }

    int main(void)
    {
    int x, y;
    int input = 1;
    int ret = 0;

    do {
    printf("************************\\n");
    printf(" 1:add 2:sub\\n");
    printf(" 3:mul 4:div\\n");
    printf(" 0:exit\\n");
    printf("************************\\n");
    printf("请选择:");
    scanf("%d", &input);

    switch (input) {
    case 1:
    printf("输入操作数:");
    scanf("%d %d", &x, &y);
    ret = Add(x, y);
    printf("ret = %d\\n", ret);
    break;
    case 2:
    printf("输入操作数:");
    scanf("%d %d", &x, &y);
    ret = Sub(x, y);
    printf("ret = %d\\n", ret);
    break;
    case 3:
    printf("输入操作数:");
    scanf("%d %d", &x, &y);
    ret = Mul(x, y);
    printf("ret = %d\\n", ret);
    break;
    case 4:
    printf("输入操作数:");
    scanf("%d %d", &x, &y);
    ret = Divide(x, y);
    printf("ret = %d\\n", ret);
    break;
    case 0:
    printf("退出程序\\n");
    break;
    default:
    printf("选择错误\\n");
    break;
    }
    } while (input);

    return 0;
    }

    在这里插入图片描述

    6.3 转移表版计算器

    #include <stdio.h>
    #include <stddef.h>

    int Add(int a, int b)
    {
    return a + b;
    }

    int Sub(int a, int b)
    {
    return a – b;
    }

    int Mul(int a, int b)
    {
    return a * b;
    }

    int Divide(int a, int b)
    {
    if (b == 0) {
    printf("除数不能为 0\\n");
    return 0;
    }
    return a / b;
    }

    int main(void)
    {
    int x, y;
    int input = 1;
    int ret = 0;

    int (*table[5])(int, int) = { NULL, Add, Sub, Mul, Divide };

    do {
    printf("************************\\n");
    printf(" 1:add 2:sub\\n");
    printf(" 3:mul 4:div\\n");
    printf(" 0:exit\\n");
    printf("************************\\n");
    printf("请选择:");
    scanf("%d", &input);

    if (input >= 1 && input <= 4) {
    printf("输入操作数:");
    scanf("%d %d", &x, &y);
    ret = table[input](x, y);
    printf("ret = %d\\n", ret);
    } else if (input == 0) {
    printf("退出计算器\\n");
    } else {
    printf("输入有误\\n");
    }
    } while (input);

    return 0;
    }

    优点:

    • 代码更简洁。
    • 扩展功能时只需增加函数和表项。
    • 很适合菜单、命令分发、状态机等场景。

    注意:

    • 使用转移表前必须检查索引范围,防止越界。
    • 函数指针数组中的函数签名必须完全一致。
    • 表中第 0 项可以放 NULL,因为 0 表示退出,不会调用。

    与 switch-case 对比
    对比项switch-case转移表
    代码长度 分支多时代码冗长 简洁,一个数组搞定
    扩展性 增加分支要改多处 增加函数和表项即可
    可读性 分支逻辑直观 需要理解函数指针数组
    运行方式 逐个比较或跳转 直接按索引取函数地址
    适用场景 分支少、逻辑差异大 分支集中、签名一致、索引连续
    索引检查 default 兜底 必须手动检查范围

    七、常见陷阱与调试建议

    7.1 易错点

  • 字符指针指向字符串字面量后修改内容
  • char *s = "hello";
    // s[0] = 'H'; // 未定义行为

    应写成:

    char s[] = "hello";
    s[0] = 'H';

  • 数组指针和指针数组混淆
  • int *p1[5]; // 指针数组
    int (*p2)[5]; // 数组指针

  • 二维数组传参写成 int **
  • 真正二维数组:

    void f(int (*p)[5], int rows);

    动态指针数组:

    void f(int **p, int rows, int cols);

    两者不是一回事。

  • 函数指针数组定义写错
  • int (*ops[3])(int, int); // 正确
    // int *ops[3](int, int); // 错误

  • 转移表越界
  • ret = table[input](x, y); // input 必须在合法范围内

    7.2 调试建议

    7.2.1 用调试宏打印地址、类型大小和步长

    #include <stdio.h>

    #define PRINT_PTR(p) printf(#p " = %p\\n", (void *)(p))
    #define PRINT_SIZE(x) printf("sizeof(" #x ") = %zu\\n", sizeof(x))
    #define ARRAY_SIZE(a) (sizeof(a) / sizeof((a)[0]))

    int main(void)
    {
    int arr[5] = {1, 2, 3, 4, 5};
    int (*p)[5] = &arr;

    PRINT_PTR(arr);
    PRINT_PTR(&arr);
    PRINT_PTR(p);
    PRINT_PTR(p + 1);

    PRINT_SIZE(arr);
    PRINT_SIZE(p);
    PRINT_SIZE(*p);

    printf("ARRAY_SIZE(arr) = %zu\\n", ARRAY_SIZE(arr));

    return 0;
    }

    在这里插入图片描述

    要点:

    • 打印指针一定要转 (void *)。
    • p + 1 跳过整个数组,所以地址差是 sizeof(int[5])。
    • ARRAY_SIZE 只对真正的数组名有效。
    7.2.2 用 assert 给函数加“合同”

    #include <stdio.h>
    #include <assert.h>

    static void print_array(const int *arr, int n)
    {
    assert(arr != NULL);
    assert(n > 0);

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

    static int calc(int (*op)(int, int), int x, int y)
    {
    assert(op != NULL);
    return op(x, y);
    }

    static int add(int a, int b) { return a + b; }
    static int sub(int a, int b) { return a – b; }

    int main(void)
    {
    int arr[] = {1, 2, 3, 4, 5};
    int n = (int)(sizeof(arr) / sizeof(arr[0]));

    print_array(arr, n);
    printf("calc(add, 10, 3) = %d\\n", calc(add, 10, 3));
    printf("calc(sub, 10, 3) = %d\\n", calc(sub, 10, 3));

    return 0;
    }

    在这里插入图片描述

    要点:

    • assert 用于检查“这里必须成立”。
    • 函数指针调用前先断言非空。
    • 发布版可用 #define NDEBUG 关闭断言。
    7.2.3 数组退化与长度传递调试

    #include <stdio.h>
    #include <assert.h>

    #define ARRAY_SIZE(a) (sizeof(a) / sizeof((a)[0]))

    static void print_array(const int *arr, int n)
    {
    assert(arr != NULL);
    assert(n > 0);

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

    int main(void)
    {
    int arr[] = {10, 20, 30, 40, 50};
    int n = (int)ARRAY_SIZE(arr);

    printf("main: sizeof(arr) = %zu, n = %d\\n", sizeof(arr), n);
    print_array(arr, n);

    return 0;
    }

    在这里插入图片描述

    要点:

    • 函数内 sizeof(arr) 得到的是指针大小,不是数组大小。
    • 正确做法是“指针 + 长度”一起传。
    • 调试时打印 sizeof(arr) 和 n,立刻能看出是否退化。
    7.2.4 函数指针与转移表调试

    #include <stdio.h>
    #include <assert.h>

    typedef int (*op_t)(int, int);

    static int add(int a, int b) { return a + b; }
    static int sub(int a, int b) { return a – b; }
    static int mul(int a, int b) { return a * b; }
    static int divide(int a, int b)
    {
    if (b == 0) {
    printf("除数不能为 0\\n");
    return 0;
    }
    return a / b;
    }

    int main(void)
    {
    op_t table[5] = { NULL, add, sub, mul, divide };

    printf("add = %p\\n", (void *)add);
    printf("table[1] = %p\\n", (void *)table[1]);
    printf("table[2] = %p\\n", (void *)table[2]);
    printf("table[3] = %p\\n", (void *)table[3]);
    printf("table[4] = %p\\n", (void *)table[4]);

    int choice = 1;
    int x = 10;
    int y = 5;

    assert(choice >= 1 && choice <= 4);
    assert(table[choice] != NULL);

    int result = table[choice](x, y);
    printf("result = %d\\n", result);

    return 0;
    }

    在这里插入图片描述

    要点:

    • 用 typedef 简化函数指针类型。
    • 打印每个表项地址,确认初始化顺序正确。
    • 使用前检查索引范围和表项非空。
    7.2.5 字符指针与字符串调试

    #include <stdio.h>
    #include <string.h>

    int main(void)
    {
    char arr[] = "hello";
    const char *p = "hello";

    printf("arr = %s\\n", arr);
    printf("p = %s\\n", p);

    printf("sizeof(arr) = %zu, strlen(arr) = %zu\\n",
    sizeof(arr), strlen(arr));
    printf("sizeof(p) = %zu, strlen(p) = %zu\\n",
    sizeof(p), strlen(p));

    printf("arr[0] = %c, p[0] = %c\\n", arr[0], p[0]);

    arr[0] = 'H';
    printf("修改后 arr = %s\\n", arr);

    /* p[0] = 'H'; // 错误:字符串字面量不可修改 */

    return 0;
    }

    在这里插入图片描述

    要点:

    • 只读字符串用 const char *。
    • 需要修改就用 char arr[]。
    • 打印 sizeof 和 strlen,立刻看清区别。
    7.2.6 二维数组与数组指针调试

    #include <stdio.h>

    static void print_matrix(int (*p)[5], int rows, int cols)
    {
    for (int i = 0; i < rows; ++i) {
    for (int j = 0; j < cols; ++j) {
    printf("%d ", p[i][j]);
    }
    printf("\\n");
    }
    }

    int main(void)
    {
    int arr[3][5] = {
    {1, 2, 3, 4, 5},
    {2, 3, 4, 5, 6},
    {3, 4, 5, 6, 7}
    };

    int (*p)[5] = arr;

    for (int i = 0; i < 3; ++i) {
    printf("p + %d = %p\\n", i, (void *)(p + i));
    }

    printf("p[1][2] = %d\\n", p[1][2]);
    printf("*(*(p + 1) + 2) = %d\\n", *(*(p + 1) + 2));

    print_matrix(arr, 3, 5);

    return 0;
    }

    在这里插入图片描述

    要点:

    • 真正二维数组传参,形参写 int (*p)[N]。
    • p + 1 跳过一整行。
    • 不要写成 int **,两者内存布局不同。
    7.2.7 最小复现与单元测试

    #include <stdio.h>
    #include <assert.h>

    static int add(int a, int b) { return a + b; }
    static int sub(int a, int b) { return a – b; }

    static void test_add(void)
    {
    assert(add(2, 3) == 5);
    assert(add(–1, 1) == 0);
    }

    static void test_sub(void)
    {
    assert(sub(5, 3) == 2);
    assert(sub(3, 5) == –2);
    }

    int main(void)
    {
    test_add();
    test_sub();

    printf("all tests passed\\n");
    return 0;
    }

    在这里插入图片描述

    要点:

    • 怀疑指针问题时,先写最小复现程序。
    • 给关键函数写简单断言测试。
    • 测试通过后再集成到完整代码中。
    7.2.8 调试清单
    检查项C语言做法
    打印地址 printf("%p", (void *)p);
    查看类型大小 printf("%zu", sizeof(x));
    数组长度 #define ARRAY_SIZE(a) (sizeof(a)/sizeof((a)[0]))
    断言非空 assert(p != NULL);
    断言索引 assert(i >= 0 && i < n);
    函数指针非空 assert(op != NULL);
    转移表索引 assert(choice >= 1 && choice <= 4);
    二维数组形参 int (*p)[N]
    释放后置空 free(p); p = NULL;
    最小复现 单独写一个小 main
    单元测试 用 assert 写测试函数

    八、总结

    主题核心结论
    字符指针变量 char * 指向字符或字符串;字符串字面量不可修改,建议用 const char *
    数组指针变量 int (*p)[N] 是指针,指向 int[N],p + 1 跳过整个数组
    二维数组传参 形参写 int (*p)[N] 或 int a[][N],第二维不能省,不能写成 int **
    函数指针变量 int (*pf)(int, int) 指向函数,pf(…) 与 (*pf)(…) 等价
    函数指针数组 int (*p[N])(int, int) 是数组,元素为函数指针
    转移表 用函数指针数组按索引调用函数,可替代长 switch,简洁且易扩展

    留个关注支持一下吧

    赞(0)
    未经允许不得转载:网硕互联帮助中心 » 深入理解指针4
    分享到: 更多 (0)

    评论 抢沙发

    评论前必须登录!