理解C语言——从小菜到大神的晋级之路(9)——多维数组

本节视频链接:点击这里

1、多维数组的定义和结构

        一个数组中可以支持各种数据类型,那么一个数组中的每一个元素同样也可以是一个数组。对于上次提到的一维数组,其每个元素都是一个简单数据类型的对象,其结构如同一个一维的数据排列;对于一个二维数组,它的每一个元素都是一个一维数组,其形式如同一个二维的表格,表格的宽度是其中作为数据元素的一维数组的长度,高度是这样的一维数组的个数。简而言之,二维数组的结构是一个矩阵的形式。

        例如,我们声明下面这样的一个二维数组:

int nMatrix[6][10];

        这个二维数组nMatrix包含了6个一维数组,每个一维数组的长度为10,总计有60个int型数据元素。它的逻辑结构如下图所示:

nMatrix                    
nMatrix[0] nMatrix[0][0] nMatrix[0][1] nMatrix[0][2] nMatrix[0][3] nMatrix[0][4] nMatrix[0][5] …... …... …... …..
nMatrix[1] nMatrix[1][0] nMatrix[1][1] nMatrix[1][2] nMatrix[1][3] nMatrix[1][4] …... …... …... …... …..
nMatrix[2] nMatrix[2][0] nMatrix[2][1] nMatrix[2][2] nMatrix[2][3] …... …... …... …... ….. …..
nMatrix[3] nMatrix[3][0] nMatrix[3][1] nMatrix[3][2] ….. …... …... ….. ….. ….. …..
nMatrix[4] nMatrix[4][0] nMatrix[4][1] ….. ….. ….. ….. ….. ….. ….. …..
nMatrix[5] nMatrix[5][0] nMatrix[5][1] ….. ….. ….. ….. ….. ….. ….. …..

        由这样的结构我们也称作是一个“6行10列”的二维数组。由于这种结构特别适合表示图像和视频数据,这样的多维数组在多媒体处理等领域有着广泛的应用。

        我们知道,对于一维数组,其存储结构是一段连续的内存空间。比如一个一维数组int nArray[3],其存储结构为:

0x0001 0x0002 0x0003 0x0004 0x0005 0x0006 0x0007 0x0008 0x0009 0x0010 0x0011 0x0012
nArray[0]       nArray[1]       nArray[2]      

        一个二维数组,相当于一个一维数据每一个元素都是另一个一维数组。二维数组首先从首地址开始按顺序存储第一行的数据,然后在第一行最末尾元素的下一个内存单元开始保存第二行数据,依次类推,直至保存完最后一行。从二维数组的下标来看,按照内存单元的向后依次遍历的顺序,最优先变化的是数组最右边的下标,即,变化顺序为:nMatrix[0][0],nMatrix[0][1],nMatrix[0][2]…...nMatrix[0][9],nMatrix[1][0],nMatrix[1][1], nMatrix[1][2]…...nMatrix[1][9], nMatrix[2][0], nMatrix[2][1]…...nMatrix[2][9], nMatrix[3][0]……….nMatrix[5][9]。C语言中二维数组的这种存储结构称为“行主序”。

        用另外一个例子说明,如果某个数值按照一天中每一小时、每分钟不同来变化,就可以定义一个二维数组int nValue[24][60]。每过一分钟,最右边的下标就增1,当增长到60时,最右边的下标重新归0,左边的下标增1。

        需要说明的是,这里将nMatrix解释为6行10列是一种表示方法,将其解释为10行6列也是可行的,具体哪种方法更优最好依据表示的数据的意义来决定。不过无论哪种表示方法,都不可能改变二维数组的在内存中的存储结构。

2、二维数组和指针

        我们已经知道,一个一级指针可以指向一个一维数组,或者说,可以将一个一维数组名赋值给一个一级指针。同样道理,如果我们定义了一个二维数组,就可以定义一个二级指针指向这个二维数组:

int nMatrix[6][10];
int **ppMatrix = nMatrix;

        这个值该如何理解?首先我们研究一下二维数组中的数值和地址的关系。在这个二维数组中,第一行的元素分别为nMatrix[0][0], nMatrix[0][1]…nMatrix[0][9],并且这些元素也构成一个数组。对比一下,数组a[0]、a[1]和a[2]的首地址为a,那么我们可以推测这第一行的各个元素的首地址是nMatrix[0]。事实上正是如此,一个二维数组中,每一行元素都是一个一维数组,这一个一维数组的首地址/数组名就是二维数组名+左索引构成。例如,第0行的首地址为nMatrix[0],第1行的首地址为nMatrix[1],第n-1行的首地址为nMatrix[n-1]。这n个首地址表示的是指向一维数组的地址,可以用一级指针指向。即:

int *pRow = nMatrix[0];

        不仅如此,这n个首地址的保存位置也构成一个一维数组。通过观察着n个首地址的命名,我们可以得知,包含着n个首地址的一维数组的首地址实际上就是二维数组名nMatrix。由于每行首地址的数据类型为int *,那么由它们组成数组的首地址类型就是int **,于是我们可以定义一个二级指针指向这个地址,即:

int **ppMatrix = nMatrix;

        这也解释了本节开始时为什么可以定义二级指针指向二维数组名。

 
      在定义了指向二维数组的指针之后,我们就可以通过该指针对二维数组进行读写等操作。例如我们可以根据该指针获取每一行元素的首地址。以下几行代码实现是等价的:

int *pRow2 = ppMatrix[2];
int *pRow2 = *(ppMatrix + 2);

        也可以直接获取二维数组中的某个元素:

int nRow1Col3 = ppMatrix[1][3];
int nRow1Col3 = *(ppMatrix[1] + 3);
int nRow1Col3 = (*(ppMatrix+1))[3];
int nRow1Col3 = *(*(ppMatrix+1)+3);

        二维数组的初始化方法的原理与一维数组原理类似。一维数组的初始化使用大括号实现,而二维数组也是如此,只是其内部每个元素也是一个用大括号初始化的一维数组:

int nMatrix[3][2] =
{
     {9, 7, 5},
     {6, 4, 2}
};

3、三维和更高维度数组

 
      事实上,C语言所支持的数组维度远不止二维数组,三维甚至更高维度的数组也是支持,甚至是很常用。经过了前面的知识,我们很容易归纳出多维数组的定义方法:

char cCubeArray[3][6][8];          //定义一个3层、6行、8列的三维数组
int nIntBuf[4][5][4][6];                 //定义一个4块、5层、4行、6列的四维数组

        以三维数组为例,对于三维数组进行遍历,使用3层循环结构即可。最右一层下标变化最快,越向左下标变化越慢。例如我们定义一个多维数组表示日期和时间,则可定义这样一个数组:

int nDateTimeValue[12][31][24][60][60];

        这几个下标从左到右分别表示月、日、小时、分、秒。当表示秒的下标循环到60后,分钟向下+1;当分钟循环到60后,小时向下+1;当小时循环到24后,日的下标向下+1,依次类推。

for (int month = 0; month < 12; month++)
{
     for(int day = 0; day < 31; day++)
     {
          for(int hour = 0; hour < 24; hour++)
          {
               for(int minute = 0;minute < 60; minute++)
               {
                    for(int second = 0; second<60; second++)
                    {
                         printf(“Value of %d month, %d day, %d hour, %d minute, %d second: %d”, month, day, hour, minute, second, nDateTimeValue[month][day][hour][minute][second]);
                    }
               }
          }
     }
}

4、指针数组和指向数组的指针

        当数组和指针相互结合的时候,程序的意义可能就会变得比较麻烦且难以理解。比如以下两种声明:

