在C语言中,指针和数组有着非常紧密的联系。数组名本质上是一个指向数组首元素的常量指针,而指针算术则可以方便地遍历数组。 理解它们之间的关系,不仅可以写出更高效的代码,还能为学习更复杂的数据结构(如动态数组、字符串处理、多维数组等)打下坚实基础。 本章将深入探讨指针与数组的多种应用场景。
在C语言中,数组名代表数组首元素的地址,即 arr 等价于 &arr[0]。数组名是一个常量指针,不能对其进行赋值操作。
#include <stdio.h>
int main() {
int arr[5] = {10, 20, 30, 40, 50};
printf("arr = %p\n", arr);
printf("&arr[0] = %p\n", &arr[0]);
printf("arr 指向的值: %d\n", *arr); // 10
// arr = &arr[1]; // 错误!数组名是常量,不能作为左值
// 但可以用指针变量指向数组
int *p = arr; // 合法,p 是变量
p++; // 现在 p 指向 arr[1]
printf("p 指向: %d\n", *p); // 20
return 0;
}
C语言中,下标运算符 [] 实际上是基于指针算术的语法糖。表达式 arr[i] 等价于 *(arr + i)。
因此,指针加整数可以实现数组元素的遍历。
#include <stdio.h>
int main() {
int arr[5] = {10, 20, 30, 40, 50};
int *p = arr;
// 使用下标访问
for (int i = 0; i < 5; i++) {
printf("arr[%d] = %d\n", i, arr[i]);
}
// 使用指针算术访问
for (int i = 0; i < 5; i++) {
printf("*(arr + %d) = %d\n", i, *(arr + i));
}
// 使用指针变量遍历
for (int i = 0; i < 5; i++) {
printf("*(p + %d) = %d\n", i, *(p + i));
}
// 甚至可以用指针的移动方式
int *q = arr;
for (int i = 0; i < 5; i++) {
printf("%d ", *q);
q++; // 移动指针
}
printf("\n");
return 0;
}
整数 * sizeof(指向类型)。这是C语言自动处理的,无需手动计算字节数。
二维数组在内存中是按行优先顺序连续存储的。二维数组名是指向第一行的指针(即行指针)。理解二维数组与指针的关系需要区分几种不同的指针类型。
#include <stdio.h>
int main() {
int matrix[3][4] = {
{1, 2, 3, 4},
{5, 6, 7, 8},
{9, 10, 11, 12}
};
// matrix 的类型是 int (*)[4],指向一个包含4个整型的一维数组
printf("matrix = %p\n", matrix);
printf("matrix[0] = %p\n", matrix[0]); // 首行首元素的地址,类型 int*
printf("&matrix[0][0] = %p\n", &matrix[0][0]);
// 访问元素
printf("matrix[1][2] = %d\n", matrix[1][2]); // 7
printf("*(*(matrix+1)+2) = %d\n", *(*(matrix+1)+2)); // 7
// 使用行指针遍历
int (*row)[4] = matrix; // 行指针,指向一行
for (int i = 0; i < 3; i++) {
for (int j = 0; j < 4; j++) {
printf("%3d ", row[i][j]); // 等价于 *(*(row+i)+j)
}
printf("\n");
}
return 0;
}
matrix 是 int (*)[4] 类型,matrix+1 跳过一行(4个int),*(matrix+1) 得到第二行的首地址(int*),再加列偏移得到元素地址。
这是初学者最容易混淆的两个概念,理解它们的声明语法至关重要。
| 术语 | 声明 | 含义 | 示例 |
|---|---|---|---|
| 指针数组 | int *p[5]; |
一个数组,包含5个元素,每个元素都是指向 int 的指针。 |
常用于字符串数组:char *names[] = {"Alice", "Bob", "Charlie"}; |
| 数组指针 | int (*p)[5]; |
一个指针,指向一个包含5个 int 元素的数组。 |
用于指向二维数组的行:int (*row)[4] = matrix; |
#include <stdio.h>
int main() {
// 指针数组:存储多个指针
int a = 10, b = 20, c = 30;
int *ptrArr[3] = {&a, &b, &c};
for (int i = 0; i < 3; i++) {
printf("%d ", *ptrArr[i]); // 10 20 30
}
printf("\n");
// 数组指针:指向一个数组
int arr[5] = {1, 2, 3, 4, 5};
int (*p)[5] = &arr; // p 指向整个数组
for (int i = 0; i < 5; i++) {
printf("%d ", (*p)[i]); // 注意:(*p)[i] 等价于 arr[i]
}
printf("\n");
return 0;
}
字符串数组通常使用指针数组实现,每个元素指向一个字符串常量或字符数组。
#include <stdio.h>
int main() {
// 字符串数组(指针数组)
char *fruits[] = {"Apple", "Banana", "Cherry", "Date"};
int n = sizeof(fruits) / sizeof(fruits[0]);
for (int i = 0; i < n; i++) {
printf("%s\n", fruits[i]);
}
// 可以通过指针修改指向(但不能修改字符串常量内容)
fruits[1] = "Blueberry"; // 重新指向另一个字符串常量
printf("修改后: %s\n", fruits[1]);
// 如果需要修改字符串内容,应使用二维字符数组
char colors[][10] = {"Red", "Green", "Blue"};
colors[0][0] = 'r'; // 可以修改
printf("%s\n", colors[0]); // "red"
return 0;
}
当数组作为函数参数时,实际传递的是数组首元素的指针,因此数组的大小信息会丢失。通常需要额外传递数组长度。
#include <stdio.h>
// 以下两种写法等价,形参 arr 都是指针
void printArray(int arr[], int size) { // 实际是 int *arr
for (int i = 0; i < size; i++) {
printf("%d ", arr[i]);
}
printf("\n");
}
void printArray2(int *arr, int size) { // 完全等价
for (int i = 0; i < size; i++) {
printf("%d ", *(arr + i));
}
printf("\n");
}
// 二维数组作为参数,必须指定第二维的大小
void printMatrix(int matrix[][4], int rows) {
for (int i = 0; i < rows; i++) {
for (int j = 0; j < 4; j++) {
printf("%3d ", matrix[i][j]);
}
printf("\n");
}
}
// 等价写法:使用行指针
void printMatrix2(int (*matrix)[4], int rows) {
for (int i = 0; i < rows; i++) {
for (int j = 0; j < 4; j++) {
printf("%3d ", matrix[i][j]);
}
printf("\n");
}
}
int main() {
int arr[5] = {1, 2, 3, 4, 5};
printArray(arr, 5);
printArray2(arr, 5);
int mat[3][4] = {
{1, 2, 3, 4},
{5, 6, 7, 8},
{9, 10, 11, 12}
};
printMatrix(mat, 3);
printMatrix2(mat, 3);
return 0;
}
sizeof(arr) 获取数组长度,因为 arr 已经退化为指针。必须将长度作为参数传递。
#include <stdio.h>
#include <string.h>
// 交换两个字符串指针
void swap(char **a, char **b) {
char *temp = *a;
*a = *b;
*b = temp;
}
// 冒泡排序字符串数组
void sortStrings(char *arr[], int n) {
for (int i = 0; i < n - 1; i++) {
for (int j = 0; j < n - i - 1; j++) {
if (strcmp(arr[j], arr[j + 1]) > 0) {
swap(&arr[j], &arr[j + 1]);
}
}
}
}
int main() {
char *fruits[] = {"Banana", "Apple", "Date", "Cherry", "Elderberry"};
int n = sizeof(fruits) / sizeof(fruits[0]);
printf("排序前:\n");
for (int i = 0; i < n; i++) {
printf("%s\n", fruits[i]);
}
sortStrings(fruits, n);
printf("\n排序后(字典序):\n");
for (int i = 0; i < n; i++) {
printf("%s\n", fruits[i]);
}
return 0;
}
int *p[5] 是指针数组,int (*p)[5] 是数组指针。arr = p; 非法。'\0' 结尾)。int arr[3][4] 传给 int **p 会导致类型错误,必须使用 int (*p)[4]。
指针与数组的结合是C语言强大灵活性的体现。掌握这些概念后,你可以更高效地操作数据,并为学习动态内存分配、链表、树等高级主题做好准备。
下一章我们将学习动态内存管理,使用 malloc 和 free 在运行时分配内存。