基本语法
数组是一组相同类型元素的集合。
声明数组
在 C 中要声明一个数组,需要指定元素的类型和元素的数量,如下所示:
type arrayName[arraySize];
这叫做一维数组。
arraySize
必须是一个大于零的整数常量,
type
可以是任意有效的 C 数据类型。例如,要声明一个类型为 double 的包含 10 个元素的数组
balance
,声明语句如下:double balance[10];
现在
balance
是一个可用的数组,可以容纳10个类型为 double 的数字。
初始化数组
在 C 中,您可以先声明,然后逐个初始化数组:
balance[0] = 1000.0; balance[1] = 2.0; balance[2] = 3.4; balance[3] = 7.0; balance[4] = 50.0;
也可以声明的同时初始化,如下所示:
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};
您将创建一个数组,它与前一个实例中所创建的数组是完全相同的。
但是要注意,如果是单独声明,不初始化,则必须指定数组长度。
#include <stdio.h> int main() { int a[]; a[0] = 1; printf("Hello, World! %s\n", a[0]); return 0; }
以上数组声明时未指定大小,则报错:
访问数组元素
数组元素可以通过数组名称加索引进行访问。元素的索引是放在方括号内,跟在数组名称的后边。例如:
double salary = balance[9];
补充和总结:
- 数组是具有相同类型的集合,数组的大小(即所占字节数)由元素个数乘以单个元素的大小。
- 数组只能够整体初始化,不能被整体赋值。只能使用循环从第一个逐个遍历赋值。
- 初始化时,数组的维度或元素个数可忽略 ,编译器会根据花括号中元素个数初始化数组元素的个数。
- 当花括号中用于初始化值的个数不足数组元素大小时,数组剩下的元素依次用0初始化。
- 字符型数组在计算机内部用的时对应的ascii码值进行存储的。
- 一般用”“引起的字符串,不用数组保存时,一般都被直接编译到字符常量区,并且不可被修改。
所有的数组都是由
连续的内存
位置组成。最低的地址对应第一个元素,最高的地址对应最后一个元素。
数组中的特定元素可以通过索引访问,第一个索引值为 0。
从内存角度来理解数组
从内存角度讲,数组变量就是一次分配多个变量,而且这多个变量在内存中的存储单元是依次相连接的。
我们分开定义多个变量(譬如int a, b, c, d;)和一次定义一个数组(int a[4]);这两种定义方法相同点是都定义了4个int型变量,而且这4个变量都是独立的单个使用的;不同点是单独定义时a、b、c、d在内存中的地址不一定相连,但是定义成数组后,数组中的4个元素地址肯定是依次相连的。
数组中多个变量虽然必须单独访问,但是因为他们的地址彼此相连,因此很适合用指针来操作,由此可见数组和指针天生就纠结在一起。
数组名的含义
创建一个数组 :char a[10] ;
a作为右值 , 很多人估计也在学习的时候,估计会把它作为数组的地址,这是错误的 !
a作为右值时代表的意义和 &a[0]的意义是一样的,代表
数组首元素的首地址
,而不是数组的地址。
其实就可以理解成首元素的地址。因为各种数据类型对应的地址都是指首地址,比如int占4个字节,其地址就是第一个字节的地址。上面说了a作为右值,我们清楚了其含义,那么a作为左值呢?
a不能作为左值 !!!编译器会认为数组名作为左值代表的是a的首元素的首地址,但是这个地址开始的一块内存是一个整体,我们只能访问数组的某个元素,而无法把数组数组当做一个整体来进行访问。所以,我们可以把a[i]当左值,无法把a当左值。也可以这么理解:a的内部是由很多小部分组成,我们只能通过访问这些小部分来达到访问a的目的。
进一步理解:
&a和a做右值时的区别:
&a是整个数组的首地址,而a是数组首元素的首地址。这两个在数字上是相等的,但是意义不相同。意义不相同会导致他们在参与运算的时候有不同的表现。我的理解是,&a指向的是数组;a指向的是数组里的数。
a和&a[0]做右值时意义和数值完全相同,完全可以互相替代。
&a是常量,不能做左值。
a做左值代表整个数组所有空间,所以a不能做左值。为什么数组的地址是常量?因为数组是编译器在内存中自动分配的。当我们每次执行程序时,运行时都会帮我们分配一块内存给这个数组,只要完成了分配,这个数组的地址就定好了,本次程序运行直到终止都无法再改了。那么我们在程序中只能通过&a来获取这个分配的地址,却不能去用赋值运算符修改它。
sizeof和数组
创建一个数组 :char a[10] ;
sizeof(a)
数组名单独在sizeof内,表示整个数组,得到的是整个数组的字节数大小,即10字节。
sizeof(a+0)
此处数组名不是单独在sizeof内,那表示的就是首元素地址,+0,那还是首元素地址,存地址的指针变量大小是4个字节。
sizeof(a[0])
得到的是单个元素的字节数大小,即1字节。
sizeof(&a)
此处&a就代表整个数组的地址,但是地址啊,放指针变量里面的,所以还是4字节。
sizeof(*&a)
&a是a的地址,那*&a就代表整个数组了,所以是10字节。
sizeof(&a+1)
此处&a代表的是数组a的地址(整个数组),虽然数组地址和数组首元素地址的值是一样的,但代表的意义完全不相同。这里(&a+1),代表的是数组a尾元素后一位的那个元素地址。
如何计算一个数组的元素个数?
sizeof(a)/sizeof(a[0])
二维数组
一个二维数组,在本质上,是一个一维数组的列表。声明一个 x 行 y 列的二维整型数组,形式如下:
type arrayName [x][y];
这个表示,有x个一维数组,每个一维数组的元素个数是y个。
比如int x[3][4]
可以进行如下赋值:
int x[3][4] = {{0, 1, 2, 3}, {4, 5, 6, 7}, {8, 9, 10, 11}};
既然二维数组都可以用一维数组来表示,那二维数组存在的意义和价值在哪里?
明确告诉大家:二维数组a和一维数组b在内存使用效率、访问效率上是完全一样的(或者说差异是忽略不计的)。在某种情况下用二维数组而不用一维数组,原因在于二维数组好理解、代码好写、利于组织。
总结:我们使用二维数组(C语言提供二维数组),并不是必须,而是一种简化编程的方式。想一下,一维数组的出现其实也不是必然的,也是为了简化编程。
二维数组的应用和更多维数组
最简单情况,有10个学生成绩要统计;如果这10个学生没有差别的一组,就用b[10];如果这10个学生天然就分为2组,每组5个,就适合用int a[2][5]来管理。
最常用情况:一维数组用来表示直线,二维数组用来描述平面。数学上,用平面直角坐标系来比拟二维数组就很好理解了。
三维数组和三维坐标系来比拟理解。三维数组其实就是立体空间。
四维数组也是可以存在的,但是数学上有意义,现在空间中没有对应(因为人类生存的宇宙是三维的)。
总结:一般常用最多就到二维数组,三维数组除了做一些特殊与数学运算有关的之外基本用不到。(四轴飞行器中运算飞行器角度、姿态时就要用到三维数组)
补充
1、指向数组的指针
上面说了,数组名表示首元素的首地址。因此,*(balance + 4) 是一种访问 balance[4] 数据的合法方式。
一旦您把第一个元素的地址存储在 p 中,您就可以使用 *p、*(p+1)、*(p+2) 等来访问数组元素。下面的实例演示了上面讨论到的这些概念:
#include <stdio.h> int main () { /* 带有 5 个元素的整型数组 */ double balance[5] = {1000.0, 2.0, 3.4, 17.0, 50.0}; double *p; int i; p = balance; /* 输出数组中每个元素的值 */ printf( "使用指针的数组值\n"); for ( i = 0; i < 5; i++ ) { printf("*(p + %d) : %f\n", i, *(p + i) ); } printf( "使用 balance 作为地址的数组值\n"); for ( i = 0; i < 5; i++ ) { printf("*(balance + %d) : %f\n", i, *(balance + i) ); } return 0; }
2、传递数组给函数
如果您想要在函数中传递一个一维数组作为参数,您必须以下面三种方式来声明函数形式参数,这三种声明方式的结果是一样的,因为每种方式都会告诉编译器将要接收一个整型指针。
方式1:形式参数是一个指针
void myFunction(int *param) { . . . }
方式2:形式参数是一个已定义大小的数组
void myFunction(int param[10]) { . . . }
方式3:形式参数是一个未定义大小的数组
void myFunction(int param[]) { . . . }
可以看到,就函数而言,数组的长度是无关紧要的,因为C不会对形式参数执行边界检查。
C语言中,数组无法进行值传递。
数组作为形参,调用函数时,把数组名传递进去即可。
这是由C/C++函数的实现机制决定的。传数组给一个函数,数组类型自动转换为指针类型,因而传的实际是地址。直接传a即可,不要传&a,没有意义。
由此,传递数组的同时,传递数组的长度常常很有必要。
因为数组的这个问题,我就对结构体的传递产生了疑惑,其实,是我心里默认就想成了传递结构体的类型定义,但事实上,传递的从来都不是类型,而是结构体变量。所以,直接按照常规变量来传递即可。因为结构体类型就相当于int等类型,该定义变量就定义变量,该定义指针就定义指针。
在将数组作为函数参数的时候,有一个需要注意的问题,卡了我好几个小时。
问题如下,51单片机编程时,我将数组传入函数,然后在函数中求取数组的元素个数,如下:sizeof(arr) / sizeof(arr[0])
如果不涉及到函数传参,其实没有什么问题,但是一旦传入函数形参,然后再在函数里求取就有问题了,因为
数组名传入函数形参时,会自动退化为指针
。那么这时候sizeof(arr)得到的就不是整个数组的字节大小了,返回的只是一个指针的大小,显然,就会出现问题了。
C语言中只会以值拷贝的方式传递参数,当向函数传递数组时,却并不采用将整个数组拷贝一份传入函数的方法,而是采用将数组名看做常量指针传数组首元素地址。原因在于:C语言以高效作为最初设计目标来开发UNIX操作系统,且参数传递的时候如果拷贝整个数组执行效率将大大下降,参数位于栈上,太大的数组拷贝将导致栈溢出,总结两个缺点就是低效以及不安全。
有什么解决方法吗?那就是不要在函数中使用sizeof,而是用sizeof计算完成后,再传入函数。这里不是说不能在函数中计算数组的元素个数,是说不要在函数外定义数组,然后再传入后求个数,二者要放在同一个地方。
3、从函数返回数组
C 语言不允许返回一个完整的数组作为函数的参数。但是,您可以通过指定不带索引的数组名来返回一个指向数组的指针。
让我们来看下面的函数,它会生成 10 个随机数,并使用数组来返回它们,具体如下:
#include <stdio.h> #include <stdlib.h> #include <time.h> /* 要生成和返回随机数的函数 */ int * getRandom( ) { static int r[10]; int i; /* 设置种子 */ srand( (unsigned)time( NULL ) ); for ( i = 0; i < 10; ++i) { r[i] = rand(); printf( "r[%d] = %d\n", i, r[i]); } return r; } /* 要调用上面定义函数的主函数 */ int main () { /* 一个指向整数的指针 */ int *p; int i; p = getRandom(); for ( i = 0; i < 10; i++ ) { printf( "*(p + %d) : %d\n", i, *(p + i)); } return 0; }
补充:字符数组
字符型数据是以字符的ACSII代码存储在代码单元格中的,一般占一个字节。由于ASCII代码也属于整数形式,所以C99标准中,把字符类型归纳为整形类型中的一种。
怎样定义字符数组?
用来存放字符型数据的数组称为字符型数组,在字符数组中一个元素内存放一个字符。定义字符型数组的方法与定义数值型数组的方法类似,例如:char arr[10];
由于字符型数组是以整数形式存放的,也可以用整形数组来存放字符型数据,缺点就是浪费空间,一个字符只占一个字节,而一个整形数据占四个字节,将字符放在整形数组中会浪费空间。
字符数组的初始化
对字符型数组进行初始化,最容易理解的方法就是:
char arr[10]={'s','d','f','e','t','p','q','z','k','r'};
如果在定义字符数组时不进行初始化,那么数组中元素的值是不可预料的;
如果花括号中提供的初值个数大于数组长度,则出现语法错误;
如果初值个数小于数组长度,那么初值只会赋给前面的元素,后面的元素会自动赋值为空值,即‘\0’。
字符串和字符串结束标志
在C语言中,可以用字符数组来表示字符串。在实际工作中,人们往往关心的是字符串的有效长度,而不是字符数组的长度。例如:定义一个字符数组长度为100,而字符串的长度为60。所以为了测字符串的实际长度,C语言规定了“字符串结束标志”,即‘\0’。
如果字符数组中有若干字符,前9个都不是空字符,而第10个是空字符,那么认为空字符之前是一个字符串,而字符串的有效字符为9个。
注意:C系统会在字符数组存储字符串常量时自动加一个‘\0’,作为字符串结束的标志,例如:“Cprogram”共八个字符,但其存放在一维数组中占9的字节,最后的‘\0’是系统自动加的。
对C语言处理字符串的方法有了了解之后,再补充一种字符数组初始化的方法,即用字符串常量来对字符数组进行初始化,例如:
char arr[]={"I am happy"}; char arr[]="I am happy";
以上两种方式均可,这里是用一个字符串作为初值,很显然这种方法直观,方便更符合人们的习惯。
注意:直接定义字符数组并不会视为字符串,也不会自动在末尾加上’\0’。当然,如果是手动在字符数组末尾加上’\0’,应该也会视为一个字符串,不过通常不会这么操作。
如果是以字符串的形式来初始化字符数组,那么就会在末尾自动加上’\0’。
注意区分字符串底层对字符数组的处理和字符数组本身:
#include <stdio.h> int main() { char a[] = {"good"}; char b[] = {'g', 'o', 'o', 'd'}; printf("Hello, World! %d\n", sizeof(a)); //5 printf("Hello, World! %d\n", sizeof(b)); //4 return 0; }
虽然可以通过字符串的形式对字符数组进行赋值,但是二者还是有点区别的。要注意字符串最后的空字符‘\0’。
字符数组的输入输出可以有两种方法。
1)逐个字符输入输出。用格式符“%c”输入或输出一个字符。
2)将整个字符串输入或输出。用格式符“%s”输入或输出一整个字符串。
注意:
◆输出的字符串中不包括结束符“\0”。
◆用printf函数输出字符串时,输出项是字符数组的名字,而不是数组元素名。写成下面这样是不对的: printf(“%s”,arr[0]);
◆如果一个字符串包括一个以上结束符“\0”,则遇到第一个就输出结束。
有个问题需要注意下,如果指定了数组长度,那么sizeof计算出来的大小就是指定的长度对应的字节数;如果字符串形式定义时没有指定数组长度,那么sizeof计算时就要按照字符串长度加上一个结束符的字节和。
没想明白这里是咋回事,不过分纠结了。
#include <stdio.h> int main() { char a[5] = "happy"; printf("Hello, World! %c\n", a[4]); //输出y printf("Hello, World! %d\n", sizeof(a)); //这种情况输出是5 return 0; }
#include <stdio.h> int main() { char a[] = "happy"; printf("Hello, World! %c\n", a[4]); //输出y printf("Hello, World! %d\n", sizeof(a)); //这种情况输出是6 return 0; }
上面指定了长度就输出5,下面没指定长度就输出6。
这里很奇怪,第一段代码的结束符‘\0’去哪了?
要知道,上面的方式是有安全隐患的。
另外要注意:用strlen计算字符串长度时,是不包括结束符的,计算的是字符串的有效长度。
关于上述的问题,看一个题目:
这里为什么CD选项不对?
如果按照上面的测试,是可以通过编译的。
其实,这里是已经越界了,数组形式的字符串,应该算上末尾的结束符\0的。
虽然不算上也可以成功,但是个很大的安全隐患。
连续内存的下标操作
今天看到一道题:
根据这个题,可以引出,如果有一块连续的内存,那么就可以按照地址+数组下标的方式去引用。
也就是说,如果有一块连续的内存,其开始地址为p,那么p[0]就表示地址开始处的值。
这里需要注意一个问题。
我测试的时候,仿照上述程序写了如下代码:
发现报错:
奇怪,题目中的程序可以++b,但是这里的a++报错了,改成++a或者a = a + 1;仍然报错。
想了很久,为什么?
对于数组的数组名来说,虽然其是个地址,但是它是个常量,++a改变了a的值,显然是不允许的。那么++b为什么可以呢?因为b是个局部变量,通过传参, 等于是将a的地址值复制了一份,再传给b,此时b是可以进行加减运算的。
为了验证这个问题,写了如下代码:
定义了一个指针来接收地址值:
运行成功:
另外要注意,下面这种操作也是有效的(但通常不推荐):