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

C++指针在数组操作中的典型用法

2023-02-046.7k 阅读

指针与数组的基本关系

在 C++ 中,指针和数组有着紧密的联系。数组名在大多数情况下会被隐式转换为指向数组首元素的指针。例如:

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

这里,arr 是数组名,它会被隐式转换为 int* 类型,指向数组的第一个元素 arr[0]。我们将 arr 赋值给 ptrptr 就指向了数组的开头。

这种关系源于数组在内存中的存储方式。数组在内存中是一段连续的存储空间,数组名实际上就是这段连续空间的起始地址。所以可以通过指针来访问数组元素。

通过指针访问数组元素

当我们有一个指向数组首元素的指针后,就可以利用指针算术运算来访问数组的其他元素。指针的算术运算基于指针所指向的数据类型。例如,对于 int* 类型的指针,每次递增操作(++)会使指针移动 sizeof(int) 个字节。

int arr[5] = {1, 2, 3, 4, 5};
int* ptr = arr;
for (int i = 0; i < 5; ++i) {
    std::cout << "Element at index " << i << " is " << *(ptr + i) << std::endl;
}

在上述代码中,*(ptr + i) 表示访问从 ptr 开始偏移 i 个元素位置处的值。这与 arr[i] 的效果是一样的。实际上,arr[i] 在编译器内部就是被处理为 *(arr + i),因为 arr 会被转换为指向首元素的指针。

指针与多维数组

多维数组在内存中同样是按顺序连续存储的。以二维数组为例,假设有一个二维数组 int matrix[3][4],它在内存中的存储方式是先存储第一行的所有元素,接着存储第二行,以此类推。

int matrix[3][4] = {
    {1, 2, 3, 4},
    {5, 6, 7, 8},
    {9, 10, 11, 12}
};
int* ptr = &matrix[0][0];
for (int i = 0; i < 3; ++i) {
    for (int j = 0; j < 4; ++j) {
        std::cout << "Element at [" << i << "][" << j << "] is " << *(ptr + i * 4 + j) << std::endl;
    }
}

这里我们将 &matrix[0][0] 赋值给 ptrptr 指向了二维数组的第一个元素。在访问元素时,我们通过 i * 4 + j 来计算偏移量,因为每行有 4 个元素。

动态数组与指针

在 C++ 中,除了使用静态数组(在栈上分配空间),还经常需要动态分配数组(在堆上分配空间)。这时候指针就发挥了关键作用。

使用 new[]delete[] 动态分配和释放数组

new[] 操作符用于在堆上动态分配一个数组,它返回一个指向分配内存起始地址的指针。

int size = 5;
int* dynamicArray = new int[size];
for (int i = 0; i < size; ++i) {
    dynamicArray[i] = i * 2;
}
for (int i = 0; i < size; ++i) {
    std::cout << "Element at index " << i << " is " << dynamicArray[i] << std::endl;
}
delete[] dynamicArray;

在上述代码中,我们使用 new int[size] 分配了一个包含 sizeint 类型元素的动态数组,并将返回的指针赋值给 dynamicArray。之后我们对数组元素进行赋值并打印。最后,使用 delete[] dynamicArray 释放动态分配的内存,避免内存泄漏。

动态多维数组

动态分配多维数组稍微复杂一些。以二维数组为例,我们可以先分配一个指针数组,每个指针再指向一个一维数组。

int rows = 3;
int cols = 4;
int** dynamicMatrix = new int* [rows];
for (int i = 0; i < rows; ++i) {
    dynamicMatrix[i] = new int[cols];
}
for (int i = 0; i < rows; ++i) {
    for (int j = 0; j < cols; ++j) {
        dynamicMatrix[i][j] = i * cols + j;
    }
}
for (int i = 0; i < rows; ++i) {
    for (int j = 0; j < cols; ++j) {
        std::cout << "Element at [" << i << "][" << j << "] is " << dynamicMatrix[i][j] << std::endl;
    }
}
for (int i = 0; i < rows; ++i) {
    delete[] dynamicMatrix[i];
}
delete[] dynamicMatrix;

在这段代码中,首先使用 new int* [rows] 分配了一个包含 rowsint* 类型指针的数组 dynamicMatrix。然后,通过内层循环为每个 dynamicMatrix[i] 分配一个包含 colsint 类型元素的一维数组。在使用完动态二维数组后,我们需要先释放每一行的一维数组,再释放 dynamicMatrix 本身,以确保没有内存泄漏。

