深入理解指针(中):数组与指针的进阶之旅
从数组名到指针数组,从冒泡排序到二维数组传参,一步步揭开指针的神秘面纱
前言
在上一讲中,我们学习了指针的基础概念,包括内存地址、指针变量、const修饰符以及野指针的规避。这一讲我们将更进一步,探讨指针与数组的密切关系,这是C语言指针学习中的核心内容。
一、数组名的深入理解
1.1 数组名就是地址
先来看一段代码:
c
#include <stdio.h>
int main() {
int arr[10] = {1,2,3,4,5,6,7,8,9,10};
printf("&arr[0] = %p\n", &arr[0]);
printf("arr = %p\n", arr);
return 0;
}
输出结果中,两行打印的地址完全相同 。这说明:数组名就是数组首元素的地址。
1.2 两个重要的例外
但是,有同学会发现下面的代码输出是40,而不是4或8:
c
int arr[10] = {1,2,3,4,5,6,7,8,9,10};
printf("%d\n", sizeof(arr)); // 输出 40(10个int × 4字节)
数组名就是首元素地址这个规则有两个例外:
| 例外情况 | 说明 |
|---|---|
sizeof(数组名) |
这里的数组名表示整个数组,计算的是整个数组的大小 |
&数组名 |
这里的数组名表示整个数组,取出的是整个数组的地址 |
1.3 &arr0、arr、&arr 的区别
通过指针运算来看它们的区别:
c
printf("&arr[0] = %p\n", &arr[0]);
printf("&arr[0]+1 = %p\n", &arr[0]+1); // +4字节
printf("arr = %p\n", arr);
printf("arr+1 = %p\n", arr+1); // +4字节
printf("&arr = %p\n", &arr);
printf("&arr+1 = %p\n", &arr+1); // +40字节(整个数组)
&arr[0]和arr都是首元素地址,+1 跳过 1个元素(4字节)&arr是整个数组的地址,+1 跳过 整个数组(40字节)
二、使用指针访问数组
有了前面的知识,我们可以很方便地使用指针来访问数组:
c
#include <stdio.h>
int main() {
int arr[10] = {0};
int sz = sizeof(arr) / sizeof(arr[0]);
int* p = arr; // p指向数组首元素
// 输入
for (int i = 0; i < sz; i++) {
scanf("%d", p + i); // 或 scanf("%d", arr + i);
}
// 输出 - 两种写法等价
for (int i = 0; i < sz; i++) {
printf("%d ", *(p + i)); // 指针方式
// printf("%d ", p[i]); // 数组方式,完全等价
}
return 0;
}
核心结论:
p[i]等价于*(p + i)arr[i]等价于*(arr + i)
编译器处理数组元素访问时,都是转换成"首元素地址 + 偏移量"的方式,然后解引用访问。
三、一维数组传参的本质
3.1 一个常见问题
c
void test(int arr[]) {
int sz2 = sizeof(arr) / sizeof(arr[0]);
printf("sz2 = %d\n", sz2); // 输出 1(而不是10)
}
int main() {
int arr[10] = {1,2,3,4,5,6,7,8,9,10};
int sz1 = sizeof(arr) / sizeof(arr[0]);
printf("sz1 = %d\n", sz1); // 输出 10
test(arr);
return 0;
}
为什么函数内部无法正确获取数组长度?
3.2 本质原因
数组传参传递的是数组首元素的地址,而不是整个数组。
test(arr)中的arr是首元素地址- 形参
int arr[]本质上是一个指针,等价于int* arr sizeof(arr)在函数内部计算的是指针的大小(4或8字节),而不是数组的大小
c
// 以下两种写法完全等价
void test(int arr[]) { } // 写成数组形式
void test(int* arr) { } // 写成指针形式(本质)
结论:一维数组传参时,形参可以写成数组形式,也可以写成指针形式。但需要在函数外部计算好数组长度,作为参数传入。
四、冒泡排序
冒泡排序的核心思想是:两两相邻元素进行比较,每一轮将最大(或最小)的元素"冒泡"到末尾。
c
void bubble_sort(int arr[], int sz) {
for (int i = 0; i < sz - 1; i++) {
int flag = 1; // 假设已经有序
for (int j = 0; j < sz - i - 1; j++) {
if (arr[j] > arr[j + 1]) {
flag = 0; // 发生交换,说明无序
int tmp = arr[j];
arr[j] = arr[j + 1];
arr[j + 1] = tmp;
}
}
if (flag == 1) { // 这一趟没有交换,已经有序
break;
}
}
}
int main() {
int arr[] = {3, 1, 7, 5, 8, 9, 0, 2, 4, 6};
int sz = sizeof(arr) / sizeof(arr[0]);
bubble_sort(arr, sz);
for (int i = 0; i < sz; i++) {
printf("%d ", arr[i]);
}
return 0;
}
优化点 :使用 flag 标记,如果某一趟没有发生任何交换,说明数组已经有序,可以提前结束排序。
五、二级指针
指针变量也是变量,也有自己的地址。存放指针变量地址的指针,就是二级指针。
c
int a = 10;
int* pa = &a; // pa是一级指针
int** ppa = &pa; // ppa是二级指针
二级指针的运算:
c
*ppa == pa; // 解引用ppa,得到pa
**ppa == *pa == a; // 两次解引用,得到a
*ppa = &b; // 等价于 pa = &b
**ppa = 30; // 等价于 a = 30
六、指针数组
6.1 什么是指针数组?
指针数组是存放指针的数组。
类比理解:
- 整型数组:存放整型的数组 →
int arr[10] - 字符数组:存放字符的数组 →
char arr[10] - 指针数组:存放指针的数组 →
int* arr[10]
c
int a = 10, b = 20, c = 30;
int* parr[3] = {&a, &b, &c}; // 每个元素都是int*类型
6.2 指针数组模拟二维数组
利用指针数组可以模拟出二维数组的效果:
c
#include <stdio.h>
int main() {
int arr1[] = {1, 2, 3, 4, 5};
int arr2[] = {2, 3, 4, 5, 6};
int arr3[] = {3, 4, 5, 6, 7};
// parr是一个指针数组,每个元素指向一个一维数组
int* parr[3] = {arr1, arr2, arr3};
for (int i = 0; i < 3; i++) {
for (int j = 0; j < 5; j++) {
printf("%d ", parr[i][j]);
}
printf("\n");
}
return 0;
}
注意 :这种方式模拟的二维数组,各行在内存中不一定是连续的,而真正的二维数组是连续存放的。
七、字符指针变量
7.1 基本用法
c
char ch = 'w';
char* pc = &ch;
*pc = 'a'; // 修改ch的值为'a'
7.2 指向字符串的字符指针
c
const char* pstr = "hello bit.";
printf("%s\n", pstr); // 输出:hello bit.
重点理解 :pstr 中存放的不是整个字符串,而是字符串首字符 'h' 的地址。
7.3 经典笔试题
c
int main() {
char str1[] = "hello bit.";
char str2[] = "hello bit.";
const char* str3 = "hello bit.";
const char* str4 = "hello bit.";
if (str1 == str2)
printf("same\n");
else
printf("not same\n"); // 输出:not same
if (str3 == str4)
printf("same\n"); // 输出:same
else
printf("not same\n");
return 0;
}
原因分析:
str1和str2是数组,用常量字符串初始化时会开辟不同的内存空间str3和str4是指针,指向同一个常量字符串(存储在只读数据区),所以地址相同
八、数组指针变量
8.1 什么是数组指针?
数组指针是指向数组的指针变量。
c
int (*p)[10]; // p是一个指针,指向一个包含10个int元素的数组
注意区分:
int* p[10]→ 指针数组(有10个元素,每个是int*)int (*p)[10]→ 数组指针(1个指针,指向int10数组)
8.2 数组指针的初始化
c
int arr[10] = {0};
int (*p)[10] = &arr; // p指向整个数组
九、二维数组传参的本质
9.1 二维数组的内存模型
二维数组可以看做是"每个元素是一维数组"的数组:
- 二维数组的首元素是第一行(一个一维数组)
- 数组名
arr表示第一行的地址,类型是int(*)[5]
9.2 传参的两种写法
c
// 写法1:形参写成二维数组
void test1(int arr[3][5], int r, int c) { }
// 写法2:形参写成数组指针
void test2(int (*p)[5], int r, int c) { }
int main() {
int arr[3][5] = {{1,2,3,4,5}, {2,3,4,5,6}, {3,4,5,6,7}};
test1(arr, 3, 5);
test2(arr, 3, 5); // 完全等价
return 0;
}
访问二维数组元素 :*(*(p + i) + j) 等价于 p[i][j]
十、函数指针变量
10.1 函数的地址
函数也有地址,函数名就是函数的地址:
c
void test() {
printf("hehe\n");
}
int main() {
printf("%p\n", test); // 输出函数地址
printf("%p\n", &test); // 同样输出函数地址
return 0;
}
10.2 函数指针的定义和使用
c
int Add(int x, int y) {
return x + y;
}
int main() {
int (*pf)(int, int) = Add; // pf是函数指针变量
// 两种调用方式等价
printf("%d\n", (*pf)(2, 3)); // 5
printf("%d\n", pf(3, 5)); // 8
return 0;
}
函数指针类型解析:
int (*pf)(int x, int y)
│ │ │
│ │ └── 参数类型
│ └── 指针变量名
└── 返回类型
10.3 typedef 简化函数指针
c
// 重命名函数指针类型
typedef void(*pfun_t)(int); // pfun_t 是 void(*)(int) 的别名
// 使用
pfun_t signal(int, pfun_t);
十一、函数指针数组(转移表)
11.1 定义
函数指针数组是存放函数指针的数组:
c
int (*parr[5])(int, int); // parr是一个数组,有5个元素,每个元素是函数指针
11.2 应用:计算器的转移表实现
传统计算器使用 switch-case 语句,代码冗长。使用函数指针数组(转移表)可以让代码更简洁:
c
#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 div(int a, int b) { return a / b; }
int main() {
int x, y, input;
// 转移表:下标0对应exit,1对应add,2对应sub,3对应mul,4对应div
int (*p[5])(int, int) = { NULL, add, sub, mul, div };
do {
printf("1:add 2:sub 3:mul 4:div 0:exit\n");
printf("请选择:");
scanf("%d", &input);
if (input >= 1 && input <= 4) {
printf("输入操作数:");
scanf("%d %d", &x, &y);
int ret = p[input](x, y); // 通过转移表调用对应函数
printf("ret = %d\n", ret);
} else if (input == 0) {
printf("退出程序\n");
} else {
printf("输入有误\n");
}
} while (input);
return 0;
}
转移表的优势:
- 代码更简洁,易于扩展
- 避免了冗长的
switch-case语句 - 新增功能只需添加函数和更新转移表
总结
| 知识点 | 核心要点 |
|---|---|
| 数组名 | 通常是首元素地址;sizeof(arr)和&arr是例外 |
| 指针访问数组 | arr[i] == *(arr + i) == p[i] == *(p + i) |
| 一维数组传参 | 传递的是首元素地址,形参可以写成数组或指针 |
| 冒泡排序 | 两两比较,优化:使用flag标记提前结束 |
| 二级指针 | 存放指针变量的地址,**ppa访问原始变量 |
| 指针数组 | 存放指针的数组,int* arr[10] |
| 字符指针 | 指向常量字符串时,多个指针指向同一地址 |
| 数组指针 | 指向数组的指针,int (*p)[10] |
| 二维数组传参 | 形参用数组指针 int (*p)[列数] 接收 |
| 函数指针 | 存放函数地址,int (*pf)(int,int) |
| 转移表 | 函数指针数组,实现高效的多路分支 |
下一讲我们将继续深入,探讨 const 与指针的更多细节、指针运算的高级应用,以及函数指针数组等更加进阶的内容!
欢迎大家点赞收藏并在评论区留言!