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

C++数组传参时的指针操作

2021-02-104.6k 阅读

C++ 数组传参时的指针操作基础

在 C++ 编程中,数组和指针紧密相关,尤其是在函数传参时。理解数组传参时指针的操作对于编写高效、正确的代码至关重要。

数组与指针的关系

在 C++ 中,数组名在很多情况下会被隐式转换为指向数组首元素的指针。例如:

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

这里 arr 是一个整型数组,而 ptr 是一个指向 int 类型的指针。通过将数组名赋值给指针,ptr 现在指向 arr 的第一个元素。

数组传参的基本形式

当我们将数组作为参数传递给函数时,实际上传递的是指向数组首元素的指针。看下面这个简单的函数:

void printArray(int* arr, int size) {
    for (int i = 0; i < size; i++) {
        std::cout << arr[i] << " ";
    }
    std::cout << std::endl;
}

在主函数中调用这个函数:

int main() {
    int numbers[4] = {10, 20, 30, 40};
    printArray(numbers, 4);
    return 0;
}

printArray 函数中,arr 是一个指向 int 类型的指针,它接收了 numbers 数组的首地址。size 参数用于指定数组的长度,因为在函数内部无法直接获取数组的实际长度(数组在传参时退化为指针,丢失了长度信息)。

指针算术运算在数组传参中的应用

指针的偏移

通过指针算术运算,我们可以访问数组中的不同元素。对于一个指向数组元素的指针,增加指针的值(例如 ptr++)会使其指向下一个元素。在数组传参的场景下,这一特性非常有用。

void accessElements(int* arr, int size) {
    for (int i = 0; i < size; i++) {
        std::cout << *(arr + i) << " ";
    }
    std::cout << std::endl;
}

在这个函数中,*(arr + i)arr[i] 是等价的。arr + i 计算出第 i 个元素的地址,然后通过 * 运算符解引用获取该地址存储的值。

多维数组传参与指针

多维数组在传参时同样遵循指针的规则。以二维数组为例:

void print2DArray(int (*arr)[3], int rows) {
    for (int i = 0; i < rows; i++) {
        for (int j = 0; j < 3; j++) {
            std::cout << arr[i][j] << " ";
        }
        std::cout << std::endl;
    }
}

这里 int (*arr)[3] 表示 arr 是一个指针,指向一个包含 3 个 int 类型元素的数组。在主函数中调用:

int main() {
    int twoD[2][3] = { {1, 2, 3}, {4, 5, 6} };
    print2DArray(twoD, 2);
    return 0;
}

二维数组在内存中是按行存储的,print2DArray 函数通过指针操作正确地遍历并打印出二维数组的所有元素。

动态数组与指针传参

动态数组的创建与传参

在 C++ 中,我们可以使用 new 运算符动态分配数组内存。动态数组在传参时同样是传递指针。

void processDynamicArray(int* dynArr, int size) {
    for (int i = 0; i < size; i++) {
        dynArr[i] *= 2;
    }
}

在主函数中创建并传递动态数组:

int main() {
    int* dynamicArray = new int[5];
    for (int i = 0; i < 5; i++) {
        dynamicArray[i] = i + 1;
    }
    processDynamicArray(dynamicArray, 5);
    for (int i = 0; i < 5; i++) {
        std::cout << dynamicArray[i] << " ";
    }
    std::cout << std::endl;
    delete[] dynamicArray;
    return 0;
}

在这个例子中,dynamicArray 是一个动态分配的整型数组,通过指针传递给 processDynamicArray 函数,在函数内部对数组元素进行操作。

动态二维数组的传参与指针操作

创建动态二维数组稍微复杂一些,但原理相同。

void process2DDynamicArray(int** twoDynArr, int rows, int cols) {
    for (int i = 0; i < rows; i++) {
        for (int j = 0; j < cols; j++) {
            twoDynArr[i][j] += 1;
        }
    }
}

主函数中创建并传递动态二维数组:

int main() {
    int rows = 2;
    int cols = 3;
    int** dynamic2DArray = new int* [rows];
    for (int i = 0; i < rows; i++) {
        dynamic2DArray[i] = new int[cols];
    }
    for (int i = 0; i < rows; i++) {
        for (int j = 0; j < cols; j++) {
            dynamic2DArray[i][j] = i + j;
        }
    }
    process2DDynamicArray(dynamic2DArray, rows, cols);
    for (int i = 0; i < rows; i++) {
        for (int j = 0; j < cols; j++) {
            std::cout << dynamic2DArray[i][j] << " ";
        }
        std::cout << std::endl;
    }
    for (int i = 0; i < rows; i++) {
        delete[] dynamic2DArray[i];
    }
    delete[] dynamic2DArray;
    return 0;
}

这里 dynamic2DArray 是一个动态分配的二维数组,int** twoDynArr 接收这个二维数组的指针。在函数内部通过双重循环对二维数组的元素进行操作。

