MK
摩柯社区 - 一个极简的技术知识社区
AI 面试

C语言指针与数组之间的微妙关系

2023-08-097.4k 阅读

指针与数组基础概念回顾

C语言指针基础

在C语言中,指针是一种特殊的变量,它存储的是内存地址。通过指针,我们可以直接访问和操作内存中的数据。例如,定义一个整型指针变量 p

int num = 10;
int *p = #

这里,* 表示 p 是一个指针变量,& 是取地址运算符,用于获取变量 num 的内存地址并赋值给指针 p。我们可以通过指针 p 来访问和修改 num 的值:

*p = 20; // 通过指针修改num的值

*p 称为间接访问运算符,它用于访问指针所指向的内存地址中的值。

数组基础概念

数组是一种聚合数据类型,它由一组相同类型的元素组成。例如,定义一个整型数组 arr

int arr[5] = {1, 2, 3, 4, 5};

这里,arr 是一个包含5个整型元素的数组。数组元素通过下标来访问,下标从0开始。例如,arr[0] 表示数组的第一个元素,值为1;arr[1] 表示第二个元素,值为2,以此类推。

指针与数组的相似性

数组名作为指针

在C语言中,数组名在大多数情况下会被自动转换为指向数组首元素的指针。例如:

int arr[5] = {1, 2, 3, 4, 5};
int *p = arr;

这里,arr 被自动转换为指向 arr[0] 的指针,然后将该指针赋值给 p。此时,parr 在数值上是相等的,都指向数组的首元素地址。

我们可以通过指针 p 来访问数组元素,就像使用数组下标一样:

printf("%d\n", p[0]); // 输出1
printf("%d\n", p[1]); // 输出2

同样,我们也可以使用指针算术运算来访问数组元素:

printf("%d\n", *(p + 2)); // 输出3

这里,p + 2 表示指针 p 向后移动两个整型元素的位置,*(p + 2) 则访问该位置上的元素。

指针与数组下标的等价性

通过指针访问数组元素和使用数组下标访问数组元素在本质上是等价的。例如,对于数组 arrarr[i]*(arr + i) 是完全等价的。这是因为编译器在处理 arr[i] 时,实际上会将其转换为 *(arr + i)

int arr[5] = {1, 2, 3, 4, 5};
for (int i = 0; i < 5; i++) {
    printf("%d ", arr[i]);
    printf("%d ", *(arr + i));
}

上述代码中,arr[i]*(arr + i) 都会输出相同的数组元素值。

指针与数组的区别

数组名的特殊性

虽然数组名在大多数情况下会被转换为指针,但它和普通指针还是有一些区别的。数组名是一个常量指针,它指向数组的首元素,并且不能被重新赋值。例如:

int arr[5] = {1, 2, 3, 4, 5};
arr = &arr[1]; // 错误,数组名不能被重新赋值

而普通指针变量是可以重新赋值的:

int arr[5] = {1, 2, 3, 4, 5};
int *p = arr;
p = &arr[1]; // 正确,指针变量可以重新赋值

内存分配与生命周期

数组的内存分配是在编译时确定的,其生命周期与所在的作用域相同。例如,在函数内部定义的数组,当函数结束时,数组占用的内存会被自动释放。

而指针变量本身只是一个存储地址的变量,它所指向的内存需要通过动态内存分配函数(如 malloc)来分配,并且需要手动释放(如使用 free)。例如:

int *p = (int *)malloc(5 * sizeof(int));
if (p != NULL) {
    // 使用指针p
    free(p); // 释放内存
}

如果忘记释放动态分配的内存,就会导致内存泄漏。

数组与指针的 sizeof 运算

sizeof 运算符对于数组和指针的行为不同。对于数组,sizeof 返回整个数组占用的内存字节数。例如:

int arr[5] = {1, 2, 3, 4, 5};
printf("%zu\n", sizeof(arr)); // 输出20,假设int占4个字节

