文章目录
9.0 导入例子
在循环章节,我们有一个例子是计算平均成绩:假设班级中有100名同学,希望输入他们的数学成绩并计算平均成绩。
int sum = 0, score, count;
count = 0;
while(count < 100){
scanf("%d", &score);
sum += score;
count++;
}
printf("平均分是%d\n", sum / 100);
如果不但要求计算平均成绩,还是找出有多少人低于平均成绩(或者高于平均成绩),就不能不记录所有的成绩了。其它一些情况,也需要把输入的值记录下来。那此处为了记录100个成绩,按照之前的知识,需要声明100个变量,比如,int x1, x2, …, x100;然后再往它们里面分别输入值,和累加求和
int sum = 0;
int x1, x2, x3, ..., x100;
scanf("%d", &x1);
sum += x1;
scanf("%d", &x2);
sum += x2;
...
scanf("%d", &x100);
sum += x100;
这里省略号表示了省去的代码,实际程序必须加上。可以看到,声明多个变量会让程序出现很多重复代码,由于这些变量本身也没有任何关系,没有办法把重复代码用循环来代替。这里,就可以使用长度为100的数组。这样变量的声明也减少了,也可以用循环语句替换代码了。
int score[100], i, avg, count_of_low;
for(i = 0, avg = 0; i < 100; i++){
scanf("%d", &score[i]);
avg += score[i];
}
avg /= 100;
for(i = 0, count_of_low = 0; i < 100; i++){
if (score[i] < = avg){
count_of_low++;
}
}
printf("平均分是%d, 有%d个同学的分数不高于平均分\n", avg, count_of_low);
9.1 一维数组
数组特点
- 数组相当于相同类型变量的容器
- 一旦创建(声明)好,不能改变大小(长度)
- 数组中的元素在内存中是连续依次排列的
- 从语言级别并不支持对数组的整体操作,而是使用数组的各个元素
9.1.1 声明数组
- 数组元素的类型 数组名[数组元素的个数];
int grades[100];
double weight[20]; - C99之前,声明数组时,数组元素的个数必须是整数字面量,不能是变量(VS2010里面必须是整数字面量,等级考试里应该还是这样认为的)
- 实际编程中可以用宏来定义数组的大小
**如果说基本类型变量是存储值的容器,那么数组就是存储变量的容器。**如果需要多个相同类型的变量保存数值,那么可以声明(定义)相应的数组。声明数组要指出数组元素的类型,数组名,以及数组元素的个数(也称为数组长度,数组的大小):
数组元素的类型 数组名[数组元素的个数];
比如,
int score[50];
float height[20];
char name[20];
如果数组元素类型和变量类型相同,那么可以一起声明,用逗号隔开:
float height[20], weight[20];
int i, j, age[30], socre[30];
单独声明的变量在内存中不一定是连续存放的,但是同一个数组的各元素在内层中是连续存储的,
变量如果仅声明没有赋值那么其值是未知的,是不能读取其值的,类似的,仅声明的数组的各元素也是没有初始化的,是不能读取其值的。所以图示中用?表示其值是未知的。
数组长度只能在声明数组时指定,声明数组后它的长度就不能改变了。C99之前(包括在VS2010中)数组的长度可以用任何整数常量表达式来指定,而不能用变量来指定。比如,下面就是错误的写法,
int n = 4;
int a[n]; // 错误的声明,长度不能是变量
这样的要求可能是希望在编译器编译程序时就可以确定数组的大小,而不是在程序运行的过程中再确定数组的大小(变量的值可以在程序运行过程中用scanf语句输入进来),这样方便控制数组的大小,不至于出现数组长度过大的情况。
如果考虑到程序以后改变时要调整数组的长度(意思是重写程序的时候在声明语句处改变数组长度,而不是程序运行时改变数组长度,那样是改不了的),或者考虑到在程序运行时可能需要在多处访问数组长度,为了方便起见,可以用宏来定义数组的长度:
#define N 10
...
int a[N];
9.1.2 使用数组(元素)
-
数组各个元素有编号(下标、索引),首元素的编号是0,最后一个元素的编号是数组长度-1
声明了含有5个元素的数组a:
int a[5];
各个数组元素是:
a[0],a[1],a[2],a[3],a[4],a[5] -
数组各个元素与单独声明的同类型的变量的使用没有任何区别
上面声明的数组a的作用,就是相当于我们有5个整型变量a[0],a[1],a[2],a[3],a[4],a[5],可以像使用一般整型变量那样使用它们
-
访问数组元素注意下标不要越界,编译器不检查下标越界的错误,下标越界不妨碍程序的运行,但是运行时程序可能会崩溃,或者出现意想不到的结果。
数组声明后,就可以使用了,程序中实际使用的是包含在数组中的各个元素,而不是数组整体。
用数组名+元素编号的方式来访问数组的各个元素,C语言中的格式为
数组名[元素编号]
这个编号是从0开始,到数组长度-1结束。元素的编号也称为下标或者索引。
声明数组时指定的类型实际上就是数组各个元素的类型,所以数组各元素可以像单独声明的那个类型的变量一样使用,没有任何区别。
int a[4];
a[0] = 1;
a[1] = 2;
a[2] = 3;
a[3] = -1;
a[0]++;
a[3] = a[0] + a[1] * a[2];
scanf("%d", &a[0]); // 输入12
scanf("%d", &a[2]); // 输入7
printf("%d\n", a[0]);
printf("%d\n", a[1]);
printf("%d\n", a[2]);
printf("%d\n", a[3]);
上图为声明数组后的状态
上图为对数组元素赋值后的状态
上图为对数组元素进行更多处理后的状态
上图为输入一些值到数组中,以及输出数组各元素值后的状态。
数组下标除了常量还允许表达式,只要表达式的结果为在0到数组长度-1中的整数。(声明数组时,长度必须为整数常量,不要搞混了)
int i = 2, a[4];
a[i - 1] = 10; // 相当于a[1] = 10;
a[i] = 20; // 相当于a[2] = 20;
a[2 * i - 1] = a[i] + a[i - 1]; // 相当于a[3] = a[2] + a[1];
上图为数组刚声明后的状态
上图为对用表达式作为数组下标来访问数组元素之后的状态。
如果下标是表达式,那么程序运行时会自动先计算表达式的值,然后再以表达式的值作为下标来访问那个元素。
由于对数组各个元素的处理往往是一致的,比如将数组的全部元素设置为0,
int a[4];
a[0] = 0;
a[1] = 0;
a[2] = 0;
a[3] = 0;
由于数组元素的下标可以是整数表达式,故可以用for循环语句来实现上述结果
int a[4], i;
for(i = 0; i < 4; i++){
a[i] = 0;
}
循环体里,i的值会依次从0遍历到3,因此a[0]到a[3]会依次被赋值为0。
上图为i的值为0,a[i] = 0;
即将第一次运行的状态
上图为i的值为1,a[i] = 0;
即将第二次运行的状态
上图为i的值为2,a[i] = 0;
即将第三次运行的状态
上图为i的值为3,a[i] = 0;
即将第四次运行的状态
上图为i的值为4,for语句结束后的状态。
对数组使用的常见错误可能有:
a. 整体上使用数组
比如希望将数组的各个元素赋值为0,正确做法是对各个数组元素分别赋值,或者用循环语句来简化代码。而不能对数组做整体操作,比如下面的代码对数组b的操作是错误的:
int a[4], b[4],i;
// 将数组各元素赋值为0,正确
for(i = 0; i < 4; i++){
a[i] = 0;
}
b = 0; // 错误
b = a; // 错误
b = {0, 0, 0, 0}; // 错误
C语言在语言级别上不支持对数组进行整体操作,如果希望对数组进行形式上的整体操作,可以使用函数。实际上,数组名本身表示数组首元素的地址值,而且是一个不能被改变的值,在指针章节会说到。
b. 数组下标溢出
访问数组元素时,提供的整数下标不在0和数组长度-1之间。比如
int a[4];
a[-1] = 0;
a[4] = 0;
这个错误编译器并不会检查出来,但是在程序运行过程中,像上面的语句会真实访问内存中相对原数组的位置上有所偏差的“假想的数组元素”,比如,a[-1]访问了a[0]这个元素前面的位置,a[4]会访问a[3]后面的位置,可是我们声明/申请的数组只是4个元素的数组a,这些“假想的数组元素”上面可能存储了其它内容,
上图就展示了一个可能的例子,内存中,声明的变量b和c恰好在a的前后。
可以想象,这样会误访问变量b和变量c的内容,造成隐藏的错误,甚至是程序的崩溃(可能崩溃会好些,让人发现有问题存在)。
9.1.3 数组的初始化
- 数组声明后其元素的值是未知的,不能直接使用(读取值)
- 可以在声明数组的同时为各个元素提供一些初值,这叫数组的初始化,下面的大括号{}里面的值列表称为初始化列表
int a[10] = {1,2,3,4,5,6,7,8,9,0}; - 上面的初始化形式只能用在声明数组时,数组声明以后,就不能用初始化列表来给数组元素赋值了。下面的用法就是错误的
int a[10];
a = {1,2,3,4,5,6,7,8,9,0}; // 错误的语法
同C语言中普通变量情况类似,在数组被声明后数组的各元素也是未初始化的,
如果直接读取它们的值,在VS2010里面会导致程序崩溃
int a[4];
printf("%d\n", a[0]);
运行这段代码会导致程序崩溃,
可以使用特殊的写法,在声明数组的同时为数组各元素赋初值
int a[4] = {10, 20, 30, 40};
赋值号右边的大括号里面的值依次赋给数组的下标为0的元素,下标为1的元素…。可以称大括号{}里面的值列表称为初始化列表。
显然,上述初始化数组的效果同下面的赋值语句是等价的,
int a[4];
a[0] = 10;
a[1] = 20;
a[3] = 30;
a[4] = 40;
如果初始化列表里面的值的个数小于数组大小,那么没有提供初值的数组元素也会被初始化,只是初始化的值为0。
int a[4] = {10, 20};
利用这一特性,可能很容易把数组初始化为全0,
int a[4] = {0};
大括号里面不提供任何值,或者值的个数超过数组的大小,都属于语法错误。
/*
int a[4] = {}; // 错误的语法
*/
/*
int b[4] = {1,2,3,4,5}; // 错误的语法
*/
如果提供了初始化式子,可以不指定数组大小,数组的大小会自动指定为和初始化列表中的值个数一样
int a[] = {1,2,3,4}; // 相当于int a[4] = {1,2,3,4};
注意,声明数组提供初始化列表的形式不能在声明语句之后使用
int a[4];
/*
a={1,2,3,4}; // 错误的语法
*/
语法上来说,声明数组之后只能单独访问数组的各个元素。初始化列表的形式只是在声明数组时为数组各个元素设置初值的“专用”形式。
9.1.3 对数组使用sizeof运算符
之前说过,数组元素在内存中是连续排列的,运算符sizeof可以得到整个数组的字节数。比如数组a有10个元素,那么sizeof(a)为40(假如单个元素占4个字节)。
int a[10];
printf("%d\n", sizeof(a[0]));
printf("%d\n", sizeof(a));
可见,无论什么类型的数组,sizeof(a)/sizeof(a[0])可以得到数组的大小。利用这个特性,遍历数组各元素可以写成
for(i = 0; i < sizeof(a)/sizeof(a[0]); i++){
...
}
这样,就可以不用宏来定义数组的大小了,用宏的话还要记忆宏的名字。
9.1.4 一些数组常见使用方式
常用for循环处理数组,
下面假设数组如下声明
#include <stdio.h>
#define N 5
int main(){
int arr[N], i;
// ...
return 0;
}
例1.输入值到数组中
printf("请输入%d个整数值:", N);
for(i = 0; i < N; i++){
scanf("%d", &arr[i]);
}
例2. 产生随机数放到数组中。比如随机数的范围是0到99。
#include <stdlib.h>
#include <time.h>
srand(time(0)); // 保证产生的随机序列不同
for(i = 0; i < N; i++){
arr[i] = rand() % 100;
}
例3. 输出数组的值
for(i = 0; i < N; i++){
printf("%d ", arr[i]);
}
例4. 复制数组 。假设数组arr2长度与arr一样,也是N,它的各元素已经有值了。复制数组arr2到数组arr中。复制数组不能使用
/*
arr = arr2; // 错误的用法
*/
需要对各个元素依次赋值
for(i = 0; i < N; i++){
arr[i] = arr2[i];
}
例5. 计算数组各元素的和。首先声明变量total,并初始化为0,然后将数组各元素的值加到total变量中。
int total = 0; // 如果数组各元素的和可能会超过整数最大值,可以将total声明为float或者double类型
for(i = 0; i < N; i++){
total += arr[i];
}
例6. 找到最大值。使用变量max来存储最大值。初始化max为数组首元素arr[0]。然后依次比较数组其余各元素与max的大小,如果比max大,更新max的值。最后max的值即为数组最大值。
int max=arr[0];
for(i = 1; i < N; i++){
if (arr[i] > max){
max = arr[i];
}
}
例7. 找到最大值的最小下标。流程和找最大值类似,只是需要用变量记录找到最大值的下标。遍历完数组各元素后,这个变量里就保存了最大值的最小下标。
int max = arr[0];
int index_of_max = 0;
for(i = 1; i < N; i++){
if (arr[i] > max){
max = arr[i];
index_of_max = i;
}
}
例8. 在数组中从头找一个值的下标,如果没有,返回-1。假设index_of_search已经存储了待查找的值。声明变量index_of_search来记录找到的下标,初始化它为-1。从头遍历数组各元素,如果发现元素值与待查找的值相等,用index_of_search记录这个元素下标,终止循环。循环之后,如果index_of_search为-1,说明没有找到那个值,否则index_of_search的值为找到数组元素的下标。
int index_of_search = -1;
for(i = 0; i < N; i++){
if (arr[i] == search_value){
index_of_search = i;
break;
}
}
例9. 元素移位。有些场景下需要向左或者向右移位数组的元素。比如,向左移位数组元素,用数组首元素来填充最后一个元素。
int temp = arr[0];
for(i = 1; i < N; i++){
arr[i - 1] = arr[i];
}
arr[N - 1] = temp;
例10. 随机打乱数组。有些场景需要随机打乱数组各元素。从后向前遍历数组,对每个arr[i],随机产生一个j,j<=i,交换arr[i]和arr[j]的值。
srand(time(0)); // 保证产生的随机序列不同
for(i = N - 1; i > 0; i--){
int j = rand() % (i + 1);
int temp = arr[i];
arr[i] = arr[j];
arr[j] = temp;
}
9.1.5 数组更多例子
9.1.6 使用数组时常见问题
9.2 字符数组
- C语言用字符数组存储字符串
- 字符数组中用空字符表示字符串的结束
- 类似数值,字符串可以被理解成一种值,字符数组被理解成存储这种值的变量,字符串可以被输入、输出和复制、拼接等等操作,有一系列标准库函数支持这些操作。
程序中怎么存储字符串呢?比如一个单词,一个句子。显然,字符串是由多个字符组成的,字符类型变量存储一个字符,那么可以用元素类型为字符的数组来存储字符串。
比如,声明字符数组
char str[10];
如果存储字符串banana,可以将各个字符存入str的各个元素中
char str[10];
str[0] = 'b';
str[1] = 'a';
str[2] = 'n';
str[3] = 'a';
str[4] = 'n';
str[5] = 'a';
// 后面可以看到,str[6]元素上应该设置一个特殊的字符
这样就解决了字符串的存储问题。那么这里还有一个字符串长度的问题,同一个字符数组中理应可以存储不同长度的字符串。有时,str可能存储了banana,但是为什么不能理解成存储了ban?或者有时str又可以存储pear,那接下来处理这个字符串的代码,它们怎样知道str字符数组里面到底存储了多长的字符串呢?有的人说程序编写者既然将banana放入了字符数组,那他也就知道字符数组中存储了6个字符。如果存储了pear,那他也就知道字符数组中存储了4个字符,后续代码可以做相应的调整就可以了,比如输出字符串内容的代码,可以写成,
char str[10];
str[0] = 'b';
str[1] = 'a';
str[2] = 'n';
str[3] = 'a';
str[4] = 'n';
str[5] = 'a';
printf("%c", str[0]);
printf("%c", str[1]);
printf("%c", str[2]);
printf("%c", str[3]);
printf("%c", str[4]);
printf("%c", str[5]);
如果数组里面存储了pear,print就输出前4个数组元素,
char str[10];
str[0] = 'p';
str[1] = 'e';
str[2] = 'a';
str[3] = 'r';
printf("%c", str[0]);
printf("%c", str[1]);
printf("%c", str[2]);
printf("%c", str[3]);
可以想象,这并不是应有的解决方案,程序不应该因为要处理的字符串长度不同就修改代码,而且万一字符串的内容是输入到程序里面的,编写程序的时候根本不知道字符串的长度,那printf又输出几个元素呢?处理字符数组的程序应该像处理数值类型的程序那样尽量用相同的程序处理不同长度的字符串。
一种解决方案,用一个整数变量来单独记录字符串的长度,比如
char str[10];
int str_len;
int i;
str[0] = 'b';
str[1] = 'a';
str[2] = 'n';
str[3] = 'a';
str[4] = 'n';
str[5] = 'a';
str_len = 6;
// 后续处理代码
for(i = 0; i < str_len; i++){
printf("%c", str[i]);
}
如果存储的是字符串pear,
char str[10];
int str_len;
int i;
str[0] = 'p';
str[1] = 'e';
str[2] = 'a';
str[3] = 'r';
str_len = 4;
// 后续处理代码
for(i = 0; i < str_len; i++){
printf("%c", str[i]);
}
可以看到,无论字符数组中存储的字符串长度如何变化,后续代码都不用修改。但是,目前并没有采用这种方案,因为这种方案在使用字符数组时还需要附带一个整数变量,不方便。
还有一种更好的方案,用字符数组本身就可以确定其存储的字符串是什么。这种解决方案就是在字符数组中放置字符串之后设置一个特殊字符来表示字符串内容结束。这个特殊字符就是ASCII字符集中的第一个字符,八个二进制位都是0的字符,即码值为0的字符,这个字符被称为空字符,书写用’\0’表示。这样的话,后续的代码可以根据数组元素是否是空字符,来判断是否已经处理完数组中的字符串。比如,
char str[10];
int i;
str[0] = 'b';
str[1] = 'a';
str[2] = 'n';
str[3] = 'a';
str[4] = 'n';
str[5] = 'a';
str[6] = '\0';
// 后续处理代码
for(i = 0; str[i] != '\0'; i++){
printf("%c", str[i]);
}
如果存储的是字符串pear,
char str[10];
int i;
str[0] = 'p';
str[1] = 'e';
str[2] = 'a';
str[3] = 'r';
str[4] = '\0';
// 后续处理代码
for(i = 0; i < str[i] != '\0'; i++){
printf("%c", str[i]);
}
实际上,printf就封装了输出字符串的功能,而且在其内部就是这样判断字符数组存储的字符串的内容是否已经结束。
char str[10];
str[0] = 'b';
str[1] = 'a';
str[2] = 'n';
str[3] = 'a';
str[4] = 'n';
str[5] = 'a';
str[6] = '\0';
// 后续处理代码
printf("%s", str);
注意,printf输出字符串的转换说明符是%s。
为了进一步显示空字符的作用,可以做个测试。把数组元素str[3]修改为空字符’\0’,再使用printf输出。
char str[10];
str[0] = 'b';
str[1] = 'a';
str[2] = 'n';
str[3] = '\0'; // 设置为空字符,后面元素不变
str[4] = 'n';
str[5] = 'a';
str[6] = '\0';
// 后续处理代码
printf("%s", str);
可见,printf函数确实以空字符作为字符数组存储的字符串内容结束标志。
上面的代码为了让字符数组存储banana,我们将它的各个字符依次存储到字符数组中,这样比较不方便,C语言标准库定义了很多函数来对字符数组进行整体处理,这些函数也都借助于空字符来实现各自的功能的。常见的字符串处理函数有:
- 输入函数scanf
- 输出函数printf
- 获取字符串长度函数strlen
- 赋值函数strcpy
- 追加函数strcat
比如,使用赋值函数strcpy,就不用将字符串中的字符一个个赋值给字符数组的元素了
char str[10];
strcpy(str, "banana");
printf("%s", str);
注意,不能用str = "banana";
这样的形式将字符串赋值给字符数组。因为在数组不支持整体操作。strcpy函数内部还是一个个字符赋值的。
再用一个例子演示一下各个函数的使用。
char name[10], sentence[20];
printf("please input a name:");
scanf("%s", name);
printf("you input name: %s\n", name);
strcpy(sentence, "hello, ");
printf("sentence: %s\n", sentence);
printf("sentence's length is: %d\n", strlen(sentence));
strcat(sentence, name);
printf("sentence: %s\n", sentence);
printf("sentence's length is: %d\n", strlen(sentence));
可见,用这些函数处理字符数组,我们就不用一个元素一个元素的处理字符数组,而是把字符数组看成存储“值”的“变量”,这个“值”就是某个字符串。
当然,这些函数的实现都有赖于空字符。概括来说,如果一个字符串处理函数需要读取字符数组中的字符串,那它会读取数组中的各元素,直到元素值为空字符的那个位置,在这之前的所有字符就是这个字符数组表示的字符串。比如输出函数printf,获取字符串长度函数strlen函数等。
char str[10];
str[0] = 'b';
str[1] = 'a';
str[2] = 'n';
str[3] = 'a';
str[4] = 'n';
str[5] = 'a';
str[6] = '\0';
printf("%s | %d\n", str, strlen(str));
str[3] = '\0';
printf("%s | %d", str, strlen(str));
反过来说,如果没有空字符作为字符串的结束标志,那么这些函数就会给出错误的结果。
char str[10];
str[0] = 'b';
str[1] = 'a';
str[2] = 'n';
str[3] = 'a';
str[4] = 'n';
str[5] = 'a';
/*
str[6] = '\0'; // 取消设置空字符
*/
printf("%s\n", str);
printf("%d", strlen(str));
出现这样的结果是因为printf函数会从数组首元素开始一直输出下去,直到遇到内存中的空字符为止。可能在各位的电脑上的运行结果与这个结果有所不同,这是因为这个空字符并不是我们设置的,它的出现有一定的随机性。
如果一个函数修改字符数组,那么它会在各数组元素中设置好字符串的内容后在字符串之后的元素上填上空字符,比如字符串输入函数scanf,复制函数strcpy,追加函数strcat等
char name[10];
int i;
显示刚初始化的数组
printf("name array created\n");
printf("printf consider name is:%s\n", name);
printf("name array is:\n");
// 以字符形式输出字符数组各元素
for(i = 0; i < 10; i++){
printf("%-5c" , name[i]);
}
printf("\n");
// 以码值形式输出字符数组各元素
for(i = 0; i < 10; i++){
printf("%-5d" , name[i]);
}
printf("\n");
显示输入了banana字符串的数组
printf("\nplease input a name:");
scanf("%s", name);
printf("printf consider name is:%s\n", name);
printf("name array is:\n");
// 以字符形式输出字符数组各元素
for(i = 0; i < 10; i++){
printf("%-5c" , name[i]);
}
printf("\n");
// 以码值形式输出字符数组各元素
for(i = 0; i < 10; i++){
printf("%-5d" , name[i]);
}
printf("\n");
显示复制了pear字符串的数组
strcpy(name, "pear");
printf("\npear copyed to name\n");
printf("printf consider name is:%s\n", name);
printf("name array is:\n");
// 以字符形式输出字符数组各元素
for(i = 0; i < 10; i++){
printf("%-5c" , name[i]);
}
printf("\n");
// 以码值形式输出字符数组各元素
for(i = 0; i < 10; i++){
printf("%-5d" , name[i]);
}
printf("\n");
这类修改字符数组的函数最可能出现的问题是数组的大小与字符串一样长,而使原数组中没有空间存储空字符了,这种情况下这些函数不是不设置空字符,而是将空字符写到字符数组最后一个元素之后,即访问了数组之外的内存了。比如,运行以下代码,并输入字符串0123456789
char name[10];
printf("please input a name:");
scanf("%s", name);
printf("you input name: %s\n", name);
printf("\n");
这个程序尽管能够运行,但是在程序运行结束之后,VS2010会弹出窗口,提示我们操作了数组范围之外的内存,因为scanf函数将空字符写到了name[10]这个不属于name数组的元素位置上了。
9.2.1 空字符
空字符用来标志字符数组中存储的字符串的结束。空字符是码值为0的字符,其八个比特上都是0的字符,也就是ASCII码表的首个字符。
空字符用转义字符来表示就是’\0’,用码值直接表示就是0,注意区别空字符和数字字符’0’的区别。数字字符’0’的码值是48。
注意区别空字符和空白字符,空白字符是对包括空格、换行、制表符在内的字符的统称,它们用来隔开输入、输出内容。
9.2.2 字符数组的声明
假设需要用一个字符串变量(字符数组)来存储最多含有80个字符的字符串。由于字符串在字符数组末尾处需要有空字符,我们需要声明含有81个字符的字符数组(当然更大的字符数组也可以)。
char str[81];
或者,利用宏来定义字符串最大长度常量STR_LEN
#define STR_LEN 80
char str[STR_LEN + 1];
注意,这样声明的字符数组在之后的使用中,它能存放长度不超过80的任何字符串。如果字符串长度超过80就导致无法在数组中没有空间来存储空字符,或者是空字符被存储到数组范围以外,程序运行过程中将出现不可以预知的结果。
9.2.3 字符数组的初始化
-
可以在声明字符数组的同时提供字符串的初值。
char laborDay[6] = "May 1"; // 或者 char laborDay[ ] = "May 1";
-
以上形式只能在声明字符数组的时候用,之后需要用strcpy函数来给字符串变量赋值。
char laborDay[6]; /* laborDay = "May 1"; // 语法错误,不支持 */ strcpy(laborDay, "May 1"); // 赋值的正确方式
-
注意初始化时字符数组中要给空字符留位置,否则字符数组不是标准的字符串变量,像标准库中的字符串处理函数就不能正确的处理它了。
/* char laborDay[5] = "May 1"; // 错误的长度 */
字符串变量可以在声明的时候初始化:
char laborDay[6] = "May 1";
编译器会把字符串“May 1”中的各个字符依次初始化到字符数组laborDay的各个元素中,并且会添加一个空字符,使字符数组可以当成字符串变量来使用。
与声明普通数组类似,除了在声明字符串变量的时候可以像这样初始化,当字符数组已经声明之后就不能这样写了,需要使用strcpy函数来给字符串变量赋值。
char laborDay[6];
/*
laborDay = "May 1"; // 语法错误,不支持
*/
strcpy(laborDay, "May 1"); // 赋值的正确方式
实际上,char laborDay[6] = "May 1";
初始化形式可以看成初始化字符串变量的简便形式,按照一维数组初始化的形式,字符串变量初始化可以写成:
char laborDay [6] = {'M', "a", 'y', ' ', '1', '\0'};
显然,这样的写法比较麻烦。
如果初始化字符数量少,再加上一个空字符也不能填满字符数组,那么编译器会添加空字符,比如
char laborDay[8] = "May 1";
这与一般数组初始化的行为是一致的,一般数组初始化时会自动补充0,而空字符就是0。
如果提供的初始化字符的个数等于或者大于字符数组的长度会怎么样?编译器只会将等于字符数组长度的初始化字符赋给字符数组的各元素,剩余初始化字符会丢弃。
/*
char laborDay[5] = "May 1"; // 错误的长度
*/
这造成的问题是由于没有给空字符留空间,编译器不会存储空字符。也就是说此时的字符数组不是符合C语言规范的字符串变量。后续不能将字符数组当成字符串使用。
如果有初始化式子,可以省略字符数组的长度,
char laborDay[] = "May 1"; // 等价于char laborDay[6] = "May 1";
编译器会为字符数组分配字符长度+1的空间,并添加空字符。
这为写程序提供了方便,可以不用数字符串的长度了。
9.2.4 字符串的读和写
字符串输出函数
- printf可以用来输出字符串,转换说明用%s。
- puts可以用来输出字符串,输出完会紧跟着输出一个换行。
字符串输入函数
- scanf可以用来输入字符串,转换说明用%s,用来读入“单词”。
- gets可以用来输入字符串,用来读入一行内容。
字符串输出函数
用printf函数和puts函数写字符串
printf函数可以用来输出基本类型的值,对于输出字符串也是支持的,在格式说明字符串中用转换说明符%s来表明输出的是字符串。
char str[10];
str[0] = ‘s’;
str[1] = ‘u’;
str[2] = ‘n’;
str[3] = ‘\0’;
printf(“%s”, str);
输出将是
printf函数会逐个输出字符串中的字符,直到遇到空字符才停止。(如果字符串没有用空字符结束,printf函数会一直输出下去,直到遇到内存中的某个空字符为止。)
除了printf函数可以输出字符串,puts函数也可以输出字符串。比如上面的字符串,将用printf输出换成用puts输出
puts(str);
输出将是
与printf函数不同的是,puts函数在输出完字符串后还会附带输出一个换行符,这样后续的输出就从下一行开始。所以它相当于
printf(“%s\n”, str);
与printf函数另一个不同的是,puts函数只有一个参数,即字符数组名或者字符串字面量,它不需要格式说明字符串,因为它只是专门用来输出字符串的函数。
字符串输入函数
可以用scanf函数或者gets函数来将用户输入的字符串读入到字符数组中。比如,在读入的字符串长度不会超过100的情况下,声明字符数组变量:
#define STR_LEN 100
…
char str[STR_LEN + 1];
之后,可以调用scanf函数来读取输入的字符串存储到str中,
scanf(“%s”, str);
str前面不需要加取地址运算符&,因为str是数组名,本身就表示数组首元素的地址。
调用时,scanf函数会跳过空白字符,然后读入字符并存储到str中,直到再遇到空白字符为止。最后scanf函数会在字符串末尾存储一个空字符。
scanf输入字符串时要注意输入的字符串的长度应该比数组的大小小一,否则输入的字符串就会不够放到字符数组中,会造成scanf函数对数组访问的溢出,可能会造成程序的崩溃。为避免这样的情况发生,可以像下面这样写代码
char string[8];
scanf("%7s", string);
在%和s之间的数字表示最多允许读入的字符的数量,这个数字应该比数组的大小小一。这样,即使用户输入较长的字符串,也最多只有7个字符被scanf读入并放到字符数组string里面。
scanf函数输入的字符串不包括空格、制表符等空白字符。因此,如果一行中有空格等空白字符,scanf函数就不能读取整行的字符。为了一次性读取一整行输入,可以使用gets函数。类似scanf函数,gets函数把读入的字符放入字符数组中,然后存储一个空字符。但是gets函数有些不同于scanf函数,
① gets函数不会跳过起始的空白字符。
② gets函数会持续读入字符,并把字符写到字符数组中,直到遇到换行符。当gets函数读到换行符,它会把一个空字符追加到字符串末尾。换行符本身不会写到字符串里面去。
可以简单的这样理解,scanf读入字符串,主要用来读入单词,而gets函数主要用来读入一行内容,即使一行中有空格、制表符等等字符。
下面的例子显示了scanf和gets函数的差异:
#define STR_LEN 10
…
char str[STR_LEN + 1];
printf(“请输入一句话:\n”);
scanf(“%s”, str);
printf(“sentence is:%s”, str);
假如用户运行程序并输入了以下内容
请输入一句话:enjoyc
这里,用表示输入的空格,在输入最后一个空格之后,用户按下回车,scanf函数开始对输入缓冲区的内容进行扫描。scanf函数会跳过第一个空格,把之后的“enjoy”读入到str中,当遇到“enjoy”之后的空格,scanf停止读入并追加一个空字符。下一个读入函数将从输入缓冲区的“enjoy”后面的空格处继续读入剩余内容。如图7-15所示:
假如用gets函数替换scanf函数:
gets(str);
当用户输入同样的内容后,gets函数会把包括空格在内的整行字符串读入到字符数组中,并添加一个空字符。
其中,空白的字符表示空格字符。
9.2.5 字符串的处理函数
C语言的库函数中有一系列处理字符串的函数,包括复制、拼接、比较字符串等函数。借助于这些函数,可以把字符数组当成存储字符串的变量来使用,而不需要自己编写程序来处理字符串中的字符。使用输入输出字符串的函数应包含头文件<stdio.h>,使用其它字符串函数则应包含头文件<string.h>,
#include <stdio.h>
#include <string.h>
常见字符串出来函数(还有其它的)
- strcpy复制字符串
- strlen获取字符串长度
- strcat拼接字符串
- strcmp字符串比较
- 数字字符串和数值的转换函数(字符串转整数:atoi,字符串转浮点数:atof,整数转字符串:itoa,浮点数转字符串:ftoa)
操作函数1.strcpy字符串复制函数
在str已经声明的情况下,为了把字符串复制到它里面,之前使用了单个字符依次赋值的方式
str[0] = ‘s’;
str[1] = ‘u’;
str[2] = ‘n’
str[3] = ‘\0’;
显然,这样处理非常麻烦,并且很容易忘记在最后添加空字符。strcpy函数用来把字符串的内容复制到字符数组中,并且它还会放置一个空字符在目标字符串之后作为字符串结束的标志。
strcpy(目标字符数组s1, 来源字符串s2);
strcpy把来源字符串s2复制到目标字符数组s1中,s2可以是字符数组,也可以是字符串字面量。这个过程不会修改s2中的字符串的内容。
#include <string.h>
…
strcpy(str, “sun”); // 将字符串“sun”复制到字符数组str中
strcpy(str2, str); // 将字符数组str中存储的字符串复制到字符数组str2中
复制字符串的时候注意以下几个问题:
①.目标字符数组的长度要足够放置来源字符串和空字符,否则会造成访问数组越界的问题。比如
char str[3];
strcpy(str, "sun");
strcpy会把字符串“sun”中的各个字符复制到str中,并且在最后还会添加一个空字符,在此过程中,strcpy并不会检查是否已经越过目标数组的边界。显然空字符被添加到了数组以外,这可能造成程序的异常。
strncpy函数提供了一个参数控制复制到目标字符数组的字符的个数,使用这个函数可以避免来源的字符串的长度过长,造成目标字符数组的越界。
char str[3];
char src[] = "sun";
strncpy(str, src, 2); // 最多复制"sun"字符串中的前2个字符到str中
②.尽管称字符数组是字符串变量,但是给字符数组赋值不能写成下面的形式
char str[10];
str = "sun";
上述形式只在初始化字符数组str的时候有效,在str已经声明后就不能这样进行赋值了。
操作函数2.strlen找出字符串长度
strlen函数计算存储在字符数组中的字符串长度。
strlen(字符数组(以空字符结束)或者字符串字面量);
例如,下面程序计算若干字符串的长度
int len;
char str[10] ;
len = strlen(“abc”); // len是3
len = strlen(“”); // len是0
strcpy(str, “abc”);
len = strlen(str); // len是3
strlen会把字符数组中第一个空字符之前的内容当作字符串的内容,找出其长度,这个长度值不包括结束的空字符。
注意字符数组的长度和字符串长度的区别。字符数组的长度是在声明数组时指定的长度,一旦确定无法修改。而字符数组中存储的字符串的长度是可以变化的,随着字符数组存储的字符串以及空字符的位置的不同而不同。因为需要用一个数组元素存储空字符,存储在字符数组中的字符串的长度应小于或者等于字符数组长度减1。
操作函数3.strcat字符串拼接函数
strcpy函数把字符串复制到字符数组中,strcat函数把字符串拼接到字符数组原有字符串的后面,形成一个更长的字符串。调用strcat的形式为
strcat(目标字符数组名,来源字符串);
下面是一些例子
strcpy(str, “abc”); // str是“abc”
strcat(str, “def”); // str是“abcdef”
strcpy(str2, “123”); // str2是“123”
strcat(str, str2); // str是“abcdef123”
拼接字符串的时候注意以下几个问题:
①.因为strcat函数会把来源字符串复制到目标字符串的末尾,并且再追加一个空字符作为结束标志。故目标字符数组的长度要大于两个字符串长度之和再加1。
可以使用strncat函数来控制向目标字符数组最多追加的字符个数,避免目标字符数组溢出。
char str[10] = "Hello";
char src[] = ",World";
strncat(str, src, sizeof(str) - 1 - strlen(str));
②.在有些语言中可以用+号来完成字符串的拼接
str1 + str2;
在C语言中是不支持这样写的,必须调用strcat函数来完成拼接。
4.strcmp字符串比较函数
数值是有大小的,字符串也有大小的概念,字符串的大小是按照字符串的字典顺序来比较的。具体来说,依次比较两个字符串对应位置的字符的ASCII码值来比较它们的大小,第一个出现码值较小的字符的字符串就是小的那个。如果一个字符串长度短于另一个,那么规定长度短的字符串小于长度长的字符串,这个规定也可以这样理解,长度短的字符串的结束位置是空字符,即0,它肯定小于长度长的字符串对应位置的任意非空字符,因此长度短的字符串小于长度长的字符串。如果两个字符串内容完全相同,那么就说这两个字符串相等。比如,
“abc”小于”abd”。
因为它们前两个字符相同,而第三个字符‘c’小于‘d’。
“abc”小于”abcdef”。
因为“abc”与“abcdef”前三个字符完全一样,而“abc”字符串只包含三个字符。如果一定要从字符大小比较的话,“abc”字符串的后面还有空字符,而空字符是小于字符‘d’的。
“abc”大于“aa”。
因为它们的第一个字符相同,而第二个字符‘b’大于‘a’。
如果要在程序中判断两个字符串的大小关系,可以调用strcmp函数:
strcmp(字符串1, 字符串2);
strcmp函数会根据字符串1是小于、等于或大于字符串2,返回一个小于、等于或大于0的整数值。
比如,strcmp(“abc”, “abd”)会返回负数
strcmp(“abc”, “abcdef”)会返回负数
或者比较字符数组中存储的字符串的大小
char str1[] = “aa”;
char str2[] = “abc”;
strcmp(str1,str2)会返回正数
为了检查str1是否小于str2,可以写
if (strcmp(str1, str2) < 0)
当比较两个字符串中的字符时,注意ASCII字符集的一些特性:
- A~Z、a~z、0~9这几组字符的数值码都是连续的。
- 所有的大写字母都小于小写字母。(在ASCII码中,65~95的编码表示大写字母,97~122的编码表示小写字母。)
- 数字小于字母。(48~57的编码表示数字。)
- 空格符小于所有打印字符。(空格符的码值是32。)
注意,判断两个字符串的大小关系,不是用==,大小关系不是用关系运算符
char str[10], str2[10];
...
if (str < str2) // 错误的用法
...
if (str == str2) // 错误的用法
9.2.6 访问字符串中的字符
由于字符串是存储在字符数组中,而且以空字符为结束,所以常用for循环和判断当前字符是否为空字符的形式来遍历整个字符串中的字符。
for(i = 0; a[i] != '\0'; i++){
..
}
比如,统计字符串中有几个空格,代码可以像这样写
int i, count;
for(i = 0, count = 0; a[i] != '\0'; i++){
if (a[i] == ' '){
count++;
}
}
9.2.7 字符串字面量
- 单引号括起来的单个字符是字符字面量,双引号括起来的是字符串字面量,它们属于不同的类型,不能混用。
- 字符串字面量以字符数组的形式存储在内存中,而且字符数组的最后放置一个空字符表示字符串字面量的结束。
- 存储字符串字面量的地方是程序的数据区,不能被修改。
字符串字面量,是用双引号括起来的不可改变的字符序列。
"Hello, World\n"
字符串字面量不同于字符字面量,字符字面量用单引号括起来,表示一个字符,字符字面量是用字符的整数码值来表示的。而字符串字面量是用存储字符串的内存的首地址来表示的。在C语言中它们是不同的类型,不能混淆。在需要字符串的地方不要用字符,反之亦然。
printf("a"); // 正确,printf的第一个参数必须是字符串,即使它里面只包含一个字符
/*
printf('a'); // 错误
*/
putchar('a'); // 正确,putchar用来输出一个字符常量或者变量
/*
putchar("a"); // 错误,"a"的类型与'a'不同,不能替代
*/
字符串字面量里面可以包含用转义字符形式书写的字符。比如,
printf("\aHello,World\nToday is a good day\nWhere do you want to play?\n\a");
还可以用八进制数和十六进制数的转义序列书写字符串字面量里面的字符,虽然它们并不常用。
如果字符串字面量比较长,那么可以将它们写在多对双引号里面,这样它们就可以写在不同的行了,不会占过宽的屏幕宽度了。
printf("\aHello,World\nToday is a good day\n"
"Where do you want to play?\n\a");
编译器会将以空白字符分开的字符串常量合并成一个长字符串。
注意,源码文件中不能直接将长字符串写到多行,那样编译器会报错。
/* // 错误的用法
printf("\aHello,World\nToday is a good day\n
Where do you want to play?\n\a");
*/
字符串字面量是以字符数组的形式存储的,比如,字符串字面量"abc"是作为有4个字符的数组来存储的
字符串字面量可以为空,即“”,但是内存中还是会存储一个空字符。
用sizeof运算符可以得到字符串字面量占的字节数。sizeof(“abc”)等于4,sizeof("")等于1。显然,sizeof计算的字节数包含了空字符。
用字符串字面量[下标]的方式可以获取到字符串中的单个字符。“abc”[0]得到字符’a’,“abc”[1]得到字符’b’,“abc”[2]得到字符’c’。
不能修改字符串字面量的内容,比如
/*
"abc"[0] = 'A'; // 错误的语法
*/
字符串字面量保存在不能修改的内存区域中。如果要修改字符串的内容,需要字符串变量,即字符数组。
字符串字面量是以存储它的字符数组的首元素地址来表示的。所以"abc"[0]可以访问字符’a’,“abc”[1]可以访问字符’b’,“abc”[2]可以访问字符’c’,这就像通过数组名来访问数组元素一样(数组名是数组首元素的地址)。
显然,可以将字符串字面量保存到字符数组中,比如,下面的代码将字符串字面量"hello!"保存到字符数组str中,
char str[10];
strcpy(str, "hello!");
但是实际上,str数组复制了一份"hello!“的内容,如果在程序中需要在多处访问字符串字面量,比如"hello!”,而且并不需要修改它里面的内容,那么可以用字符指针来保存它(指向它的首地址),这样可以节约内存,
char *p = "hello!";
在需要这个字符串字面量的地方,使用指针变量p就可以了,
printf("%s", p);
有关指针变量的知识,请参考指针和字符串
9.2.8 字符数组的常见问题
9.3 二维数组
9.3.1 二维数组的声明和使用
一个班级中要存储一门考试的成绩,可以使用一维数组,如果要存储多门课程的成绩,那就需要声明多个同样长度的一维数组,这时可以考虑使用二维数组,即以一维数组为元素的数组。比如,声明含有3个一维数组,而每个一维数组都含有5个整型元素的二维数组m:
int m[3][5];
如果将一维数组看成数学中的向量,二维数组就可以看成是矩阵。上述声明也可以看成创建了一个3行5列的矩阵。
声明二维数组通用格式:
元素类型 数组名[一维数组个数(矩阵行数)][一维数组长度(矩阵列数)]
一维数组元素用从0开始的下标来访问,二维数组元素用它所在的行和列的下标来访问,行和列下标都从0开始,为了访问第i行第j列的元素,需要写成m[i][j]。这里,如果把二维数组理解为一维数组的数组,也可以把二维数组元素的引用方式m[i][j]理解为m[i]指明了二维数组的第i个一维数组(从0开始),m[i][j]则指明为该一维数组中的第j个元素(从0开始),
内存中是没有二维的概念的,二维数组是按照行主序的方式存储的,即先存储第0行的全部元素,接着第1行的,以此类推:
一维数组可以用一重循环来处理,二维数组可以用两重循环来处理,例如:
①.按行输入值到二维数组中
#define M 2
#define N 3
…
double matrix[M][N];
int row, col;
for(row = 0; row < M; row++){
for(col = 0; col < N; col++){
scanf("%d", &matrix[row][col]);
}
}
②.把方阵赋值为单位阵
#define SIZE 5
…
double matrix[SIZE][SIZE];
int row, col;
for(row = 0; row < SIZE; row++){
for(col = 0; col < SIZE; col++){
if (row == col){
matrix[row][col] = 1;
}else{
matrix[row][col] = 0;
}
}
}
注意,在引用二维数组元素时,不要把m[i][j]写成了m[i,j]。如果这样写,C语言会把逗号看成是逗号运算符,所以m[i,j]就等于m[j]。
9.3.2 二维数组的初始化
与普通变量和一维数组一样,没有初始化的二维数组元素的值是未知的。可以在声明二维数组时为数组元素指定初值。二维数组的初始化可以通过嵌套一维数组初始化式的方式来书写:
int m[3][5] = {{1, 2, 3, 4, 5},
{2, 3, 4 ,5, 6},
{3, 4, 5, 6, 7}};
初始化结果:
如果初始化式子没有提供足够多的行,那么那些行的元素自动初始化为0:
int m[3][5] = {{1, 2, 3, 4, 5},
{2, 3, 4, 5, 6}};
上面的二维数组只提供了两行初始化式子,没有提供初始化式子的最后一行,因此最后一行各元素被初始化为0,如图所示:
如果内层的初始化式子没有足够的初值来初始化数组的一整行,那么此行剩余元素初始化为0:
int m[3][5] = {{1, 2, 3},
{2, 3, 4, 5}};
初始化结果如图
初始化列表可以省略掉内层的花括号:
int m[3][5] = {1,2,3,4,5,
2,3,4,5,6,
3,4,5,6,7};
这种初始化的形式更像是一维数组初始化(这与二维数组本身是按行存储在内存中是呼应的),但是不建议这样初始化,各行初始化列表最好还是用花括号括起来。
不论是否省略掉内层的花括号,只要初始化列表列出了全部行,或者可以推算出行数,那么可以省略行数,例如
int m[][5] = {{1, 2, 3, 4, 5},
{2, 3, 4 ,5, 6},
{3, 4, 5, 6, 7}};
或者,
int m[][5] = {1, 2, 3, 4, 5,
2, 3, 4 ,5, 6,
3, 4, 5, 6, 7};
它们都等价于:
int m[3][5] = {{1, 2, 3, 4, 5},
{2, 3, 4 ,5, 6},
{3, 4, 5, 6, 7}};
9.3.3 二维数组的应用
例1. 计算课程和学生的平均成绩
班级中有多位学生,每位学生都有数学、英语、语文考试成绩,计算每门课的平均成绩以及每位同学的平均成绩。
声明行数为课程数,列数为学生数的二维数组,各行存储一门课程的全部学生的成绩,计算某门课程的平均成绩就是计算一行的平均值,计算某个学生的平均成绩就是计算一列的平均值。为了简化程序,假设有3门课程,班级人数为5人,成绩已经在程序中初始化了。
/*average.c*/
#include <stdio.h>
#define LESSON_NUMBER 3
#define STUDENT_NUMBER 5
int main(void)
{
int score[LESSON_NUMBER][STUDENT_NUMBER] =
{{80, 80, 80, 80, 80},
{70, 70, 70, 70, 70},
{90, 90, 90, 90, 90}};
int i, j, sum;
for(i = 0; i < LESSON_NUMBER; i++){
for(j = 0, sum = 0; j < STUDENT_NUMBER; j++){
sum += score[i][j];
}
printf("第%d门课的平均成绩是%d\n", i, sum / STUDENT_NUMBER);
}
printf("\n");
for(j = 0; j < STUDENT_NUMBER; j++){
for(i = 0, sum = 0; i < LESSON_NUMBER; i++){
sum += score[i][j];
}
printf("第%d个学生的平均成绩是%d\n", j, sum / LESSON_NUMBER);
}
getch();
return 0;
}
例2. 方阵转置
输入数据到方阵33中,然后转置此方阵。方阵是具有相同行数和列数的矩阵,转置方阵是把方阵的元素以从左上角到右下角的对角线为对称轴进行交换。比如,把第2行第3列的元素和第3行第2列的元素进行交换。程序运行示例如下:
请输入33方阵的第0行:1 3 5
请输入33方阵的第1行:2 4 6
请输入33方阵的第2行:3 5 7
转置后方阵是:
1 2 3
3 4 5
5 6 7
/*transpose.c*/
#include <stdio.h>
#define SIZE 3
int main(void)
{
double matrix[SIZE][SIZE];
int row, col;
for(row = 0; row < SIZE; row++){
printf("请输入%d*%d方阵的第%d行:", SIZE, SIZE, row);
for(col = 0; col < SIZE; col++){
scanf("%lf", &matrix[row][col]);
}
}
for(row = 0; row < SIZE; row++){
for(col = 0; col < row; col++){ // 语句1
// 交换matrix[col][row]和matrix[row][col]的值
temp = matrix[col][row];
matrix[col][row] = matrix[row][col];
matrix[row][col] = temp;
}
}
printf(“转置后方阵是:\n”);
for(row = 0; row < SIZE; row++){
for(col = 0; col < SIZE; col++){
printf("%.2f ", matrix[row][col]);
}
printf("\n");
}
getchar();
return 0;
}
注意,语句1中的循环条件是col<row,而不是col<SIZE,否则对需要交换的一对元素就做了两次交换,相当于没有交换。