数组传参时指针操作的注意事项

指针空值检查

在函数中操作传递进来的数组指针时,一定要进行空值检查。因为如果传递的是一个空指针,对其进行解引用操作会导致程序崩溃。

void safePrintArray(int* arr, int size) {
    if (arr == nullptr) {
        return;
    }
    for (int i = 0; i < size; i++) {
        std::cout << arr[i] << " ";
    }
    std::cout << std::endl;
}

内存管理

当传递动态分配的数组指针时,要注意内存的释放。如果在函数内部对动态数组进行了分配新内存的操作,调用者需要确保在合适的时机释放这些内存,避免内存泄漏。

int* createAndModifyArray(int* arr, int size) {
    int* newArr = new int[size];
    for (int i = 0; i < size; i++) {
        newArr[i] = arr[i] * 2;
    }
    return newArr;
}

在主函数中调用这个函数并释放内存:

int main() {
    int original[3] = {1, 2, 3};
    int* result = createAndModifyArray(original, 3);
    for (int i = 0; i < 3; i++) {
        std::cout << result[i] << " ";
    }
    std::cout << std::endl;
    delete[] result;
    return 0;
}

数组边界检查

在通过指针访问数组元素时,要确保访问的索引在有效范围内。越界访问可能导致未定义行为,例如程序崩溃或数据损坏。

void safeAccessElement(int* arr, int size, int index) {
    if (index < 0 || index >= size) {
        std::cout << "Index out of bounds" << std::endl;
        return;
    }
    std::cout << "Element at index " << index << " is " << arr[index] << std::endl;
}

利用指针操作优化数组传参性能

减少数据拷贝

在 C++ 中,传递大型数组时,如果按值传递(虽然数组不能直接按值传递,但如果是封装在结构体中的数组按值传递结构体)会导致大量的数据拷贝,降低性能。而传递指针可以避免这种数据拷贝。例如,假设有一个结构体包含一个大数组:

struct LargeArrayStruct {
    int data[10000];
};
void processByValue(LargeArrayStruct s) {
    // 对 s.data 进行操作
}
void processByPointer(LargeArrayStruct* s) {
    // 对 s->data 进行操作
}

在主函数中:

int main() {
    LargeArrayStruct largeStruct;
    // 初始化 largeStruct.data

    // 按值传递,开销大
    processByValue(largeStruct);

    // 按指针传递,开销小
    processByPointer(&largeStruct);

    return 0;
}

通过传递指针,processByPointer 函数直接操作原始数据,避免了数据拷贝带来的性能损耗。

指针与迭代器的结合

C++ 中的迭代器本质上也是一种指针,在处理数组时,可以结合迭代器来提高代码的可读性和效率。例如,使用标准库算法时,迭代器可以方便地与数组指针配合。

#include <algorithm>
#include <iostream>
int main() {
    int arr[5] = {5, 4, 3, 2, 1};
    std::sort(arr, arr + 5);
    for (int i = 0; i < 5; i++) {
        std::cout << arr[i] << " ";
    }
    std::cout << std::endl;
    return 0;
}

这里 std::sort 函数接收两个迭代器(也就是指针),分别指向数组的起始和结束位置(实际结束位置的下一个位置),通过这种方式对数组进行排序。

数组传参时指针操作的高级话题

模板与数组指针

模板可以使代码更加通用,在处理数组传参时,模板可以让我们编写适用于不同类型数组的函数。

template <typename T, size_t N>
void printTemplateArray(T (&arr)[N]) {
    for (size_t i = 0; i < N; i++) {
        std::cout << arr[i] << " ";
    }
    std::cout << std::endl;
}

在主函数中调用:

int main() {
    int intArr[3] = {1, 2, 3};
    double doubleArr[2] = {1.5, 2.5};
    printTemplateArray(intArr);
    printTemplateArray(doubleArr);
    return 0;
}

这里 printTemplateArray 函数模板接受一个数组引用,通过模板参数 TN 分别确定数组元素类型和数组长度。在函数内部可以像操作普通数组一样操作 arr,并且编译器会为不同类型的数组生成相应的函数实例。

智能指针与数组传参

智能指针可以帮助我们更好地管理动态分配的数组内存,避免内存泄漏。std::unique_ptrstd::shared_ptr 都可以用于管理数组。

#include <memory>
void processUniquePtrArray(std::unique_ptr<int[]> arr, int size) {
    for (int i = 0; i < size; i++) {
        arr[i] += 1;
    }
}

在主函数中使用 std::unique_ptr

int main() {
    std::unique_ptr<int[]> uniqueArray(new int[4]);
    for (int i = 0; i < 4; i++) {
        uniqueArray[i] = i;
    }
    processUniquePtrArray(std::move(uniqueArray), 4);
    // uniqueArray 已经被移动,不能再使用
    return 0;
}