指针在数组遍历中的应用

数组遍历是程序中常见的操作,指针在数组遍历中有多种用法。

指针遍历一维数组

除了之前提到的通过指针算术运算遍历数组,还可以使用指针的自增操作来遍历数组。

int arr[5] = {1, 3, 5, 7, 9};
int* ptr = arr;
while (ptr < arr + 5) {
    std::cout << *ptr << std::endl;
    ++ptr;
}

在这个例子中,我们初始化 ptr 指向 arr 的首元素,然后在 while 循环中,只要 ptr 没有超过 arr + 5(即数组的末尾),就输出 *ptr 并将 ptr 自增。这种方式与使用下标遍历数组的效果相同,但在某些场景下可能更加灵活。

指针遍历多维数组

对于多维数组,同样可以使用指针进行遍历。以二维数组为例:

int matrix[3][4] = {
    {1, 2, 3, 4},
    {5, 6, 7, 8},
    {9, 10, 11, 12}
};
int* ptr = &matrix[0][0];
for (int i = 0; i < 3 * 4; ++i) {
    std::cout << *ptr << std::endl;
    ++ptr;
}

这里将 &matrix[0][0] 赋值给 ptr,然后通过循环和指针自增操作遍历整个二维数组。由于二维数组在内存中是连续存储的,这种方式可以有效地遍历所有元素。

指针与数组作为函数参数

在函数中传递数组时,实际上传递的是指向数组首元素的指针。这是因为数组在作为函数参数传递时会发生退化,退化为指针。

一维数组作为函数参数

void printArray(int* arr, int size) {
    for (int i = 0; i < size; ++i) {
        std::cout << arr[i] << std::endl;
    }
}
int main() {
    int arr[5] = {1, 2, 3, 4, 5};
    printArray(arr, 5);
    return 0;
}

在上述代码中,printArray 函数接受一个 int* 类型的指针 arr 和数组的大小 size。在 main 函数中,我们将数组名 arr 作为参数传递给 printArray 函数,此时数组名被隐式转换为指向首元素的指针。

多维数组作为函数参数

多维数组作为函数参数时,同样会发生退化,但需要注意数组第二维及以后的维度必须在函数参数中明确指定。

void printMatrix(int (*matrix)[4], int rows) {
    for (int i = 0; i < rows; ++i) {
        for (int j = 0; j < 4; ++j) {
            std::cout << matrix[i][j] << std::endl;
        }
    }
}
int main() {
    int matrix[3][4] = {
        {1, 2, 3, 4},
        {5, 6, 7, 8},
        {9, 10, 11, 12}
    };
    printMatrix(matrix, 3);
    return 0;
}

这里 printMatrix 函数的参数 int (*matrix)[4] 表示 matrix 是一个指针,指向一个包含 4 个 int 类型元素的数组。因为二维数组在内存中的存储方式依赖于第二维的大小,所以必须明确指定。

指针在数组排序中的应用

排序是数组处理中常见的操作,指针在排序算法的实现中可以起到优化和灵活操作的作用。

冒泡排序中的指针应用

冒泡排序是一种简单的排序算法,通过比较相邻元素并交换位置,将最大(或最小)的元素逐步“冒泡”到数组末尾。

void bubbleSort(int* arr, int size) {
    for (int i = 0; i < size - 1; ++i) {
        for (int j = 0; j < size - i - 1; ++j) {
            if (*(arr + j) > *(arr + j + 1)) {
                int temp = *(arr + j);
                *(arr + j) = *(arr + j + 1);
                *(arr + j + 1) = temp;
            }
        }
    }
}
int main() {
    int arr[5] = {5, 4, 3, 2, 1};
    bubbleSort(arr, 5);
    for (int i = 0; i < 5; ++i) {
        std::cout << arr[i] << std::endl;
    }
    return 0;
}

在这个冒泡排序的实现中,我们使用指针 arr 来访问数组元素。通过指针算术运算 *(arr + j)*(arr + j + 1) 来比较和交换相邻元素。

快速排序中的指针应用

快速排序是一种高效的排序算法,它采用分治思想。在快速排序中,指针可以用来方便地划分和操作子数组。

