一、什么叫数组?
数组:顾名思义就是很多数的组合。
特点是:1、这些数的类型都是相同的;2、在数组里面,这些数在内存里是连续储存的,是一组有序数据的集合;3、用一个数组名和下标来唯一地确定数组中的元素。
二、一维数组
1、一维数组的定义
一般形式:数据类型 数组名 [ 常量表达式]
例如:定义一个有10个整型元素,名叫a的数组
int a[10];
它表示定义了一个整型数组,数组名为 a,定义的数组称为数组a,
数组名 “a” 除了表示该数组之外,还表示该数组的首地址(关于地址以后讲指针的时候再说)。
数组长度为10(即:数组a中有10个元素,这里的元素就是变量的意思,在数组上习惯称为元素),
数组大小是40,因为int型是4个字节,那么10个就4*10=40字节,所以所占的内存空间是40字节,
而且它们的地址是连续分配的。
说明:
(1)数组名的命名规则和变量名的命名规则一致,都是要遵循标识符命名规则。
(2)在定义数组的时候,需要指定数组的长度(即:元素个数),"[ ]"方括号中的常量表达式
就是要来表示元素的个数。
(3)数组下标是从0开始。
区分:int a[5] 与 a[5]
第一个是定义一个数组,一个名字叫a的5位的数组,此时该数组中下标最大的是a[4],
包含的5位分别是:a[0]、a[1]、a[2]、a[3]、a[4] 。
第二个是一个元素(一个值),数组a中下标为5的元素。
(4)常量表达式中可以包括 常量 和 符号常量,不能是变量。
如:int a[5]、int a[5+4]这些是合法的,但int a[n],n是变量,这是不合法的。
特例:如果数组是在被调用的函数中(不包括主函数),其长度可以是变量或非常量表达式,
这情况称为“可变长数组”。
如:调用func函数时,形参n从实参得到值,但在执行时形参的n的值是不变的,
数组长度是固定的。
void func(int n)
{
int a[2*n];
}
(5)若指定数组为静态(static)存储方式,则不能用“可变长数组”。
如:static int a[2*n]; 这是不合法的。
2、一维数组的初始化
初始化: 给各数组元素赋值。
初始化的方法:
(1) 对所有元素赋初值(“完全初始化”):
如:int a[5]={1,2,3,4,5};
初始化过后的效果:a[0]=1,a[1]=2,a[2]=3,a[3]=4,a[4]=5
数组依次将花括号里面的数,从左到右赋值给数组中的每一位元素。
注意:
(1)初始化时,花括号里每个值用逗号“,” 隔开。
(2)若是采用“完全初始化”的方式(即:初始化的个数与数组长度大小是一致的),此时在
定 义数组的时候可以省略数组长度, 如: int a[]={1,2,3}; 因为此时元素的个数
是已经确定了的,此时数组长度为3.
特别注意:
必须定义跟初始化一起写才可以,若是分开,则是不合法的。
如:int a[];
a[]={1,2,3};
这样写是错误的,会提示你没有给数组指定长度。
(2)只给部分元素赋初值(“不完全初始化”):
如:int a[5]={1,2};
初始化过后的效果:数组定义了有5个元素,但进行了初始化只有两个,a[0]=1、a[1]=2,后面的三个元素都没有被初始化。
注意:没有被初始化的元素系统默认给 0 ,若是字符型数组,则系统会默认给“ \0 ”,
若是指针,则系统会默认给NULL,设置为空指针。
“完全不初始化”:即只定义“int a[5];” ,没有进行任何的赋值行为。此时要与“不完全初始化”区别开,此时的各元素值并不是0,而是一些无意义的值。
注意:
(1) “ int a[5]={}; ”这样写是不合法的,是极其严重的语法错误,大括号中至少需要
写一个,比如: int a[5]={0}; 这时是让数组中的元素全部置零。
(2)“ int a[3]={1,2,3,4,5}; ” 赋初值的数量大于数组的长度,这也会产生语法错误,
是不合法的。
3、一维数组的引用
引用格式: 数组名[下标]
[下标]: 可以是 常量表达式 或 整形表达式
如:引用数组a给变量sum赋值 sum=a[5]+a[2*3];
**注意:** 定义数组时用到的格式是:“ 数组名[常量表达式] ”,
引用数组元素时用到的格式是:“ 数组名[下标] ”,
两者的区别:
定义:int a[5]; //这a[5]表示的是定义一个整形数组,数组长度为5个元素
引用:t=a[6]; //这a[6]表示的是引用数组a下标为6的元素
三、二维数组(多维数组)
二维数组:常称为矩阵,可以看成是一种特殊的一维数组,在学习二维数组的时候可以把一个二维数组想象成一个表格,在一维数组的行形式上多加列维度,形成行列的排列形式(先行后列)。
多维数组就在二维数组的基础上延申即可。
1、二维数组的定义
定义的形式: 类型说明符 数组名[常量表达式][常量表达式]
例如:int a[3][4];
定义了一个名字为a的3x4(3行4列)二维数组
可以看成是:
a[0] ------ a[0][0] a[0][1] a[0][2] a[0][3]
a[1] ------ a[1][0] a[1][1] a[1][2] a[1][3]
a[2] ------ a[2][0] a[2][1] a[2][2] a[2][3]
*注意*:
二维数组的储存方式:
在c语言中,二维数组中的元素排列的顺序是按行存放的,是线性的,即在内存中先存放完
第一行所有元素,接着存第二行元素。
2、二维数组的初始化
-
完全初始化:
- 方法1: 分行给二维数组赋值。
- 例如:int a[3][4]={{1,2,3,4},{5,6,7,8},{9,10,11,12}};(里面一对花括号代表一行)
- 方法2: 一对花括号过。
- 例如:int a[3][4]={1,2,3,4,5,6,7,8,9,10,11,12};(按数组元素在内存中的排序顺序对各元素赋值)
以上两种方式的效果都是一样,但个人推荐使用第一种,一一对应,界限清晰,可以避免遗漏。
-
不完全初始化:
- 方法3: 对部分元素赋初值。
- 例如:int a[3][4]={{1},{0,5},{0,2,9}};
- 赋值后的结果:
- 1 0 0 0
- 0 5 0 0
- 0 2 9 0
注意:省略第1维的表达方式
(1)当定义的同时采用完全初始化的时候,系统可以根据第2维推算出第1维。
例如:int a[ ][4]={1,2,3,4,5,6,7,8,9,10,11,12};等价于int a[3][4]={1,2,3,4,5,6,7,8,9,10,11,12};
(2)当定义的同时采用分行初始化的时候,系统可以根据花括号推算出第1维。
例如:int a[ ][4]={{0,0,3},{ },{0,10}}
3、二维数组的引用
引用格式: 数组名 [下标][下标]
注意:跟一维数组的方式相同。
四、字符数组
1、字符数组的定义
字符数组的定义与数值型数组的定义方式方法类似,区别在于类型而已
定义格式:char 数组名[常量表达式] 或 char 数组名[常量表达式][常量表达式]
注意:字符数组也一样存在着二维字符数组的概念,方式方法都一样。
2、字符数组的初始化
初始化的方式也是与普通的数值数组没什么大的区别。
数组的一个下标元素位只能存一个字符
例如:用数组a存放" I am happy "
char a[10]={‘I’,’ ‘,‘a’,‘m’,’ ',‘h’,‘a’,‘p’,‘p’,‘y’};
注意:(1)每一个字符都用单引号(' ')来括着;
(2)没有赋初值的元素的元素系统自动定为空字符(即'\0')。
3、字符数组的引用
字符数组的引用格式也是与普通的数值数组没啥两样
引用格式: 数组名[下标] 或 数组名[下标][下标]
4、字符串
字符数组实际上是一系列字符的集合,也就是字符串(String)。在C语言中,没有专门的字符串变量,没有string类型,通常就用一个字符数组来存放一个字符串。
在c语言中,可以将字符串直接赋值给字符数组,例如:
char str[15]={"I am happy."};
char str[15]="I am happy."; //可以有花括号{},也可以没有,两者都合法。
同时,为了方便也可以不指定数组长度,例如:
char str[]={"I am happy."};
char str[]="I am happy.";
所以,通常给字符数组赋值时,我们通常都是使用字符串一次性赋值的方式,而不用单个单个字符来赋值。
但要注意:
(1)字符数组只有在定义的时候进行初始化时才可以利用字符串一次性赋值,一旦定义完,那就只能一个一个字符地赋值。
例如:
示范1:
char str[7];
str = "abc123"; //错误
//下面是正确
str[0] = 'a'; str[1] = 'b'; str[2] = 'c';
str[3] = '1'; str[4] = '2'; str[5] = '3';
示范2:正确的字符串赋值使用:
char str[7]="abc123";
(2)当有效长度相同时,不指定长度的字符串数组与指定长度的字符串数组所占的内存可能会不一致。
如下图,当指定长度的时候所占的字节数就是指定的长度大小,没有指定长度的则是有效长度+1(这1是给“\0”的位置,“\0”的原因下面会讲)。
但要注意的是,虽说没有指定长度可以任意进行初始化,但初始化过后的长度就成了该字符串数组的长度上限。
注意:
(1) 单个的字符用单引号'',字符串用双引号" ";
(2) 字符串的有效长度不一定等于字符数组的长度。
字符串结束的标志
为了测定字符串的实际长度,C语言规定了“ \0 ”作为“字符串结束标志”。
'\0'是 ASCII 码表中的第 0 个字符,英文称为 NUL,中文称为“空字符”。该字符既
不能显示,也没有控制功能,输出该字符不会有任何效果,它在C语言中唯一的作用就是
作为字符串结束标志。
C系统在用字符数组存储字符常量时,会自动在最后加一个" \0 "作为结束符。所以才会导致上面所提及到的“有效长度+1”的说法。
也正因为该结束标志的存在,所以在字符串进行初始化的时候可以省略花括号
“{ }”,不需要借助花括号来表示结束的位置。
注意: 在输出的时候,但遇到了’\0’时,无论后面还有多少字符都直接结束。
例如:假设下面的表格情况是一个字符串
H | e | l | l | o | \0 | r | a | m | \0 |
---|
输出的情况是:Hello
5、字符数组的 格式化输入输出
字符数组的输入输出有两种:
(1)逐个字符输入输出, 用格式符“ %c ”。
scanf 输入格式:scanf("%c",&数组名[下标]);
printf 输出格式:printf("%c",数组名[下标]);
(2)整串字符串输入输出, 用格式符“ %s ”。
scanf 输入格式:scanf("%s",数组名);
printf 输出格式:printf("%s",数组名);
注意:
(1)当采用单个字符数组的方式来输入输出时,系统不会自动在字符串背后加 ’ \0 ‘,当采用字符串的形式时,系统会在末尾结束处加结束标志符’ \0 '。
例:
(2)当采用“%s”字符串的形式来输出时,printf函数中的输出项是字符数组名,而不是数组元素名(即:数组名[下标] 的格式)。
例:正确形式:printf("%s",c); 错误形式:printf("%s",c[0]);
(3) 如果数组长度大于字符串的实际长度,也是只输出到遇到’\0’结束。
(4)使用scanf函数进行多个字符串输入时,在输入的时候以空格分隔。如下:
但要注意的是:无法直接读取含有空格的字符串。
由于系统把空格字符作为输入的字符串之间的分隔符,因此当输入一句话的时候,句中有空格,则在输出的时候空格以后的部分不能存到同一个字符串当中。如下:
若想能用scanf函数读取到空格,则需要在格式控制符上下功夫,自己规定格式控制符的内容,具体操作如下:
备注:[ ]内是匹配的字符,^表示求反集,fflush(stdin)
例一:
例二:
(5)采用输入函数scanf函数时,它的输入项如果是字符数组名,不要再加取地址符&,因为数组名代表的是该数组的起始地址。
(6)输出函数printf函数的输出原理:按变量名找到其起始地址,然后逐个输出,直到遇到 ’ \0 ’ 为止。
6、去掉多余的回车方法
(1)压缩输入法: 在格式码前加上星号“ * ”,则用户就可以告诉scanf()读这个域,但不把它赋予任何变量。
用法: 在格式符的最后处多加%*c,如:scanf("%c%*c, &ch);
使用此方法可以在字符处理时吃掉多余的回车。
(2)清空缓冲区法: fflush(stdin);
用法:在scanf函数结束后使用,如下:
char str[10];
scanf("%s",str);
fflush(stdin); //清空缓冲区
1. fflush函数包含在stdio.h头文件中,用来强制将缓冲区中的内容写入文件。
2. 函数原型:int fflush(FILE *stream) ;
3. 函数功能:清除一个流,即清除文件缓冲区,当文件以写方式打开时,将缓冲区内容写入文件。
4. 函数返回值:如果成功刷新,fflush返回0。指定的流没有缓冲区或者只读打开时也返回0值。返回EOF指出一个错误。
fflush(stdin)刷新标准输入缓冲区,把输入缓冲区里的东西丢弃 (去掉输入时的回车时用这个)
fflush(stdout)刷新标注输出缓冲区,把输出缓冲区里的东西打印到标准输出设备上
std即standard(标准),in即input(输入),out即output(输出)
(3)将缓冲区东西读出来
1) getchar()
用法:在scanf函数结束后使用,如下:
char str[10];
scanf("%s",str);
getchar(); //将缓冲区东西读出来
2)gets()
用法:先定义一个字符数组,然后使用gets()函数去把多余的缓冲区内容读出来。
char str[10],a[5];
scanf("%s",str);
gets(a); //将缓冲区东西读出来
7、字符串处理函数
1、gets 函数——输入字符串的函数
格式:gets(字符数组名)
作用:从终端(如:键盘)输入一串字符串到定义好了的数组中,并得到一个函数值,该函数值是字符数组的起始地址。
与scanf()函数的区别:
(1)对空格的不同处理方式。
scanf() 读取字符串时以空格为分隔,遇到空格就认为当前字符串结束了,所以无法读取含有空格的字符串。
gets() 认为空格也是字符串的一部分,只有遇到回车键时才认为字符串输入结束,所以,不管输入了多少个空格,只要不按下回车键,对 gets() 来说就是一个完整的字符串。换句话说,gets() 用来读取一整行字符串。
(2)输入的数据类型
**scanf():**通过格式控制符来控制输入的数据类型。
**gets():**只能输入字符串,获取回来的数据都是字符类型。
(3)输入变量的个数
scanf(): 可以一次性输入多个变量,且类型可以不一样。
gets(): 每次只能输入一个字符串。
(4)获取了输入后的回车遗留问题
scanf(): 有遗留回车。
gets(): 全部读出,没有遗留回车。
2、puts 函数——输出字符串的函数
格式:puts(字符数组名)
作用:将一个字符串(以‘ \0 ’结束的字符序列)输出到终端(如:屏幕)。
与printf( )函数的区别:
(1)换行问题
printf( ): 不能自动换行,需要在结尾加 \n 。
puts( ): 能自动换行,因为在输出的时候会把字符串结束标志’ \0 ‘转换成’\n’。
(2)输出的数据类型
printf( ): 可以根据格式控制符来控制输出的类型。
puts( ): 只能输出字符串类型。
(3)输出变量的个数
printf( ): 可以一次性输出多个变量,且类型可以不一样。
puts( ): 每次只能输出一个字符串。
3、strcat 函数——字符串连接函数
格式:strcat(字符数组1名,字符数组2名)
作用:把两个字符数组中的字符串连接起来,把字符串2接到字符串1后面,结果存放在字符串数组1中,函数返回值是字符数组1的地址。
注意说明:
(1)字符数组1必须足够大,足够存入自己本身+字符数组2的总和长度,否则会越界。
(2)字符数组1在定义的时候不能采用缺省数组长度的形式来定义(如:a[]="china";),
如果采取该定义初始化方式,则没有足够的空间来存储,因为缺省的定义方式在定义完之后
长度就定下来了,它的长度就是你所初始化的长度大小。
(3)连接前两个字符串的后面都有‘\0’,连接时将字符串1后面的‘\0’取消,只在新串最
后保留'\0'。
4、strcpy和strncpy 函数——字符串复制函数
strcpy 函数
格式:strcpy(字符数组1名,字符串2)
作用:将字符数组2复制到字符数组1中去。
strncpy 函数
格式:strncpy(字符数组1名,字符串2,复制的长度n)
作用:将字符串2的最前面的n个之前的字符复制到字符数组1中去。
注意说明:
(1)strcpy 函数中,“字符数组1” 必须足够大,以便容纳下“字符串2”;
strncpy 函数中,“字符数组1” 的长度大小必须大于需要复制的长度大小,若需要复制的
长度大小大于 “字符串2” 的总长,则把整个字符串2复制完之后,后面剩下的都给NULL;
(2)两个函数的参数:“字符数组1”必须写成数组名形式,"字符串2" 可以是字符数组名,
也可以是字符常量。 【如:strcpy(str1,"China"); 或者 strcpy(str1,str2); 】
(3)若 “字符数组1” 中原本是有字符的,所复制的“字符串2”的长度比“字符数组1”中的
字符长度少时,这时候会用复制的内容覆盖掉“字符数组1”中前面相应长度的对于位置。
(4)若想把一个字符串常量或字符数组赋值给另一个字符数组时,这时只能用字符复制函
数(strcpy函数 和 strncpy函数)或者 用赋值语句一个一个字符进行赋值。
5、strcmp 函数——字符串比较函数
格式:strcmp(字符串1,字符串2)
用法:用来比较字符串1和字符串2,
- 若两个字符 相同,则函数返回值为0,
- 若字符串1 大于 字符串2,则函数返回大于0的值,
- 若字符串1 小于 字符串2,则函数返回小于0的值。
字符串比较规则:
将两个字符串自左至右逐个字符相比,按ASCII码值大小来比较,直至出现不同字符或遇到“\0”为止。
若出现不同的字符,则以第一对不同的字符的比较结果为准,来判断大于还是小于。
注意:两对字符串比较,不能用比较运算符(>、<、= 等等),只能用strcmp函数。
6、strlen 函数——测字符串长度的函数
格式:strlen(字符数组名 或 字符串)
作用:测试字符串长度,函数返回值为字符串的实际长度(也称:有效长度),不包括 ‘ \0 ’在内。
例如:
char str[10]="China";
printf("%d",strlen(str));
得到的结果是 5 ,而不是10或6.
也可以测字符串长度,如:strlen("China");
知识拓展:
对于 sizeof() 的东西太多太复杂了,想详细了解的可以上百度百科查询,我以下的东西大部分都是出至百度百科
sizeof()
是C语言中用来求一个对象(类型、变量、表达式……)所占内存大小(以字节为单位)的运算符。
sizeof有两种语法形式,如下:
sizeof(type_name);//sizeof(类型);
sizeof object;//sizeof对象;
例如:
int i;
sizeof(i);//ok
sizeof i;//ok
sizeof(int);//ok
sizeof int;//error
所以,通常为了方便记忆,常采取第一种:sizeof(x)这种带括号的形式
【x可以是 变量、数组、类型、表达式、指针 等等】
在C语言中,对 sizeof() 的处理都是在 编译阶段进行 ,所以它可以被当作 常量表达式 使用。其 返回值类型为size_t,在头文件stddef.h中定义。这是一个依赖于编译系统的值(意思就是结果的值的大小受操作系统的影响)。
一般定义的类型为 typedef unsigned int size_t; 【无符号整型%u】
sizeof计算对象的大小也是转换成对对象类型的计算,也就是说,同种类型的不同对象其sizeof值都是一致的。sizeof对一个表达式求值,编译器根据表达式的最终结果类型来确定大小,一般不会对表达式进行计算。如:
sizeof(2);//2的类型为int,所以等价于sizeof(int);
sizeof(2+3.14);
//3.14的类型为double,2也会被提升成double类型,所以等价于sizeof(double);
注意:
(1)函数、不能确定类型的表达式以及位域(bit-field)成员不能被计算sizeof值;
(2)指针变量的sizeof值与指针所指的对象没有任何关系,在32位系统中,指针变量的
sizeof通常为4个字节,在64位系统中指针变量的sizeof通常为8。
(3)数组的sizeof值等于数组所占用的内存字节数。
(4)与strlen的区别
a)strlen()函数是求字符串的实际长度,而sizeof()不是实际长度;
b)sizeof是算符,strlen是函数。
c)sizeof可以用类型做参数,strlen只能用char*做参数,且必须是以''\0''结尾的。
关于sizeof()的一个易错难题:(这是在百度百科上看到的一到题)
7、strlwr 函数——转换为小写的函数
格式:strlwr(字符串)
作用:将字符串中大写字母转换为小写字母。
8、strupr 函数——转换为大写的函数
格式:strupr(字符串)
作用:将字符串中小写字母转换为大写字母。
以上介绍了8种常用字符串处理函数,他们的头文件是:string.h文件,使用时记得导入头文件。
还要强调的是:库函数并非C语言本身的组成部分,而是C语言编译系统为了方便用户使用而提供的公共函数,所以可能会存在函数的数量、函数名、函数功能等差异。
五、数组的越界溢出
数组越界: 指数组下标变量的取值超过了初始定义时的大小,导致对数组元素的访问出现在数组的范围之外。
一般情况下,数组的越界错误主要包括两种:数组下标取值越界与指向数组的指针的指向范围越界,这些问题最常发生在因为循环体的终止条件设定不当而导致的。
数组的越界溢出所导致的问题就是:1、数据的丢失 ;2、溢出的部分留在缓冲区可能会影响到后面邻接变量的值。
1、数组下标取值越界
问题所在: 在访问数组的时候,下标的取值不在定义好的取值范围,访问的是无法获取的内存地址。错误示范如下:
int a[3],b;
b=a[3];
类似这种情况就是数组下标取值越界,因为int a[3]实际上就只有a[0]、a[1]、a[2],
而并没有a[3]。
2、指向数组的指针的指向范围越界
问题所在: 定义数组时会返回一个指向第一个变量的头指针,对这个指针进行加减运算可以向前或向后移动这个指针,进而访问数组中所有的变量。但在移动指针时,如果不注意移动的次数和位置,会使指针指向数组以外的位置,导致数组发生越界错误。错误示范如下:
int i;
int *p;
int a[5];
/*数组a的头指针赋值给指针p*/
p=a;
for(i=0;i<10;i++) //问题所在:数组只有5个数,但循环控制却后移10次。
{
/*指针p指向的变量*/
*p=i+10;
/*指针p下一个变量*/
p++;
}
// 所以正确的答案应该是把 i<10 改成 i<5
避免的方法:
(1) 尽量使用显式的方式指定数组的边界(即:int a[10],而少用 int a[ ]),采用显式的方式既清晰显示数组的边界,方便代码的阅读,也方便在对数组进行读写操作时进行相应的检查。
(2) 使用宏定义的形式来指定数组的边界(这也是最常用的指定方式)如:
#define MAX 10
…
int a[MAX]={1,2,3,4,5,6,7,8,9,10};
(3) 可以写一个数组越界检查函数进行处理。
六、数组的动、静态问题
动态数组:
【在这先简单提一下,后面讲到相关知识在详细展开来讲】
利用标准库函数中的内存的申请和释放函数,在程序的运行过程中,根据实际需要指定数组的大小,分配的存储空间在堆上。其本质是一个指向数组的指针变量。
常用的内存管理函数有以下三个:
- 分配内存空间函数malloc
- 分配内存空间函数 calloc
- 释放内存空间函数free
静态数组:
在声明时就已经确定大小的数组,即数组元素的个数固定不变,分配的存储空间在栈上,上面一直所提到的都是静态的数组,静态的数组也是最普通形式的数组。
定义的形式:int a[10];