指针是C语言的灵魂,也是初学者最头疼的知识点。它像一把锋利的刀,用得好能大幅提升代码效率,用不好则会让程序漏洞百出。今天这篇文章,我们从数组与指针的基础关系讲起,一步步揭开指针进阶类型的神秘面纱,最后用实战案例巩固所学——保证通俗易懂,还会标注所有重点和坑点。
一、数组与指针:绕不开的基础关系
1.1 数组名的本质:不是简单的地址
很多人以为"数组名就是首元素地址",这句话对但不完整。数组名的本质有两个例外,这是初学者最容易掉的坑!
重点结论:
- 一般情况:数组名表示数组首元素的地址。
例:int arr[10] = {1,2,...,10};
中,arr
与&arr[0]
地址相同。 - 两个例外(必须牢记):
sizeof(数组名)
:数组名代表整个数组,计算整个数组的字节大小。
例:sizeof(arr)
结果为40
(10个int,每个4字节),而非指针大小(4/8)。&数组名
:数组名代表整个数组,取出的是整个数组的地址(与首元素地址值相同,但偏移量不同)。
例:&arr + 1
偏移40字节(跳过整个数组),而arr + 1
偏移4字节(跳过一个元素)。
实战验证:
#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); // 首元素地址(等价于上一行)printf("&arr = %p\n", &arr); // 整个数组的地址(值相同,意义不同)// 关键差异:+1操作printf("&arr[0]+1 = %p\n", &arr[0]+1); // 跳过1个元素(+4字节)printf("arr+1 = %p\n", arr+1); // 跳过1个元素(+4字节)printf("&arr+1 = %p\n", &arr+1); // 跳过整个数组(+40字节)return 0;
}
输出结果解析:
前三个地址值相同,但&arr+1
会跳过整个数组(10个int,共40字节),而前两者只跳过1个元素(4字节)。这证明&arr
指向的是整个数组,而非单个元素。
1.2 用指针访问数组:灵活但要谨慎
有了对数组名的理解,我们可以用指针灵活访问数组元素。核心逻辑是:数组元素的访问本质是"首地址+偏移量"。
等价关系(重点):
arr[i]
等价于*(arr + i)
- 指针
p
指向首元素时,p[i]
等价于*(p + i)
示例代码:
#include <stdio.h>
int main() {int arr[5] = {10,20,30,40,50};int* p = arr; // p指向首元素// 两种访问方式等价printf("arr[2] = %d\n", arr[2]); // 30printf("*(p+2) = %d\n", *(p + 2)); // 30printf("p[2] = %d\n", p[2]); // 30(指针也支持下标)return 0;
}
1.3 一维数组传参:别被"数组形式"骗了
当数组作为参数传递给函数时,形参看似是数组,本质是指针。这也是为什么在函数内部用sizeof
求不出数组长度的原因。
- 数组传参实际传递的是首元素地址,而非整个数组。
- 函数形参两种写法(等价):
void test(int arr[]); // 数组形式(本质是指针) void test(int* arr); // 指针形式(更直观)
易错点演示:
#include <stdio.h>
// 形参写成数组形式,本质还是指针
void test(int arr[]) {printf("函数内sizeof(arr) = %d\n", sizeof(arr)); // 4或8(指针大小)
}int main() {int arr[10] = {0};printf("主函数内sizeof(arr) = %d\n", sizeof(arr)); // 40(整个数组大小)test(arr);return 0;
}
1.4.冒泡排序(指针应用)
- 核心:相邻元素比较交换,通过指针访问数组元素。
- 优化版(提前终止有序数组):
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]) {int tmp = arr[j];arr[j] = arr[j+1];arr[j+1] = tmp;flag = 0; // 发生交换,无序}}if(flag) break; // 无交换,直接退出} }
二、指针的进阶类型:从二级指针到数组指针
2.1 二级指针:指针的指针
指针变量也是变量,它的地址需要用"二级指针"存储。可以理解为:一级指针指向数据,二级指针指向一级指针。
示例图解:
int a = 10; // 数据
int* pa = &a; // 一级指针(指向a)
int** ppa = &pa; // 二级指针(指向pa)
操作逻辑:
*ppa
等价于pa
(通过二级指针获取一级指针)**ppa
等价于*pa
等价于a
(通过二级指针获取数据)
2.2 指针数组:存放指针的数组
指针数组是数组,其元素类型是指针。比如int* arr[5]
表示:一个有5个元素的数组,每个元素是int*
类型的指针。
用途:存储多个同类型地址
#include <stdio.h>
int main() {int arr1[] = {1,2,3};int arr2[] = {4,5,6};int arr3[] = {7,8,9};// 指针数组存储三个一维数组的首地址int* parr[3] = {arr1, arr2, arr3};// 访问arr2的第2个元素(5)printf("%d\n", parr[1][1]); // 等价于*(parr[1] + 1)return 0;
}
2.3 数组指针:指向数组的指针
数组指针是指针,它指向一个完整的数组。比如int (*p)[5]
表示:一个指针,指向"有5个int元素的数组"。
易混淆对比(重点):
定义 | 本质 | 解读 |
---|---|---|
int* p[5] | 指针数组 | 先与[] 结合,是数组,元素为int* |
int (*p)[5] | 数组指针 | 先与* 结合,是指针,指向int[5] 数组 |
数组指针的用法:
#include <stdio.h>
int main() {int arr[3][5] = {{1,2,3,4,5}, {6,7,8,9,10}, {11,12,13,14,15}};int (*p)[5] = arr; // arr是首行地址(指向第一行数组)// 访问第二行第三列元素(8)printf("%d\n", *(*(p + 1) + 2)); // 等价于p[1][2]return 0;
}
三、字符串与字符指针:藏着坑的常量
3.1 字符指针的两种用法
字符指针(char*
)既可以指向单个字符,也可以指向字符串的首字符。后者更常见,但要注意常量字符串的特性。
示例:
#include <stdio.h>
int main() {// 指向单个字符char ch = 'a';char* pc = &ch;// 指向字符串首字符(重点)const char* pstr = "hello"; // "hello"是常量字符串,不可修改printf("%s\n", pstr); // 打印整个字符串(从首字符开始直到'\0')return 0;
}
3.2 常量字符串的存储:节省空间的小技巧
C/C++会把相同的常量字符串存储在同一块内存中,这是容易踩坑的点。
示例(面试常考):
#include <stdio.h>
int main() {char str1[] = "hello"; // 数组:开辟新空间,存储"hello"char str2[] = "hello"; // 数组:再开辟新空间,存储"hello"const char* str3 = "hello"; // 指针:指向常量区的"hello"const char* str4 = "hello"; // 指针:指向同一块常量区空间printf("str1 == str2 ? %d\n", str1 == str2); // 0(地址不同)printf("str3 == str4 ? %d\n", str3 == str4); // 1(地址相同)return 0;
}
结论:
- 用常量字符串初始化数组时,每次都会开辟新空间
- 用常量字符串初始化字符指针时,多个指针可能指向同一块空间(节省内存)
四、二维数组传参:首行地址是关键
二维数组可以理解为"数组的数组"(每个元素是一维数组)。因此,二维数组的数组名表示首行的地址(即第一个一维数组的地址),类型是数组指针。
二维数组传参的正确方式:
#include <stdio.h>
// 形参可写成二维数组形式,或数组指针形式
void print_arr(int (*p)[5], int row, int col) {for (int i = 0; i < row; i++) {for (int j = 0; j < col; j++) {printf("%d ", p[i][j]); // 等价于*(*(p+i)+j)}printf("\n");}
}int main() {int arr[3][5] = {{1,2,3,4,5}, {6,7,8,9,10}, {11,12,13,14,15}};print_arr(arr, 3, 5); // 传递首行地址return 0;
}
五、函数指针:让指针指向代码
函数也有地址(函数名就是地址),用函数指针可以存储函数地址,实现更灵活的调用(比如回调函数)。
5.1 函数指针的定义与使用
定义格式:返回类型 (*指针名)(参数类型列表)
#include <stdio.h>
int add(int a, int b) {return a + b;
}int main() {// 定义函数指针,指向add函数int (*pf)(int, int) = add; // 等价于&add// 两种调用方式printf("add(2,3) = %d\n", add(2,3)); // 直接调用printf("pf(2,3) = %d\n", pf(2,3)); // 用指针调用printf("(*pf)(2,3) = %d\n", (*pf)(2,3)); // 等价写法return 0;
}
5.2 函数指针数组与转移表:简化多分支逻辑
函数指针数组是存储函数指针的数组,适合实现"菜单-功能"类逻辑(如计算器),替代冗长的switch-case。
实战案例:用函数指针数组实现计算器
#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;// 函数指针数组(转移表):下标1-4对应加减乘除int (*operate[5])(int, int) = {0, add, sub, mul, div};do {printf("1:加 2:减 3:乘 4:除 0:退出\n");printf("请选择:");scanf("%d", &input);if (input >= 1 && input <= 4) {printf("输入操作数:");scanf("%d %d", &x, &y);// 用数组下标调用对应函数printf("结果: %d\n", operate[input](x, y));} else if (input != 0) {printf("输入错误!\n");}} while (input != 0);return 0;
}
优势:
- 新增功能只需添加函数并更新数组,无需修改分支逻辑
- 代码更简洁,可读性更高
六、typedef:给复杂类型起"小名"
typedef
可以为复杂类型(如指针、数组指针、函数指针)重命名,简化代码。但要注意与#define
的区别。
6.1 用法示例:
#include <stdio.h>
// 重命名基本类型
typedef unsigned int uint;// 重命名指针类型
typedef int* int_ptr;// 重命名数组指针
typedef int (*arr_ptr)[5]; // 指向int[5]数组的指针// 重命名函数指针
typedef int (*calc_func)(int, int); // 指向"int(int,int)"函数的指针int main() {uint a = 10; // 等价于unsigned intint_ptr p1, p2; // p1和p2都是int*(指针)arr_ptr parr; // 等价于int (*parr)[5]calc_func pf = add; // 等价于int (*pf)(int,int)return 0;
}
6.2 与#define的区别(易错点):
#define
是简单替换,而typedef
是真正的类型重命名。
#include <stdio.h>
typedef int* int_ptr; // 类型重命名
#define INT_PTR int* // 宏替换int main() {int_ptr p1, p2; // p1和p2都是int*(正确)INT_PTR p3, p4; // 替换后为int* p3, p4; → p4是int(错误)return 0;
}
七、总结与易错点回顾
- 数组名的两个例外:
sizeof(数组名)
和&数组名
表示整个数组 - 数组传参本质:形参是指针,需额外传递长度
- 指针数组vs数组指针:前者是数组(存指针),后者是指针(指向数组)
- 常量字符串存储:相同常量字符串可能共享内存,数组初始化则不共享
- 函数指针数组:适合实现多功能菜单,替代switch-case
- typedef与#define:typedef是类型重命名,#define是文本替换
指针虽然复杂,但只要抓住"地址"和"类型"两个核心(地址决定指向哪里,类型决定+1跳过多少字节),就能逐步掌握。多写代码验证,少死记硬背,才是学好指针的关键!