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

C++ 指针和多维数组详解

2022-10-166.7k 阅读

C++ 指针与多维数组基础概念

指针基础回顾

在深入探讨 C++ 指针与多维数组的关系之前,我们先来回顾一下指针的基本概念。指针是一种特殊的变量,它存储的是另一个变量的内存地址。通过指针,我们可以间接访问和修改该内存地址处存储的值。

例如,下面的代码展示了如何声明一个指针变量,并使用它来访问一个整数变量的值:

#include <iostream>

int main() {
    int num = 10;
    int* ptr = &num; // 声明一个指向整数的指针,并将其初始化为 num 的地址
    std::cout << "The value of num is: " << num << std::endl;
    std::cout << "The address of num is: " << &num << std::endl;
    std::cout << "The value stored in ptr is: " << ptr << std::endl;
    std::cout << "The value of num accessed through ptr is: " << *ptr << std::endl;

    *ptr = 20; // 通过指针修改 num 的值
    std::cout << "The new value of num is: " << num << std::endl;

    return 0;
}

在上述代码中,int* ptr 声明了一个名为 ptr 的指针,它指向一个 int 类型的变量。&num 操作符获取 num 的内存地址,并将其赋值给 ptr*ptr 则用于访问 ptr 所指向的内存地址处的值,通过它我们可以对 num 进行读取和修改操作。

多维数组基础回顾

多维数组是数组的数组,它可以用来表示具有多个维度的数据结构,比如二维数组常用于表示矩阵,三维数组可用于表示立体空间中的数据等。

以二维数组为例,其声明方式如下:

int matrix[3][4];

上述代码声明了一个名为 matrix 的二维数组,它有 3 行 4 列,总共可以存储 12 个 int 类型的元素。二维数组在内存中是按行顺序存储的,即先存储第一行的所有元素,再存储第二行,以此类推。

我们可以通过两个下标来访问二维数组中的元素,例如 matrix[i][j] 表示访问第 i 行第 j 列的元素,其中 ij 都从 0 开始计数。

指针与一维数组的关系

数组名作为指针

在 C++ 中,数组名可以被看作是一个指向数组首元素的常量指针。例如,对于一个 int 类型的数组 arr

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

arr 就相当于一个 int* 类型的指针,并且它指向 arr[0] 的地址。我们可以像使用指针一样使用数组名来访问数组元素:

#include <iostream>

int main() {
    int arr[5] = {1, 2, 3, 4, 5};
    std::cout << "The first element of the array: " << *arr << std::endl;
    std::cout << "The second element of the array: " << *(arr + 1) << std::endl;

    return 0;
}

在上述代码中,*arr 等同于 arr[0]*(arr + 1) 等同于 arr[1]。这是因为数组在内存中是连续存储的,通过指针的算术运算,我们可以方便地访问数组中的不同元素。

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

我们还可以使用指针来动态分配数组的内存。通过 new 操作符,我们可以在堆上分配一块连续的内存空间来存储数组元素。例如:

#include <iostream>

int main() {
    int size = 5;
    int* dynamicArr = new int[size];
    for (int i = 0; i < size; ++i) {
        dynamicArr[i] = i + 1;
    }
    for (int i = 0; i < size; ++i) {
        std::cout << "dynamicArr[" << i << "] = " << dynamicArr[i] << std::endl;
    }
    delete[] dynamicArr; // 释放动态分配的内存

    return 0;
}

在上述代码中,new int[size] 在堆上分配了一块可以存储 sizeint 类型元素的内存空间,并返回一个指向该内存块首地址的指针 dynamicArr。我们可以像使用普通数组一样通过下标访问这些元素。使用完毕后,需要通过 delete[] 操作符释放这块内存,以避免内存泄漏。

指针与二维数组

二维数组名与指针

二维数组名同样可以被看作是一个指针,但它的类型更为复杂。对于一个二维数组 int matrix[3][4]matrix 可以看作是一个指向包含 4 个 int 类型元素的数组的指针,即 int (*)[4] 类型。

下面的代码展示了如何通过指针来访问二维数组的元素:

#include <iostream>

