二维数组的存储与传参

发布于 2022-07-25  39 次阅读


由一道题目引出的思考,二维数组的存储方式和传参方法。另外有一道指针题的思路。

题干

编写一个矩阵转置的函数,矩阵的行、列数在程序中由用户输入。

出自清华大学郑莉老师编写的教材《C++语言程序设计》第五版,题号6-26。

思路

函数

题目本身没有什么不好处理的逻辑。写两个函数:

  1. switchPo用于交换矩阵中的两个元素位置,参数有:
    1. 矩阵:matrix
    2. 矩阵的阶数:order^1
    3. 需要交换位置的两个元素的行、列数:p1r, p1c, p2r, p2c
  2. reverseMatrix用于转置矩阵,参数有:
    1. 矩阵:matrix
    2. 矩阵的阶数:order

唯一需要稍作考虑的点在于,如何定义这个二维数组,以及如何将其作为参数传入上述两个函数。

构造二维数组

考虑两种方式,一是直接int matrix[m][n],二是用new动态分配空间。但由于行、列数是由用户输入决定,而动态申请多维数组空间,new 类型名T[表达式][常量表达式]……时,仅第一维数可为变量。即int (*matrix)[n] = new int[m][n]中,,m可以是变量,而n必须为常量^2。因此,我只得使用第一种方式来定义二维数组。

参数传递

要决定传递参数的方式,首先明确下二维数组在计算机中的存储方式。

二维数组的存储

我们知道,一维数组中的元素,在逻辑上是连续存放的。实际上,二维数组,乃至多维数组,都是连续存放在内存中的。以二维数组int arr[2][3]为例,在我们的理解中,它的元素是排成两行三列的,而在存储逻辑上,它的六个元素是连续存放的,而使用行列下标去访问元素,是经过内部转换的结果。

回过头来看动态分配空间构造二维数组的语句:int (*matrix)[n] = new int[m][n]。从(*matrix)可以知道,等号左边定义了一个指针matrix,它指向一个一维数组,该一维数组中有n个int型元素。而等号右边动态分配了一个二维数组空间,并返回了其首行元素的首地址^3

下面在调试中更好理解下上述内容。

二维数组的存储与传参-2022-07-25-19-35-08

可以看到,我们用动态分配空间的方法构造了一个3*4的二维数组,并用指针matrix接收其返回的首地址。

从左侧变量栏看到,*matrix,即指针matrix指向的内容,是数组{1,2,3,4},也就是二维数组的首行元素。并且,在监视栏中的变量*(matrix + 1)为二维数组的第二行元素,也就是说,matrix + 1指向了二维数组的第二行元素。故不难理解,指针matrix指向二维数组首行元素,存储了该二维数组的首行元素的首地址。

理解了二维数组的存储,和动态分配方法,下面来看如何将其作为参数传递。

实现参数传递的方式

传递二维数组,可以有三种方式。

方式一:直接传递

这种方式没有太多说道,正常按下标访问即可^4。只是需要在设置形参时,给第二维数一个常量值。形如:

void reverseMatrix(int matrix[][3]);`

方式二:传递指向一维数组的指针

这种方式亦不难理解,因为动态分配方式时我们定义了指向一维数组的指针来接收二维数组的首行元素的首地址,将这一指针作为参数传递即可。此外,跟方式一类似,需要设置形参时确定作为一维数组维数的常量,形如:

void reverseMatrix(int (*matrix)[3]);

方式三:传递指针

首先,方式三传递指针应形如:

void reverseMatrix(int **matrix);

显然,这种方式无需事先指定第二维数为某一常量,这是此方式的最大优点。然而,有得必有失,使用此方式传递二维数组,无法直接用行列下标的形式访问数组元素。

同样是传递指针,要与方式二中传递的指针进行区别。下面在实例中体会。

二维数组的存储与传参-2022-07-25-20-42-34

依然是用动态分配空间的方法构造了一个3*4的二维数组,并用指针matrix接收其返回的首地址,传参使用本小节的方法三,为与主函数中的matrix区别,形参名取matrix_get监视栏中添加了若干相关变量,留作之后比较之用。

再看程序运行到被调函数内后的情况:

二维数组的存储与传参-2022-07-25-20-47-10

可以看到,此时指针matrix_get中内容依然是地址0xdf6cc0,与matrix相同。但注意到,其所指向的内容,不再是二维数组的首行元素,而是另一串内存地址(该地址的意义暂且不深究),并且matrix_get + 1值为0xdf6cc8,而matrix + 1值为0xdf6cd0。也就是说,matrix_get所指向的“东西”占8字节,而matrix所指向的“东西”占16字节。不难理解,前者指向了另一个指针^5,后者指向了二维数组的一行元素。这便是方式三与方式二所传递指针的不同之处。

此时,要访问二维数组元素,需要对matrix_get指针做强制类型转换,(int*)matrix_get,从监视栏可以看到,强制类型转换后,其存储内容(二维数组首地址)不变,而其内容变成了二维数组首行的第一个元素。更进一步,(int*)matrix_get + 1中的内容为0xdf6cc4,即二维数组的首行第二个元素地址,内容也相符。

综上,可以理解,做强制类型转换后的指针(int*)matrix_get是指向二维数组中单个元素的指针。或者说,此时的二维数组被“恢复”为一维数组,(int*)matrix_get指向该一维数组的首元素地址。因而,对这个“名为”二维数组,“实为”一维数组的访问,不能像前述两种传参方式一样,使用数组行列下标。而应该自行增加一维到二维的转换逻辑。即使用形如:*((int*)matrix_get + {行下标}*{总列数} + {列下标})的形式,对二维数组进行访问。

至此,是我对二维数组存储和传参的全部理解。

完整代码

解题所需的所有要素都已齐全,最后来看完整代码:

#include <iostream>

void switchPos(int **matrix, const int &order, const int &p1r, const int &p1c, const int &p2r, const int &p2c); //交换矩阵中两元素位置
void reverseMatrix(int **matrix, const int &order); //转置矩阵

int main()
{
    //设置矩阵并初始化
    int order=0;
    std::cout << "pls enter the order: ";
    std::cin >> order;
    int matrix[order][order];
    std::cout << "pls enter the matrix: " << std::endl;
    for (int row = 0;row < order;++row)
    {
        for (int col = 0;col < order;++col)
        {
            std::cin >> matrix[row][col];
        }
        std::cout << "row end." << std::endl;
    }
    std::cout << "enter complete." << std::endl;

    //调试参数
    // int matrix[4][4] = {1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16};
    // int row=4,col=4;

    //转置矩阵
    reverseMatrix(((int**)matrix),order);

    //输出转置后的矩阵
    std::cout << "the reversed matrix is: " << std::endl;
    for (int row = 0;row < order;++row)
    {
        for (int col = 0;col < order;++col)
        {
            std::cout << matrix[row][col];
            std::cout << " ";
        }
        std::cout << std::endl;
    }
    return 0;
}

void switchPos(int **matrix, const int &order, const int &p1r, const int &p1c, const int &p2r, const int &p2c)
{
    const int intermediation = *((int*)matrix+p1r*order+p1c);
    *((int*)matrix+p1r*order+p1c) = *((int*)matrix+p2r*order+p2c);
    *((int*)matrix+p2r*order+p2c) = intermediation;
}

void reverseMatrix(int **matrix, const int &order)
{
    for (int row = 0;row<order;++row)
    {
        for (int col = 0;col < order;++col)
        {
            if (row<col) switchPos(matrix, order, row, col, col, row); //只针对上三角形区域内元素调用一次switchPo,不包括主对角线元素
            else continue;
        }
    }
}

一点尾巴

另一个简单的论述题

题干

书中6-29题展示了这样一段代码:

#include <iostream>
using namespace std;
int main()
{
    int arr[] = {1, 2, 3};
    double *p = reinterpret_cast<double *>(&arr[0]);
    *p = 5;
    cout << arr[0] << " " << arr[1] << " " << arr[2] << endl;
    return 0;
}

并要求读者运行、观察和指出其中存在的安全性问题。这里简单叙述一点我浅薄的看法。

运行结果

二维数组的存储与传参-2022-07-25-21-28-35

可以看到,在通过double型指针对数组的第二个元素赋值5后,第二个元素实际上变成了1075052544

分析

double型变量在存储时占用64个二进制位,double型的5在机器中以如下二进制串存储:

0100000000010100000000000000000000000000000000000000000000000000

int型在存储时占用32个二进制位,截取上述二进制串的前32位,即:

01000000000101000000000000000000

作为一个int型变量,这个32位二进制数值为1075052544,与程序运行结果相符。

也就是说,通过指针给对象赋值时,右值类型随指针类型而定。当该类型与对象实际类型不符时,则可能会出现“意想不到”的错误。

由此,我们在使用指针时,应该合理选择其类型,并谨慎使用reinterpret_cast

全文完

参考

  1. 关于二维数组传参 by 天涯一缕清枫