CottLi
3/11/2020 - 9:55 AM

C语言数组

C语言中的数组

数组的理解

  1. 程序在编译期间为数组分配连续的内存。如果程序在编译时不能确定数组的大小,则程序编译不能通过!这要是为什么变量不能用于定义数组大小的原因;
  2. 数组的理解务必关注两个关键点:连续存储和类型相同;
  3. C语言不允许对数组的大小进行动态说明,数组的说明必须在可执行语句之前; 数组的特性:变量可控、连续存放、类型相同,使用过程中需要保留原始数据;
  4. 可以对数组中任何一个元素进行单独的输入输出,每个元素等同于一个普通变量;也可以对数组整体进行访问,但仅限于字符串数组,如scanf("%s", a); printf("%s", a);
  5. 字符串数组的输出推荐puts(str),也可用printf("%s", str);字符串数组的输入推荐str = gets(),也可用scanf("%s", str)。注意,这些函数都只能处理那些以\0结尾的字符串;
  6. 二维数组按行顺序存放:也就是一行一行地连续存储数据;

数组元素的数据类型为何要相同

数据在内存中存放,为了访问数据的内容,需要知道数据的内存地址。普通的变量本身就和内存地址关系密切,通过变量名即可快速获取变量所存储的数据。将多个数据连续存放到一块内存中时,为了随机访问多个数据中任意一个,就必须能快速获取数据的内存地址。如果多个数据所占的内存大小不固定,那么数据的内存地址就无法准确计算。而如果数组中每个数据都占用相同大小的内存空间,在数据连续存放的前提下,根据第一个数据的内存地址以及需要访问的数据在第几个位置就可以计算出需访问数据的内存地址,进而可获得数据的值。

数组的定义不是在计算机执行程序的时候完成的。

下标运算符[]

借助于下标运算符[](subscript operator),可以获取数组中单独的元素。下标运算符需要两个操作数,最简单且最常见的情况下,左操作数是一个数组名称,而另一个操作数是一个整数,即a[2]中,数组名a[]的左操作数,而2[]的右操作数。

运算符[]的左操作数必须是一个指针类型表达式,它不一定需要是数组名称,而其右操作数必须是整数(因为涉及地址计算)。表达式iArr[x]等价于*(iArr+x),即获取指针iArr的内存地址移动x个位置后的内存中存储的内容。

二维数组

二维数组数组名的探索

    int iArr[2][3]={0,1,2,3,4,5};       // 定义整型二维数组
    int **pp = iArr;    // 出现编译错误:`error C2440: “初始化”: 无法从“int [2][3]”转换为“int **” `。说明,二维数组名不是二级指针!
  • 二维数组名不是二级指针,实际上二维数组名是一个数组指针。数组指针是指向数组的指针,它也是一种指针类型,类似于函数指针:int (*pArr)[3]
  • iArr指向一个数组,iArr+1指向下一个数组,系统会根据指针的类型自动计算地址增量。在本例中,每个一维数组有3个整型数据,假设每个整型数占据4字节,那么下一个数组的地址为当前数组的地址往高地址增加12个字节的地址处。
  • 由上所述,iArr为“指向数组的指针”,因此 iArr[i] == *(iArr+i) 取出数组,这意味着iArr[i]是一个数组(注意到数组名表示数组,因此iArr[i]可看成数组名)。iArr[i]既是数组名,也是该数组首元素的地址
  • 因此,二维数组iArr[2][3]的每一行都可以看成一个一维数组,数组名分别为iArr[0], iArr[1], iArr[2],每个数组都有三个元素iArr[0][3], iArr[1][3], iArr[2][3](注意把iArr[0], iArr[1], iArr[2]整体看成数组名),因此iArr[0], iArr[1], iArr[2]}是地址;
  • 在C/C++中没有所谓的二维数组,书面表达就是数组的数组,但为了表述方便,通常称为“二维数组”;
  • 另外需注意的是:地址本身不占内存,存储地址的指针才占用内存
  • 对上述二维数组iArr,虽然iArr[0]iArr的值都和数组首元素地址相同,但二者指向的对象不同iArr为整型数组指针,iArr[0]为数组名,也是数组首元素的地址,因此它的指针类型为整型指针。指针指向的对象不同,则指针自加 $1$ 时地址的增量也不同:
    • iArr[0]是一维数组的名字,它指向的是名为iArr[0]的数组的首元素,对其进行*算,得到的是一个数组元素值,即iArr[0]数组首元素值,因此,*iArr[0]iArr[0][0]是同一个值;
    • iArr是二维数组的名字,它指向的是它所属元素的首元素,它的每一个元素都是一个行数组。因此,它的指针移动单位是“行”,所以iArr+i指向的是第i个行数组,即指向iArr[i]。对iArr进行“*”运算,得到的是一维数组iArr[0]的首地址,即*iArriArr[0]是同一个值。
    • 当用int *p;定义指针p时,p的指向是一个int型数据,而不是一个地址,因此,用iArr[0]p赋值是正确的,而用iArrp赋值是错误的。
  • 很多情况下,数组名会隐式转化成它的首元素指针,此时,数组名等于数组首元素的地址。而当sizeof(iArr)的时候,则不会产生这样的转化,所以iArr占用内存该多大就多大;
  • 数组名是常量指针,指向数组首元素的地址,即数组名指向的地址无法改变;

