二维数组传参与动态声明详解

二维数组传参

基本形式

二维数组在栈上分配,各行地址空间连续

定义的时候,拥有两种形式

第一种是指明行数和列数

1
int array[3][3] = {{1,2,3},{4,5,6},{7,8,9}};

第二种是不指明第一维,而指明第二维

1
int array[][3] = {{1,2,3},{4,5,6},{7,8,9}};

而在子函数声明的时候,有三种方式

一种是指明形参的行数和列数

1
2
3
4
5
6
7
8
9
10
int func(int array[][3], int m, int n) {
int i,j;
for(i=0;i<m;i++) {
for(j=0;j<n;j++)
//printf("\t%d", *(*(array +i) +j)); //可行
printf("\t%d", array[i][j]);
printf("\n");
}
return 0;
}

第二种是不指明形参的第一维,而指明第二维

1
2
3
4
5
6
7
8
9
10
int func(int array[][3], int m, int n) {
int i,j;
for(i=0;i<m;i++) {
for(j=0;j<n;j++)
//printf("\t%d", *(*(array +i) +j)); //可行
printf("\t%d", array[i][j]);
printf("\n");
}
return 0;
}

第三种是形参使用指针的形式,

1
2
3
4
5
6
7
8
9
10
int func(int (*array)[3], int m, int n) {
int i,j;
for(i=0;i<m;i++) {
for(j=0;j<n;j++)
//printf("\t%d", *(*(array +i) +j)); //可行
printf("\t%d", array[i][j]);
printf("\n");
}
return 0;
}

而定义的两种形式与调用的三种形式是可以互相混合使用,因此基本形式一共有2x3=6中方法

运行结果均为:

这种分配是在栈上进行的,能够保证所有元素的地址空间连续。这样,array[i][j]*(*(array +i) +j)是一样的,程序是知道array+ii实际上偏移了i*N个单位,这也导致了在二维数组array[3][3]中,使用下标array[2][1]array[1][4]是访问的同一个元素,尽管后者的下标对于一个3*3矩阵来说是非法的,但这并不影响访问。

这种形式,无论是数组定义还是函数都不够泛用,两个维度在编译前就定好了,唯一可以做的就是把维度M、N声明为宏或者枚举类型,但这仍不能避免每次修改后都要重新编译。

注意:在主函数中调用子函数的方法与在子函数中访问数组的方法

在主函数中调用子函数

1
func(array,m,n);

在子函数中使用形参数组有两种方式array[i][j]*(*(array +i) +j)

数组传参形式

二维数组在栈上分配,各行地址空间连续,函数参数使用指针形式

当把这种二维数组的指针直接作为参数传递时,数组名退化为指针,函数并不知道数组的列数,N对它来说是不可见的,即使使用*(*(array +i) +j),第一层解引用失败。这时,编译器会报warning,运行生成的文件会发生segment fault。那么,为了指导这个函数如何解引用,也就是人为地解引用,需要把这个二维数组的首元素地址传给函数,于是就变成了下面的形式:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
#include <stdio.h>
#include <stdlib.h>
#include <assert.h>
int func(int *array, int m, int n) {
int i,j;
for(i=0;i<m;i++) {
for(j=0;j<n;j++)
printf("\t%d", *(array +i*n +j));
printf("\n");
}
return 0;
}

int main(int argc,char** argv) {
int m=3,n=3,i;
int array[][3] = {{1,2,3},{4,5,6},{7,8,9}};
func(*array,m,n);
return 0;
}

上面代码如果将主函数调用子函数语句改为func(array,m,n);不可行,则会报错error: cannot convert 'int (*)[3]' to 'int*' for argument '1' to 'int func(int*, int, int)'

当然也可以写成

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
int fun(int *array,int m,int n) {
int i,j;
for(i=0;i<m;i++)
for(j=0;j<n;j++)
printf("%d ", *((int*)array + n*i + j));
return 0;
}
int main() {
int array[3][3] = {
{1,2,3},
{4,5,6},
{7,8,9}
};
fun((int *)array,3,3);
return 0;
}

但是意图没有上一种清晰,并不推荐。

你可能会问,为什么下面的不行?原因其实和上面提到的一样,第一次解引用时,函数并不知道数组的列数,从而导致失败。准确的说,是因为数组实际类型是int [3][3],在作为右值时可以被转化为int (*)[3],它们都和int **不同,自然不可用。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
int func(int **array, int m, int n) {
...
printf("\t%d", *(*array +i*n +j));
...
}

int main() {
  int array[3][3] = {
{1,2,3},
{4,5,6},
{7,8,9}
};
...
func(array,3,3);
 ...
}

动态数组形式

