3. 数组与指针:数组名作为指针、指针遍历数组、数组指针与指针数组的区别

好,咱们进入第三个实战课题。数组和指针,这俩在C/C++里就像一对双胞胎,长得像,但性格完全不同。很多新手甚至工作两三年的朋友,都在这上面栽过跟头。我个人习惯把这块叫做“指针的入门试炼”,过了这关,后面的路就好走了。

3.1 数组名真的是指针吗?

先说结论:数组名不是指针,但大多数时候表现得像指针。这句话有点绕,我解释一下。

你声明一个 int arr[5],arr 是一个数组对象,它占据 5 个 int 的空间。而指针是一个变量,存放的是地址。arr 本身没有自己的存储空间(除了数组元素),它只是一个符号,代表数组首元素的地址。

核心区别:

  • sizeof(arr) 返回整个数组的大小(20字节,假设int占4字节)
  • sizeof(ptr) 返回指针本身的大小(8字节,64位系统)
  • &arr 的类型是 int (*)[5],指向整个数组
  • &ptr 是指针变量的地址

我记得有一次帮同事排查一个内存泄漏,他写了个函数 void func(int arr[]),然后在函数里用 sizeof(arr) 想获取数组长度。结果可想而知,拿到的永远是 8(指针大小)。这就是典型的把数组名当指针用的坑。

// 错误示范
void print_array(int arr[]) {
    int len = sizeof(arr) / sizeof(arr[0]);  // 这里 arr 已经退化为指针了!
    for (int i = 0; i < len; i++) {
        printf("%d ", arr[i]);
    }
}

// 正确做法
void print_array(int arr[], int len) {
    for (int i = 0; i < len; i++) {
        printf("%d ", arr[i]);
    }
}

3.2 指针遍历数组的几种姿势

用指针遍历数组,说白了就是玩地址的加减法。我个人最常用的是下面这几种,你想想看哪种最顺手。

方式一:下标法(最直观)

int arr[5] = {1, 2, 3, 4, 5};
for (int i = 0; i < 5; i++) {
    printf("%d ", arr[i]);
}

方式二:指针偏移法(最灵活)

int arr[5] = {1, 2, 3, 4, 5};
int *p = arr;
for (int i = 0; i < 5; i++) {
    printf("%d ", *(p + i));  // p+i 指向第 i 个元素
}

方式三:指针递增法(最高效)

int arr[5] = {1, 2, 3, 4, 5};
int *p = arr;
for (int i = 0; i < 5; i++) {
    printf("%d ", *p++);  // 先取值,再移动指针
}

我的建议:日常开发用下标法,可读性最好。性能敏感的场景用指针递增法,少一次乘法运算。但别过度优化,编译器比你聪明。

3.3 数组指针 vs 指针数组——名字像,本质完全不同

这两个概念,我当年学的时候也晕过。咱们用一张表说清楚:

概念 定义 本质 典型用法
数组指针 int (*p)[5] 一个指针,指向一个包含5个int的数组 用于二维数组的行指针
指针数组 int *p[5] 一个数组,每个元素都是int*指针 用于存储多个字符串地址

怎么记?看运算符优先级:[] 优先级高于 *。所以 int *p[5] 先和 [] 结合,说明 p 是数组。而 int (*p)[5] 用括号强制 * 先结合,说明 p 是指针。

避坑指南:我曾经在写一个图像处理模块时,需要传递一个 3x3 的卷积核。我声明了 int *kernel[3],结果传参时发现类型不匹配。后来改成 int (*kernel)[3] 才搞定。记住:二维数组传参,形参必须是数组指针。

3.4 实战:用数组指针遍历二维数组

二维数组在内存中是连续存储的,按行优先排列。用数组指针来遍历,代码会非常清晰:

#include <stdio.h>

void print_matrix(int (*matrix)[4], int rows) {
    for (int i = 0; i < rows; i++) {
        for (int j = 0; j < 4; j++) {
            printf("%3d ", matrix[i][j]);  // 等价于 *(*(matrix + i) + j)
        }
        printf("\n");
    }
}

int main() {
    int arr[3][4] = {
        {1, 2, 3, 4},
        {5, 6, 7, 8},
        {9, 10, 11, 12}
    };
    print_matrix(arr, 3);
    return 0;
}

这里 matrix 就是数组指针,它知道每一行有 4 个 int。所以 matrix + 1 跳过了 16 个字节(4个int),直接指向下一行。这就是数组指针的威力——它保留了数组的维度信息。

3.5 指针数组的典型应用:字符串数组

指针数组最常见的场景就是存储多个字符串。每个字符串长度不同,用指针数组最合适:

const char *weekdays[] = {
    "Monday",
    "Tuesday",
    "Wednesday",
    "Thursday",
    "Friday",
    "Saturday",
    "Sunday"
};

// 遍历
for (int i = 0; i < 7; i++) {
    printf("%s\n", weekdays[i]);  // weekdays[i] 是 const char*
}

这里 weekdays 是一个指针数组,每个元素指向一个字符串常量。注意我用的是 const char *,因为字符串常量不可修改。嗯,这里要注意,如果你试图修改 weekdays[0][0],程序会崩溃。

3.6 知识体系结构图

下面这张图帮你理清本章的核心逻辑:

数组与指针核心知识体系 数组与指针 数组名 ≠ 指针 指针遍历数组 易混概念辨析 sizeof(arr) &arr 类型 下标法 指针偏移 指针递增 数组指针 指针数组 函数传参退化为指针 二维数组行遍历 字符串数组管理 核心:理解类型,避免越界,善用维度信息

3.7 避坑总结

  • 数组名做函数参数时一定会退化为指针,别指望在函数里用 sizeof 获取数组长度。我建议把长度作为参数显式传递。
  • 指针遍历数组时注意边界,别让指针跑到数组外面去。我曾经在循环里多写了一个 ++,结果访问了非法内存,查了一下午才找到。
  • 数组指针和指针数组声明时多写括号,拿不准就查一下优先级。我习惯先写 int (*p)[5] 再填内容,这样不容易错。
  • 用指针数组管理字符串时记得加 const,防止意外修改字符串常量导致崩溃。

好了,数组与指针这块就聊到这儿。记住一句话:数组有大小,指针有方向,别搞混了。下一节咱们聊聊更刺激的——函数指针和回调机制。