int partition(int* arr, int low, int high) {
    int pivot = *(arr + high);
    int i = low - 1;
    for (int j = low; j < high; ++j) {
        if (*(arr + j) <= pivot) {
            ++i;
            int temp = *(arr + i);
            *(arr + i) = *(arr + j);
            *(arr + j) = temp;
        }
    }
    int temp = *(arr + i + 1);
    *(arr + i + 1) = *(arr + high);
    *(arr + high) = temp;
    return i + 1;
}
void quickSort(int* arr, int low, int high) {
    if (low < high) {
        int pi = partition(arr, low, high);
        quickSort(arr, low, pi - 1);
        quickSort(arr, pi + 1, high);
    }
}
int main() {
    int arr[5] = {3, 6, 8, 10, 1};
    quickSort(arr, 0, 4);
    for (int i = 0; i < 5; ++i) {
        std::cout << arr[i] << std::endl;
    }
    return 0;
}

在快速排序的代码中,partition 函数通过指针操作对数组进行划分,quickSort 函数递归地对划分后的子数组进行排序。指针的使用使得代码能够灵活地操作数组元素,实现高效的排序。

指针与数组的内存管理

在使用指针操作数组时,内存管理是至关重要的。不正确的内存管理可能导致内存泄漏、悬空指针等问题。

避免内存泄漏

如前面提到的动态数组,使用 new[] 分配内存后,一定要使用 delete[] 释放内存。例如:

int* dynamicArray = new int[10];
// 使用 dynamicArray
// 忘记调用 delete[] dynamicArray; // 这将导致内存泄漏

如果在动态分配内存后没有释放,随着程序的运行,会不断消耗内存,最终可能导致系统内存不足。

处理悬空指针

悬空指针是指指向已释放内存的指针。例如:

int* ptr = new int[5];
delete[] ptr;
// 此时 ptr 成为悬空指针
// 再次访问 *ptr 会导致未定义行为

为了避免悬空指针问题,在释放内存后,可以将指针赋值为 nullptr

int* ptr = new int[5];
delete[] ptr;
ptr = nullptr;

这样,当尝试再次访问 ptr 时,程序会明确知道 ptr 不指向有效的内存,从而避免未定义行为。

指针在数组相关算法优化中的应用

在一些数组相关的算法中,合理使用指针可以提高算法的效率。

减少内存访问开销

现代计算机的内存访问速度相对较慢,尤其是在处理大规模数组时。通过指针的合理使用,可以减少内存访问的次数。例如,在一些计算数组元素总和的算法中:

int sumArray(int* arr, int size) {
    int sum = 0;
    int* end = arr + size;
    while (arr < end) {
        sum += *arr;
        ++arr;
    }
    return sum;
}

在这个 sumArray 函数中,我们提前计算出数组的末尾位置 end,然后在循环中通过比较 arrend 来判断是否遍历结束。这样可以避免每次循环都计算 arr + size,减少了内存访问开销。

利用缓存局部性

缓存局部性是指程序在访问内存时,倾向于访问相邻的内存位置。指针的合理使用可以更好地利用缓存局部性。例如,在遍历二维数组时:

int matrix[100][100];
// 按行遍历,利用缓存局部性
for (int i = 0; i < 100; ++i) {
    int* row = matrix[i];
    for (int j = 0; j < 100; ++j) {
        // 处理 row[j]
    }
}

在上述代码中,通过 int* row = matrix[i] 获取每行的起始地址,然后通过 row[j] 访问元素。这样的访问方式使得内存访问更连续,有利于利用缓存局部性,提高程序性能。

指针在数组操作中的常见错误及解决方法

在使用指针进行数组操作时,容易出现一些错误,下面介绍一些常见错误及解决方法。

越界访问

越界访问是指访问数组范围之外的内存位置。例如:

int arr[5] = {1, 2, 3, 4, 5};
int* ptr = arr;
// 访问越界
std::cout << *(ptr + 10) << std::endl; 

解决方法是在访问数组元素时,确保索引或指针偏移量在有效范围内。在循环遍历数组时,要仔细检查循环条件,避免超出数组边界。

野指针

野指针是指未初始化的指针。例如:

int* ptr;
// 使用未初始化的 ptr
std::cout << *ptr << std::endl; 