二维数组与指针数组

int iArr[3][3] = {1,2,3,4,5,6,7,8,9};           // 声明一:定义二维数组
int *piArr[3] = {iArr[0], iArr[1], iArr[2]};    // 声明二:定义一个指针数组
  • 声明二:声明了一个由 $3$ 个整型指针组成的指针数组。数组的三个元素(整型指针)分别赋值为iArr[0], iArr[1], iArr[2]。那么:iArr[0], iArr[1], iArr[2]是整型地址吗?
    • 前面提到了iArr为“指向数组的指针”,而iArr[i] == *(iArr+i) 取出了指针指向的数组,也就是iArr[i]是一个一维数组,它既是数组名,也是数组首元素的地址。由于数组元素为整型数,故iArr[i]为整型指针;
  • 使用指针数组可以有多种方式访问二维数组中的元素:
    1. *(piArr[i]+j)piArr[i]为指针数组piArr中第i+1个元素,代表二维数组第i+1个行数组首元素地址,piArr[i]+j获取第i+1行数组第j个元素的地址,因此*(piArr[i]+j)取得二维数组的元素iArr[i][j]
    2. piArr[i]是指针数组piArr中的元素i,它等价于*(piArr+i)。因此,*(piArr[i]+j)等价于*(*(piArr+i)+j)
  • 本例所述指针数组的方法本质上只是根据数组指针iArr(二维数组的数组名)找到二维数组每一行首元素的地址(这些元素的位置比较特殊),然后把这些地址存到一个指针数组中。事实上,你可以把二维数组其他元素的地址保存到指针数组中,并不限于每一行首元素的地址。相对有意义的是,可以根据一定规律把数组切分成更多的片段。但首元素地址相比其他元素的地址更容易获得,同时也把数组等分,更有现实意义。
  • 使用指针数组访问二维数组和使用数组指针访问二维数组虽然形式很像,但两者原理不同。指针数组本质上只是在一块同类型数据连续存储的区域上“抽取”了几个地址,然后保存在指针数组上,在访问二维数组的元素时就基于这几个“标记地址”访问;而使用二维数组名(数组指针)访问二维数组,是根据指针类型和地址增量间的关系,直接计算出每个元素的地址。
  • 由上所述,我们其实可以将二维数组任意一个元素的地址抽取出来放到指针数组中,然后以这些地址为基点访问其他元素。此外,也可以将多个分别定义且元素个数不同的一维数组的首元素地址放到指针数组中,进行综合管理和访问;

二维数组的地址

易错点

  1. 用变量说明数组大小:int k, iArr[k]是错误的;
  2. 下标越界时,编译器不会报错,但访问到的数据是错误的。务必注意下标不能越界
  3. 字符串长度strlen(str)为有效字符的长度,即字符串长度不包括\0。因此,字符串数组占用的内存长度为:字符串长度 + 1;
  4. 任何不以\0标记结尾的字符串都不能使用标准的字符串处理函数。比如以char ch[5] = {'C','H','I','N','A'}定义的字符串,它的末尾无标记字符\0
  5. 二维数组iArr[2][3]并不存在iArr[1]这样的元素;
  6. iArr, iArr[0], *(iArr+0), *iArr, &iArr[0][0]虽然地址相等,但仍有不少差别:
    1. iArr[0], *(iArr+0), *iArr是等价的:首先显而易见的是*(iArr+0)*iArr是一样的,其次根据下标运算符[]的意义可知:iArr[0]等价于*(iArr+0);这三者和一维数组的数组名等同,都指向一维数组首元素的地址。
    2. iArr是一个指向一维数组的数组指针,虽然它的地址和iArr[0]是相同的,但iArr指向的数据类型不同,从而在执行自加 $1$ 运算时地址的增量也不同。
    3. &iArr[0][0]先获取iArr[0][0]的值(也就是二维数组第一个元素的值),再使用取地址运算符&获取第一个元素的地址。该地址和第一个一维数组iArr[0]的首元素地址相同,地址指向的类型也相同!