int main() {
    int matrix[3][4] = {
        {1, 2, 3, 4},
        {5, 6, 7, 8},
        {9, 10, 11, 12}
    };
    int (*ptr)[4] = matrix; // 声明一个指向包含 4 个 int 元素数组的指针,并初始化为 matrix
    for (int i = 0; i < 3; ++i) {
        for (int j = 0; j < 4; ++j) {
            std::cout << "matrix[" << i << "][" << j << "] = " << *(*(ptr + i) + j) << std::endl;
        }
    }

    return 0;
}

在上述代码中,int (*ptr)[4] 声明了一个指针 ptr,它指向一个包含 4 个 int 类型元素的数组。*(ptr + i) 指向第 i 行的数组,*(*(ptr + i) + j) 则访问第 i 行第 j 列的元素,等同于 matrix[i][j]

动态分配二维数组的内存

动态分配二维数组的内存有多种方法。一种常见的方法是先分配一个指针数组,每个指针再分别分配一块内存来存储一行元素。例如:

#include <iostream>

int main() {
    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 j = 0; j < cols; ++j) {
            dynamicMatrix[i][j] = i * cols + j + 1;
        }
    }
    for (int i = 0; i < rows; ++i) {
        for (int j = 0; j < cols; ++j) {
            std::cout << "dynamicMatrix[" << i << "][" << j << "] = " << dynamicMatrix[i][j] << std::endl;
        }
    }
    for (int i = 0; i < rows; ++i) {
        delete[] dynamicMatrix[i];
    }
    delete[] dynamicMatrix;

    return 0;
}

在上述代码中,new int* [rows] 首先分配了一个包含 rowsint* 类型指针的数组 dynamicMatrix。然后,通过内层循环,为每个指针分配一块可以存储 colsint 类型元素的内存空间,从而构成一个二维数组。释放内存时,需要先释放每一行的内存,再释放 dynamicMatrix 本身。

另一种动态分配二维数组内存的方法是分配一块连续的内存空间来存储所有元素,并通过指针运算来模拟二维数组的访问。例如:

#include <iostream>

int main() {
    int rows = 3;
    int cols = 4;
    int* flatMatrix = new int[rows * cols];
    for (int i = 0; i < rows; ++i) {
        for (int j = 0; j < cols; ++j) {
            flatMatrix[i * cols + j] = i * cols + j + 1;
        }
    }
    for (int i = 0; i < rows; ++i) {
        for (int j = 0; j < cols; ++j) {
            std::cout << "flatMatrix[" << i << "][" << j << "] = " << flatMatrix[i * cols + j] << std::endl;
        }
    }
    delete[] flatMatrix;

    return 0;
}

在这种方法中,new int[rows * cols] 分配了一块连续的内存空间,其大小足以存储 rowscols 列的所有元素。通过 i * cols + j 的计算,我们可以模拟二维数组的下标访问。这种方法的优点是内存连续性好,在某些情况下可能提高访问效率,但代码的可读性可能会稍差一些。

指针与多维数组的应用场景

矩阵运算

在数学和计算机图形学等领域,经常需要进行矩阵运算,如矩阵乘法、转置等。二维数组结合指针可以方便地实现这些运算。例如,矩阵乘法的实现代码如下:

#include <iostream>

void multiplyMatrices(int** matrix1, int** matrix2, int** result, int rows1, int cols1, int cols2) {
    for (int i = 0; i < rows1; ++i) {
        for (int j = 0; j < cols2; ++j) {
            result[i][j] = 0;
            for (int k = 0; k < cols1; ++k) {
                result[i][j] += matrix1[i][k] * matrix2[k][j];
            }
        }
    }
}