int *ptArr1[6];
int (*ptArr2)[6];

        我们需要理解清楚这二者是否相同,以及两个指针ptArr1和ptArr2分别代表的含义。这两个复杂的声明包含了间接运算符、括号运算符、下标运算符等,要理解这两种声明,首先需要清楚不同运算符的顺序。下标运算符[ ]和括号( )的优先级为1,为各种运算符最高;间接运算符优先级为2,次于下标和括号。因此对于声明int
*ptArr1[6],首先计算的是ptArr1[6]部分,因此ptArr1是一个长度为6的一维数组;然后间接运算符所代表的是数据类型,即为指向整型变量的指针。这种声明方式声明了一个指针数组,这是一个长度为6的一维数组,保存了6个整型变量指针,数据类型等同于int *。

 
      另一种声明方式int (*ptArr2)[6],由于括号( )优先级最高,因此首先计算的是(*ptArr2)。从整个声明的格式上来看,(*ptArr2)是一个整型一维数组的数组名即一维数组的首地址。所以ptArr2是一个指向一维数组首地址的指针,数据类型等同于int
**。因此,可以将二维数组名初始化给这个指针:

int (*ptArr2)[6] = nMatrix;

        这种类型的指针定义和初始化创造了一个指向二维数组第一行的指针。对指针ptArr2,其增加某个整数的作用是一行一行地移动指针。因为在定义时制定了指向的数组的长度,因此在移动时系统会根据指向数组的长度进行调整。所以我们需要注意的是,如果我们希望以此方式处理数组的数据,定义时数组的长度不要指定错误,也不要设为空,如同下面的样子:

int (*ptArr2)[] = nMatrix;     //错误的用法

        因为,编译器只有知道了第二个和后面各维的长度,才能在移动指针时确定每一步移动的实际长度,对各个下标进行求值。

时间: 2025-01-30 15:56:37

理解C语言——从小菜到大神的晋级之路(9)——多维数组的相关文章

理解C语言——从小菜到大神的晋级之路(8)——数组、指针和字符串

       本期视频点击这里        在前面几次我们接触的数据类型都是简单数据类型,使用一个数据个体表示一个元素.C语言中还提供了多种复杂数据类型,其中最简单的一种就是数组.数组这一结构使用内存中一段连续的内存空间保存一组相同类型的变量,这些变量通过数组的下标/索引的不同相互区分.数组与指针有着十分紧密的联系,通常使用数组下标能实现的操作都可以使用指针完成,而且使用指针的程序通常效率更高.但是指针和数组也存在着一些明显的差别,如果误用将导致错误.另外,C语言中还定义了一种极为常用的特殊的

理解C语言——从小菜到大神的晋级之路(1)——引言:C语言的前世今生

第一课的视频链接点这里 C语言是现在应用最为广泛的编程语言之一,也是现在依然流行的编程语言中历史最悠久的一种之一.在目前业界广泛使用的编程语言中,许多 种语言是以C为基础发展而来.在多类大学的工程类专业尤其是信息类专业的教学计划中,C语言也是极为重要的基础课之一. 而对于一名以编译型语言为主要开发工具的程序员来说,熟练掌握C语言的用法和理论也可以对其他编程语言获得更深的理解.因此,在这一系列教程中我们希望可以深入理解C语言的方方面面,为后续理解更高级的技术奠定更好的基础. 1.参考资料 <C程序

理解C语言——从小菜到大神的晋级之路(12)——动态内存管理

      本节视频链接:点击这里         在前面的内容中,我们通常使用数组来利用一段连续的内存空间来保存数据.我们前面用到的数组基本保存在栈内存中,其内存空间由系统自动分配和释放,使用非常方便,也不用担心内存管理的问题.但是在栈中分配的数组存在一个严重的问题,就是它的长度必须在建立时明确指定,且无法再运行时修改.为了防止运行时出现内存空间不够的问题,在编程时就必须定义一个非常大的数组来容纳理论上可能的最多个的元素,这样就会导致内存利用率底下,因为如果元素个数较少时大部分的内存空间都被浪

