深入理解C语言指针(二):数组名、二级指针与指针数组

深入理解C语言指针(二)

1. 数组名的含义

多数表达式中,数组名会转换为指向首元素的指针:

1
2
int arr[5] = {1, 2, 3, 4, 5};
int *p = arr; // 等价于 &arr[0]

但有两个典型例外:

  1. sizeof(arr) 中,arr 表示整个数组;
  2. &arr 中,取得的是整个数组的地址。
1
2
3
printf("%zu\n", sizeof(arr)); // 5 * sizeof(int)
printf("%p\n", (void*)arr);
printf("%p\n", (void*)&arr);

arr 与 &arr 的数值地址看起来相同,但类型不同。arr + 1 跨过一个 int,&arr + 1 跨过整个数组。

2. 使用指针访问数组

1
2
for (int i = 0; i < 5; ++i)
printf("%d ", *(arr + i));

arr[i] 定义上等价于 *(arr + i)。为了可读性,普通数组遍历通常仍推荐下标写法。

3. 一维数组传参的本质

1
2
void print_array(const int arr[], int size);
void print_array(const int *arr, int size);

以上两个函数声明等价。数组传参后,函数得到的是首元素地址,不知道原数组有多少元素,因此长度必须额外传递。

4. 冒泡排序

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
void bubble_sort(int arr[], int size)
{
for (int end = size - 1; end > 0; --end)
{
int exchanged = 0;
for (int i = 0; i < end; ++i)
{
if (arr[i] > arr[i + 1])
{
int tmp = arr[i];
arr[i] = arr[i + 1];
arr[i + 1] = tmp;
exchanged = 1;
}
}
if (!exchanged)
break;
}
}

平均与最坏时间复杂度为 O(n²),增加交换标记后,有序数组的最好情况可提前结束。

5. 二级指针

二级指针保存一级指针的地址:

1
2
3
4
5
6
int value = 10;
int *p = &value;
int **pp = &p;

printf("%d\n", **pp);
**pp = 20;

常见用途包括:

  • 函数中修改调用者的指针;
  • 管理动态二维结构;
  • 处理字符串数组和命令行参数。
1
2
3
4
void reset_pointer(int **pp, int *target)
{
*pp = target;
}

6. 指针数组

指针数组首先是数组,数组中的每个元素都是指针:

1
2
3
int a = 1, b = 2, c = 3;
int *arr[3] = {&a, &b, &c};
printf("%d\n", *arr[1]);

int *arr[3] 与数组指针 int (*p)[3] 完全不同。判断方法是先看变量名与谁结合:arr 先与 [] 结合,所以是数组;p 被括号包裹并先与 * 结合,所以是指针。

7. 指针数组模拟二维数组

1
2
3
4
5
6
int row1[] = {1, 2, 3};
int row2[] = {4, 5, 6};
int row3[] = {7, 8, 9};
int *matrix[] = {row1, row2, row3};

printf("%d\n", matrix[1][2]); // 6

这种结构的每一行可以位于不同内存区域,行长度也可以不同。它不同于真正连续存储的 int matrix[3][3]。

8. 总结

数组名、数组指针、指针数组和二级指针名称相近但结构不同。分析声明时先找到变量名,再按优先级向外阅读,同时结合它实际指向的对象判断是否合法。