目录
多维数组
如果某个数组的维数不止1个,它就被称为多维数组。例如,下面这个声明
int matrix[6][10];
怎么解释这个多维数组呢?创建一个包含60个元素的矩阵。但是,它是6行每行10个元素,还是10行每行6个元素?
为了回答这问题?你需要从不同视点观察多维数组。考虑下列这些维数不断增加的声明:
int a;
int Array_b[10];
int Array_c[6][10];
int Array_d[3][6][10];
a是一个简单的整型标量。
接下来的那个声明增加了一维,所以Array_b是一个向量,它包含10个整型元素。
Array_c只是在Array_b的基础上再增加一维,所以我们可以把Array_c看作是一个包含6个元素的向量,只不过它的每个元素本身是一个包含10个整型元素的向量。换句话说,Array_c是一维数组的一维数组。
Array_d也是如此:它是一个包含3个元素的数组,每个元素都是包含6个元素的数组,而这6个元素中的每一个又包含10个整型元素的类型。
对于上述的理解(视点)是非常重要的,因为它正是C实现多维数组的基础。为了加强多维数组的概念,我们先看数组在内存中的存储顺序是怎样的?
存储顺序
考虑下面这个数组:
int Array[3];
它包含3个元素,如下图所示:
但现在你被告知这3个元素中的每一个实际上都有包含6个元素的数组,情况又将如何实现呢?下面是这个新的声明:
int Array[3][6];
下面是它在内存中存储方式:
实线方框表示第1维数组的三个元素,虚线用于划分第2维的6个元素。按照从左到右的顺序,上面每个元素的下标值分别是:
0,0 0,1 0,2 0,3 0,4 0,5
1,0 1,1 1,2 1,3 1,4 1,5
2,0 2,1 2,2 2,3 2,4 2,5
这个例子充分说明了数组元素的存储顺。在C中,多维数组的元素存储顺序按照最右边的下标率先变化的原则,称之为行主主序。在实际应用场景中,用的比较多的是二维数组。本篇文章以二维数组作为讲解。
二维数组基础知识
包含两个维度的数组,我们称之为二维数组。例如,下面这个声明:
int Array_b[3][6];
在学习二维数组过程中,对于初学者可能会有疑惑:数组名在声明中起到什么作用?(有经验的程序员可略过)
数组名
一维数组名的值是一个指针常量,它的类型是“指向元素类型的指针”,它指向数组的第1个元素。多维数组也差不多简单,它的类型是“指向具有多个元素的数组的指针”。可用通过数组名和下标结合使用这样就非常简单的访问数组中的元素。继续往下看下标在数组中起到了什么不一样的效果。
下标
数组为什么要通过下标访问数组中的元素呢?在C中,如果要标识一个多维数组的某个元素,必须按照与数组声明时相同的顺序为每一维都提供一个下标,而且每个下标都单独于一对方括号([])内。比如下面的声明中:
int matrix[3][10];
表达式matrix[1][5]访问下面这个元素;
但是,下标引用实际上只是间接访问表达式的一种伪装形式,即在多维数组中也是如此。在使用数组中,我们不可避免的考虑其类型是什么?既然讲到数组类型,那么matrix的类型是什么?它的类型是“指向包含10个整型元素的数组的指针”,它的值是:
它指向包含10个整数的第1个子数组。
表达式
matrix + 1
也是一个“指向包含10个整型元素的数组的指针”,但它指向matrix的另一行:
为什么?因为1这个值根据包含10个整型元素的数组的长度进行调整,所以它指向matrix的下一行。如果对其执行间接操作,就如下图随箭头选择中间这个子数组:’
所以表达式*(matrix +1)实际上标识了一个包含10个整型元素的子数组,且与&matrix[0]等价。数组名的值是指针常量,它指向数组的第1个元素,在这个表达式中也是如此。它的类型是“指向整型的指针”。接下来猜猜下面表达式的结果是什么?
*(matrix+1) +5
前一个表达式是个指向整型值的指针,所以5这个值根据整型的长度进行调整。整个表达式的结果是指针,它指向的位置比上一个表达式所指向的位置向后移动了5个整型元素。
对其执行间接访问操作:
*(*(matrix+1) +5)
它所访问的正是图中的那个整型元素。如果它作为右值使用,你就取得存储于那个位置的值。如果它作为左值使用,这个位置将存储一个新值。
我们仔细再回想一下表达式matrix + 1,它的类型是“指向包含10个整型元素的数组的指针”,接下来详细地解读“指向数组的指针”。
指向数组的指针
先看如下声明是否合法么?
int vector[10], *vp = vector;
itn matrex[3][10], *tmp = matrix;
第1个声明是合法的。它为一个整型数组分配内存,并把vp声明为一个指向整型的指针,并把它初始化为指向vector数组的第1个元素。vector和vp具有相同的类型:指向整型的指针。但是,第2个声明是非法的。它正确地创建了matrix数组,并把tmp声明为一个指向整型的指针。但是,tmp的初始化是不正确的。因为matrix并不是指向整型的指针,而是指向整型数组的指针。
那如何怎样去声明一个指向整型数组的指针呢?
int (*pINT)[10];
这个声明比上面的所有的声明都显得复杂,但它事实上并不难。你只要假定它是一个表达式并对它求值。求值过程如下:下标的优先级高于间接访问,但由于括号的存在,首先执行的还是间接访问。所以pINT是指针,它指向一个包含具有10个整型元素的数组。接着上面的声明是否合法的问题,第2个非法声明修正之后如下:
int matrix[3][10], (*tmp)[10] = matrix;
它使tmp指向matrix的第1行,且tmp是一个指向拥有10个整型元素的数组的指针。
二维数组使用
初始化
在初始化二维数组时,数组元素的存储顺序就变得非常重要。(如果忘记了请回看)编写初始化列表有两种形式。
第1种只给出长长的初始值列表,如下面的例子所示。
int matrix[2][3] = {100, 101, 102, 103, 104, 105};
上面的初始化为何可以像一维数组一样初始化呢?这就涉及到“数组长度自动计算”的知识,其内容大概是:在多维数组中,只有第1维才能根据初始化列表缺省第提供。剩余的几维必须显式的写出,这样编译器就能自动推断出每个子数组维数的长度。所以上述的初始化实现了和一维数组的初始化一样。
第2种方法基于多维数组实际上是复杂元素的一维数组这个概念。例如,下面是一个二维数组的声明:
int matrix[3][5];
我们可以把matrix看成是一个包含3个(复杂的)元素的一维数组。为了初始化这个包含3个元素的数组,我们使用一个包含3个初始内容的初始化列表:
int matrix[3] = {#,#,#};
但是该数组每个元素实际上都是包含5个元素的整型数组,所以每个#的初始化列表都应该是一个由一对花括号包围的5个整型值。用这里列表替换每个#将产生如下代码:
int matrix[3][5] = {
{1, 2, 3, 4, 5},
{6, 7, 8, 9, 10},
{11, 12, 13, 14, 15},
};
这样子能更加简单明白二维数组初始化过程。
作为函数参数的二维数组
作为函数参数的二维数组名的传递方式和一维数组名相同---实际传递的是个指向数组第1个元素的指针。两者的区别在于,二维数组的每个元素本身是另外一个数组,编译器需要知道它的维数,以便为函数形参的下标表达式进行求值。
这里有两个例子,说明了它们之间的区别:
int vector[10]
...
function(vector);
参数vector的类型是指向整型的指针,所以function的原型可能有以下两种:
void function(int* vec);
void function(int vec[])
现在我们来看二维数组:
int matrix[3][10];
...
function(matrix);
这里,参数matrix的类型是指向包含10个整型元素的数组指针。所以function的原型也两种形式:
void function(int (*mat)[10]);
void function(int mat[][10]);
这里的关键在于编译器必须知道第2个及以后各维的长度才能对各个下标进行求值,因此在原型中必须声明这些维的长度。
上述总结:
在编写一维数组形参的函数原型时,你既可以把它写成数组的形式,也可以把它写成指针的形式。但是,对于二维数组,只有第1维可以进行如此选择。尤其,把function写下面这样的原型是不正确的。
void function(int **mat);
这个例子把mat声明为一个指向整型指针的指针,它和指向整型数组的指针并不是一回事。
具体的使用过程如下:
#include <stdio.h>
// 参数为一个指针数组,该指针指向一个包含3个int型的数组
int sum(int (*pArray)[3], int iSize){
int iSum = 0, i, j;
for(i = 0; i < 2; i++){
for(j = 0; j < 3; j++){
iSum += *(*(pArray + i) + j) //深刻理解这个表达式的含义
}
}
return iSum;
}
函数使用注意事项:在使用过程中必须写出列数。因为数组在调用时同样有数组降价的问题,实际上函数得到是一个指针,指向行向量构成的一维数组,这样每个一维数组的大小(size)必须提前定义好,便于编译器分配栈空间 。
二维数组返回值与申请内存问题
返回输入数组的指针,容易让人产生错误的点,函数返回的是数组指针(指向数组的指针),但是如果返回之后指针指向的数组被销毁了么?
我们从内存空间来解释,内存空间的四个区域,栈空间,堆空间,数据区(静态区),代码区。静态变量、全局变量是放在数据区的,作用域是全局的,而局部变量通常位于栈空间,随着被函数调用的退出自动释放空间被销毁。
以数组中螺旋矩阵为例,本例子的思想:坚持循环不变量原则
-
填充上行从左到右
-
填充右列从上到下
-
填充下行从右到左
-
填充左列从下到上
int** piGenerateMatrix(int Row, int Col){
int iStartx = 0, iStarty = 0;
int iLoop = Row / 2;
int iMid = Row / 2;
int iCount = 1;
int iOffset = 1;
int i, j,pm;
int **Res = (int **)malloc(sizeof(int*)*Row); //开辟行空间
for(pm = 0; pm < Col; pm++){ //开辟列空间
Res[pm] = (int*)malloc(sizeof(int) * Col);
}
while(iLoop--){
i = iStartx;
j = iStarty;
// 面开始的四个for就是模拟转了一圈
// 模拟填充上行从左到右(左闭右开)
for(j = iStarty; j < iStarty + Row - iOffset; j++){
Res[iStartx][j] = iCount++;
}
// 模拟填充右列从上到下(左闭右开)
for(i = iStartx; i < iStartx + Row - iOffset; i++){
Res[i][j] = iCount++;
}
// 模拟填充下行从右到左(左闭右开)
for(; j > iStarty; j--){
Res[i][j] = iCount++;
}
// 模拟填充左列从下到上(左闭右开)
for(; i > iStartx; i--){
Res[i][j] = iCount++;
}
// 第二圈开始的时候,起始位置要各自加1,
iStartx++;
iStarty++;
// offset 控制每一圈里每一条边遍历的长度
iOffset += 2;
}
// 如果n为奇数的话,需要单独给矩阵最中间的位置赋值
if(Row % 2){
Res[iMid][iMid] = iCount;
}
return Res;
}
int main(void)
{
int **p = piGenerateMatrix(3,3);
int i = 0;
for(i = 0; i < 3; i++){
printf("%d\n", (*(p+i))[0]);
}
for(i = 0; i < 3; i++){
// 释放列空间
free(p[i]);
}
//释放行空间
free(p);
return 0;
}