Ninth------数组(Array)
一、定义
C 语言支持数组数据结构,它可以存储一个固定大小的相同类型元素的顺序集合。数组是用来存储一系列数据,但它往往被认为是一系列相同类型的变量。
数组的声明并不是声明一个个单独的变量,比如 runoob0、runoob1、…、runoob99,而是声明一个数组变量,比如 runoob,然后使用 runoob[0]、runoob[1]、…、runoob[99] 来代表一个个单独的变量。
所有的数组都是由连续的内存位置组成。最低的地址对应第一个元素,最高的地址对应最后一个元素。
数组中的特定元素可以通过索引访问,第一个索引值为 0。
二、声明数组
在 C 中要声明一个数组,需要指定元素的类型和元素的数量,如下所示:
type arrayName [ arraySize ];
这叫做一维数组。arraySize 必须是一个大于零的整数常量,type 可以是任意有效的 C 数据类型。例如,要声明一个类型为 double 的包含 10 个元素的数组 balance,声明语句如下:
double balance[10];
现在 balance 是一个可用的数组,可以容纳 10 个类型为 double 的数字。
三、初始化数组
在 C 中,您可以逐个初始化数组,也可以使用一个初始化语句,如下所示:
double balance[5] = {1000.0, 2.0, 3.4, 7.0, 50.0};
大括号 { } 之间的值的数目不能大于我们在数组声明时在方括号 [ ] 中指定的元素数目。
如果您省略掉了数组的大小,数组的大小则为初始化时元素的个数。因此,如果:
double balance[] = {1000.0, 2.0, 3.4, 7.0, 50.0};
您将创建一个数组,它与前一个实例中所创建的数组是完全相同的。下面是一个为数组中某个元素赋值的实例:
balance[4] = 50.0;
上述的语句把数组中第五个元素的值赋为 50.0。所有的数组都是以 0 作为它们第一个元素的索引,也被称为基索引,数组的最后一个索引是数组的总大小减去 1。以下是上面所讨论的数组的的图形表示:
下图是一个长度为 10 的数组,第一个元素的索引值为 0,第九个元素 runoob 的索引值为 8:
四、访问数组元素
数组元素可以通过数组名称加索引进行访问。元素的索引是放在方括号内,跟在数组名称的后边。例如:
double salary = balance[9];
上面的语句将把数组中第 10 个元素的值赋给 salary 变量。下面的实例使用了上述的三个概念,即,声明数组、数组赋值、访问数组:
#include <stdio.h>
int main ()
{
int n[ 10 ]; /* n 是一个包含 10 个整数的数组 */
int i,j;
/* 初始化数组元素 */
for ( i = 0; i < 10; i++ )
{
n[ i ] = i + 100; /* 设置元素 i 为 i + 100 */
}
/* 输出数组中每个元素的值 */
for (j = 0; j < 10; j++ )
{
printf("Element[%d] = %d\n", j, n[j] );
}
return 0;
}
当上面的代码被编译和执行时,它会产生下列结果:
Element[0] = 100
Element[1] = 101
Element[2] = 102
Element[3] = 103
Element[4] = 104
Element[5] = 105
Element[6] = 106
Element[7] = 107
Element[8] = 108
Element[9] = 109
C 中数组详解
在 C 中,数组是非常重要的,我们需要了解更多有关数组的细节。下面列出了 C 程序员必须清楚的一些与数组相关的重要概念:
概念 | 描述 |
---|---|
多维数组 | C 支持多维数组。多维数组最简单的形式是二维数组。 |
传递数组给函数 | 您可以通过指定不带索引的数组名称来给函数传递一个指向数组的指针。 |
从函数返回数组 | C 允许从函数返回数组。 |
指向数组的指针 | 您可以通过指定不带索引的数组名称来生成一个指向数组中第一个元素的指针。 |
五、一维数组
在考虑多维数组之前,我们还需要学习很多一维数组的知识,首先我们了解一个概念,很多人认为这是C语言设计的一个缺陷。
5.1 数组名
考虑下面这些声明
int a ;
int b[10] ;
我们把变量a称为标量,因为它是一个单一的值,这个变量的类型是在一个整数,我们把变量b称为数组,因为它是一些值得集合。下标和数组名一起使用,用于表示该集合中某个特定的值。
b[4]的类型是int,但b的类型呢?一个合乎逻辑的答案是它表示整个数组,但是事实并非如此,在C中,在几乎所有使用数组名的表达式中,数组名的值是一个指针常量,也就是数组第1个元素的地址。他的类型取决于数组元素的类型。如果他们是int尅性,那么数组名的类型就是"指向int的常量指针"。那么数组名的类型就是"指向其他类型的常量指针"。
但是**数组和指针是相同的---->这个结论是错误的。**数组具有一些和指针完全不同的特征。例如数组具有确定数量的元素,而指针只是一个标量值。编译器用数组名记住这些属性。只有数组名在表达式中使用,编译器才会为它产生一个指针常量。
warning:
数组名在表达式中是指针常量,意味着,数组名的地址是不能被修改的。仔细回想一下,这个限制是合理的,指针常量所指向的是内存中数组的起始位置,如果修改这个指针,唯一可行的操作就是把整个数组移动到内存中的其他位置。但是在程序完成链接之后,内存中数组的位置是固定的,所以当程序运行时,在想移动数组就已经为时已晚。
只有在两种场合下,数组名并不用指针常量来表示—当数组名作为sizeof操作符或者单目操作符&的操作数时。sizeof返回整个数组的长度。
int a[10] ; int b[10] ; int *c ; ... c = &a[0] ;
表达式&a[0]是一个指向数组第1个元素的指针。但那是数组名的本身,所以下面这条赋值语句和上面那条赋值语句的效果一致。
c = a ;
这条赋值语句说明了表达式中数组名的真正含义是非常重要的。实际上,被赋值的是一个指针的拷贝,c所指向的是数组的第1个元素。
b = a ; a = c ;
b = a 是非法的。
c = a 是非法的。
5.2 下标引用
int b[10] ;
表达式*(b+3) 的含义。首先b是一个指向整型的指针,所以3这个值是根据int的长度进行调整。如果加法运算的结果是另一个指向int的指针。它所指向的是数组第1个元素向后移动3个int长度的位置。
int array[10] ;
int *ap = array + 2 ;
在下面各个涉及ap的表达式中,写出对等的array表达式
表达式 | 对等式 | 含义 |
---|---|---|
ap | array + 2 | 阅读初始化表达式 |
*ap | array[2] | 间接访问跟随指针的访问它所指向的位置 |
ap[0] | array[2] | 跟上题一致 |
ap + 6 | array + 8 | array[2]向后移动6个元素 |
*ap + 6 | array[2] + 6 | 运算符优先级,先访问值,在相加 |
*(ap+6) | array[8] | |
ap[6] | array[8] | |
&ap | 暂无 | 表达式合法,含义是ap的地址 |
ap[-1] | array[1] | |
ap[9] | array[11] | array[2]向后移动9个元素,但是发生溢出 |
warning:
表达式:2[array]
首先是合法的,对等于*( 2 + ( array ) ),与 *(2 + array )一致。
但是这种技巧不推荐,影响可读性。
5.3 指针与下标
互换地使用指针与下标表达式。假设这两种方式都正确,下标绝不会比指针更有效率,但指针有时会比下标更有效率。
5.4 指针的效率
前面曾提过,指针有时比下标更有效率,**前提是他们被正确地使用。**指针的效率往深层次的说,则需要结合汇编代码进行解释。
结论
1、当根据某个固定数目的增量在一个数组中移动时,使用指针变量将比使用下标产生效率更高的代码。当这个增量为1时并且机器具有自动增量模型时,这一点表现得尤为突出。
2、声明 为寄存器变量的指针通常比位于静态内存和堆栈中的指针效率更高(具体提高的幅度取决于及其所使用的机器)。
3、如果可以通过测试一下已经初始化并经过调整的内容来判断循环是否应该终止,就不需要使用一个单独的计数器。
4、那些必须在运行时求值的表达式较之诸如&array[SIZE]或array+SIZE这样的常量表达式往往代价更高。
5.5 数组和指针
指针和数组并不是相等的。
int a[5] ;
int *b ;
a和b都具有指针值,而且都可以间接进行访问和下标引用操作。但是,他们还是存在相当大的区别。
声明一个数组时3,编译器会根据声明所指定的元素数量为数组保留内存空间,然后再创建数组名,它的值是一个常量,指向这段空间的起始位置;声明一个指针变量,编译器只会为指针本身保留内存空间,并不会为任何整型值分配内存空间。此外,指针变量并未被初始化为指向任何现有的内存空间。如果它是一个自动变量,它甚至根本不会被初始化。把这两个声明用图的方法来表示。可以发现它们之间存在显著不同。
因此,表达式* a是完全合法的,但是表达式* b却是非法的。因为* b访问内存中某个不确定的位置,或者导致程序终止。
另外表达式 b ++ 可以通过编译。但a ++ 却不行,因为a的值是个常量。
5.6 作为函数参数的数组名
当数组名作为参数传递给一个函数时,传递给函数的是一份该指针的拷贝。函数如果执行了下标引用,实际上是对这个指针执行间接访问操作,并通过这种间接访问,函数可以访问和修改程序的数组元素。
现在可以解释C源于参数传递的表面上的矛盾之处。所有传递给函数的参数都是通过传值方式进行的,但数组名参数的行为却仿佛它是通过传址调用传递的。传址调用是通过传递一个指向所需元素的指针,然后在函数中对该指针执行间接访问操作实现对数据的访问。作为函数参数的数组名是个指针,下标引用实际执行的就是间接访问。
所有参数都是通过传值方式传递的。当然,如果传递了一个指向一个变量的指针,而函数对该指针执行了间接访问操作,那么函数就可以修改那个变量。尽管看上去并不明显。这个参数(指针)实际上是通过传值方式传递的,函数得到的是该指针的一份拷贝,它可以被修改,但调用程序传递的参数并不受影响。
5.7 声明数组参数
如果把一个数组名参数传递给函数,正确的函数形参应声明为一个指针,还是一个数组?
调用函数时实际传递的是一个指针,所以函数的形参实际上是个指针。
int strlen(char *string) ;
int strlen(char string[]) ;
***这个相等性暗示指针和数组名实际上是相等的,但是不要被糊弄了!!!***这两个声明确实是相等。但只是在当前这个上下文环境中。
尽管可以使用任何一种声明,但是指针"更加准确"。因为实参实际上是个指针。
Answer:
现在我们清楚为什么函数原型中一维数组无序写明它的元素数目,因为函数并不为数组参数分配空间,形参只是一个指针。
5.8 初始化
int vector[5] = { 10 , 20, 30 , 40 , 50} ;
5.9 不完整初始化
int vector[5] = { 1, 2, 3 ,4 ,5 ,6 } ;
int vector[5] = { 1, 2 ,3 ,4 } ;
5.10 自动计算数组长度
int vector[] = {1 , 2 , 3, 4 ,5}
5.11 字符数组的初始化
char message[] = "hello world" ;
char mess[] = {'H', 'e' , 'l' , 'l' , 'o'} ;
尽管看上去像是一个字符串常量,实际上并不是。
快速区分字符串常量和初始化列表是根据所处的上下文环境进行区分的。+
举个例子
char message[] = "Hello" ;
char *message = "Hello" ;
这两个初始化很像,但它们具有不同的含义。前者初始化一个字符数组的元素,而后者则是一个真正的字符串常量。
这种*message指针变量被初始化为指向这个字符串常量的存储位置。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-ByySLQGy-1675126543004)(E:\Yjy\Typora_notes\PICTURES\image-20230129234502009.png)]
六、多维数组
如果某数组的位数不止一个,那么它会被称为多维数组。
int matrix[6][10] ;
创建了一个包含60个元素的矩阵。是6行10列,还是6列10行?
我们接下来考虑不同维数的数组:
int a ;
int b[10] ;
int c[6][10] ;
int d[3][6][10] ;
a是一个整数;b是一个向量,它包含了10个整数;c在b的基础上有增加了一维,所以可以把c看做包含6个元素的向量,且它的每个元素本身是一个包含10个整数的向量。即c是个一维数组的一维数组;d也是如此,它是包含3个元素的向量,每个元素包含6个元素的数组,而这6个元素中的每一个元素是包含10个整型元素的数组的。简介地说,d是一个3排,6行,10列的整型三维数组。
理解这个视角是非常重要的,因为它正是C实现多维数组的基础。
6.1 存储顺序
int array[3] ;
内存图
int array[3][6] ;
内存图
实线方框表示的第1维的3个元素,虚线表示第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
这个实例说明了数组元素的存储顺序(storage order)。在C中,多维数组的元素存储顺序按照最右边的下标率先变化的原则,
回到开头的问题
int matrix[6][10]
matrix到底是6行10列,还是10行6列
在某些上下文环境中,这两种答案都对。
因为如果根据下标把数据存放于数组中并在以后根据下标查找数组中的值,那么不管把第1个下标解释为行还是列,都不会有什么区别,只要每次坚持使用同一种方法,这两种解释都是可行的。但是不管按行还是按列。都不能修改内存中数组元素的实际存储方式,这个顺序是有标准定义的。
6.2 数组名
一维数组名的值是一个指针常量,它的类型是"指向元素类型的指针",它指向数组的第1 个元素。多维数组也很简单。唯一的区别是多维数组第一维的元素实际上是另一个数组。
int martix[3][10] ;
martix这个名字的值是一个指向它第1个元素的指针,所以martix是一个指向一个包含10个整型元素的数组的指针。
6.3 下标名
如果要标识一个多维数组的某个元素,必须按照与数组声明时相同的顺序为每一维都提供下标,并且每个下标都单独位于一对方括号内。
int martix[3][10] ;
/*表达式martix[3][5]*/
图示:
访问的是上图中正方形实线框柱的位置。
下标引用实际上只是间接访问表达式的一种形式,在多维数组也是如此,考虑下面这个表达式。
martix
它的类型,是"指向包含10个整型元素的数组的指针",它的值如下:
它指向包含10个整型元素的第一个子数组。
表达式
martix + 1
它的类型也是指向包含10个整型元素的数组的指针。它指向martix的另一行。
因为1这个值根据包含10个整型元素的数组的长度进行调整,所以它指向martix的下一行,如果对其执行间接访问操作,就选择中间这个子数组,如下图所示。
所以表达式
*(martix + 1)
事实上标识了一个包含10个整型元素的子数组。数组名的值是个常量指针,它指向数组的第1元素,在这个表达式中也是如此。它的类型是"指向整型的指针", 图示
表达式
*(martix + 1 ) + 5
前一个表达式是指向整型值的指针,所以5这个值根据整型的长度进行调整。整个表达式的结果是一个指针,它指向的位置比原先那个表达式所指向的位置向后移动了5个整型元素。
对其执行间接访问操作
*(*(martix + 1) + 5)
它所访问的正是上图中的那个整型元素。如果它作为右值使用,就取得存储于那个位置的值,如果它作为左值使用,这个位置将存储于一个新值。我们还变化这种表达式的写法:
*(martix[1] + 5) // martix[1] 与 *(martix + 1)是一个意思
martix[1][5]
6.4 指向数组的指针
看一下下面的声明:
int vector[10] , *vp = vector ; //合法的
int martix[3][10] , *mp = martix ; // 非法的
第一个声明是合法的。它为一个整型数组分配内存,把vp声明为一个指向整型的指针,并把它初始化为指向vector数组的第1个元素。vector和vp具有相同的类型:指向整型的指针。第二个是非法的。它正确的创建了martix数组,并把mp初始化为指向整型的指针,但是mp的初始化是不正确的,因为martix并不是一个指向整型的指针,而是一个指向整型数组的指针。
声明一个指向整型数组的指针的格式为:
int (*p)[10] ;
这个声明并不复杂。下标引用的优先级高于间接引用,但由于括号的存在,首先执行的是间接引用。所以p是个指针。
接下里执行的是下标引用,所以p指向某种类型的数组。这个声明表达式中并没有更多的操作符,所以数组的每个元素都是整数。
声明并没有直接告诉你p是什么,但推断它的类型并不复杂,在对它进行间接访问操作时,我们得到的是个数组,对数组进行下标引用操作得到的是一个整型值,所以p是一个指向整型数组的指针。
在声明中加上初始化后是下面这个样子:
int (*p)[10] = matrix ;
它使p指向matrix的第1行。
p是一个指向拥有10个整型元素的数组的指针。当把p与一个整数相加时,该整数值首先根据10个整数值得长度进行调整,然后再执行加法。所以可以使用这个指针一行一行地在matrix中移动。
如果需要一个指针逐个访问整型元素而不是逐行在数组中移动,可以采用下面两个声明
int *pi = &matrix[0][0] ;
int *pi = matrix[0] ;
增加这个指针的值使它指向下一个整型元素。
warning:
如果打算在指针上执行任何指针运算,应该避免这种类型的声明
int (*p)[] = matrix ;
p仍然是一个指向整形数组的指针,但是数组的长度不见了。当某个整数与这种类型的指针执行指针运算时,它的值将根据空数组的长度进行调整(也就是说,与0相乘),这很可能不是你所设想的,有些编译器可以捕捉,但有些编译器却不行。
6.5 作为函数参数的多维数组
作为函数参数的多维数组名的传递方式和一维数组名相同------实际传递的是个指向数组第1个元素的指针。但是二者之间的区别在于,多维数组的每个元素本身是另外一个数组,编译器需要知道它的维数,以便为函数形参的下标表达式进行求值,这里有两个例子:
int vector[10] ;
......
funcl(vector) ;
参数vector的类型是指向整型的指针,所以func1的原型可以是下面两种中的任意一种:
void funcl (int *vec) ;
void funcl(int vec[]) ;
作用于vec上面的指针运算把整型长度作为它的调度因子。
我们来观察一个矩阵
int matrix[3][10] ;
....
func2(matrix) ;
这里,参数mastrix的类型是指向 包含10个整型元素的指针。func2的原型:
void func2(int (*mat)[10])
void func2(int mat[][10])
在这个函数中,mat的第1个下标根据包含10个元素的整型数组的长度进行调整,接着第2个下标根据整型的长度进行调整。
这里的关键是编译器必须知道第二个及以后各维的长度才能对各下标进行求值。因此在原型中必须声明这些维的长度。第一维的长度并不需要,因此在计算下标值时用不到它。
在编写一维数组形参的函数原型时,既可以把它写成数组的形式,又可以把他写成指针的形式,但是对于多维数组,只有第一位才可以进行选择,尤其是func2写成下面这样的函数原型是不正确的:
void func2(int **mat)
这个例子把mat声明为一个指向整型指针的指针,它和指向整型数组的指针并不是一样的。
6.6 初始化
初始化多维数组时,数组元素的存储顺序就变得十分重要。编写初始化列表有两种。第一种是给出很长的初始化值列表。
int array[2][2] = { 2 ,3 ,4 ,5} ;
多维数组的存储顺序是根据最右边的下标率先变化的原则确定的,所以这条初始化语句和下面这些赋值语句的结果是一样的的。
array[0][0] = 2 ;
array[0][1] = 3 ;
array[1][0] = 4 ;
array[1][1] = 5 ;
第二种方式则是
int tow_dim[3][4] = {
{0, 1, 2 ,3} ,
{4 ,5 ,6 ,7},
{8, 9, 10, 11}
} ;
当然我们使用的缩进和空格无关紧要,只是方便阅读。
6.7 数组长度的自动计算
在多维数组中,只有第1维才能根据初始化列表缺省地提供,剩余的几维必须显式地写出,这样编译器就能推断出每个子数组的长度。例如,
int two_dim[][4] ={
{00, 01, 02} ,
{03, 04, 06, 07},
{09, 10}
} ;
编译器只要数一下初始化列表中所包含的初始值个数,就可以推断出最左边一维为3。
七、指针数组
除类型外。指针变量和其余变量很相似,所以我们也可以创建指针数组。
int *api[10] ;
下标引用优先级高于间接引用。所以在这个表达式中,先执行下标引用,因此,api是某种类型的数组(顺便,它所包含的元素的个数为10)。在取得一个数组元素后,随机执行的是间接访问操作。这个表达式不再有其他操作符,所以它的结果是一个整型值。由此。我们也弄清楚api的元素类型就是指向整型的指针。
指针数组的用途:一个方面
char const *keyword[] = {
"do" ,
"what" ,
"you",
"like"
};
#define N_KEYWORD (sizeof (keyword) / sizeof(keyword[0]))
注意sizeof的用途,它用于对数组中的元素进行自动计数。sizeof(keyword)的结果是整个数组所占用的字节。而sizeof(keyword[0])的结果则是数组每个元素所占用的字节数。这两个数相处,结果就是数组元素的个数。
八、警告总结
1、当访问多维数组时,无用逗号分隔下标。
2、在一个指向未指定长度的数组的指针上执行指针运算。
九、编程的总结
1、一开始就编写良好的代码显然比依赖编译器修正劣质代码要好。
2、源代码的可读性几乎总是比程序员的运行时效率更为重要。
3、只要有可能,函数的形参都应该声明为const。
4、在有些环境中,可使用register关键字提高程序的运行时效率。
5、在多维数组的初始值类表中使用完成的多层花括号能提高可读性。