解决方法是在定义指针时立即初始化,或者在使用指针之前确保其已经指向有效的内存。对于动态分配内存的指针,在释放内存后将其赋值为 nullptr,避免成为野指针。

内存重复释放

重复释放内存会导致程序崩溃。例如:

int* dynamicArray = new int[5];
delete[] dynamicArray;
// 重复释放
delete[] dynamicArray; 

解决方法是在释放内存后,将指针置为 nullptr,并且在再次释放前检查指针是否为 nullptr

指针在不同场景下数组操作的性能分析

指针在不同场景下对数组操作的性能会有所不同,下面从几个方面进行分析。

静态数组与动态数组

静态数组在编译时分配内存,位于栈上,其大小在编译期确定。访问静态数组元素通过数组下标或指针偏移,由于栈内存的连续性,访问速度较快。但静态数组的大小固定,不适合需要动态调整大小的场景。

动态数组通过 new[] 在堆上分配内存,大小可以在运行时确定。动态数组的灵活性高,但堆内存的分配和释放相对栈内存较慢,且频繁的动态内存分配和释放可能导致内存碎片,影响性能。

不同遍历方式的性能

使用指针算术运算遍历数组和使用下标遍历数组在性能上基本相同,现代编译器会对这两种方式进行优化。但在某些特定情况下,如需要频繁计算数组末尾位置时,使用指针提前计算末尾位置并进行遍历可能会更高效。

在多维数组遍历中,按行遍历(利用缓存局部性)通常比按列遍历性能更好,因为内存访问更连续,有利于缓存命中。

排序算法中指针的性能影响

在冒泡排序中,指针的使用对性能影响不大,因为冒泡排序本身的时间复杂度较高。但在快速排序中,指针的合理使用可以提高划分和交换元素的效率,从而提升整体性能。快速排序通过指针灵活地操作子数组,减少了不必要的内存访问和数据移动,使得排序过程更加高效。

指针在数组操作中的高级应用

智能指针与数组

C++11 引入了智能指针,如 std::unique_ptrstd::shared_ptr,它们可以自动管理动态分配的内存,从而避免内存泄漏。对于数组,std::unique_ptr 提供了专门的数组版本 std::unique_ptr<T[]>

#include <memory>
int main() {
    std::unique_ptr<int[]> dynamicArray(new int[5]);
    for (int i = 0; i < 5; ++i) {
        dynamicArray[i] = i * 3;
    }
    // 离开作用域时,dynamicArray 会自动释放内存
    return 0;
}

在上述代码中,std::unique_ptr<int[]> 自动管理 new int[5] 分配的内存,当 dynamicArray 离开作用域时,内存会自动释放,无需手动调用 delete[]

函数指针数组

函数指针数组是一个数组,其元素为函数指针。这在实现状态机、回调函数表等场景中非常有用。

void func1() {
    std::cout << "Function 1" << std::endl;
}
void func2() {
    std::cout << "Function 2" << std::endl;
}
int main() {
    void (*funcArray[2])() = {func1, func2};
    for (int i = 0; i < 2; ++i) {
        funcArray[i]();
    }
    return 0;
}

在这个例子中,funcArray 是一个函数指针数组,每个元素指向一个函数。通过遍历 funcArray,可以依次调用不同的函数。

模板与指针在数组操作中的结合

模板可以使代码更加通用,与指针结合可以实现对不同类型数组的通用操作。

template <typename T>
void printArray(T* arr, int size) {
    for (int i = 0; i < size; ++i) {
        std::cout << arr[i] << std::endl;
    }
}
int main() {
    int intArr[3] = {1, 2, 3};
    double doubleArr[2] = {1.5, 2.5};
    printArray(intArr, 3);
    printArray(doubleArr, 2);
    return 0;
}

在上述代码中,printArray 模板函数可以接受不同类型的数组指针,实现了对不同类型数组的通用打印操作。

通过以上对 C++ 指针在数组操作中典型用法的深入介绍,包括指针与数组的基本关系、动态数组、遍历、作为函数参数、排序、内存管理、性能分析以及高级应用等方面,希望读者能够对指针在数组操作中的应用有更全面和深入的理解,从而在实际编程中能够更加高效、准确地使用指针来处理数组相关的任务。在实际应用中,需要根据具体场景选择合适的指针操作方式,以达到最佳的性能和代码质量。同时,要时刻注意内存管理等问题,避免出现各种错误。