第2种虽然函数参数的限定降低了,但仍需要在栈上预先分配一定大小的二维数组,程序整体并不是完全的泛用。为了进一步提高泛用性,把二维数组空间的分配也动态化,使用malloc()在堆上分配空间:

1
2
3
4
int **array;
array = (int **)malloc(m *sizeof(int *));
for(i=0;i<m;i++)
array[i] = (int *)malloc(n *sizeof(int));

画出该动态数组的内存分布如下图所示,其中array与array[i]因为使用到了malloc,因此需要被free.

这时,在分配空间的作用域里,对0<i<m,0<j<n,array[i][j]的访问完全没有问题。那么,对应地,函数写作

1
2
3
4
5
int func(int **array,int m,int n) {
...
printf("%d ", *(*(array+i)+j));
...
}

值得注意的是,虽然malloc()每次分配的空间在地址上是连续的,但是多次malloc()分配的空间之间并不一定是连续的,这与在栈上分配的二维矩阵有着根本的不同,对于二维数组array[3][3],不能再用array[1][4]来访问array[2][1]了,前者地址越界。而且如果malloc申请的空间太多了 堆上可能就容纳不下了 到头来还是得考虑用栈实现分配。

折中形式

用堆上分配的一维数组表示二维数组,函数参数使用指针形式

用一维数组来实现二维数组,是一种折中方案,但是很好理解,也不易出错。这样分配的数组空间是连续的。使用时需要把两维下标转化为一维下标。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
#include <stdio.h>
#include <stdlib.h>
#include <assert.h>
int func(int *array, int m, int n) {
int i,j;
for(i=0;i<m;i++) {
for(j=0;j<n;j++)
printf("\t%d",*(array+i*n+j));
printf("\n");
}
return 0;
}

int main(int argc,char** argv) {
int m,n,i;
int *array;
assert(argc == 3);
m = atoi(argv[1]);
n = atoi(argv[2]);
array = (int*)malloc(m*n*sizeof(int));
for(i=0;i<m*n;i++)
array[i] = i;
func(array,m,n);
return 0;
}

较新的编译器

用栈上分配的直到执行时才确定大小的二维数组

C90不支持这种形式,C99支持,因此一些较新的编译器可以对下面的代码进行执行。注意print()的参数顺序不能改变。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
void print(int x, int y, int a[x][y]){
printf("\n");
int i, j;
for(i = 0; i < x; i++){
for(j = 0; j < y; j++)
printf("%d ", a[i][j]);
printf("\n");
}
}

// Function to initialize the two-dimensional array
void init_2d(int *a, int x, int y){
int i, j;
for(i = 0; i < x; i++){
for(j = 0; j < y; j++){
a[i*y + j] = i + j;
}
printf("\n");
}
}

int main(){
int m , n ;
scanf("%d %d",&m,&n);
int a[m][n]; // a two dimensional whose size has been defined using variables
init_2d(a, m, n);
print(m, n, a);
}

另外,这种分配方式仍然是在栈上,相关讨论可见于http://bbs.csdn.net/topics/90350681。

小结

  • 其实所谓的二维数组,在K&R上只是指预先分配好大小的形如int a[M][M]这样的数组,它存在于栈上;而实际使用的在堆空间利用malloc动态分配空间的并不是这种,只是用的人多了,把后者叫成二维数组了(我不认为把后者也称为二维数组是标准的说法)。再加上我们经常用它来处理矩阵,”标准的”二维数组、”动态的”二维数组、矩阵这三个概念就混在了一起。矩阵是可以用这两种二维数组表示的,而对于这两种不同的二维数组,函数传参的方式也不完全相同,不能随意混用。

  • C99对于多维数组的描述:
    If E is an n -dimensional array ( n ≥ 2) with dimensions i × j × … × k , then E (used as other than an lvalue) is converted to a pointer to an ( n − 1)-dimensional array with dimensions j × … × k . If the unary * operator is applied to this pointer explicitly, or implicitly as a result of subscripting, the result is the pointed-to ( n − 1)-dimensional array which itself is converted into a pointer if used as other than an lvalue. It follows from this
    that arrays are stored in row-major order (last subscript varies fastest).

  • 栈上分配的二维数组数组名int array[3][3]的真实类型是int [ ][ ],在作为右值时才被转化为(int *array)[N] (感谢 garbageMan指出),和int **是不同的。把前者进行强制转换为后者,在函数中对元素操作也会导致段错误。而这样就能解释为什么int array[3][3]既可以用f(int array[3][3]) 来传参,也可以用f(int (*array)[3]) 来传参了下面用图来说明二者区别:

固定矩阵的大小,可以省略二维矩阵的第一维

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
#include <stdio.h>
#include <stdlib.h>
#include <assert.h>
int func(int *array, int m, int n) {
int i,j;
for(i=0;i<m;i++) {
for(j=0;j<n;j++)
printf("\t%d", *(array +i*n +j));
printf("\n");
}
return 0;
}

int main(int argc,char** argv) {
int m=3,n=3,i;
int array[][3] = {{1,2,3},{4,5,6},{7,8,9}};
//或者使用这种定义方式,结果相同
//int array[3][3] = {{1,2,3},{4,5,6},{7,8,9}};
func(*array,m,n);
return 0;
}

运行结果为:

动态二维数组的声明与赋初值

当从键盘读取一维与二维的长度时,可以动态二维数组分配内存,但是a[][]不行,会报错。

C++中有三种方法来动态申请多维数组

  • C中的malloc/free
  • C++中的new/delete
  • STL容器中的vector

malloc/free

动态开辟一维数组

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
//动态开辟一维数组
void dynamicCreate1Array()
{
int m;
int i;
int *p;

printf("请输入开辟的数组长度:");
scanf("%d",&m);
p = (int*)malloc(sizeof(int)*m);//动态开辟

printf("请输入数据:");
for(i = 0; i < m ; i++)
scanf("%d",&p[i]);

printf("输出数据:\n");
for(i = 0; i < m; i++)
printf("%d ",p[i]);
free(p);
}

img

动态开辟二维数组

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
#include <stdio.h> //包含输入输出函数
#include <stdlib.h>//包含动态内存分配函数。
int main()
{
int **p; //定义二维指针。
int m, n;//行数和列数。
int i,j;
scanf("%d%d", &m, &n); //输入行数和列数。
if(m <= 0 || n <= 0) return -1; //行数或列数非法。
p = (int **) malloc(sizeof(int *) *m);//申请一组一维指针空间。
for(i = 0; i < m; i ++)
p[i] = (int *)malloc(sizeof(int) * n); //对于每个一维指针,申请一行数据的空间。
for(i = 0; i < m; i ++)
for(j = 0; j < n; j ++)
scanf("%d",&p[i][j]);//输入第i行第j列的数据。其中&p[i][j]也可以写作p[i]+j或者是 *(p+i) + j. 功能相同。
printf("输入的数组为%d行%d列:\n", m, n);
for(i = 0; i < m; i ++)
{
for(j = 0; j < n; j ++)//这个循环可以输出一行元素。
printf("%d ", p[i][j]);//输出i行j列的元素。
printf("\n");//每行输入结束加换行符。
}
//释放内存
for(i = 0; i < m; i ++)
free(p[i]);
free(p);
return 0;
}

img

记住要使用free

new/delete

动态开辟一维数组

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
void DynamicCreate1Array()
{
int len;

cout<<"请输入长度:";
cin>>len;

int *p = new int[len];

cout<<"请输入数据:";
for(int i = 0; i < len; i++)
cin>>p[i];

cout<<"输出数据:"<<endl;
for(i = 0; i < len; i++)
cout<<setw(4)<<p[i];

delete[] p;
}

动态开辟二维数组

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
void DynamicCreate2Array()
{
int m,n;
cout<<"请输入行和列:";
cin>>m>>n;

//动态开辟空间
int **p = new int*[m]; //开辟行
for(int i = 0; i < m; i++)
p[i] = new int[n]; //开辟列

cout<<"请输入数据:";
for(i = 0 ; i < m ; i++)
for(int j = 0; j < n; j++)
cin>>p[i][j];

cout<<"输出数据:"<<endl;
for(i = 0; i < m; i++)
{
for(int j = 0; j < n; j++)
cout<<setw(3)<<p[i][j];
cout<<endl;
}

//释放开辟的资源
for(i = 0; i < m; i++)
delete[] p[i];
delete[] p;

}

STL中的vector

动态开辟二维数组

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
void VectorCreate()
{
int m,n;
cout<<"请输入行和列:";
cin>>m>>n;

//注意下面这一行:vector <int后两个 "> "之间要有空格!否则会被认为是重载 "> > "。
vector<vector<int> > p(m,vector<int>(n));

cout<<"请输入数据:";
for(int i = 0 ; i < m ; i++)
for(int j = 0; j < n; j++)
cin>>p[i][j];

cout<<"输出数据:"<<endl;
for(i = 0; i < m; i++)
{
for(int j = 0; j < n; j++)
cout<<setw(3)<<p[i][j];
cout<<endl;
}

}

参考

C++中动态申请二维数组并释放方法

------ 本文结束------
坚持原创技术分享,您的支持将鼓励我继续创作!

欢迎关注我的其它发布渠道