目录
一、一维数组
1.数组的定义
为了存放鞋子,假设你把衣柜最下面的一层分成了10个连续的格子。此时,让他人帮你拿鞋子就会很方便,例如你可直接告诉他拿衣柜最下面一层第二个格子中的鞋子。同样假设现在我们有10个整数存储在内存中,为方便存取,我们可以借助C语言提供的数组,通过一个符号来访问多个元素 。
某班学生的学习成绩、一行文字、一个矩阵等数据的特点如下:
(1) 具有相同的数据类型。
(2) 使用过程中需要保留原始数据。
C语言为了方便操作这些数据,提供了一种构造数据类型------数组 。所谓数组,是指一组具有相同数据类型的数据的有序集合 。
一维数组的定义格式为:
类型说明符 数组名 [常量表达式]:
例如:
int al10];
定义一个整型数组,数组名为a,它有10个元素。
声明数组时要遵循以下规则:
(1) 数组名的命名规则和变量名的相同,即遵循标识符命名规则。
(2) 在定义数组时,需要指定数组中元素的个数,方括号中的常量表达式用来表示元素的个数,即数组长度。
(3) 常量表达式中可以包含常量和符号常量,但不能包含变量。也就是说,C语言不允许对数组的大小做动态定义,即数组的大小不依赖于程序运行过程中变量的值。
以下是错误的声明示例(最新的C标准支持,但是最好不要这么写):
int n;
/* 在程序中临时输入数组的大小 */
scanf("%d", &n);
int a[n];
数组声明的其他常见错误如下:
/* 数组大小为0没有意义 */
① float a[0];
/* 不能使用圆括号*/
② int b(2)(3);
/* 不能用变量说明数组大小*/
③ int k=3, a[k];
2.一维数组在内存中的存储
语句int arr[100];定义的一维数组 arr在内存中的存放情况如下图所示,每个元素都是整型元素,占用4字节,数组元素的引用方式是"数组名[下标]",所以访问数组 arr中的元素的方式是arr[0], arr[1]... arr[99]。 注意,没有元素 arr[100],因为数组元素是从0开始编号的。
下面介绍一维数组的初始化方法:
(1) 在定义数组时对数组元素赋初值,例如:
int a[10]={0, 1, 2, 3, 4, 5, 6, 7, 8, 9};
不能写成:
int a[10]; a[10]={0, 1, 2, 3, 4, 5, 6, 7, 8, 9};
(2) 可以只给一部分元素赋值。例如:
int a[10]={0, 1, 2, 3, 4};
定义a数组有10个元素,但花括号内只提供5个初值,这表示只给前5个元索赋初值,后5个元索的值为0。
(3) 如果要使一个数组中全部元素的值为0,那么可以写为:
int a[10] ={0, 0, 0, 0,0, 0, 0, 0, 0, 0};
或
int a[10]={0};
(4)在对全部数组元索赋初值时,由于数据的个数已经确定,因此可以不指定数组的长度。例如:
int a[ ]={1, 2, 3, 4, 5};
二、数组访问越界与数组的传递
1.数组的访问越界
下面借助一个数组的实例来掌握数组元素的赋值、访问越界。下例中给出了该例的全部代码。
【例】一维数组的存储及访问越界
#include <stdio.h>
//数组越界
int main(){//定义数组时,数组长度必须固定
int a[5]={1, 2, 3, 4, 5};
int j=20;
int i=10;
a[5]=6;//越界访问
a[6]=7;//越界访问会造成数据异常
printf("i=%d\n", i);//i发生改变
return 0;
}
下图显示了代码运行情况。如下图所示,在第8行左键打上断点,然后单击"小虫子"按钮,在内存视图依次输入&j、&a、&i 来查看整型变量j、整型数组a、整型变量i的地址,即可看到三个变量的地址,这里就像我们给衣柜的每个格子的编号,第一格、第二格···...一直到柜子的最后一格。操作系统对内存中的每个位置也给予一个编号,对于 Windows 32 位控制台应用程序来说,这个编号的范围是从 0x00 00 00 00 到 OxFF FF FF FF,总计为2的32次方,大小为4G。这些编号称为地址(我们是64 位程序,地址显示的是64位)。
【图】代码运行情况1
在变量窗口中输入sizeof(a) , 可以看到数组a的大小为20字节,计算方法其实就是sizeof(int)*5:数组中有5个整型元素,每个元素的大小为4字节,所以共有20字节。访间元素的顺序是依次从a[0]到 a[4],a[5]=6、a[6]=7 均为访问越界,下图显示了代码运行情况,从中看出,执行到第 1行时,变量i的值被修改了,这就是访问越界的危险性------未对变量i赋值,其值却发生了改变!
【图】代码运行情况2
**数组另一个值得关注的地方是,编译器并不检查程序对数组下标的引用是否在数组的合法范围内。**这种不加检查的行为有好处也有坏处,好处是不需要浪费时间对有些已知正确的数组下标进行检查,坏处是这样做将无法检测出无效的下标引用,一个良好的经验法则是:如果下标值是通过那些已知正确的值计算得来的,那么就无须检查;如果下标值是由用户输入的数据产生的,那么在使用它们之前就必须进行检查,以确保它们位于有效范围内。
2.数组的传递
cs
#include <stdio.h>
//一维数组的传递,数组长度无法传递给子函数
//C语言的函数调用方式是值传递
void print(int b[],int len) {
int length = sizeof(b);
int i;
for (i = 0; i < len; i++) {
printf("%d\t", b[i]);
}
b[4] = 20;//在子函数中修改版数组元素
printf("\n");
}
//数组越界
//一维数组的传递
#define N 6
int main(){
int a[]={1,3,5,7,9,11};
print(a,5);
print("a[4]=%d\n",a[4]);//a[4]发生改变
return 0;
}
如下图1所示,在第18行点击向下箭头,进入 print 函数,这时会发现数组b的大小变为8字节,如下图2所示,这是因为一维数组在传递时,其长度是传递不过去的,所以我们通过 len 来传递数组中的元素个数。实际数组名中存储的是数组的首地址,在调用函数传递时,是将数组的首地址给了变量b(其实变量b 是指针类型,具体原理会在指针节讲解),在[b]的方括号中填写任何数字都是没有意义的。这时我们在print 函数内修改元索 b[4]=20,可以看到数组b的起始地址和 main 西数中数组a的起始地址相同,即二者在内存中位于同一位置,当数执行结束时,数组a中的元素 a[4]就得到了修改。
【图1】代码运行情况3
【图1】代码运行情况4
三、字符数组与scanf读取字符串
1.字符数组的初始化及传递
字符数组的定义方法与前面介绍的一-维数组类似。例如:
char c[10];
字符数组的初始化可以采用以下方式。
(1) 对每个字符单独赋值进行初始化。例如:
c[0]='I';c[1]=' ';c[2]='a';c[3]='m';c[4]=' ';c[5]='h';c[6]='a';c[7]='p';c[8]='p';c[9]='y';
(2)对整个数组进行初始化。例如:
char c[10]={'I','a','m',h',a','p','p;y'};
但工作中一般不用以上两种初始化方式,因为字符数组一般用来存取字符串。通常采用的初始化方式是char c[10]= "hello"。因为C语言规定字符串的结束标志为'\0',而系统会对字符串常量自动加一一个"\0',为了保证处理方法一-致,- -般会人为地在字符数组中添加'\0',所以字符数组存储的字符串长度必须比字符数组少1字节。例如,char c[10]最长存储9个字符,剩余的1个字符用来存储'\0'。
【例】字符数组初始化及传递
cpp
#include <stdio.h>
void print(char c[]){
int i=0;
while (c[i]){
printf("%c",c[i]);
i++;
}
print("\n");
}
//字符数组存储字符串,必须存储结束符"\0"
int main(){
char c[5]={'h','e','l','l','o'};
char d[5]="how";
printf("%s\n",c);//会出现打印了乱码
printf("%s\n",d);
print(d);
return 0;
}
上例中代码的执行结果如下图所示。为什么对数组赋值"hello"却打印出乱码,这是因为printf通过%s打印字符串时,原理是依次输出每个字符,当读到结束符'\0'时,结束打印:
我们通过print函数模拟实现printf 的%s打印效果,当c[i]为'\0'时,其值是0,循环结束,也可以写为c[i]!='\0'。
2.scanf读取字符串
【例】scanf读取字符串
cpp
#include <stdio.h>
//scanf读取字符串使用%s
int main(){
char c[10];
char d[10];
scanf("%s",c);
printf("%s\n",c);
scanf("%s%s",c,d);
printf("c=%s,d=%s\n",c,d);
return 0;
}
scanf通过%s读取字符串,对c和d分别输入"are"和"you" (中间加一个空格),scanf在使用%s读取字符串时,会忽略空格和回车(这一点与%d和%f类似)。
输入顺序及执行结果如下图,
四、gets函数与puts函数,str系列字符串操作函数
1.gets函数与puts函数
gets函数类似于scanf函数,用于读取标准输入。前面我们已经知道scanf函数在读取字符串时遇到空格就认为读取结束,所以当输人的字符串存在空格时,我们需要使用gets函数进行读取。
gets函数的格式如下:
char *gets(char *str);
gets函数从STDIN (标准输人)读取字符并把它们加载到str (字符串)中,直到遇到换行符(\n) 。如下例所示,执行后,我们输入"how are you",共11个字符,可以看到gets会读取空格,同时可以看到我们并未给数组进行初始化赋值,但是最后有'\0', 这是因为gets遇到\n后,不会存储\n,而是将其翻译为空字符'\0'。
puts函数类似于printf函数,用于输出标准输出。puts 函数的格式如下:
int puts(char *str);
函数puts把str (字符串)写人STDOU (标准输出)。puts 会将数组C中存储的"how are you"字符串打印到屏幕上,同时打印换行,相对于printf 函数,puts 只能用于输出字符串,同
时多打印一-个换行符,等价于printf("%s\n",c)。
cpp
#include <stdio.h>
//gets一次读取一行
int main(){
char c[20];
gets(c);
puts(c);
return 0;
}
上例的运行结果如下图:
2.strlen-strcmp-strcpy函数
str系列字符串操作函数主要包括strlen、strcpy、 strcmp、 strcat 等。strlen 函数用于统计字符串长度,strcpy 函数用于将某个字符串复制到字符数组中,strcmp 函数用于比较两个字符串的大小,strcat 函数用于将两个字符串连接到一起。各个函数的具体格式如下所示:
#include <string.h>
size_ t strlen(char *str);
char *strcpy(char *to, const char *from);
int strcmp(const char *str1, const char *str2);
char *strcat(char *str1, const char *str2);
对于传参类型char*,直接放人字符数组的数组名即可。
接下来我们通过下例来具体学习str系列字符串操作函数,掌握每个函数的内部实现。
【例】str系列字符串操作函数的使用。
cpp
#include <stdio.h>
#include <string.h>
int mystrlen(char c[]){
int i =0;
while (c[i++]);
return i-1;
}
//统计strlen统计字符串长度
int main(){
int len;//用于存储字符串的长度
char c[20];
char d[100]="world";
gets(c);
puts(c);
len=strlen(c);
printf("len=%d\n",len);
len= mystrlen(c);
printf("mystrlen len=%d\n",len);
strcat(c,d);
strcpy(d,c);//c中的字符串赋值给d
puts(d);
printf("C?D %d\n",strcmp(c,d));
puts(c);
return 0;
}
下图所示为我们输人"hello"后的执行结果,通过strlen函数计算的字符串长度 为5,我们自己写的函数就是strlen函数的计算原理,即通过判断结束符来确定字符串的长度**,stropy函数用来将字符串中的字符逐个地赋值给目标字符数组**,例中我们将c复制给d,就是将c中的每个字符依次赋值给d,也会将结束符赋值给d。注意,目标数组一定要大于字符串大小,即sizeof()>strlen(c).否则会造成访问越界。
strcmp函数用来比较两个字符串的大小,由于字符数组c中的字符串与d相等,所以这里的返回值为0。如果c中的字符串大于d,那么返回值为1;如果c中的字符串小于d,那么返回值为-1.如何比较两个字符串的大小呢?具体操作是从头开始,比较相同位置字符的ASCII码值,若发现不相等则直接返回,否则接着往后比较。例如,strcmp("hello" ,"how")的返回值是-1。即hello"小于"how", 因为第一一个字符h相等,接着比较第二个位置的字符,e的ASCII码值小于o的,然后返回-1。strcat函数用来将一一个字 符串接到另外一一个字符串的末尾。例中字符数组c中存储的是"hello",我们将d中的"world"与c拼接,最终结果为"hello hhy world".注意,目标数组必须大于拼接后的字符串大小,即sizeof(c)>strlen( "hello hhy world")。
五、练习题及解析
1、数组内两个元素可以存储不同的数据类型
A正确 B错误
2、int mark[100];我们可以做mark[100]=3;
A正确 B错误
3、int a[10]= {0,1,2,3,4};定义a数组有10 个元素,但花括号内只提供5个初值,这表示只给
前5个元素赋初值,后5个元素的值为0。
A正确 B错误
4、数组int arr[5];我们做arr[5]=20这个操作造成了访问越界
A正确 B错误
5、访问越界是非常危险的,因为C和C++语言没有针对访问越界进行检测
A正确 B错误
6、数组传递时,可以把自身长度传递给子函数
A正确 B错误
7、在子函数中改变数组中某个元素的值,子函数结束后,数组内元素值发生变化
A正确 B错误
8、char c[ 10]= {T,a','m',h','a',p'p'y' }这种初始化方式是常用的方式
A正确 B错误
9、char c[5]我们可以放5个字符来使用
A正确 B错误
10、char c[10];scanf("%s",c);读取字符串时会读取到空格和\n
A正确 B错误
11、gets 一次可以读取一行,能够读取空格
A正确 B错误
12、puts(c)等 价于print("%s\n",c)
A正确 B错误
13、strlen函数用于统计字符串长度,strcpy函数用于将某个字符串复制到字符数组中,
strcmp函数用于比较两个字符串的大小,strcat 函数用于将两个字符串连接到一起
A正确 B错误
14、如果字符串没有结束符'\0',也可以使用strlen 正确统计长度
A正确 B错误
解析:
1、B 解释:数组内的元素必须存储相同的数据类型。
2、B 解释:数组下标从零开始,定义int mark[100],只能方位mark[0]到mark[99]
3、A 解释:初始化元素时,剩余的未赋值元素,会被初始化为零
4、A 解释: int arr[5]数组访问arr[0]到arr[4],5及以后的值都是访问越界
5、A 解释:我们的实例演示了访问越界会改变其他变量的值,因此非常危险
6、B 解释:数组传递时,只是把数组的起始地址传递给了子函数,长度不能传递给子函数,我们需要使用额外的变量,来把长度传递给子函数
7、A 解释:当我们把数组名传递给子函数后,在子函数内就可以访问数组的某个元素,同时可以进行修改
8、B 解释:字符数组的初始化最常用的方式是char c[10]="Iamhappy"这种方式
9、B 解释: char c[5]我们只能放4个字符,第五个字符需要放置结束符\0',否则prinft("%s\n",c);
输出会造成乱码
10、B 解释: scanf("%s",c); 会忽略空格和\n,因此无法读取到空格和\n
11、A 解释:如果要一次读一行,同时需要把空格读到字符数组中,那么就需要用gets
12、A 解释:如果输出字符串,使用puts更加方便,注意puts只能用于输出字符串,不能输出其
他类型
13、A 解释:这个需要记住
14、B 解释: strlen 是通过结束符'\0'来判断字符串长度的,如果没有结束符,无法统计字符串长度
下一期: