17、指针(三):指针数组与数组指针、多级指针、void指针

指针这东西,学的时候觉得绕,用起来是真爽。今天咱们把指针数组、数组指针、多级指针和void指针这几个“硬骨头”啃下来。我当年刚接触C时,也被这些概念绕晕过,后来在项目里调试过一个野指针导致的死机,才真正把它们刻在脑子里。

17.1 指针数组 vs 数组指针

这两个名字长得像双胞胎,但性格完全不同。说白了:

  • 指针数组:是一个数组,里面每个元素都是指针。
  • 数组指针:是一个指针,它指向一个数组。

怎么记?看最后两个字。指针数组最后是“数组”,所以它是数组;数组指针最后是“指针”,所以它是指针。

17.1.1 指针数组

我在项目中常用指针数组来处理字符串列表。比如命令行参数解析,用指针数组存各个选项的字符串,遍历起来特别方便。

#include <stdio.h>

int main() {
    // 指针数组:每个元素是 char*
    char *fruits[3] = {"apple", "banana", "cherry"};
    
    for (int i = 0; i < 3; i++) {
        printf("%s\n", fruits[i]);  // fruits[i] 就是字符串首地址
    }
    return 0;
}
我的习惯:用指针数组管理固定数量的字符串时,记得初始化列表。如果后期需要动态增减,我会改用二级指针。

17.1.2 数组指针

数组指针指向的是整个数组,不是数组的第一个元素。你想想看,如果有一个二维数组,用数组指针来遍历,代码会清晰很多。

#include <stdio.h>

int main() {
    int arr[2][3] = {{1,2,3}, {4,5,6}};
    int (*p)[3];  // 数组指针:指向含有3个int的数组
    
    p = arr;      // p 指向第一行(整个一维数组)
    
    for (int i = 0; i < 2; i++) {
        for (int j = 0; j < 3; j++) {
            printf("%d ", p[i][j]);  // 像二维数组一样用
        }
        printf("\n");
    }
    return 0;
}
我曾经踩过的坑:int *p[3]int (*p)[3] 时,括号位置搞反了。前者是指针数组,后者是数组指针。少一个括号,意思天差地别。

17.2 多级指针(指向指针的指针)

多级指针,说白了就是指针的指针。为什么需要它?最常见的场景是:你想在函数里修改传入的指针本身(比如分配内存),就得传指针的地址,也就是二级指针。

17.2.1 二级指针的基本用法

#include <stdio.h>
#include <stdlib.h>

void allocate_memory(int **p, int size) {
    *p = (int*)malloc(size * sizeof(int));
    if (*p == NULL) {
        printf("内存分配失败\n");
        exit(1);
    }
    // 初始化
    for (int i = 0; i < size; i++) {
        (*p)[i] = i * 10;
    }
}

int main() {
    int *arr = NULL;
    allocate_memory(&arr, 5);  // 传指针的地址
    
    for (int i = 0; i < 5; i++) {
        printf("%d ", arr[i]);
    }
    free(arr);
    return 0;
}

嗯,这里要注意:如果只传一级指针,函数内部修改的是指针的副本,外面的指针还是NULL。我早期写链表时就在这里栽过跟头。

17.2.2 三级指针?

三级指针理论上可以用,但实际项目中很少见。我见过最深的也就是二级指针。如果你发现需要三级指针,多半是设计上可以简化。比如用结构体封装一下,比裸指针嵌套更安全。

核心原则:多级指针每多一级,代码可读性就下降一个档次。能用结构体就别用三级指针。

17.3 void指针

void指针(void*)是C语言里的“万能容器”。它可以指向任何类型的数据,但你不能直接解引用它——因为编译器不知道它指向什么类型。

17.3.1 基本用法

#include <stdio.h>

int main() {
    int a = 100;
    double b = 3.14;
    char c = 'X';
    
    void *ptr;
    
    ptr = &a;
    printf("int: %d\n", *(int*)ptr);  // 必须强制转换
    
    ptr = &b;
    printf("double: %.2f\n", *(double*)ptr);
    
    ptr = &c;
    printf("char: %c\n", *(char*)ptr);
    
    return 0;
}
我常用的场景:写通用数据结构时,比如链表、栈,节点里存 void* 数据,这样一套代码可以支持int、float、甚至结构体。但记得在释放时,要清楚实际类型,否则内存泄漏。

17.3.2 void指针与函数指针

标准库的 qsort 就用 void* 实现通用排序。你只需要提供一个比较函数,qsort内部通过void指针操作数据。

#include <stdio.h>
#include <stdlib.h>

int compare(const void *a, const void *b) {
    return (*(int*)a - *(int*)b);
}

int main() {
    int arr[] = {5, 2, 9, 1, 5, 6};
    int n = sizeof(arr)/sizeof(arr[0]);
    
    qsort(arr, n, sizeof(int), compare);
    
    for (int i = 0; i < n; i++) {
        printf("%d ", arr[i]);
    }
    return 0;
}
注意:void指针不能进行指针运算。比如 ptr++ 是不允许的,因为编译器不知道步长。必须先转换成具体类型再运算。

17.4 知识体系图

下面这张图帮你理清今天这几个概念的关系。我画图时习惯把容易混淆的放在一起对比,这样记忆更深刻。

指针进阶核心概念 指针数组 char *arr[3]; 数组里存指针 常用于字符串列表 数组指针 int (*p)[3]; 指针指向整个数组 常用于二维数组遍历 多级指针 int **p; 指针的指针 用于函数内修改指针 void指针 void *ptr; 万能指针,需强制转换 用于通用数据结构 易混淆点:指针数组 vs 数组指针,重点看最后两个字

17.5 避坑指南与总结

最后,我把自己这些年踩过的坑总结一下:

  • 指针数组初始化:如果声明 char *arr[3],记得给每个元素赋初值,否则是野指针。我曾经在嵌入式项目里忘了初始化,结果指针乱指,系统直接挂掉。
  • 数组指针的括号:int (*p)[3] 的括号不能省。省了就成了指针数组。我建议你写的时候先写括号,再写星号,最后写变量名。
  • 多级指针的传参:如果函数要修改指针本身(比如分配内存),必须传二级指针。一级指针只能修改指针指向的内容,不能修改指针的地址。
  • void指针的转换:解引用void指针前,一定要强制转换成具体类型。而且转换后的类型要和实际存储的类型一致,否则数据解释会出错。
一句话总结:指针数组是数组存指针,数组指针是指针指数组;多级指针用于修改指针本身;void指针是万能容器,但用之前必须“穿上类型的外衣”。

好了,这一章的内容就到这里。指针这部分,多写代码、多调试,慢慢就熟练了。下一章咱们继续聊指针和函数的关系,到时候见。

专注资料整理