在搞图论算法的时候,又遇到了如何又好又方便地传递二维数组的问题,今天我就来把他完全解决掉。
一. 数组类型和指针类型
哪怕是富有经验的程序员,有时候也会把数组类型和指针类型给弄混。
与二维数组有关的类型,共有三种,以int类型为例,他们分别是:
- int**a;————指针的指针(剧透:这个才是导致我们混淆的万恶之源)
- int (*a)[const_expression];————数组的指针
- int a[const_expression1][const_expression2];————数组
这三种类型完全不同。下面我通过两个例子来证明这一点:
(P.S.第一个例子说明1和2不同,第二个例子说明3和1,2不同)
- 给函数传递多维数组,如果函数的参数被设定为是指针的指针类型,则会出错:
void Show(int** a, int m, int n) {
for (int i = 0; i < m; ++i) {
for (int j = 0; j < n; ++j) {
cout << a[i][j] << " ";
}
cout << endl;
}
}
int main(void) {
int a[5][5] = { { } };
Show(a, 5, 5);
return 0;
}
编译器有如下报错:
既然int (*)[5]和int** 之间不能转换,说明他们根本就不是同一类型
2. 使用sizeof测所占空间的字节数不同:
综上所述,我们那三种类型是完全不同的。
二. 数组退化为指针
为什么很多人都会把数组和指针类型弄混?大概也是三方面的原因。
- 指针和数组有着相似的表达方式,以上三种类型我们都可以用a[m][n]来访问相应的元素。
- 在向函数传递参数的时候,数组会退化为指针。
- 动态申请内存的时候,我们也得使用指针(这个我在下面会有更详细的讨论)
在这个小标题下,我着重讲解第二点——为什么数组会“退化”为指针?
其本质原因在于,一般我们认为向函数传递数组的时候,传递的是理应是整个数组对象。
// 注意!!这种写法是错误的,这里只是为了演示!!
int a[m][n];
my_fun(a); // 我们希望把整个数组传进去,那当然是把a当作object传喽
但是在C语言中,a代表的不是整个数组对象(可以与C++中vector对比),而是数组类型的首地址。
这就会有一个非常严重的问题——信息的丢失(其实就是数组维度的丢失)。
这个可以和一维数组类比,我们把一维数组当作参数传入函数时,会再提供一个关于元素个数的信息,其实这就是丢失了维度。
StackOverflow上这个问题最高票的回答每个例子都有问题,你看出来了吗?
三. 给二维数组申请内存
在C中有个被人津津乐道的内容,那就是用malloc等函数动态申请内存。
但用malloc有个特点,就是它返回的是地址,我想再heap中申请一个int类型的变量,结果我得到的却是一个int*的指针:
int* p = (int *)malloc(sizeof(int)); //现在我只能用*p来间接访问那个值了
对数组而言也是一样的,如果我想再heap中申请一个数组,我返回的也是一个关于数组的指针——这个我们在一中已经提到过了:
int (*my_array)[Y] = (int (*)[Y])malloc(sizeof(int[X][Y]));
要注意到这里的Y一般只能是常量或常量表达式,这样的申请本质上是int(*)[m]与int[n][m]之间的转换。动态申请二维数组还有一个常见的错误的方法,见下面一个标题。
四. 错误的动态二维数组
在学习C的时候我们常常有这样的困惑——比如我想要获得一个MxN的矩阵,我能不能在运行时确定它的大小呢?换句话说,我在命令行输入相应m,n,就能得到一个m*n的矩阵。
有些人就想到了这种办法:
int m, n;
scanf("%d%d", &m, &n);
// 为指针的指针开辟空间
int **p = (int**)malloc(m * sizeof(int*));
// 为每一个指针开辟空间
for (int i = 0; i < m; ++i) {
p[i] = (int*)malloc(n * sizeof(int));
}
这样我们就可以在函数的参数列表中写int**这种东西了。。
先上结论:这种申请方法得到的完全不是数组。
有如下两个原因:
- 数组的元素在内存里必须是连续的,但是你每一个元素都是通过malloc,就不能保证在heap中连续。
- 用memcpy不能进行数组的复制。
这个详情请见StackOverflow,讲得肯定比我好。
总之,这个问题最后可以归结为,int**类型与数组无关。
五. 正确的动态二维数组
实话实说,你想要在heap内申请动态二维数组,只能通过三中的方法。不过这样就达不到我们输入m,n,获得m*n数组的目的了。
真的没有办法了吗?其实是有的,那就是用一维数组模拟二维数组——弊端是无法用a[m][n]这样看上去很舒服的语法来写代码了。
// 这里稍微把这种一维数组模拟二维数组的写法提一下
// 先是声明部分
int *p = (int*)malloc(m * n * sizeof(int));
// 对于访问i行j列的元素,可以这么做(注意从0开始计数)
printf("%d\n", a[i * m + j]);
相关的例子:
(写到这,笔者总算是明白为什么机器人工程师杨硕学长会要求我们用一维数组和指针来进行矩阵的运算了。)
六. 优雅地向函数传递数组
这个标题的灵感来源——同城的华中师范大学的myk学长的一篇文章。
他采用C++中引用的方法,不过指针和引用其实可以达到完全一致的效果。这个在我另一篇blog中会提到,这里要讲的是——既然向函数传数组至少要两个参数(数组退化而成的指针和损失的维度),有没有办法只传一个参数呢?
他的文中说是用引用,我们这里是C语言,采用指针实现一下:
// 不需要传递损失维度的函数(C实现)
void Show(int(*p)[2][2]) {
for (int i = 0; i < 2; ++i) {
for (int j = 0; j < 2; ++j) {
printf("%d\t", (*p)[i][j]); // 利用\t实现输出的对齐操作
}
puts(""); // 换行
}
}
int main() {
int a[2][2] = { {1, 2},
{3, 4}
};
Show(&a);
return 0;
}
很显然,成功了。
七. 总结
- 从类型上看,C中与二维数组相关的只有int(*)[n]和int[m][n]类型了,涉及到二维数组指针是因为当参数传会变指针,动态分配会变指针。实在是没什么办法。而int**不是数组。(这么理解把,所谓数组类型,就是要明确地指出各个维度;数组的指针,在此基础上类似于普通的指针)
- 给二维数组动态分配内存。
- 使用动态数组的方法之一——用一维数组模拟矩阵。
- 利用指针的技巧向函数传递不带多余参数的数组。
- 其它的,这么多演示下来,应该明白[]的优先级高于*了。(逃
还有其它的技巧:比如sizeof用%zu转换啊,用{{}}初始化自动变量类的数组啊,用\t对齐输出啊等等,这个自己看吧。