本章例程
15.1打开和关闭文件
#include <stdlib.h> #include <stdio.h> int main(int ac, char **av) { int exit_status = EXIT_SUCCESS; FILE *input; while(*++av != NULL){ input = fopen(*av, "r"); if(input == NULL){ perror(*av); exit_status = EXIT_FAILURE; continue; } if(fclose(input) != 0){ perror("fclose"); exit(EXIT_FAILURE); } } return exit_status; }
15.2把字符转换为整数
#include <stdio.h> #include <ctype.h> int read_int(){ int value; int ch; value = 0; while((ch = getchar()) != EOF && isdigit(ch)){ value *= 10; value += ch - '0'; } ungetc(ch, stdin); return value; }
15.3从一个文件向另一个文件复制文本行
#include <stdio.h> #define MAX_LINE_LENGTH 1024 void copylines(FILE *input, FILE *output) { char buffer[MAX_LINE_LENGTH]; while(fgetc(buffer, MAX_LINE_LENGTH, input) != NULL) fputs(buffer, output); }
15.4用sscanf处理行定向输入
#include <stdio.h> #define BUFFER_SIZE 100 void function(FILE *input) { int a, b, c, d, e; char buffer[BUFFER_SIZE]; while(fgets(buffer, BUFFER_SIZE, input) != NULL){ if(sscanf(buffer,"%d %d %d %d %d", &a, &b, &c, &d, &e) != 4){ printf(stderr,"Bad input!%s",buffer); continue; } //handle input... } }
15.5用sscanf处理可变格式的输入
#include <stdio.h> #include <stdlib.h> #define DEFAULT_A 1 #define DEFAULT_B 2 void function(char *buffer) { int a, b, c; if(sscanf(buffer,"%d %d %d", &a, &b, &c) != 3){ a = DEFAULT_A; if(sscanf(buffer,"%d %d", &b, &c) != 2){ b = DEFAULT_B; if(sscanf(buffer, "%d", &c) != 1){ printf(stderr,"bad input:%s",buffer); exit(EXIT_FAILURE); } } } //handle a,b,c... }
15.6随机文件访问
#include <stdio.h> #include "student_info.h" int read_random_record(FILE *f, size_t rec_number, StudentInfo *buffer) { fseek(f, (long)rec_number * sizeof(StudentInfo), SEEK_SET); return fread(buffer, sizeof(StudentInfo), 1, f); }
本章问题
1.如果对fopen函数的返回值不进行检查可能会出现什么后果?
answer: 如果由于任何原因导致打开失败,函数的返回值将是NULL,当这个值传递给后续的I/O函数时,该函数就会失败。至于程序是否失败,则取决于编译器,如果程序并不终止,那么I/O操作可能会修改内存中不可预料的位置的内容。
2.如果试图对一个从未打开过的流进行I/O操作会发生什么情况?
answer: 程序将会失败,因为你试图使用的FILE结构并没有被适当的初始化,某个不可预料的内存地址的内容将可能会被修改。
3.如果一个fclose调用失败,但程序并未对它的返回值进行错误检查可能会出现什么后果?
answer: The fact that it failed at all indicates that something is wrong;the most likely possibility is a bug in the program.By not checking,this bug goes undetected and may cause problems later,Also,the FILE structure used for that stream will not be released,There is a limited number of these available,so if this happens very often,the program will run out of them and be unable to open any more files.
(事实上程序将会失败因为有些东西是错误的,最大的可能是在程序中有一个bug,由于没有检查,这个bug没有检测到就可能导致在程序的后面出现问题,FILE这个结构对应的流并没有被释放,而可用的流数量有限,如果这种情况发生的比较多,这个程序将会消耗所有的流而导致不能打开更多的文件)
4.如果一个程序在执行时它的标准输入已重定向到一个文件,程序如何检测到这个情况?
answer: 不同的操作系统提供不同的机制来检测这种重定向,但程序通常并不需要知道输入来自文件还是键盘。操作系统负责处理绝大多数人与设备无关的输入操作的许多方面,剩余部分则由库函数负责。对于绝大多数应用程序,程序从标准输入读取的方式相同,不管输入实际来自何处。
5.如果调用fgets函数时使用一个长度为1的缓冲区会发生什么呢?长度为2呢?
answer:Space is always left for the NUL type,so with a buffer size of one there is no room for any characters from the stream,with a buffer size of two,character are read one by one.
(最左边总是会留空白给NUL字节,所以缓冲区的长度为1会没有内存存储流中的任何字符,如果长度为2,则会一个一个的读取字符)
6.为了保证下面这条sprintf语句所产生的字符串不溢出,缓冲区至少有多大?
假定你的机器上整数的长度为2个字节。
sprintf(buffer, "%d %c %x", a, b, c);
answer: The first value can take up to six characters,the second at most one,and the third at most four,counting the two spaces and the terminating NUL byte,the buffer must be at least 14 bytes long.
(第一个值最多占6位,第二个值占一位,第三个值占4位,两个空格加上末尾的NUL字节,buffer至少有14个字节长)
7.为了保证下面这条sprintf语句所产生的字符串不溢出,缓冲区至少有多大?
sprintf(buffer, "%s", a);
answer: This is quite unsafe as there is no way to tell how large the buffer must be;strings may be any length,So if the length of a is not checked prior to this statement,the buffer might be overrun no matter how large it is.
(这是非常不安全的因为没有办法知道buffer最大的长度是多少,字符串的长度可能是任何值,所以如果事先没有检查它的长度,无论buffer有多大都有可能溢出)
8.%f格式代码所打印的最后一位数字是经过四舍五入呢?还是未打印的数字被简单的裁掉?
answer:It rounds, If 3.14159 is printed with a code of %.3f the result is 3.142.
(四舍五入,如果3.14159用%3.f格式代码打印出来,结果将会是3.142)
9.你如何得到perror函数可能打印的所有错误信息列表?
answer:Write a program to store all possible integer values in erron and then call perror.you must watch the output,as garbage may be produced for values that are not legitimate error codes.
(写一个程序存储所有的erron可能出现的整型值之后调用perror,你必须要注意输出,如果没有合理的错误代码可能有垃圾值产生)
10.为什么fprintf、fscanf、fputs和fclose函数都接受一个指向FILE结构的指针作为参数而不是FILE结构本身?
answer:Becuase they changed the state of the stream,The call by value semantics of C would not allow the caller's stream variable to be changed if a copy if it were passed as the argument.
(因为它们改变了流的状态,在C中不允许通过改变一个复制的参数的值来改变流变量)
11.你希望打开一个文件进行写入,假定1)你不希望原先的文件内容丢失2)你希望能够写入到文件的任何位置,那么你该怎样设置打开模式呢?
answer:the mode r+ does the job,The w modes truncate the file,and the a modes restrict writing to the end of the file.
(r+ 这个模式可以做这个工作,w模式会截断文件,a模式会限制只能在文件末尾添加)
12.为什么需要freopen函数?
answer:It allows a particular stream to be reopened to a new file,For example,in order for a program that uses printf to begin writing to a different file,the program would have to reopen stdout,this function is the reliable way to do this.
(允许一个特殊的流被一个文件重新打开,例如,一个程序为了让输出写入到一个不同的文件中,这个程序需要重新打开标准输出流,这个函数可以办到)
13.对于绝大多数程序,你觉得有必要考虑fgetc或getchar哪个更好吗?
answer:It is not worth it, Only if a program is not fast enougn or not small enougn should you spend time thinking about things like this.
(不值得,除非一个程序不够快或者不够小到需要你花时间来考虑这个问题)
14.在你的系统上,下面的语句将打印什么内容?
printf("%d\n", 3.14);
answer: The result depend on the system, but it wou't be 3.
(取决于你的编译器,但绝对不会是3)
我的想法是3.14是一个字面值常量,它可能是某个值肯定不等于3,读取的时候按照整形值读取,如果首先声明一个int变量比如int a = 3.14,然后用printf打印出来就是3没问题了。
15.请解释使用%-6.10s格式代码将打印出什么形式字符串。
The strings will be left justified.At least six characters,but no more than ten,will be printed.
(打印的字符串是左对齐,至少有6个字符但不会超过10个)
16.当一个特定的值用格式码%3.f打印时,其结果是1.405,但这个值用格式吗%.2f打印时,其结果是1.40,似乎出现了明显错误,请解释其原因。
answer:如果一个值是1.4049,格式代码%3.f打印将导致四舍五入后结果为1.405,不过当使用%.2f时,没有进行四舍五入,因为它在4的时候就被截取了。
本章练习
1.编写一个程序,把标准输入的字符逐个复制到标准输出。
answer:
#include <stdio.h> int main() { int ch; while((ch = getchar()) != EOF) putchar(ch); return 0; }
2.修改你对练习1的解决方案,使它每次读写一整行。你可以假定文件中每一行所包含的字符数不超过80个(不包括结尾的换行符)
answer:
#include <stdio.h> #define BUFFERSIZE 81 int main() { char buffer[BUFFERSIZE]; while(gets() != NULL) puts(buffer); return 0; }
3.修改你对练习2的解决方案,去除每行80个字符的限制,处理这个文件时,你仍应该每次处理一行,但对于那些长于80个字符的行,你可以每次处理其中的一段。
answer:
#include <stdio.h> #define BUFSIZE 256 main() { char buf[BUFSIZE]; while(fgets(buf, BUFSIZE, stdin) != NULL) fputs(buf, stdout); return 0; }
4.修改你对练习2的解决方案,提示用户输入两个文件名,并从标准输入读取它们。第一个作为输入文件,第二个作为输出文件,这个修改后的程序应该打开这两个文件并把输入文件的内容按照前面的方式复制到输出文件。
answer:
#include <stdio.h> #include <stdlib.h> #define BUFFERSIZE 256 FILE *Openfile(char *prompt, char *mode) { char buffer[BUFFERSIZE]; FILE *file; printf("%s filename?",prompt); if(gets(buffer) == NULL){ fprintf(stderr, "Missing %s file name.\n", prompt); exit(EXIT_FAILURE); } if((file = fopen(buffer, mode)) == NULL){ perror(buffer); exit(EXIT_FAILURE); } return file; } int main() { char buffer[BUFFERSIZE]; FILE *input; FILE *output; input = Openfile("Input", "r"); output = Openfile("Output", "w"); while(fgets(buffer, BUFFERSIZE, input) != NULL) fputs(buffer, output); fclose(input); fclose(output); return 0; }
5.修改你对练习4的解决方案,使它寻找那些以一个整数开始的行,这些整数值应该进行求和,其结果应该写入到输出文件的末尾,除了这个修改之外,这个修改后的程序其他部分应该和练习4一样。
answer:
//只需要更改一部分 int value, total = 0; while(fgets(buffer, BUFFERSIZE, input) != NULL){ if(sscanf(buffer, "%d", &value) == 1) total += value; fputs(buffer, ouput); } fprintf(output, "%d\n",total;
6.在第九章中,你编写了一个名为palindrome的函数,用于判断一个字符串是否回文,在这个练习中,你需要编写一个函数,判断一个整形变量的值是不是回文。例如245不是回文,但14741是回文,这个函数的原型应该如下:
int numeric_palindrome(int value);
如果value是回文返回真,否则返回假。
answer:
int numeric_palindrome(int value) { char buffer[50]; sprintf(buffer, "%d", value); return palindrome(buffer); }
7.某个数据文件包含了家庭成员的年龄,一个家庭各个成员的年龄都位于同一行,由空格分隔,例如:
45 42 22
36 35 7 3 1
22 20
描述了三个家庭的所有成员的年龄,它们分别有3个、5个和2个成员。编写一个程序,计算用这种文件表示的每个家庭所有成员的平均年龄,程序应该用格式代码%5.2f打印出平均年龄,后面是一个冒号和输入数据,你可以假定每个家庭的成员数量不超过10个。
answer:
#include <stdio.h> #include <stdlib.h> #define BUFFER_SIZE 512 int main() { char buffer[BUFFER_SIZE]; while(fgets(buffer, BUFFER_SIZE, stdin) != NULL){ int age[10]; int members; int sum; int i; members = sscanf(buffer, "%d %d %d %d %d %d %d %d %d %d", age, age+1, age+2, age+3, age+4, age+5, age+6, age+7, age+8, age+9); if(members == 0) continue; sum = 0; for(i = 0; i < members; i++) sum += age[i]; printf("%5.2f: %s",(double)sum/members, buffer); } return 0; }
8.编写一个程序,产生一个文件的十六进制倾印码,它应该从命令行接受单个参数,也就是需要进行倾印的文件名,如果命令行中未给出参数,程序就打印标准输入的倾印码。倾印码的每行都应该具有下面的格式:
所有的十六进制数应该使用大写的A-F而不是小写的a-f。
下面是一些样例,用于说明这种格式
(关于倾印码的有关知识,在网上可能找不到,不过,如果你熟悉汇编语言的话,那就应该见过,当你在debug里面查询磁盘中的某个地方存储的内容时应该就会看到所谓的倾印码形式,前面是偏移量,然后后面的32位十六进制表示每一段偏移量中存储的内容,再后面就是它的字符表示,所以你实际可以把倾印码看成这种意思,最开始的六个字节表示磁盘的某个存储位置,比如000200的话就表示距离一个磁盘起始位置200(注意是十六进制)的地方,存储了十六个字节的内容,它们用十六进制存储,而最后的那些字符显示则是打印出那些十六进制存储的实际内容(它们的字符代表),并且可以发现000210 - 000200 刚好是16个字节)
answer:
#include <stdio.h> #include <stdlib.h> #include <ctype.h> #include <memory.h> #define BUFFER_SIZE 64 void dump(FILE *stream) { long offset; unsigned char data[16]; int len; char buffer[BUFFER_SIZE]; memset(buffer, ' ', BUFFER_SIZE - 1); buffer[45] = '*'; buffer[62] = '*'; buffer[BUFFER_SIZE - 1] = '\0'; offset = 0; while((len = fread(data, 1, 16, stream)) > 0){ char *hex_ptr; char *char_ptr; int i; /*将offset的值用十六进制存储到buffer中*/ sprintf(buffer, "%06X", offset); /*初始化指向十六进制的指针和指向字符的指针*/ hex_ptr = buffer + 8; char_ptr = buffer + 46; memset(hex_ptr, ' ', 35); memset(char_ptr, ' ', 16); for(i = 0; i < len; i++){ sprintf(hex_ptr, "%02X", data[i]); hex_ptr += 2; *hex_ptr = ' '; /*每隔8个字节添加一个空格*/ if(i % 4 == 3) hex_ptr++; if(isprint(data[i]) || data[i] == ' ') *char_ptr++ = data[i]; else *char_ptr++ = '.'; } puts(buffer); offset += len; } } int main(int ac, char **av) { if(ac <= 1) dump(stdin); else{ FILE *file; file = fopen(av[1], "rb"); if(file == NULL){ perror(av[1]); exit(EXIT_FAILURE); } dump(file); fclose(file); } return EXIT_SUCCESS; }
9.UNIX的fgrep程序从命令行接受一个字符串和一系列文件名作为参数。然后,它逐个查看每个文件的内容,对于文件中每个包含命令行中给定字符串的文本行,程序将打印出它所在的文件名、一个冒号和包含该字符串的行。
编写这个程序,首先出现的是字符串参数,它不包含任何换行字符。然后是文件名参数。如果没有给出任何文件名,程序应该从标准输入读取。在这种情况下,程序所打印的行不包括文件名和冒号。你可以假定各文件所有文本行的长度都不会超过510个字符。
answer:
//自己的答案,可以运行 #include <stdio.h> #include <string.h> #include <stdlib.h> #define LINE 511 #define NAME 256 #define BUFFERSIZE ((LINE)+(NAME)+3) /*line + filename + : + two of ' '*/ void Lookfor(char *string, char *filename, int isstdin) { char buffer[BUFFERSIZE]; FILE *file; char line[LINE]; int len; if(isstdin == 1) file = stdin; else file = fopen(filename,"rb"); while(fgets(line, LINE, file) != NULL){ len = strlen(line); buffer[0] = '\0'; if(strstr(line, string) != NULL){ if(isstdin == 0){ strcat(buffer, filename); strcat(buffer, " : "); } strcat(buffer, line); printf("%s",buffer); } } fclose(file); } int main(int ac, char **av) { char string[LINE]; int i; if(ac <= 1){ printf("not argument!"); return 1; } strcpy(string, av[1]); if(ac == 2) Lookfor(string, NULL, 1); else for(i = 2; i < ac; i++) Lookfor(string, av[i], 0); return 0; }
//答案指导 #include <stdio.h> #include <string.h> #include <stdlib.h> #define BUFFERSIZE 512 void search(char *filename, FILE *stream, char *string) { char buffer[BUFFERSIZE]; while(fgets(buffer, BUFFER_SIZE, stream) != NULL){ if(strstr(buffer, string) != NULL){ if(filename != NULL) printf("%s : ", filename); fputs(buffer, stdout); } } } int main(int ac, char **av) { char *string; if(ac <= 1){ printf(stderr,"Usage: fgrep string file ...\n"); exit(EXIT_FAILURE); } string = *++av; if(ac <= 2) search(NULL, stdin, string); else{ while(*++av != NULL){ FILE *stream; stream = fopen(*av, "r"); if(stream == NULL) perror(*av); else{ search(*av, stream, string); fclose(stream); } } } return EXIT_SUCCESS; }
10.编写一个程序,计算文件的校验和。改程序按照下面的方式进行调用:
$ sum [-f] [file ...]
其中,-f选项是可选的。稍后我将描述它的含义。
接下来是一个可选的文件名列表,如果未给出任何文件名,程序就处理标准输入。否则,程序根据各个文件在命令行中出现的顺序逐个对它们进行处理。“处理文件”就是计算和打印文件的校验和。
计算校验和的算法是很简单的。文件中的每个字符都和一个16位的无符号整数相加,其结果就是校验和的值。不过,虽然它很容易实现,但这个算法可不是个优秀的错误检查方法。在文件中对两个字符进行互换将不会被这种方法检测出是个错误。
正常情况下,当到达每个文件的文件尾时,校验和就写入到标准输出。如果命令行中给出了-f选项,校验和就写入到一个文件而不是标准输出。如果输出文件的名字是file,那么这个输出文件的名字应该是file.cks,当程序从标准输入读取时,这个选项是非法的,因为此时并不存在输入文件名。
下面是这个程序运行的几个例子,它们在那些使用ASCII字符集的系统中是有效的。文件hw包含了文本行“Hello,World!",后面跟一个换行符。文件hw2包含了两个这样的文本行.所有的输入都不包含任何缀尾的空格或制表符.
#include <stdio.h> #include <stdlib.h> #include <string.h> void sum(FILE *input, FILE *output) { unsigned int check = 0; int value; while((value = fgetc(input)) != EOF) check += value; fprintf(output, "%d",check); } int main(int ac, char **av) { if(ac <= 1) sum(stdin, stdout); else if(strcmp(av[1], "-f") == 0){ if(ac == 2){ printf("Illegal, it is not input filename\n"); return 1; }else{ int i; FILE *input, *output; for(i = 2; i < ac; i++){ char filename[256]; strcpy(filename, av[i]); strncat(filename, ".cks", 4); output = fopen(filename, "w"); input = fopen(av[i], "r"); if(output == NULL){ perror("input"); exit(EXIT_FAILURE); } sum(input, output); fclose(input); fclose(output); } } }else{ int i; FILE *input; for(i = 1; i < ac; i++){ input = fopen(av[i], "r"); if(input == NULL){ perror("input"); exit(0); } sum(input, stdout); fclose(input); } } return 0; }
注:里面用来存储文件名的字符串,在答案指导中是用动态内存分配来完成的,因为不知道文件名的长度,我直接用字符数组实现,因为linux文件系统下,最长的文件名不超过256个字符.
11.编写一个程序,保存零件和它们的价值的存货记录.每个零件都有一份描述信息,其长度为1-20个字符.当一个新零件被添加到存货记录,程序将下一个可用的零件号指定给它,第一个零件的零件号为1,程序应该存储每个零件的当前数量和总价值.
这个程序应该从命令行接受单个参数,也就是存货记录文件的名字,如果这个文件并不存在,程序就创建一个空的存货记录文件,然后程序要求用户输入需要处理的事物类型并逐个对它们进行处理,
程序允许处理下列交易.
new description, quantity, cost-each
new交易向系统添加一个新零件,description是该零件的描述信息,它的长度不超过20个字符,quantity是保存到存货记录文件中该零件的数量,它不可以是个负值,cost-each是每个零件的单价,一个新零件的描述信息如果和一个现有零件相同并不是错误,程序必须计算和保存这些零件的总价值,对于每个新增加的零件,程序为其制定下一个可用的零件号,零件号从1开始,线性递增,被删除的零件号可以重新分配给新添加的的零件.
buy part-number, quantity, cost-each
buy交易为存货记录中一个现存的零件增加一定的数量,part-number是该零件的零件号,quantity是购入的零件数量,cost-each是每个零件的单价,程序应该把新的零件数量和总价值添加到原先的存货记录中.
sell part-number, quantity, prich-each
sell交易从存货记录中一个现存的零件增加一定的数量,part-number是该零件的零件号,quantity是出售的零件数量(它不能是负数,也不能超过当前零件的总数),prich-each是每个零件出售的所获得的金额,程序应从存货记录中减去这个数量,并减少该零件的总价值,然后,它应该计算销售所获得的利润,也就是零件的购买价格和零件的出售价格之间的差价,
delete part-number
这个交易从存货记录文件中删除指定的零件.
print part-number
这个交易打印指定零件的信息,包括描述信息,现存数量和零件的总价值.
print all
这个交易以表格的形式打印记录中所有零件的信息.
total
这个交易计算和打印记录中所有零件的总价值.
end
这个交易终止程序的执行.
当零件以不同的购买价格获得时,计算存货记录的真正价值将变得很复杂,而且取决于首先使用的是最便宜的零件还是最昂贵的零件.这个程序所使用方法比较简单:只保存每种零件的总价值,每种零件的单价被认为是相等的.例如,假定10个回形针先以每个$1.00的价格购买,这个零件的总价值便是10.00,以后又以每个$1.25的价格购入另外10个回形针,这样这个零件的总价值便成了$22.50,此时,每个回形针的当前单价便是$1,125,存货记录并不保存每批零件的购买记录,即便它们的购买价格不同,当有回形针出售时,利润根据上面计算所得的单价进行计算.
这里有一些关于设计这个程序的提示,首先,使用零件号判断存货记录文件中的一个零件的写入位置,第一个零件号是1,这样记录文件中零件号为0的位置可以用于保存一些其他信息,其次,你可以在删除零件时把它的描述信息设置为空字符串,便于以后检测该零件是否已被删除.
answer:
There are innumberable details that were not specified in the description of the program,so student's answer may vary in many areas,The solution shown here contians three modules:main.c obtains the command line argument and implements the transaction processing loop,process.c implements the functions needed to decode and process the transactions,and io.c implements the function to open,read,write,and close the inventory file,function prototypes and other definitions needed by these modules are found in the associated header files.There are is an additional header file part.h.which contains definitions relating to the structure that holds infomation about a part,typedefs are used to help ensure that variables are declared with their proper types, This also makes it easier to change the type of a variable later if the need arises,The declarations for TRUE and FALSE don't really have anything to do with a part,and appear in part.h only because there was no better place to put them.A larger program might have enough global definitions to justify putting them in a seprate include file.
(在程序中有许多细节没有明确的描述,所以不同的学生的答案可能不同,在这里显示的解决方案包含三个模块,main.c获得命令行参数并用processing处理执行命令,process.c用相应的函数实现指令,io.c执行函数中存货记录的打开,读取,写入和关闭操作,函数原型和其他定义在相应的头文件中,这里有一个额外的头文件part.h,它包括一个零件的信息的结构的定义,typedef用来帮助确定变量用合适类型定义,这使以后如果需要改变或增加变量的类型变得更加容易,描述TRUE和FALSE并不为零件做任何事,把他们放在这里只是因为没有其他更好的地方来存放它们,较大的程序可能会有许多全局变量,在分开的包含文件中调整它们)
这个题目相对复杂,在答案指导中给出的方法有些僵硬,比方说,先创建这个文件,对它进行一些操作后保存,下一次再打开这个文件的时候,part_number[]这个数组的内容又被清零了,有些数据仅仅做全局变量可能还不够,可能要存在文件中会比较好,比如在文件的开头位置存着零件号的相关信息,下次使用的时候从文件中直接读取,而不是用静态变量保存,另外一个小问题就是条件编译,每个头文件应该使用#ifndef#endif这种形式,避免重复包含头文件,下面给出答案指导中的代码,自己可能暂时不会重写这个程序.