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 知识体系结构图
下面这张图帮你理清本章的核心逻辑:
3.7 避坑总结
- 数组名做函数参数时一定会退化为指针,别指望在函数里用 sizeof 获取数组长度。我建议把长度作为参数显式传递。
- 指针遍历数组时注意边界,别让指针跑到数组外面去。我曾经在循环里多写了一个
++,结果访问了非法内存,查了一下午才找到。 - 数组指针和指针数组声明时多写括号,拿不准就查一下优先级。我习惯先写
int (*p)[5]再填内容,这样不容易错。 - 用指针数组管理字符串时记得加 const,防止意外修改字符串常量导致崩溃。
好了,数组与指针这块就聊到这儿。记住一句话:数组有大小,指针有方向,别搞混了。下一节咱们聊聊更刺激的——函数指针和回调机制。