而对于指针,sizeof 返回指针变量本身占用的内存字节数,通常为4(32位系统)或8(64位系统)。例如:

int arr[5] = {1, 2, 3, 4, 5};
int *p = arr;
printf("%zu\n", sizeof(p)); // 输出4(32位系统)或8(64位系统)

指针数组与数组指针

指针数组

指针数组是一个数组,数组的每个元素都是一个指针。例如,定义一个指向整型的指针数组:

int num1 = 10, num2 = 20, num3 = 30;
int *ptrArr[3] = {&num1, &num2, &num3};

这里,ptrArr 是一个包含3个指针元素的数组,每个指针分别指向一个整型变量。我们可以通过指针数组来访问这些变量:

printf("%d\n", *ptrArr[0]); // 输出10
printf("%d\n", *ptrArr[1]); // 输出20

指针数组常用于处理多个字符串。因为在C语言中,字符串是以字符数组的形式存储的,而字符数组名可以看作是指向字符串首字符的指针。例如:

char *strArr[3] = {"Hello", "World", "C语言"};
for (int i = 0; i < 3; i++) {
    printf("%s\n", strArr[i]);
}

上述代码中,strArr 是一个指针数组,每个元素都是一个指向字符串的指针。通过遍历指针数组,可以依次输出每个字符串。

数组指针

数组指针是一个指针,它指向一个数组。例如,定义一个指向包含5个整型元素的数组的指针:

int arr[5] = {1, 2, 3, 4, 5};
int (*p)[5] = &arr;

这里,p 是一个数组指针,它指向数组 arr。注意,(*p) 两边的括号是必需的,否则 int *p[5] 就会被解析为指针数组。

通过数组指针访问数组元素时,需要使用双重下标。例如:

printf("%d\n", (*p)[0]); // 输出1
printf("%d\n", (*p)[1]); // 输出2

这里,*p 表示取指针 p 所指向的数组,(*p)[0] 表示取该数组的第一个元素。

数组指针在处理二维数组时非常有用。例如,对于一个二维数组 matrix

int matrix[3][4] = {
    {1, 2, 3, 4},
    {5, 6, 7, 8},
    {9, 10, 11, 12}
};
int (*p)[4] = matrix;
for (int i = 0; i < 3; i++) {
    for (int j = 0; j < 4; j++) {
        printf("%d ", (*(p + i))[j]);
    }
    printf("\n");
}

这里,p 是一个指向包含4个整型元素的数组的指针,p + i 表示指向 matrix[i] 这一行,(*(p + i))[j] 则表示访问 matrix[i][j] 这个元素。

指针与数组在函数参数中的传递

数组作为函数参数

当数组作为函数参数传递时,实际上传递的是数组首元素的指针。例如:

void printArray(int arr[], int size) {
    for (int i = 0; i < size; i++) {
        printf("%d ", arr[i]);
    }
    printf("\n");
}

int main() {
    int arr[5] = {1, 2, 3, 4, 5};
    printArray(arr, 5);
    return 0;
}

printArray 函数中,arr 实际上是一个指针,它指向调用函数中数组的首元素。这就是为什么在函数内部无法通过 sizeof(arr) 得到数组的实际大小,而需要额外传递一个参数 size 来表示数组的大小。

指针作为函数参数

指针作为函数参数与数组作为函数参数在本质上是类似的,因为数组名在传递时会被转换为指针。例如:

void incrementArray(int *p, int size) {
    for (int i = 0; i < size; i++) {
        (*p)++;
        p++;
    }
}

int main() {
    int arr[5] = {1, 2, 3, 4, 5};
    incrementArray(arr, 5);
    for (int i = 0; i < 5; i++) {
        printf("%d ", arr[i]);
    }
    printf("\n");
    return 0;
}

incrementArray 函数中,p 是一个指针,通过它可以访问和修改调用函数中数组的元素。

处理二维数组的函数参数

当处理二维数组作为函数参数时,需要特别注意。例如:

