8. 多维数组:二维数组、矩阵操作、数组作为函数参数

说实话,很多初学者一听到「多维数组」就觉得头大。我当年刚学C++时也这样——总觉得一维数组已经够用了,干嘛还要搞个二维的?直到我在项目中遇到一个实际问题:要处理一张灰度图像,每个像素点都有行和列两个坐标。这时候我才明白,二维数组不是花架子,它是实实在在的工程工具。

这一章,我们就来聊聊二维数组。说白了,它就是「数组的数组」。你把它想象成一个表格或者棋盘,就很好理解了。

8.1 二维数组的声明与初始化

先看最基本的声明方式:

// 声明一个3行4列的二维数组
int matrix[3][4];

// 声明并初始化
int grid[2][3] = {
    {1, 2, 3},
    {4, 5, 6}
};

// 也可以全部写在一行里
int arr[2][3] = {1, 2, 3, 4, 5, 6};

嗯,这里要注意:行和列的概念容易搞混。我个人习惯把第一个维度叫「行」,第二个叫「列」。你想想看,matrix[行][列],是不是跟数学里的矩阵写法一致?

内存布局小知识:C++里的二维数组是行优先存储的。也就是说,先存完第一行的所有列,再存第二行。这个特性在你做性能优化时会很有用。

8.2 矩阵操作实战

我在项目中做过一个简单的图像滤镜程序,核心就是矩阵运算。下面这个例子,咱们实现一个矩阵转置:

#include <iostream>
using namespace std;

int main() {
    int matrix[3][2] = {
        {1, 2},
        {3, 4},
        {5, 6}
    };
    
    int transposed[2][3] = {0};  // 转置后是2行3列
    
    // 转置操作
    for (int i = 0; i < 3; i++) {
        for (int j = 0; j < 2; j++) {
            transposed[j][i] = matrix[i][j];
        }
    }
    
    // 输出转置结果
    for (int i = 0; i < 2; i++) {
        for (int j = 0; j < 3; j++) {
            cout << transposed[i][j] << " ";
        }
        cout << endl;
    }
    return 0;
}

输出结果:

1 3 5
2 4 6

你看,原来3行2列的矩阵,变成了2行3列。行和列互换了位置。

小技巧:写矩阵操作时,我习惯把外层循环写成行,内层循环写成列。这样代码读起来更直观,不容易出错。

8.3 数组作为函数参数

这是很多新手容易踩坑的地方。我曾经在面试时被问到:「C++里数组作为函数参数,到底传的是什么?」

答案是:传的是指针。数组名本质上就是指向第一个元素的指针。

// 错误的写法:试图直接传二维数组
void printMatrix(int matrix[][], int rows) {  // 编译错误!
    // ...
}

// 正确的写法:必须指定列数
void printMatrix(int matrix[][4], int rows) {
    for (int i = 0; i < rows; i++) {
        for (int j = 0; j < 4; j++) {
            cout << matrix[i][j] << " ";
        }
        cout << endl;
    }
}

// 或者用指针的指针
void printMatrix2(int (*matrix)[4], int rows) {
    // 用法同上
}

我曾经踩过的坑:有一次我写了一个函数,想接收任意大小的二维数组。我用了 int** 作为参数类型,结果传进去后访问 matrix[i][j] 直接段错误。后来才明白,int**int[][4] 在内存布局上完全不同。二维数组是连续内存块,而指针的指针是分散的。

8.4 动态二维数组

如果数组大小在编译时不确定,怎么办?我推荐用 vector 或者动态分配:

#include <iostream>
#include <vector>
using namespace std;

int main() {
    int rows = 3, cols = 4;
    
    // 方法1:使用vector(推荐)
    vector<vector<int>> mat(rows, vector<int>(cols, 0));
    
    // 方法2:动态分配(C风格)
    int** dynMat = new int*[rows];
    for (int i = 0; i < rows; i++) {
        dynMat[i] = new int[cols];
    }
    
    // 使用...
    
    // 别忘了释放内存!
    for (int i = 0; i < rows; i++) {
        delete[] dynMat[i];
    }
    delete[] dynMat;
    
    return 0;
}

我个人强烈建议用 vector。为什么?因为它自动管理内存,不会出现内存泄漏。你想想看,如果代码里到处都是 newdelete,多容易出 bug 啊。

8.5 知识体系总览

下面这张图,帮你理清本章的核心脉络:

二维数组 声明与初始化 矩阵操作(转置、加减乘) 数组作为函数参数 int a[3][4] 初始化列表 双重循环 行优先遍历 int[][4] int (*)[4] 核心要点 行优先存储 · 传参需指定列数 · 动态数组推荐vector

8.6 避坑指南

最后,我总结几个常见的坑,你写代码时多留个心眼:

  • 越界访问:二维数组的索引从0开始,访问 matrix[rows][cols] 时,最大行号是 rows-1,最大列号是 cols-1。我曾经在循环里写成了 <=,结果数组越界,程序崩溃得莫名其妙。
  • 函数传参忘记列数:编译器需要知道每行有多少个元素,才能正确计算偏移量。所以 int matrix[][] 是错的,必须写成 int matrix[][4]
  • 动态内存忘记释放:用 new 分配的二维数组,一定要用 delete[] 释放。而且释放顺序要和分配顺序相反——先释放每一行,再释放行指针数组。

好了,二维数组的内容就聊到这儿。说白了,它就是一张表格,行和列的概念搞清楚了,剩下的就是多写多练。你试试自己实现一个矩阵乘法,或者用二维数组存一张小图片的像素值,动手做一遍比看十遍都管用。


公众号:蓝海资料掘金营,微信deep3321