std::unique_ptr<int[]> 表示一个指向动态分配整型数组的智能指针,processUniquePtrArray 函数接收这个智能指针,并且在函数结束后,智能指针会自动释放数组内存。

对于需要共享所有权的场景,可以使用 std::shared_ptr

#include <memory>
void processSharedPtrArray(std::shared_ptr<int[]> arr, int size) {
    for (int i = 0; i < size; i++) {
        arr[i] *= 2;
    }
}

主函数中:

int main() {
    std::shared_ptr<int[]> sharedArray(new int[3]);
    for (int i = 0; i < 3; i++) {
        sharedArray[i] = i + 1;
    }
    processSharedPtrArray(sharedArray, 3);
    // sharedArray 仍然有效,因为是共享所有权
    for (int i = 0; i < 3; i++) {
        std::cout << sharedArray[i] << " ";
    }
    std::cout << std::endl;
    return 0;
}

std::shared_ptr 允许多个指针指向同一动态数组,通过引用计数来管理内存释放。当最后一个指向数组的 std::shared_ptr 被销毁时,数组内存才会被释放。

数组传参与函数重载

函数重载时,数组传参的指针形式可能会导致一些微妙的问题。例如:

void printArray(int* arr, int size) {
    std::cout << "Printing int array: ";
    for (int i = 0; i < size; i++) {
        std::cout << arr[i] << " ";
    }
    std::cout << std::endl;
}
void printArray(double* arr, int size) {
    std::cout << "Printing double array: ";
    for (int i = 0; i < size; i++) {
        std::cout << arr[i] << " ";
    }
    std::cout << std::endl;
}

这里根据数组元素类型进行了函数重载。但如果有一个函数同时有数组和指针参数:

void doSomething(int* arr, int size, int num) {
    // 函数体
}
void doSomething(int num, int* arr, int size) {
    // 函数体
}

这种情况下,当调用 doSomething 时可能会出现歧义,因为数组在传参时退化为指针,编译器难以确定应该调用哪个函数。所以在进行函数重载时,要特别注意数组传参的指针形式可能带来的问题。

实际应用场景中的数组传参与指针操作

图形处理中的像素数组

在图形处理中,图像通常被表示为像素数组。例如,一个简单的灰度图像可以用一个一维数组表示,每个元素表示一个像素的灰度值。当需要对图像进行操作,如滤波、缩放等,就需要将像素数组传递给相应的函数,并通过指针操作来访问和修改像素值。

void applyFilter(unsigned char* pixels, int width, int height) {
    for (int i = 0; i < width * height; i++) {
        // 简单的滤波操作,例如将像素值减半
        pixels[i] /= 2;
    }
}

这里 unsigned char* pixels 指向表示图像像素的数组,widthheight 用于确定数组的大小。通过指针操作可以高效地遍历和修改每个像素。

科学计算中的矩阵运算

在科学计算中,矩阵通常用二维数组表示。矩阵的加法、乘法等运算需要将矩阵数组传递给相应的函数,并利用指针操作进行高效计算。

void matrixAddition(int (*matrix1)[3], int (*matrix2)[3], int (*result)[3], int rows) {
    for (int i = 0; i < rows; i++) {
        for (int j = 0; j < 3; j++) {
            result[i][j] = matrix1[i][j] + matrix2[i][j];
        }
    }
}

这里 matrix1matrix2result 都是指向包含 3 个 int 类型元素的数组的指针,代表矩阵的行。通过指针操作实现矩阵加法。

数据通信中的数据包处理

在数据通信中,数据包通常被封装为数组形式。当接收到数据包后,需要将其传递给处理函数,并通过指针操作解析数据包中的各个字段。

struct Packet {
    char header[4];
    int data[10];
    char footer[4];
};
void processPacket(Packet* packet) {
    // 解析 header
    for (int i = 0; i < 4; i++) {
        std::cout << "Header byte " << i << ": " << static_cast<int>(packet->header[i]) << std::endl;
    }
    // 处理 data
    for (int i = 0; i < 10; i++) {
        packet->data[i] *= 2;
    }
    // 解析 footer
    for (int i = 0; i < 4; i++) {
        std::cout << "Footer byte " << i << ": " << static_cast<int>(packet->footer[i]) << std::endl;
    }
}

这里 Packet 结构体包含数组类型的成员,processPacket 函数接收一个指向 Packet 结构体的指针,通过指针操作访问和处理数据包的各个部分。

通过以上内容,我们深入探讨了 C++ 数组传参时指针操作的各个方面,包括基础概念、指针算术运算、动态数组、注意事项、性能优化以及高级话题和实际应用场景。掌握这些知识对于编写高效、健壮的 C++ 代码至关重要。在实际编程中,要根据具体需求选择合适的数组传参和指针操作方式,以实现最佳的程序性能和代码质量。