二维数组与二级指针

本文最后更新于:2023年12月17日 下午

前言

最近再刷《剑指Offer》的面试题,其中遇到有一题跟二维数组相关,所以需要这样形式的函数,void func(int** matrix, int rows, int columns),那么问题来了,再调用这个函数的时候,应该传入怎样的实参呢

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// 需要被调用的函数
void func(int** matrix, int rows, int columns) {
cout << matrix[rows-1][columns-1] << endl;
}

// 第一种传入实参方式
int matrix[2][2] = {1,2,3,4};
func(matrix, 2, 2);

// 第二种传入实参
int** matrix = new int*[2];
for (int i=0; i < 2; i++) {
matrix[i] = new int[2];
for (int j=0; j < 2; j++) {
matrix[i][j] = i*2 + j + 1;
}
}
func(matrix, 2, 2);

踩坑过程

其实上面的答案很简单,直接敲代码测试一下,就知道第一种传参方式编译器直接报错,提示你类型不匹配。但是我头铁啊,愣是用强制转换 func((int**)matrix, 2, 2)的方式,然后就是程序崩溃报错了。

这是为什么呢?现在来理性分析一波,以下面的测试代码来进行说明。

首先matrix 是一个二维数组,它(数组名)对应的指针类型实际上是 int(*)[2] ,即指向数组的指针(有人称作是行数组指针)[1],也就是说如果把matrix看成是一个指针的话,指向的类型是一个长度为2的int数组,因此在对matrix执行指针加法(matrix+1)的时候,一次移动的字节数为一个数组的长度即 2*4 = 8 字节。那么执行解引用时, matrix[1] 得到是第2行一维数组的首地址,其类型就是一维数组。同时一维数组名对应指针类型为int *,因此再对 matrix[1] 执行指针加法操作(matrix[1] + 1)时,一次移动的字节数为一个int类型的字节长度。

其次对于二级指针(指针的指针)p 来说,因为p 指向的是一个int *指针,所以 p 执行指针加法的时候,一次移动的长度为 int * 指针类型的字节数(即8字节),那么解引用得到数据类型也就是 int * ,所以p[0] 得到的元素就是一个int * 指针,打印 p[0] 的值就是打印该指针的值,而指针的值就是一个地址。为什么这里打印出来的值这么怪异呢?因为它把matrix二维矩阵存储的int值当做指针的值来进行解析了,例如3 和 5 两个int值合起来为8字节,这个8个字节被当做地址的值而不是元素int的值,再加上我的电脑是小端(即低字节在低地址),最后打印出来的 p[0] 就是0x500000003 了。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
int matrix[2][2] = {3,5,7,8};
cout << "matrix address: " << matrix << endl; // 0x16d92b610
cout << "matrix[0] address: " << matrix[0] << endl; // 0x16d92b610
cout << "matrix+1 address: " << matrix+1 << endl; // 0x16d92b618
cout << "matrix[1] address: " << matrix[1] << endl; // 0x16d92b618

int** p = (int**) matrix;
cout << "p address: " << p << endl; // 0x16d92b610
cout << "p[0] address: " << p[0] << endl; // 0x500000003
cout << "p+1 address: " << p+1 << endl; // 0x16d92b618
cout << "p[1] address: " << p[1] << endl; // 0x800000007

int ** p_matrix = new int*[2];
p_matrix[0] = new int[2];
p_matrix[1] = new int[2];
p_matrix[0][0] = 3; p_matrix[0][1] = 5;
p_matrix[1][0] = 7; p_matrix[1][1] = 8;
cout << "p_matrix address: " << p_matrix << endl; // 0x60000081c020
cout << "p_matrix[0] address: " << p_matrix[0] << endl; // 0x60000081c030
cout << "p_matrix+1 address: " << p_matrix+1 << endl; // 0x60000081c028
cout << "p_matrix[1] address: " << p_matrix[1] << endl; // 0x60000081c040

啰里啰嗦说了这么多,可能还是没解释清楚,反而把自己绕晕了,所以再用下面这张图解释一下。

内存图解

弄清楚内存中数据分布后,现在应该知道为什么不能直接把二维数组matrix直接传给二级指针了吧。不过还有一个点,可能还是比较令人感到疑惑的,就是为什么matrix+1matrix[1] 打印出来的值是相同的?

