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

C语言指针数组的定义与使用

2022-05-014.7k 阅读

一、指针数组的定义

在C语言中,指针数组是一种特殊的数组,其数组元素都是指针类型。指针数组的定义形式为:数据类型 *数组名[数组长度]。这里的数据类型是指针所指向的数据的类型,数组名是自定义的标识符,数组长度则规定了该数组能够容纳的指针数量。

例如,定义一个指向 int 类型数据的指针数组:

int *ptrArray[5];

在这个例子中,ptrArray 是一个指针数组,它可以容纳5个指向 int 类型数据的指针。

(一)数据类型的选择

指针数组所指向的数据类型可以是C语言中的任何基本数据类型,如 charintfloatdouble 等,也可以是自定义的数据类型,比如结构体和联合体。

1. 指向基本数据类型

以指向 float 类型为例:

float *floatPtrArray[3];

这里 floatPtrArray 是一个指针数组,它的每个元素都是指向 float 类型数据的指针。

2. 指向结构体类型

假设有如下结构体定义:

struct Student {
    char name[20];
    int age;
};

我们可以定义一个指向 struct Student 类型的指针数组:

struct Student *studentPtrArray[2];

这样,studentPtrArray 数组中的每个元素都可以指向一个 struct Student 类型的结构体变量。

(二)数组长度的确定

数组长度必须是一个常量表达式。在C99标准之前,数组长度必须是在编译时就能确定的常量。例如:

#define ARRAY_SIZE 4
int *intPtrArray[ARRAY_SIZE];

在C99标准中,引入了变长数组(VLA)的概念,允许使用变量来定义数组长度,但这在实际应用中并不常见,因为变长数组的生命周期和作用域受到限制,而且可能会导致栈溢出等问题。例如:

int size = 3;
int *vlaPtrArray[size]; // C99 支持的变长数组定义

二、指针数组的初始化

指针数组在定义后可以进行初始化,初始化的方式有多种,这取决于具体的需求和使用场景。

(一)静态初始化

静态初始化是在定义指针数组的同时对其元素进行初始化。

1. 指向常量数据

int num1 = 10;
int num2 = 20;
int *ptrArray[2] = {&num1, &num2};

在这个例子中,ptrArray 数组的第一个元素指向 num1,第二个元素指向 num2

2. 指向字符串常量

指针数组经常用于处理字符串。例如:

char *strArray[3] = {"apple", "banana", "cherry"};

这里 strArray 是一个指针数组,每个元素都是一个指向字符串常量的指针。实际上,每个字符串常量在内存中是以字符数组的形式存储的,并且以 '\0' 结尾。这些指针分别指向各个字符串常量的首地址。

(二)动态初始化

动态初始化是在程序运行过程中,根据实际需要为指针数组的元素分配内存并进行赋值。

1. 为指向基本数据类型的指针数组动态初始化

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

int main() {
    int n = 3;
    int *ptrArray[n];
    for (int i = 0; i < n; i++) {
        ptrArray[i] = (int *)malloc(sizeof(int));
        if (ptrArray[i] == NULL) {
            printf("内存分配失败\n");
            return 1;
        }
        *ptrArray[i] = i * 10;
    }

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

    return 0;
}

在这个程序中,首先定义了一个指针数组 ptrArray,然后使用 malloc 函数为每个指针元素分配内存,并赋值。最后记得使用 free 函数释放分配的内存,以避免内存泄漏。

2. 为指向结构体类型的指针数组动态初始化

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

struct Student {
    char name[20];
    int age;
};

int main() {
    int n = 2;
    struct Student *studentPtrArray[n];
    for (int i = 0; i < n; i++) {
        studentPtrArray[i] = (struct Student *)malloc(sizeof(struct Student));
        if (studentPtrArray[i] == NULL) {
            printf("内存分配失败\n");
            return 1;
        }
        if (i == 0) {
            strcpy(studentPtrArray[i]->name, "Alice");
            studentPtrArray[i]->age = 20;
        } else {
            strcpy(studentPtrArray[i]->name, "Bob");
            studentPtrArray[i]->age = 22;
        }
    }

    for (int i = 0; i < n; i++) {
        printf("Student %d: Name = %s, Age = %d\n", i + 1, studentPtrArray[i]->name, studentPtrArray[i]->age);
        free(studentPtrArray[i]);
    }

    return 0;
}

这个程序为指向 struct Student 结构体的指针数组动态分配内存,并初始化结构体成员。同样,最后需要释放分配的内存。

三、指针数组的使用

指针数组在实际编程中有广泛的应用,下面将介绍一些常见的使用场景。

(一)处理字符串数组

如前文所述,指针数组非常适合处理字符串数组。由于每个字符串的长度可能不同,使用指针数组可以更灵活地管理这些字符串,而不必像二维字符数组那样为每个字符串分配固定长度的空间。

#include <stdio.h>

int main() {
    char *strArray[3] = {"hello", "world", "c language"};
    for (int i = 0; i < 3; i++) {
        printf("%s\n", strArray[i]);
    }
    return 0;
}

在这个例子中,strArray 是一个指针数组,每个元素指向一个字符串常量。通过遍历指针数组,可以方便地输出每个字符串。

(二)实现动态二维数组

在C语言中,二维数组的大小在编译时就需要确定。而使用指针数组可以模拟动态二维数组的行为。

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

