学习目标
- 一维数组的声明和使用
- 二维数组的声明和使用
- 字符串的声明、赋值、比较和连接
- ASCII码和ctype.h中的字符函数
- 正确认识“++”、“+=”等能修改变量的运算符
- fgetc和getchar的使用方法
- 了解不同操作系统中换行符的表示
- 掌握fgets的使用方法并了解gets的“缓冲区溢出”漏洞
- 学会用常量表简化代码
一、数组
程序1: 输入5个数,逆序输出
#include<stdio.h> #define maxn 8 int a[maxn]; int main() { int x, n = 0; while(scanf("%d", &x) == 1){ a[n++] = x; } for(int j = n-1 ; j >= 0; j--) printf("%d ",a[j]); return 0; }
最后的^D 用来结束输入【即Ctrl +D】
# 在算法竞赛中,常常难以精确计算出需要的数组大小,数组一般会声明得稍 大一些。在空间够用的前提下,浪费一点不会有太大影响,所以即使是 输入5个数,保险起见完全可以定义数组大小为8
语句:a[n++]=x —— 做了两件事:首先赋值a[n]=x,然后执行n=n+1
# 对于变量n,n++和++n都会给n加1,但当它们用在一个表达式中时,行为有所差别:n++会使用加1前的值计算表达式【即n的原值】,而++n会使用加1后的值计算表达式【即n+1后的值】 ———— 谁在前面,就先用谁 【n++,n在前mian所以用n的原值,++n,++在前面,就将n先+1】
为什么要把a的定义放在main函 数的外面?
只有放在外面时,数组a的大小才可以开得很大;放在main函数内时,数组稍大就会异常退出
# 所以,比较大的数组应尽量声明在main函数外,否则程序可能无法运行
但数组不能进行赋值操作:
在程序3-1中,如果声明的是“int a[maxn],b[maxn]”,是不能赋值b=a的
如果要从数组a复制k个元素到数组b,可以这样做 —— memcpy(b,a,sizeof(int)*k)
当然,如果数组a和b 都是浮点型的,复制时要写成 —— memcpy(b,a,sizeof(double)*k)。另外需要注意的是, 使用memcpy函数要包含头文件string.h
如果需要把数组a全部复制到数组b中,可以写得简单 一些:memcpy(b,a,sizeof(a))
程序2:开灯问题
有n盏灯,编号为1~n。第1个人把所有灯打开,第2个人按下所有编号为2 的倍数的开关(这些灯将被关掉),第3个人按下所有编号为3的倍数的开关(其中关掉的灯 将被打开,开着的灯将被关闭),依此类推。一共有k个人,问最后有哪些灯开着?输 入n和k,输出开着的灯的编号。k≤n≤1000
样例输入:
7 3
样例输出:
1 5 6 7
我的思路: n个灯 k个人,如果 灯的编号 % 人的编号 == 0,就将灯的状态取反 ————> 可以利用 数组 + 双层for循环 + 状态值 实现
#include<stdio.h> #include<string.h> #define maxn 1010 int a[maxn]; int main() { int n,k,first =1; memset(a,0,sizeof(a)); scanf("%d %d",&n,&k); for(int i = 1;i <= k;i++){ for(int j = 1;j <= n;j++){ if(j % i == 0) a[j] = !a[j]; } } for(int i = 1;i <= n;i++){ if(a[i]){ //如果第i个灯是亮的,即a[i] = 1 if(first) first = 0; else printf(" "); printf("%d",i); } } printf("\n"); return 0; }
技巧:
- memset(a,0,sizeof(a))—— 作用是把数组a清零,在string.h中定义
- 为了避免输出在最开始和最后输出多余空格,设置了一个标志变量first,可以表示当前要输出的变量是否为第一个。第一个变 量前不应有空格,但其他变量都有
采用样例数据测试时【即第1 5 6 7个灯是亮的】,可以在 程序 最后加入 printf("%d",a[1]); 输出结果是1,即
a[j] = !a[j];
取反后,由0变1,或由1变0
程序3:蛇形填数
在n×n方阵里填入1,2,…,n×n,要求填成蛇形。例如,n=4时方阵为:
上面的方阵中,多余的空格只是为了便于观察规律,不必严格输出。n≤8。
我的思路:。。。我不会 =_=...... =__= .....=___=....
用二维数组解决 —— int a[n][n] ——> 然后从a[0][n] --> a[n][n]初始化 ---> a[n][n]--->a[n][0] ---->a[0][0]--->a[0][n-1]。。。。for循环 【但含有多次不定长的转向,短时间内无法做出来】
书的思路:
也是二维数组,从1开始依次填写。设“笔”的坐标为(x,y),则一开始x=0,y=n-1,即第0行,第n-1列 (行列的范围是0~n-1,没有第n列)。“笔”的移动轨迹是:下,下,下,左,左,左, 上,上,上,右,右,下,下,左,上。总之,先是下,到不能填为止,然后是左,接着是 上,最后是右。“不能填”是指再走就出界(例如4→5),或者再走就要走到以前填过的格子 (例如12→13)。如果把所有格子初始化为0,就能很方便地加以判断。
#include<stdio.h> #include<string.h> #define maxn 20 int a[maxn][maxn]; int main() { int n, x, y, tot = 0; scanf("%d", &n); memset(a, 0, sizeof(a)); tot = a[x=0][y=n-1] = 1;//从1开始写 while(tot < n*n) { while(x+1<n && !a[x+1][y]) a[++x][y] = ++tot; //【1】 while(y-1>=0 && !a[x][y-1]) a[x][--y] = ++tot; //【2】 while(x-1>=0 && !a[x-1][y]) a[--x][y] = ++tot; //【3】 while(y+1<n && !a[x][y+1]) a[x][++y] = ++tot; //【4】 } for(x = 0; x < n; x++) { for(y = 0; y < n; y++) printf("%3d", a[x][y]); printf("\n"); } return 0; }
【1】:这一行中,保持y不变,x每次移动加一,从上往下“填”数字,同时笔在往下移动的时候要满足 x不能超过n,因为是 n* n的数字阵列,且为了循环填完外层数后逐步循环填内层数,要保证下一次将要“填数”的位置 是0,即还没有填入数字【填上数后 !a[x][y] = 0】
此时x = 0,y =2 在a[x][++y]打印16,即y = 3,此时开始循环判定,y + 1 <n true,但 a[x][y+1] 已被填入 “1”,无法继续往右填,开始往下写(下一轮循环)
如果去除所有的预判是否过界:【去掉 && !a[x][y] 】
其余过程同理
# 在很多情况下,最好是在做一件事之前检查是不是可以做,而不要做完再后 悔。因为“悔棋”往往比较麻烦
# 如果x+1<n为假,将不会计算“!a[x+1][y]”,也就不会越界 ———— &&是短路运算符 【这里提过】
为什么是++tot而不是tot++?
#include<stdio.h> int main(){ int i = 1; printf("%d\n",i); i++; printf("%d\n",i); ++i; printf("%d\n",i); return 0; }
回顾刚才提到的:
# 对于变量n,n++和++n都会给n加1,但当它们用在一个表达式中时,行为有所差别:n++会使用加1前的值计算表达式【即n的原值】,而++n会使用加1后的值计算表达式【即n+1后的值】 ———— 谁在前面,就先用谁 【n++,n在前mian所以用n的原值,++n,++在前面,就将n先+1】
二、字符数组
在C语言中,字符串其实就是字符数组——可以像处理普通数组一样,处理字符串,只需要注意输入输出和字符串函数的使用
程序4:竖式问题
找出所有形如abc*de(三位数乘以两位数)的算式,使得在完整的竖式中,所有数字都属于一个特定的数字集合。输入数字集合(相邻数字之间没有空格),输出所有竖式。每个竖式前应有编号,之后应有一个空行。最后输出解的总数。具体格式见样例输出(为了便于观察,竖式中的空格改用小数点显示,但所写程序中应该输出空格,而非小数点)。
样例输入: 2357 样例输出: <1> ..775 X..33 ----- .2325 2325. ----- 25575 The number of solutions = 1
我的思路:通过for循环遍历尝试所有的abc和de,判断是否满足题目要求,如果满足,打印出来
#include<stdio.h> #include<string.h> int main() { int count = 0; char s[20], buf[99]; scanf("%s", s); for(int abc = 111; abc <= 999; abc++) for(int de = 11; de <= 99; de++) { int x = abc*(de%10), y = abc*(de/10), z = abc*de; sprintf(buf, "%d%d%d%d%d", abc, de, x, y, z); int ok = 1; for(int i = 0; i < strlen(buf); i++) if(strchr(s, buf[i]) == NULL) ok = 0; if(ok) { printf("<%d>\n", ++count); printf("%5d\nX%4d\n-----\n%5d\n%4d\n-----\n%5d\n\n", abc, de, x, y, z); } } printf("The number of solutions = %d\n", count); return 0; }
解释:
【1】
printf("%5d\nX%4d\n-----\n%5d\n%4d\n-----\n%5d\n\n", abc, de, x, y, z);
因为最大不过999*99 = 98901,5位数字,所以最后结果z 用%5d 输出即可,满足当结果5位时,显示5位,结果4位时,前留空格,满足格式要求
【2】
int x = abc*(de%10), y = abc*(de/10), z = abc*de;
在样例输入中,分别是: abc=775,ad=33,x=775*3[de的个位数],y=775*3,z=755*33
【3】
char s[20], buf[99];
buf 是缓冲字符串,用来验证“在完整的竖式中,所有数字都属于一个特定的数字集合”
【4】
sprintf(buf, "%d%d%d%d%d", abc, de, x, y, z);
将完整的竖式所有数字 输出到 缓冲字符串,遍历每一项查询是否 在输入的数字集合中存在
char是“字符型”的意思,而字符是一种特殊的整数
每一个字符都有一个整数编码,称为ASCII码。
为了方便书写,C语言允许用直接的方法表示字符,例如,“a”代表的就是a的ASCII码。不过,有一些字符直接表示出来并不方便,例如,回车符是“\n”,而空字符是“\0”,它也是C语言中字符串的结束标志。其他例子包括“\\”(注意必须有两个反斜线)、“\'”(这个是单引号),甚至还有的字符有两种写法:“\"”和“"”都表示双引号。像这种以反斜线开头的字符称为转义序列
# C语言中的字符型用关键字char表示,它实际存储的是字符的ASCII码。字符常量可以用单引号法表示。在语法上可以把字符当作int型使用。
# scanf("%s", s)。和 scanf("%d", &n) 类似,它会读入一个不含空格、TAB和回车符的字符串,存入字符数组s。注意,不是“scanf("%s", &s)”,s前面没有“&”符号。
# 在“scanf("%s", s)”中,不要在s前面加上“&”符号。如果是字符串数组chars[maxn] [maxl],可以用“scanf("%s", s[i])”读取第i个字符串。注意,“scanf("%s", s)”遇到空白字符会停下来。
判断和输出:
先考虑输出,因为它比较简单。每个竖式需要打印7行,但不一定要用7条printf语句,1条足矣。首先计算第一行乘积x=abc*e,然后是第二行y=abc*d,最后是总乘积z=abc*de,然后一次性打印出来:
printf("%5d\nX%4d\n-----\n%5d\n%4d\n-----\n%5d\n\n", abc, de, x, y, z);
注意这里的%5d,它表示按照5位数打印,不足5位在前面补空格
sprintf和strchr:
strchr的作用是在一个字符串中查找单个字符,而这个sprintf似曾相识:之前用过printf和fprintf。printf输出到屏幕,fprintf输出到文件,而sprintf输出到字符串。多数情况下,屏幕总是可以输出的,文件一般也能写(除非磁盘满或者硬件损坏),但字符串就不一定了:应该保证写入的字符串有足够的空间
字符空间多大才足够?
答案是字符个数加1,因为C语言的字符串是以空字符“\0”结尾的。
空间够用的情况下,浪费一点没关系
函数strlen(s)
作用是获取字符串s的实际长度。什么叫实际长度呢?字符数组s的大小是20,但并不是所有空间都用上了。如果输入是“2357”,那么实际上s只保存了5个字符(不要忘记了还有一个结束标记“\0”),后面15个字符是不确定的(变量在赋值之前是不确定的)。strlen(s)返回的就是结束标记之前的字符个数。因此这个字符串中的各个字符依次是s[0], s[1],…, s[strlen(s)-1],而s[strlen(s)]正是结束标记“\0”
# 由于字符串的本质是数组,只能用strcpy(a, b),strcmp(a, b), strcat(a, b)来执行“赋值”、“比较”和“连接”操作,而不能用“=”、“==”、“<=”、“+”等运算符。上述函数都在string.h中声明
实验:
三、竞赛题目选讲
例题:3-1 TeX中的引号
在TeX中,左双引号是“``”,右双引号是“''”。输入一篇包含双引号的文章,你的任务是把它转换成TeX的格式。
样例输入:
"To be or not to be," quoth the Bard, "that
is the question".
样例输出:
``To be or not to be,'' quoth the Bard, ``that
is the question''.
本题有两个关键:
- 如何判断一个双引号是左双引号还是右双引号
- 输入字符串
输入字符串:
scanf("%s",xxx); 本题中无法使用 ----> 因为它碰到空格或者TAB 就会停下来
方法1: ———— fgetc(fin)
读取一个打开的文件fin,读取一个字符,然后返回一个int值。为什么返回的是int而不是char呢?因为如果文件结束,fgetc将返回一个特殊标记EOF,它并不是一个char。如果把fgetc(fin)的返回值强制转换为char,将无法把特殊的EOF和普通字符区分开。如果要从标准输入读取一个字符,可以用getchar,它等价于fgetc(stdin)。
# 一般情况下应当在检查它不是EOF后再将其转换成char值。从标准输入读取一个字符可以用getchar,它等价于fgetc(stdin)
陷阱!!! ———— 在使用fgetc和getchar时,应该避免写出和操作系统相关的程序
不同操作系统的回车换行符是不一致的。Windows是“\r”和“\n”两个字符,Linux是“\n”,而MacOS是“\r”。如果在Windows下读取Windows文件,fgetc和getchar会把“\r""吃掉”,只剩下“\n”;但如果要在Linux下读取同样一个文件,它们会忠实地先读取“\r”,然后才是“\n”。如果编程时不注意,所写程序可能会在某个操作系统上是完美的,但在另一个操作系统上就错得一塌糊涂。当然,比赛的组织方应该避免在Linux下使用Windows格式的文件,但正如前面所强调过的:选手也应该把自己的程序写得更鲁棒,即容错性更好。
方法2 ———— fgets(buf, maxn, fin)
读取完整的一行,其中buf的声明为char buf[maxn]。这个函数读取不超过maxn-1个字符,然后在末尾添上结束符“\0”,因此不会出现越界的情况。之所以说可以用这个函数读取完整的一行,是因为一旦读到回车符“\n”,读取工作将会停止,而这个“\n”也会是buf字符串中最后一个有效字符(再往后就是字符串结束符“\0”了)。只有在一种情况下,buf不会以“\n”结尾:读到文件结束符,并且文件的最后一个不是以“\n”结尾。尽管比赛的组织方应避免这样的情况(和输出文件一样,保证输入文件的每行均以回车符结尾),但正如刚才所说,选手应该把自己的程序写得更鲁棒
# fgets(buf, maxn, fin) 将读取完整的一行放在字符数组buf中。应当保证buf足够存放下文件的一行内容。除了在文件结束前没有遇到“\n”这种特殊情况外,buf总是以“\n”结尾。当一个字符都没有读到时,fgets返回NULL
# fgets也有一个"标准输入版"gets。遗憾的是,gets和它的"兄弟"fgets差别比较大:其用法是gets(s),没有指明读取的最大字符数。这里就出现了一个潜在的问题:gets将不停地往s中存储内容,而不管是否存储得下!
c语言并不禁止程序读写“非法内存”,例如char s[100],完全可以赋值 s[10000] = 'a'。甚至-Wall 也不警告 ,但,后果自负
因此gets 函数在c11 标准里已被正式删除
#include<stdio.h> int main() { int c, q = 1; while((c = getchar()) != EOF) { if(c == '"') { printf("%s", q ? "``" : "''"); q = !q; } else printf("%c", c); } return 0; }
本题细节:
1.可以边读边处理,不需要吧输入字符串完整地存下来 ———— 可以选择 getchar()
2.表达式"a?b:c"的含义是:当a为真时值为b,否则为c
3.用到了赋值语句"c = getchar()"的返回值,把它和EOF进行比较,让代码更简洁