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

C语言数组与指针的关系及常见误区分析

2021-08-242.8k 阅读

C语言数组与指针的关系及常见误区分析

数组与指针的基本概念

在C语言中,数组是一种构造数据类型,它是由相同类型的元素组成的集合,这些元素在内存中连续存储。例如,定义一个整型数组int arr[5];,这里arr就是数组名,它代表数组的起始地址,数组元素从arr[0]arr[4]

指针则是一种特殊的变量类型,它存储的是另一个变量的内存地址。例如,int *ptr;定义了一个指向整型变量的指针ptr。通过指针,我们可以间接访问和操作其所指向的变量。

数组与指针的紧密联系

  1. 数组名与指针常量
    • 在很多情况下,数组名可以被当作指针来使用。例如,对于数组int arr[5];arr的值就是数组首元素的地址,这与指针所存储的地址概念是一致的。在表达式中,arr&arr[0]是等价的。
    • 但是需要注意的是,数组名本质上是一个指针常量,这意味着它的值不能被修改。例如,下面的代码是错误的:
int arr[5];
arr = &arr[1]; // 错误,数组名是指针常量,不能修改其值
  1. 通过指针访问数组元素
    • 由于数组元素在内存中是连续存储的,我们可以使用指针来遍历和访问数组元素。假设int arr[5] = {1, 2, 3, 4, 5};,我们可以定义一个指针int *ptr = arr;,此时ptr指向数组arr的首元素。
    • 通过指针的算术运算,我们可以访问数组的其他元素。例如,*(ptr + 1)等价于arr[1]*(ptr + 2)等价于arr[2],以此类推。下面是一个完整的代码示例:
#include <stdio.h>

int main() {
    int arr[5] = {1, 2, 3, 4, 5};
    int *ptr = arr;
    for (int i = 0; i < 5; i++) {
        printf("arr[%d] = %d, *(ptr + %d) = %d\n", i, arr[i], i, *(ptr + i));
    }
    return 0;
}
  • 在这个示例中,我们通过指针ptr和数组下标两种方式来访问数组元素,并打印出来。从结果可以看出,它们的访问效果是一样的。
  1. 函数参数中的数组与指针
    • 在C语言中,当数组作为函数参数传递时,实际上传递的是数组首元素的地址,也就是一个指针。例如:
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函数中,int arr[]这种形式等价于int *arr。编译器会把arr当作指针来处理,而不是一个真正的数组。这就是为什么在函数内部无法通过sizeof(arr)获取数组的实际大小,因为此时arr是一个指针,sizeof(arr)得到的是指针变量的大小(通常在32位系统上是4字节,64位系统上是8字节)。

数组与指针的区别

  1. 本质区别
    • 数组是由多个相同类型元素组成的集合,在内存中占据一段连续的存储空间,数组名代表数组的起始地址,并且是一个常量,其值不能被修改。
    • 指针是一个变量,它存储的是其他变量的内存地址,指针的值可以在程序运行过程中根据需要进行修改,指向不同的内存位置。
  2. 内存分配方式
    • 数组的内存分配是在定义数组时自动进行的,并且其大小在编译时就已经确定。例如int arr[5];,系统会为arr分配5个连续的整型存储空间。
    • 指针变量本身的内存分配也是在定义时进行的,但其所指向的内存空间需要通过动态内存分配函数(如malloc等)或者指向已有的变量来确定。例如:
int *ptr1 = &arr[0]; // 指向已有数组元素
int *ptr2 = (int *)malloc(5 * sizeof(int)); // 通过malloc动态分配内存
  1. 运算行为差异
    • 数组名除了在初始化或者取地址操作(&arr)时,在其他表达式中会被隐式转换为指向首元素的指针。但是数组名和指针在一些运算上还是有区别的。
    • 对于指针,我们可以进行指针的算术运算,如ptr++ptr += 2等,这些运算会根据指针所指向的数据类型调整指针的值。而数组名作为指针常量,不能进行类似的自增、自减等修改其值的运算。
    • 另外,sizeof运算符对数组和指针的作用也不同。sizeof(arr)对于数组arr会返回整个数组占用的字节数,而sizeof(ptr)对于指针ptr会返回指针变量本身的大小(通常为4或8字节,取决于系统架构)。

常见误区分析

  1. 数组名等同于指针
    • 虽然数组名在很多情况下可以像指针一样使用,但它们并不是完全等同的。数组名是一个指针常量,它的值在数组定义后就不能改变,而指针是变量,可以随时改变其指向。
    • 例如,下面这段代码就展示了两者的不同:
#include <stdio.h>

int main() {
    int arr[5] = {1, 2, 3, 4, 5};
    int *ptr = arr;
    ptr++; // 指针可以自增
    // arr++; // 错误,数组名是常量,不能自增
    return 0;
}
  1. 函数参数中数组的大小问题
    • 如前文所述,当数组作为函数参数传递时,实际上传递的是指针。这就导致在函数内部无法直接获取数组的真实大小。很多初学者可能会尝试在函数内部使用sizeof来获取数组大小,这是错误的。
    • 例如:
void wrongSize(int arr[]) {
    int size = sizeof(arr) / sizeof(arr[0]);
    printf("Wrong size: %d\n", size);
}