void printMatrix(int matrix[][4], int rows) {
    for (int i = 0; i < rows; i++) {
        for (int j = 0; j < 4; j++) {
            printf("%d ", matrix[i][j]);
        }
        printf("\n");
    }
}

int main() {
    int matrix[3][4] = {
        {1, 2, 3, 4},
        {5, 6, 7, 8},
        {9, 10, 11, 12}
    };
    printMatrix(matrix, 3);
    return 0;
}

printMatrix 函数中,matrix 是一个指向包含4个整型元素的数组的指针,matrix[i] 表示第 i 行,matrix[i][j] 表示第 i 行第 j 列的元素。这里第二维的大小必须明确指定,因为编译器需要知道每行的元素个数来正确计算内存地址。

动态内存分配与指针数组、数组指针

动态分配指针数组

有时我们需要动态分配指针数组。例如,假设我们要存储多个字符串,并且字符串的数量在运行时才能确定:

#include <stdio.h>
#include <stdlib.h>
#include <string.h>

int main() {
    int numStrings = 3;
    char **strArr = (char **)malloc(numStrings * sizeof(char *));
    if (strArr == NULL) {
        perror("malloc");
        return 1;
    }

    char *strings[] = {"Hello", "World", "C语言"};
    for (int i = 0; i < numStrings; i++) {
        int len = strlen(strings[i]) + 1;
        strArr[i] = (char *)malloc(len * sizeof(char));
        if (strArr[i] == NULL) {
            perror("malloc");
            for (int j = 0; j < i; j++) {
                free(strArr[j]);
            }
            free(strArr);
            return 1;
        }
        strcpy(strArr[i], strings[i]);
    }

    for (int i = 0; i < numStrings; i++) {
        printf("%s\n", strArr[i]);
    }

    for (int i = 0; i < numStrings; i++) {
        free(strArr[i]);
    }
    free(strArr);

    return 0;
}

在上述代码中,首先动态分配了一个指针数组 strArr,然后为每个指针元素分配内存来存储字符串,并将字符串复制到相应的内存位置。最后,记得释放所有动态分配的内存,以避免内存泄漏。

动态分配数组指针

动态分配数组指针通常用于创建动态二维数组。例如:

#include <stdio.h>
#include <stdlib.h>

int main() {
    int rows = 3, cols = 4;
    int (*matrix)[cols] = (int (*)[cols])malloc(rows * cols * sizeof(int));
    if (matrix == NULL) {
        perror("malloc");
        return 1;
    }

    for (int i = 0; i < rows; i++) {
        for (int j = 0; j < cols; j++) {
            matrix[i][j] = i * cols + j;
        }
    }

    for (int i = 0; i < rows; i++) {
        for (int j = 0; j < cols; j++) {
            printf("%d ", matrix[i][j]);
        }
        printf("\n");
    }

    free(matrix);

    return 0;
}

这里,通过 malloc 动态分配了一个指向包含 cols 个整型元素的数组的指针 matrix,并将其当作二维数组来使用。最后,释放动态分配的内存。

指针与数组在结构体中的应用

结构体中的数组成员

结构体可以包含数组成员。例如,定义一个包含整型数组的结构体:

struct Point {
    int coordinates[3];
};

int main() {
    struct Point p = {1, 2, 3};
    printf("X: %d, Y: %d, Z: %d\n", p.coordinates[0], p.coordinates[1], p.coordinates[2]);
    return 0;
}

在上述代码中,Point 结构体包含一个整型数组 coordinates,用于存储点的三维坐标。

结构体中的指针成员

结构体也可以包含指针成员。例如,定义一个包含指向字符串的指针的结构体:

struct Person {
    char *name;
    int age;
};

int main() {
    struct Person p;
    p.name = "John";
    p.age = 30;
    printf("Name: %s, Age: %d\n", p.name, p.age);
    return 0;
}

这里,Person 结构体包含一个指向字符串的指针 name 和一个整型成员 age

结构体数组与指针

