指针与数组

在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;
}
二维数组的内存布局(行优先):
[1][2][3][4][5][6][7][8][9][10][11][12]
理解: matrixint (*)[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语言强大灵活性的体现。掌握这些概念后,你可以更高效地操作数据,并为学习动态内存分配、链表、树等高级主题做好准备。 下一章我们将学习动态内存管理,使用 mallocfree 在运行时分配内存。