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

数组和指针,这对冤家在C语言里纠缠了几十年。我刚开始学的时候,总觉得它们就是一回事。后来在项目里踩了坑才明白——嗯,它们长得像,但骨子里完全不同。

这一节,咱们就把这层窗户纸捅破。

3.1 数组名就是指针吗?

很多人说「数组名就是指针」。这话对,也不对。

先看个例子:

int arr[5] = {1, 2, 3, 4, 5};
int *p = arr;   // 这行能编译通过

你看,arr可以直接赋值给int*。所以数组名确实可以当作指针用。但别急着下结论——试试这个:

printf("sizeof(arr) = %zu\n", sizeof(arr));  // 输出 20(5个int)
printf("sizeof(p) = %zu\n", sizeof(p));      // 输出 8(64位系统)

为什么会这样?
因为arr是数组名,sizeof(arr)返回整个数组的大小。而p是指针,sizeof(p)只返回指针本身的大小。

核心结论:数组名在大多数表达式中会「退化」为指向首元素的指针。但有两个例外——sizeof和取地址运算符&。

我个人习惯把数组名理解成「带有数组大小信息的指针常量」。它不能自增自减,不能重新赋值。你试试arr++?编译器直接报错。

3.2 用指针遍历数组

既然数组名能当指针用,那用指针遍历数组就顺理成章了。

int arr[] = {10, 20, 30, 40, 50};
int *p = arr;

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

输出结果完全一样。但指针方式有个好处——灵活。比如你想从中间开始遍历:

int *mid = arr + 2;  // 指向 arr[2]
printf("%d\n", *mid);  // 输出 30

我在项目中遇到过一种场景:需要把数组的一部分传给函数处理。用指针就特别方便:

void process(int *start, int count) {
    for (int i = 0; i < count; i++) {
        start[i] *= 2;  // 这里用下标操作,可读性更好
    }
}

// 调用:只处理后3个元素
process(arr + 2, 3);

小技巧:用指针遍历时,我习惯同时维护一个「结束指针」而不是计数器。这样代码更简洁:

int *end = arr + 5;
for (int *p = arr; p < end; p++) {
    printf("%d ", *p);
}

3.3 指针与数组的区别

说白了,数组是「一块连续的内存」,指针是「一个存放地址的变量」。它们的内存布局完全不同。

对比项 数组 指针
内存分配 编译时分配,固定大小 运行时分配,可指向任意位置
存储内容 实际数据 地址值
赋值操作 不能整体赋值 可以重新指向
sizeof结果 整个数组字节数 指针本身字节数
作为函数参数 退化为指针 传递指针副本

我曾经在代码里写过这样的bug:

// 错误示范
void init_array(int arr[]) {
    arr = malloc(10 * sizeof(int));  // 这改不了外面的arr!
}

数组作为函数参数时,已经退化成指针了。你修改形参的值,影响不了实参。要改,得传指针的指针。

避坑指南:我曾经在嵌入式项目里用sizeof(arr)计算数组长度,结果arr是函数参数,sizeof返回了指针大小(8字节),而不是数组大小。从那以后,我只要把数组传给函数,就同时传一个长度参数。

3.4 数组指针 vs 指针数组

这两个概念,名字就差一个字,意思天差地别。你想想看:

  • 指针数组:一个数组,里面存的是指针。先有数组,元素是指针。
  • 数组指针:一个指针,指向一个数组。先有指针,指向的是数组。

看声明方式就明白了:

int *p_arr[5];   // 指针数组:5个int*类型的元素
int (*arr_p)[5]; // 数组指针:指向一个包含5个int的数组

优先级规则:[]优先级高于*,所以int *p_arr[5]先被解释成数组,元素类型是int*。而(*arr_p)加了括号,先是指针,再指向int[5]。

指针数组的典型用法:

char *fruits[] = {"apple", "banana", "cherry"};
for (int i = 0; i < 3; i++) {
    printf("%s\n", fruits[i]);  // 每个元素是字符串指针
}

数组指针的典型用法——指向二维数组的行:

int matrix[3][4] = {{1,2,3,4}, {5,6,7,8}, {9,10,11,12}};
int (*row)[4] = matrix;  // row指向第一行(4个int)

for (int i = 0; i < 3; i++) {
    for (int j = 0; j < 4; j++) {
        printf("%2d ", row[i][j]);  // 用行指针访问
    }
    printf("\n");
}

记忆口诀:「指针数组」——数组里存指针;「数组指针」——指针指向数组。看最后两个字,就是核心。

3.5 知识结构图

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

指针与数组知识结构 指针与数组 数组名作为指针 退化规则与例外 指针遍历数组 灵活访问与结束指针 指针与数组的区别 内存布局与sizeof 数组指针 vs 指针数组 声明方式与典型用法 sizeof(arr) ≠ sizeof(p) &arr 取整个数组 数组不能整体赋值 参数传递退化为指针 指针数组:int *p[5] 数组指针:int (*p)[5] 核心:理解声明优先级,区分「存指针」和「指向数组」

这张图把四个知识点串起来了。你从上往下看,先理解数组名的退化规则,再学会用指针遍历,然后搞清楚数组和指针的本质区别,最后攻克数组指针和指针数组这对易混概念。

嗯,这一节的内容就到这里。记住一句话:数组不是指针,但数组名可以当指针用。搞清楚了这一点,后面再学动态内存和多级指针,就会轻松很多。

可可精品资料库