int main() {
    int rows1 = 2;
    int cols1 = 3;
    int cols2 = 2;
    int** matrix1 = new int* [rows1];
    int** matrix2 = new int* [cols1];
    int** result = new int* [rows1];
    for (int i = 0; i < rows1; ++i) {
        matrix1[i] = new int[cols1];
        result[i] = new int[cols2];
    }
    for (int i = 0; i < cols1; ++i) {
        matrix2[i] = new int[cols2];
    }

    // 初始化矩阵1
    matrix1[0][0] = 1; matrix1[0][1] = 2; matrix1[0][2] = 3;
    matrix1[1][0] = 4; matrix1[1][1] = 5; matrix1[1][2] = 6;

    // 初始化矩阵2
    matrix2[0][0] = 7; matrix2[0][1] = 8;
    matrix2[1][0] = 9; matrix2[1][1] = 10;
    matrix2[2][0] = 11; matrix2[2][1] = 12;

    multiplyMatrices(matrix1, matrix2, result, rows1, cols1, cols2);

    std::cout << "Result of matrix multiplication:" << std::endl;
    for (int i = 0; i < rows1; ++i) {
        for (int j = 0; j < cols2; ++j) {
            std::cout << result[i][j] << " ";
        }
        std::cout << std::endl;
    }

    // 释放内存
    for (int i = 0; i < rows1; ++i) {
        delete[] matrix1[i];
        delete[] result[i];
    }
    for (int i = 0; i < cols1; ++i) {
        delete[] matrix2[i];
    }
    delete[] matrix1;
    delete[] matrix2;
    delete[] result;

    return 0;
}

在上述代码中,multiplyMatrices 函数实现了矩阵乘法的核心逻辑。通过指针操作,我们可以方便地访问和计算矩阵元素。

三维图形渲染

在三维图形渲染中,常常需要使用三维数组来表示场景中的物体、像素等信息。例如,一个三维数组可以用来存储一个立体场景中每个点的颜色值。结合指针,我们可以高效地访问和处理这些数据。下面是一个简单的示例,展示如何使用三维数组和指针来初始化一个简单的三维场景颜色数据:

#include <iostream>

struct Color {
    int r;
    int g;
    int b;
};

int main() {
    int sizeX = 10;
    int sizeY = 10;
    int sizeZ = 10;
    Color*** scene = new Color** [sizeX];
    for (int i = 0; i < sizeX; ++i) {
        scene[i] = new Color* [sizeY];
        for (int j = 0; j < sizeY; ++j) {
            scene[i][j] = new Color[sizeZ];
            for (int k = 0; k < sizeZ; ++k) {
                scene[i][j][k].r = i * 10;
                scene[i][j][k].g = j * 10;
                scene[i][j][k].b = k * 10;
            }
        }
    }

    // 输出场景中部分点的颜色值
    std::cout << "Color at (2, 3, 4): R = " << scene[2][3][4].r
              << ", G = " << scene[2][3][4].g
              << ", B = " << scene[2][3][4].b << std::endl;

    // 释放内存
    for (int i = 0; i < sizeX; ++i) {
        for (int j = 0; j < sizeY; ++j) {
            delete[] scene[i][j];
        }
        delete[] scene[i];
    }
    delete[] scene;

    return 0;
}

在上述代码中,我们定义了一个 Color 结构体来表示颜色。通过三维数组 scene 和指针操作,我们初始化了一个简单的三维场景的颜色数据,并演示了如何访问其中的元素。在实际的三维图形渲染中,还会涉及到更复杂的算法和数据处理,但这种基于指针和多维数组的数据结构是基础。

指针与多维数组的注意事项

指针类型匹配

在使用指针访问多维数组时,必须确保指针的类型与数组的类型相匹配。例如,对于二维数组 int matrix[3][4],不能使用 int* 类型的指针来直接访问,而应该使用 int (*)[4] 类型的指针。否则,可能会导致未定义行为,程序可能会崩溃或产生错误的结果。

内存管理

在动态分配多维数组的内存时,要注意正确地释放内存。对于像 int** dynamicMatrix 这种先分配指针数组,再为每个指针分配内存的方式,需要先释放每一行的内存,再释放指针数组本身。如果遗漏了任何一步,就会导致内存泄漏,使程序占用的内存不断增加,最终可能导致系统资源耗尽。

例如,在前面动态分配二维数组内存的代码中,如果忘记了 delete[] dynamicMatrix[i] 这一步,那么每一行分配的内存就无法释放,从而造成内存泄漏。

数组越界

无论是使用指针还是下标访问多维数组,都要注意避免数组越界。数组越界会导致访问到不属于该数组的内存区域,这可能会破坏其他数据,引发未定义行为。在编写代码时,要确保对数组下标的操作在合法的范围内。例如,对于二维数组 int matrix[3][4]matrix[i][j] 中的 i 必须在 0 到 2 之间,j 必须在 0 到 3 之间,否则就会发生数组越界。

通过在代码中添加边界检查,可以有效地避免数组越界问题。例如:

#include <iostream>