int main() {
    int rows = 3;
    int cols = 4;
    int **dynamicArray = (int **)malloc(rows * sizeof(int *));
    if (dynamicArray == NULL) {
        printf("内存分配失败\n");
        return 1;
    }

    for (int i = 0; i < rows; i++) {
        dynamicArray[i] = (int *)malloc(cols * sizeof(int));
        if (dynamicArray[i] == NULL) {
            printf("内存分配失败\n");
            for (int j = 0; j < i; j++) {
                free(dynamicArray[j]);
            }
            free(dynamicArray);
            return 1;
        }
        for (int j = 0; j < cols; j++) {
            dynamicArray[i][j] = i * cols + j;
        }
    }

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

    for (int i = 0; i < rows; i++) {
        free(dynamicArray[i]);
    }
    free(dynamicArray);

    return 0;
}

在这个程序中,首先通过 malloc 分配了一个指针数组 dynamicArray,其大小为 rows。然后为每个指针元素分配 colsint 类型的空间,从而模拟了一个二维数组。最后记得释放分配的内存。

(三)函数指针数组

函数指针数组是指针数组的一种特殊应用,其数组元素都是函数指针。函数指针指向一个函数的入口地址,通过函数指针可以调用相应的函数。函数指针数组常用于实现函数表,根据不同的条件调用不同的函数。

#include <stdio.h>

// 定义一些函数
int add(int a, int b) {
    return a + b;
}

int subtract(int a, int b) {
    return a - b;
}

int multiply(int a, int b) {
    return a * b;
}

int main() {
    // 定义函数指针数组
    int (*funcPtrArray[3])(int, int) = {add, subtract, multiply};
    int a = 5;
    int b = 3;
    for (int i = 0; i < 3; i++) {
        int result = funcPtrArray[i](a, b);
        printf("Operation %d result: %d\n", i + 1, result);
    }
    return 0;
}

在这个例子中,funcPtrArray 是一个函数指针数组,它包含三个函数指针,分别指向 addsubtractmultiply 函数。通过遍历函数指针数组,可以依次调用这些函数。

四、指针数组与其他概念的对比

为了更好地理解指针数组,将其与其他相关概念进行对比是很有必要的。

(一)指针数组与数组指针

数组指针是一个指针,它指向一个数组。其定义形式为:数据类型 (*指针名)[数组长度]。例如:

int (*arrayPtr)[5];

这里 arrayPtr 是一个数组指针,它指向一个包含5个 int 类型元素的数组。而指针数组是数组,其元素是指针。例如:

int *ptrArray[5];

这是一个指针数组,包含5个指向 int 类型数据的指针。两者在本质上是不同的,数组指针指向一个整体的数组,而指针数组是多个指针的集合。

(二)指针数组与二维数组

虽然指针数组可以模拟二维数组的行为,但它们在内存布局上是有区别的。二维数组在内存中是连续存储的,例如:

int twoDArray[3][4];

twoDArray 在内存中是一块连续的区域,大小为 3 * 4 * sizeof(int)。而指针数组实现的“二维数组”,其指针数组本身占据一块连续的内存空间,而每个指针所指向的内存空间可能是不连续的。例如:

int **dynamicArray = (int **)malloc(3 * sizeof(int *));
for (int i = 0; i < 3; i++) {
    dynamicArray[i] = (int *)malloc(4 * sizeof(int));
}

这里 dynamicArray 是指针数组,它的每个元素所指向的内存是通过 malloc 动态分配的,不一定连续。这种内存布局的差异在性能和内存管理上会有不同的影响。

五、指针数组使用中的注意事项

在使用指针数组时,有一些重要的注意事项需要牢记。

(一)内存管理

当使用动态分配内存的指针数组时,必须确保在不再需要这些内存时及时释放。否则会导致内存泄漏,尤其是在循环中多次分配内存的情况下。例如:

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

int main() {
    int n = 5;
    int *ptrArray[n];
    for (int i = 0; i < n; i++) {
        ptrArray[i] = (int *)malloc(sizeof(int));
        *ptrArray[i] = i;
    }
    // 这里忘记释放内存
    return 0;
}

在这个程序中,如果没有释放 ptrArray 中每个指针所指向的内存,就会造成内存泄漏。正确的做法是在程序结束前使用 free 函数释放内存。

(二)越界访问

和普通数组一样,指针数组也存在越界访问的风险。由于指针数组的元素是指针,越界访问可能导致访问到非法的内存地址,从而引发程序崩溃或未定义行为。例如:

#include <stdio.h>

int main() {
    int *ptrArray[3];
    int num = 10;
    ptrArray[0] = &num;
    // 访问越界
    printf("%d\n", *ptrArray[3]);
    return 0;
}

在这个例子中,ptrArray 只有3个元素,但尝试访问 ptrArray[3],这是越界访问,会导致未定义行为。

(三)指针类型匹配

在使用指针数组时,必须确保指针的类型与所指向的数据类型匹配。否则,可能会导致数据错误或程序崩溃。例如:

#include <stdio.h>

int main() {
    int num = 10;
    float *floatPtrArray[1];
    // 类型不匹配
    floatPtrArray[0] = (float *)&num;
    printf("%f\n", *floatPtrArray[0]);
    return 0;
}

在这个例子中,floatPtrArray 是指向 float 类型的指针数组,但将其元素指向了 int 类型的变量 num,这会导致类型不匹配,输出结果可能是错误的。

通过深入理解指针数组的定义、初始化、使用以及注意事项,开发者可以在C语言编程中更灵活、高效地使用指针数组,解决各种实际问题。无论是处理字符串、实现动态数据结构还是构建函数表,指针数组都能发挥重要的作用。但同时,由于指针操作的复杂性,需要谨慎对待,避免出现内存泄漏、越界访问等常见错误。