int main() {
    int arr[5] = {1, 2, 3, 4, 5};
    wrongSize(arr);
    return 0;
}
  • wrongSize函数中,sizeof(arr)得到的是指针arr的大小,而不是数组的大小。正确的做法是在调用函数时传递数组的大小作为参数。
void correctSize(int arr[], int size) {
    printf("Correct size: %d\n", size);
}

int main() {
    int arr[5] = {1, 2, 3, 4, 5};
    correctSize(arr, 5);
    return 0;
}
  1. 二维数组与指针
    • 二维数组在内存中也是连续存储的,但它的存储方式和指针的关系更为复杂。对于二维数组int arr[3][4];arr同样代表数组的起始地址,但arr是一个指向包含4个整型元素的一维数组的指针。即arr的类型是int (*)[4]
    • 常见的误区是将二维数组简单地当作指针的指针来处理。例如,下面的代码是错误的:
#include <stdio.h>

void wrong2D(int **ptr) {
    // 错误处理,ptr不是指向二维数组的正确方式
}

int main() {
    int arr[3][4] = {
        {1, 2, 3, 4},
        {5, 6, 7, 8},
        {9, 10, 11, 12}
    };
    wrong2D(arr); // 错误,不能将二维数组名直接传给int **类型的参数
    return 0;
}
  • 正确的方式是使用指向一维数组的指针来处理二维数组。例如:
#include <stdio.h>

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

int main() {
    int arr[3][4] = {
        {1, 2, 3, 4},
        {5, 6, 7, 8},
        {9, 10, 11, 12}
    };
    correct2D(arr, 3);
    return 0;
}
  1. 指针数组与数组指针
    • 指针数组是一个数组,其元素都是指针。例如int *ptrArray[5];,这里ptrArray是一个包含5个指向整型变量的指针的数组。
    • 数组指针是一个指针,它指向一个数组。例如int (*arrPtr)[5];,这里arrPtr是一个指向包含5个整型元素的数组的指针。
    • 这两者很容易混淆。在使用时,要注意其定义和用法的区别。例如,对于指针数组,我们可以这样使用:
#include <stdio.h>

int main() {
    int a = 1, b = 2, c = 3, d = 4, e = 5;
    int *ptrArray[5] = {&a, &b, &c, &d, &e};
    for (int i = 0; i < 5; i++) {
        printf("%d ", *ptrArray[i]);
    }
    printf("\n");
    return 0;
}
  • 对于数组指针,我们可以这样使用:
#include <stdio.h>

int main() {
    int arr[5] = {1, 2, 3, 4, 5};
    int (*arrPtr)[5] = &arr;
    for (int i = 0; i < 5; i++) {
        printf("%d ", (*arrPtr)[i]);
    }
    printf("\n");
    return 0;
}
  1. 动态内存分配与数组指针
    • 当使用动态内存分配创建类似二维数组的结构时,容易出现误区。例如,想要创建一个34列的二维数组,错误的做法可能是:
#include <stdio.h>
#include <stdlib.h>

int main() {
    int **ptr = (int **)malloc(3 * sizeof(int *));
    for (int i = 0; i < 3; i++) {
        ptr[i] = (int *)malloc(4 * sizeof(int));
    }
    // 这里ptr不是真正意义上的二维数组指针,访问方式和二维数组不同
    free(ptr);
    return 0;
}
  • 这种方式创建的是一个指针数组,虽然可以模拟二维数组的行为,但和真正的二维数组在内存布局和指针类型上是有区别的。如果要创建类似二维数组的动态内存结构且使用数组指针的方式访问,可以这样做:
#include <stdio.h>
#include <stdlib.h>

int main() {
    int (*ptr)[4] = (int (*)[4])malloc(3 * sizeof(int [4]));
    for (int i = 0; i < 3; i++) {
        for (int j = 0; j < 4; j++) {
            ptr[i][j] = i * 4 + j;
        }
    }
    for (int i = 0; i < 3; i++) {
        for (int j = 0; j < 4; j++) {
            printf("%d ", ptr[i][j]);
        }
        printf("\n");
    }
    free(ptr);
    return 0;
}
  • 在这个示例中,ptr是一个指向包含4个整型元素的数组的指针,通过malloc分配了足够的内存来模拟二维数组,并且可以像二维数组一样进行访问。

总结数组与指针关系及避免误区的建议

  1. 理解本质
    • 深刻理解数组和指针的本质区别,数组是数据的集合,有固定的内存布局和大小,数组名是指针常量;指针是存储地址的变量,可以灵活指向不同的内存位置。
  2. 注意函数参数
    • 当数组作为函数参数时,要明确传递数组大小,避免在函数内部错误地使用sizeof获取数组大小。
  3. 区分二维数组相关概念
    • 对于二维数组,要清楚其指针类型,不要将其与指针的指针混淆。在处理动态二维数组时,要根据需求选择合适的内存分配和指针类型。
  4. 区分指针数组和数组指针
    • 仔细区分指针数组和数组指针的定义和用法,根据实际需求正确使用。在使用动态内存分配时,也要注意选择合适的方式来匹配指针类型。

通过深入理解C语言中数组与指针的关系,并避免常见的误区,程序员可以更加准确和高效地使用这两种重要的语言特性,编写出健壮且性能良好的C语言程序。