我是这么理解的(如果不对,还请在评论区中指出):因为matrix 类型时 int(*)[2],是一个指向一维数组的指针,因此解引用得到的应该就是个数组,所以可以认为 matrix[1] 就是个一维数组,而一维数组名不就是数组首元素的地址嘛,因此我觉得编译器在对 matrix+1这个地址解引用取值的时候,并没有像 二级指针 int** p 那样去取内存里的值作为地址值,而是直接将当前的地址值作为数组名的地址值反悔了。所以最终 matrix+1matrix[1] 打印得到的结果相同。


其实还遇到了另一种比较奇怪的形式,见下面的函数。我试了好几种传参方式,但总是编译通过,编译器一直提示我类型不匹配,告诉我形参matrix的真实类型为int(*)[columns],这就和尴尬,因为columns是个变量,我怎么传一个数组长度为变量的数组指针?

我们都知道C++里,数组的长度是一个固定值,那么这里的func2为什么可以通过编译?更痛苦的是我还无法传参。。。

1
2
3
4
// 这里 martix 类型等价为 int(*)[columns]
void func2(int rows, int columns, int matrix[rows][columns]) {
cout << matrix[rows-1][columns-1];
}

后来我在stackoverflow上提了这个问题(结果标记为 模板参数自动推导 的问题,但我感觉还是不太一样),有人回复我说,这种函数定义方式在C里面是有效的,但在C++里是不合法,所以这不是如何传参的问题,而是这样的写法在C++标准里是不允许的。[2]虽然感觉还是没说明白为什么编译可行,但是姑且认为是C++标准不允许这样的写法,所以导致没法传参。

结论

最后总结一下,二维数组是不能直接赋值给二级指针的,二维数组对应的指针类型是一个数组指针,下面表格给出数组和指针的参数匹配。[3]

实参例子所匹配的形参
二维数组char c[4][3]char (*c)[3]
指针数组char * c[4]char ** c
数组指针char (*c)[3]char (*c)[3]
二级指针char ** cchar ** c

然后再给出几种正确的传递二维数组的姿势。

1.形参使用二级指针

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
void func(int **matrix, int rows, int columns) {
cout << matrix[rows - 1][columns - 1];
}
int main() {
int rows = 2, columns = 2;
int **matrix = new int *[rows];
for (int i = 0; i < rows; i++) {
matrix[i] = new int[columns];
for (int j = 0; j < columns; j++) {
matrix[i][j] = i * columns + j + 1;
}
}
func(matrix,rows, columns);

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

2.形参使用一级指针

1
2
3
4
5
6
7
8
9
10
11
12
13
14
void func(int *matrix, int rows, int columns) {
cout << matrix[(rows - 1) * columns + (columns - 1)];
}

int main() {
int rows = 2, columns = 2;
int *matrix = new int[rows * columns];
for (int i = 0; i < rows; i++) {
for (int j = 0; j < columns; j++) {
matrix[i * columns + j] = i * columns + j + 1;
}
}
func(matrix, rows, columns);
}

3.形参使用数组指针

采用这种方式,需要形参指定二维数组的列数

1
2
3
4
5
6
7
8
9
10
// columns = 2
void func(int (*matrix)[2], int rows, int columns) {
cout << matrix[rows-1][columns-1];
}

int main() {
int rows = 2, columns = 2;
int matrix[2][2] = {1,2,3,4};
func(matrix, rows, columns);
}

4.形参使用指针数组

采用这种方式,需要形参指定二维数组的行数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// rows = 3
void func(int* matrix[3], int rows, int columns) {
cout << matrix[rows - 1][columns - 1];
}
int main() {
int rows = 3, columns = 2;
int *matrix[rows];
for (int i = 0; i < rows; i++) {
matrix[i] = new int[columns];
for (int j = 0; j < columns; j++) {
matrix[i][j] = i * columns + j;
}
}
func(matrix, rows, columns);

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

5.形参使用 vector 容器

1
2
3
4
5
6
7
8
9
10
11
12
13
14
void func(vector<vector<int>>& matrix, int rows, int columns) {
cout << matrix[rows - 1][columns - 1];
}

int main() {
int rows = 3, columns = 2;
vector<vector<int>> matrix(rows);
for (int i=0;i<rows;i++) {
for (int j=0;j<columns;j++) {
matrix[i].push_back(i * columns + j + 1);
}
}
func(matrix, rows, columns);
}

备注/参考


二维数组与二级指针
https://2017zhangyuxuan.github.io/2022/02/28/2022-02/2022-02-28 二维数组与二级指针/
作者
Zhang Yuxuan
发布于
2022年2月28日
许可协议