理解C语言——从小菜到大神的晋级之路(6)——函数与调用

        视频观看:点击这里         在前面的程序中,由于程序的功能非常简单,所有的代码都在main()函数中实现.如果程序复杂度增加之后,在main()中实现所有代码将显得非常臃肿且缺乏条理.如果可以将一段大的计算任务分解为若干个小任务则可以有效解决这个问题.另外,分解出来的模块还可以进一步构造和重用,而不用每次都编写同样的代码.因此,绝大部分实际的C程序都是由一个简单的主函数和多个规模不同的子函数构成,而不是全部实现在一个很大的main函数中. 1.函数调用简介        

理解C语言——从小菜到大神的晋级之路(2)——开发环境的选择和HelloWorld程序

视频观看:点击这里 一.常用系统及IDE简介        常言道,工欲善其事必先利其器.除了少数奇葩之外,大部分人应不会去使用记事本或者Word文档去编程吧?几乎所有编程语言都需要一个高效易用的开发环境,C语言也不例外.那么该如何选择C语言开发的环境呢?一个编程开发环境需要考虑操作系统和编译器两部分.个人PC上常用的操作系统和编译器主要有以下几种: 1.Windows        在PC市场上,Windows操作系统一直占据着超过9成的比例.自从20多年前的Windows 3.x逐渐成熟以来

理解C语言——从小菜到大神的晋级之路(10)——结构体、联合体

本节视频链接:点击这里         上篇中讲述的数组是复合数据类型中最简单的一种,一个数组使用一段连续的内存保存了若干个类型相同的数据元素.由于类型和长度相同,数组的每个元素通过数组下标和指针变量访问.如果我们希望一个结构保存多个不同类型的数据元素,那么数组将无能为力.为了实现这样的功能,C语言提供了结构体和联合体. 1.结构体基本概念 (1)结构体的定义         假设我们需要定义一个图形中的点的概念.在一个使用笛卡尔坐标系表示图像的系统中,点的位置使用两个坐标分量表示,即横坐标x和

理解C语言——从小菜到大神的晋级之路(15)——完结篇:C编程风格

本期视频链接:点击这里 有人说过:"程序源代码其实是跟人阅读的,只是恰好机器可以编译而已".编程初学者常常会有这样一个观念,就是我的程序只要编译通过了,运行没有问题那就万事大吉了.至于代码的编写规不规范,完全就是无关紧要的小事情.如果是处于学习阶段,比如为了完成在学校的C语言课的作业,那么花心思在代码规范上的确没有特别的必要,因为这些代码基本不会进入实用工程,也不会被很多人阅读到. 但是,如果应用到了工程领域,比如在软件/互联网企业的技术研发部门,或者Github等平台上的开源工程,那

理解C语言——从小菜到大神的晋级之路(4)——数据类型、运算符和表达式

本期视频点击这里 一.数据类型         对数据进行处理是程序的基本功能之一,因此对于任何编程语言,数据类型都是重要组成部分之一.C语言中定义了较为完善的数据类型体系用于处理不同类型的数据. (1)标识符         C语言中的标识符可以用作变量名.符号名.函数名.文件名等等功能.标识符可以包含字母.数字和下划线(不能以数字开头).C语言是对大小写敏感的语言,因此组成相同但大小写不同的两个标识符将被当作两个不同的标识符处理.         C语言中的标识符可分为三类: 关键字:C语言

理解C语言——从小菜到大神的晋级之路(3)——C源程序的基本结构与调试方法

    本期视频点击这里        在上一篇中,我们进行了Visual Studio 2013的安装以及第一个demo程序"HelloWorld"的建立.现在我们看一下其中的源代码及相关的C语言基础知识.打开工程,可以通过在源文件标签栏的右键菜单中快速打开源代码的所在目录.HelloWorld的简单代码: #include <stdio.h> int main() { printf ( "Hello World! \n" ); return 0 ;