我们可以定义结构体数组和指向结构体的指针。例如:

struct Point {
    int x;
    int y;
};

int main() {
    struct Point points[2] = {{1, 2}, {3, 4}};
    struct Point *p = points;

    for (int i = 0; i < 2; i++) {
        printf("Point %d: (%d, %d)\n", i + 1, (p + i)->x, (p + i)->y);
    }

    return 0;
}

在上述代码中,points 是一个结构体数组,p 是指向结构体数组首元素的指针。通过指针 p 可以遍历结构体数组,并访问每个结构体的成员。

指针与数组在链表中的应用

链表节点中的指针与数组

链表是一种重要的数据结构,它由一系列节点组成,每个节点包含数据和指向下一个节点的指针。例如,定义一个简单的链表节点结构体,其中数据部分是一个整型数组:

struct ListNode {
    int data[5];
    struct ListNode *next;
};

在这个结构体中,data 是一个整型数组,用于存储数据,next 是一个指针,指向下一个链表节点。

使用指针遍历链表

通过指针可以方便地遍历链表。例如,创建一个简单的链表并遍历它:

#include <stdio.h>
#include <stdlib.h>

struct ListNode {
    int data;
    struct ListNode *next;
};

struct ListNode* createNode(int value) {
    struct ListNode *newNode = (struct ListNode *)malloc(sizeof(struct ListNode));
    if (newNode == NULL) {
        perror("malloc");
        return NULL;
    }
    newNode->data = value;
    newNode->next = NULL;
    return newNode;
}

void printList(struct ListNode *head) {
    struct ListNode *current = head;
    while (current != NULL) {
        printf("%d -> ", current->data);
        current = current->next;
    }
    printf("NULL\n");
}

int main() {
    struct ListNode *head = createNode(1);
    head->next = createNode(2);
    head->next->next = createNode(3);

    printList(head);

    // 释放链表内存
    struct ListNode *temp;
    while (head != NULL) {
        temp = head;
        head = head->next;
        free(temp);
    }

    return 0;
}

在上述代码中,通过指针 head 来表示链表的头节点,通过 next 指针来遍历链表。注意在使用完链表后,要释放所有动态分配的节点内存,以避免内存泄漏。

链表与数组的转换

有时我们需要在链表和数组之间进行转换。例如,将链表中的数据复制到数组中:

#include <stdio.h>
#include <stdlib.h>

struct ListNode {
    int data;
    struct ListNode *next;
};

struct ListNode* createNode(int value) {
    struct ListNode *newNode = (struct ListNode *)malloc(sizeof(struct ListNode));
    if (newNode == NULL) {
        perror("malloc");
        return NULL;
    }
    newNode->data = value;
    newNode->next = NULL;
    return newNode;
}

int listToArray(struct ListNode *head, int *arr) {
    int count = 0;
    struct ListNode *current = head;
    while (current != NULL) {
        arr[count++] = current->data;
        current = current->next;
    }
    return count;
}

int main() {
    struct ListNode *head = createNode(1);
    head->next = createNode(2);
    head->next->next = createNode(3);

    int arr[10];
    int size = listToArray(head, arr);

    for (int i = 0; i < size; i++) {
        printf("%d ", arr[i]);
    }
    printf("\n");

    // 释放链表内存
    struct ListNode *temp;
    while (head != NULL) {
        temp = head;
        head = head->next;
        free(temp);
    }

    return 0;
}

在上述代码中,listToArray 函数将链表中的数据复制到数组 arr 中,并返回数组中实际存储的数据个数。这种转换在实际应用中非常有用,例如在需要对链表数据进行排序或其他数组相关操作时。

通过以上详细的介绍和丰富的代码示例,我们深入探讨了C语言中指针与数组之间的微妙关系,希望能帮助读者更好地理解和运用这两个重要的概念。无论是在日常编程还是复杂项目开发中,准确把握指针与数组的特性和相互关系,都能让我们编写出更高效、更健壮的C语言程序。