int main() {
    int matrix[3][4] = {
        {1, 2, 3, 4},
        {5, 6, 7, 8},
        {9, 10, 11, 12}
    };
    int row = 2;
    int col = 3;
    if (row >= 0 && row < 3 && col >= 0 && col < 4) {
        std::cout << "matrix[" << row << "][" << col << "] = " << matrix[row][col] << std::endl;
    } else {
        std::cout << "Index out of bounds" << std::endl;
    }

    return 0;
}

在上述代码中,通过检查 rowcol 是否在合法范围内,我们可以避免数组越界访问。

指针与多维数组的优化

缓存友好性

现代计算机的 CPU 缓存对于程序性能有重要影响。在访问多维数组时,保持内存访问的连续性可以提高缓存命中率,从而提升程序性能。对于二维数组,按行访问通常比按列访问更缓存友好,因为二维数组在内存中是按行顺序存储的。

例如,下面的代码展示了按行访问和按列访问二维数组的性能差异:

#include <iostream>
#include <chrono>

const int rows = 10000;
const int cols = 10000;

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

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

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

    auto start = std::chrono::high_resolution_clock::now();
    accessByRow(matrix);
    auto end = std::chrono::high_resolution_clock::now();
    auto durationRow = std::chrono::duration_cast<std::chrono::milliseconds>(end - start).count();

    start = std::chrono::high_resolution_clock::now();
    accessByColumn(matrix);
    end = std::chrono::high_resolution_clock::now();
    auto durationColumn = std::chrono::duration_cast<std::chrono::milliseconds>(end - start).count();

    std::cout << "Time taken to access by row: " << durationRow << " ms" << std::endl;
    std::cout << "Time taken to access by column: " << durationColumn << " ms" << std::endl;

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

    return 0;
}

在上述代码中,accessByRow 函数按行访问二维数组,accessByColumn 函数按列访问。通过测量时间,我们可以发现按行访问通常会比按列访问快,因为按行访问更符合内存的存储顺序,提高了缓存命中率。

减少指针间接访问

在动态分配的多维数组中,如 int** dynamicMatrix,每次通过 dynamicMatrix[i][j] 访问元素时,实际上经历了两次指针间接访问(先通过 dynamicMatrix[i] 获取指向某一行的指针,再通过这个指针访问具体元素)。这种间接访问会增加 CPU 的开销。

为了减少这种开销,可以考虑使用连续内存分配的方式,如前面提到的 int* flatMatrix = new int[rows * cols],通过计算偏移量来访问元素。虽然这种方式代码可读性可能稍差,但在性能敏感的场景下可能会带来显著的性能提升。

使用智能指针

在处理动态分配的多维数组时,使用智能指针(如 std::unique_ptrstd::shared_ptr)可以简化内存管理,避免手动释放内存时可能出现的错误,如内存泄漏。例如,对于动态分配的二维数组,可以使用 std::unique_ptr<std::unique_ptr<int>[]> 来管理内存:

#include <iostream>
#include <memory>

int main() {
    int rows = 3;
    int cols = 4;
    std::unique_ptr<std::unique_ptr<int>[]> dynamicMatrix(new std::unique_ptr<int>[rows]);
    for (int i = 0; i < rows; ++i) {
        dynamicMatrix[i].reset(new int[cols]);
        for (int j = 0; j < cols; ++j) {
            dynamicMatrix[i][j] = i * cols + j + 1;
        }
    }
    for (int i = 0; i < rows; ++i) {
        for (int j = 0; j < cols; ++j) {
            std::cout << "dynamicMatrix[" << i << "][" << j << "] = " << dynamicMatrix[i][j] << std::endl;
        }
    }

    // 智能指针会自动释放内存,无需手动操作

    return 0;
}

在上述代码中,std::unique_ptr<std::unique_ptr<int>[]> 管理了一个动态分配的二维数组。当 dynamicMatrix 超出作用域时,它会自动释放所管理的内存,从而简化了内存管理过程,降低了出错的可能性。

通过深入理解指针与多维数组的关系,并注意上述的注意事项和优化方法,开发者可以在 C++ 编程中更高效地使用多维数组,编写出性能更好、更健壮的程序。无论是在科学计算、图形处理还是其他领域,这些知识都具有重要的应用价值。