C: 指针与数组
数组和指针就像同一个人的正面和侧面——从不同角度看,它们呈现出不同的形态,但底层指向同一片连续的内存空间。
1. 数组名即首元素地址
数组名在大多数表达式中会自动转换为指向首元素的指针:
int arr[] = {10, 20, 30, 40, 50};
int *p = arr;
printf("%p\n", (void *)arr);
printf("%p\n", (void *)&arr[0]);
printf("%p\n", (void *)p);
三个地址完全相同。arr和&arr[0]等价,都是首元素的地址。
但也有例外:sizeof(arr)返回整个数组的大小,而非指针大小。&arr是指向整个数组的指针,类型是int (*)[5],步长是整个数组。
printf("%zu\n", sizeof(arr));
printf("%zu\n", sizeof(p));
在64位系统上分别输出20(5×4字节)和8(指针大小)。
2. 指针遍历数组
既然指针可以指向数组元素,就能通过指针运算遍历整个数组:
int arr[] = {10, 20, 30, 40, 50};
int *p;
int len = sizeof(arr) / sizeof(arr[0]);
for (p = arr; p < arr + len; p++) {
printf("%d ", *p);
}
printf("\n");
arr + len指向数组末尾之后的位置,作为循环终止标志。这是C语言中非常常见的模式。
也可以用下标方式:
int i;
for (i = 0; i < len; i++) {
printf("%d ", *(arr + i));
}
或者:
int *p = arr;
int i;
for (i = 0; i < len; i++) {
printf("%d ", p[i]);
}
p[i]和*(p + i)完全等价。下标运算是指针算术的语法糖。
3. 指针算术运算
指针支持有限的算术运算:
(1) 加减整数
int arr[] = {10, 20, 30, 40, 50};
int *p = arr + 2;
printf("%d\n", *p);
printf("%d\n", *(p + 1));
printf("%d\n", *(p - 1));
输出30、40、20。p+1向后移动一个int,p-1向前移动一个int。
(2) 指针减指针
同一数组内两个指针相减,得到元素个数差:
int *q = &arr[4];
printf("%ld\n", (long)(q - p));
输出2,因为p指向arr[2],q指向arr[4],相差2个元素。
(3) 指针比较
同一数组内的指针可以用<、<=、>、>=比较位置前后:
int *start = arr;
int *end = arr + len;
while (start < end) {
printf("%d ", *start);
start++;
}
(4) 指针步长总结
| 类型 | 步长(p+1移动字节数) |
|---|---|
char * |
1 |
int * |
4 |
double * |
8 |
int (*)[5] |
20 |
步长 = sizeof(指向的类型)
4. 指针与数组的等价关系
以下两种访问方式完全等价:
| 表达式 | 等价形式 |
|---|---|
arr[i] |
*(arr + i) |
&arr[i] |
arr + i |
p[i] |
*(p + i) |
&p[i] |
p + i |
编译器将下标运算转换为指针运算。arr[3]和3[arr]在C语言中甚至都合法(因为*(arr+3) == *(3+arr)),后者只是没人这么写。
但指针和数组有本质区别:
int arr[5] = {1, 2, 3, 4, 5};
int *p = arr;
arr是数组,sizeof(arr)= 20,不能重新赋值p是指针,sizeof(p)= 8(64位),可以重新赋值指向别处
▶ 示例
用指针实现数组逆序:
#include <stdio.h>
void reverse(int *arr, int len) {
int *left = arr;
int *right = arr + len - 1;
while (left < right) {
int temp = *left;
*left = *right;
*right = temp;
left++;
right--;
}
}
int main(void) {
int data[] = {1, 2, 3, 4, 5, 6, 7};
int len = sizeof(data) / sizeof(data[0]);
int i;
reverse(data, len);
for (i = 0; i < len; i++) {
printf("%d ", data[i]);
}
printf("\n");
return 0;
}
7 6 5 4 3 2 1
双指针法:left从头向尾移动,right从尾向头移动,交换它们指向的元素,直到相遇。
5. 数组作为函数参数的本质
当数组传给函数时,编译器只传递首元素地址,数组长度信息丢失。这是"数组退化为指针":
void func(int arr[]) {
}
参数int arr[]等价于int *arr。在函数内部:
sizeof(arr)是指针大小,不是数组大小- 不能用
sizeof计算元素个数 - 必须额外传递长度参数
void print_array(int *arr, int len) {
int i;
for (i = 0; i < len; i++) {
printf("%d ", arr[i]);
}
printf("\n");
}
四种等价写法:
void func(int *arr, int len);
void func(int arr[], int len);
void func(int arr[5], int len);
void func(int arr[100], int len);
方括号中的数字被编译器忽略,只表示"这是一个指针"。
▶ 示例
用指针实现数组去重(已排序数组):
#include <stdio.h>
int unique(int *arr, int len) {
if (len == 0) return 0;
int *dst = arr;
int *src = arr + 1;
int *end = arr + len;
while (src < end) {
if (*src != *dst) {
dst++;
*dst = *src;
}
src++;
}
return (int)(dst - arr + 1);
}
int main(void) {
int data[] = {1, 1, 2, 2, 2, 3, 4, 4, 5};
int len = sizeof(data) / sizeof(data[0]);
int new_len = unique(data, len);
int i;
for (i = 0; i < new_len; i++) {
printf("%d ", data[i]);
}
printf("\n");
return 0;
}
1 2 3 4 5
dst记录去重后的写入位置,src扫描原数组。返回新长度。这是原地去重算法,不需要额外数组。
❓ 常见问题
arr = p;非法,数组名是常量指针。但p = arr;合法,指针变量可以赋值。arr和&arr有什么区别?arr是首元素地址,类型int *,步长sizeof(int);&arr是整个数组的地址,类型int (*)[5],步长sizeof(int[5])。数值相同但类型不同。p[-1]合法吗?*(p-1)。如果p指向数组中间,p[-1]访问前一个元素是安全的。但如果超出数组范围则是越界。📖 小节
- 数组名在表达式中退化为首元素指针,但sizeof和&除外
arr[i]与*(arr+i)完全等价,下标是指针算术的语法糖- 指针算术以步长为单位,步长=sizeof(指向类型)
- 数组传参退化为指针,必须额外传递长度
- 双指针技巧可以高效解决逆序、去重等问题
📝 作业
- 编写函数用指针方式实现数组元素左移一位(第一个元素移到末尾),不使用额外数组。
- 编写函数
int *find(int *arr, int len, int target),返回第一个等于target的元素指针,未找到返回NULL。 - 给定一个已排序数组,用指针实现二分查找,找到返回元素指针,未找到返回NULL。