HeadFirstC笔记_3 创建小工具:做一件事并把它做好

先写个小工具
这个工具能从命令行读取用逗号分隔的数据,然后以JSON格式显示
   
    
    
  1. #include <stdio.h>
  2. int main() {
  3. float latitude;
  4. float longitude;
  5. char info[80];
  6. int started = 0;
  7. puts("data=[");
  8. while (scanf("%f,%f,%79[^\n]", &latitude,&longitude, info) == 3) {
  9. if (started)
  10. printf(",\n");
  11. else
  12. started = 1 ;
  13. printf("{latitude: %f, longitude: %f, info: '%s'}", latitude, longitude, info);
  14. }
  15. puts("\n]");
  16. return 0;
  17. }
运行结果:

程序是工作了,但是输入和输出数据混作一团
对于大量数据来说,如果不用手工输入,而是能 从文件中直接读取,那就事半功倍了。

标准输入和标准输出
       在用 scanf() 从键盘读取数据、printf() 向显示器写数据时, 这两个函数其实并没有直接使用键盘、显示器,而是用了标
准输入和标准输出。程序运行时,操作系统会创建标准输入 和标准输出。
       操作系统控制数据如何进出标准输入、标准输出。如果在命令 提示符或终端运行程序,操作系统会把所有键盘输入都发送
到标准输入;默认情况下,如果操作系统从标准输出中读到 数据,就发送到显示器。
       scanf() 和 printf() 函数并不知道数据从哪里来,也不知道 数据要到哪里去,它们也不关心这点,它们只管从标准输入 读数据,
向标准输出写数据。 听起来有些故弄玄虚,为什么不让程序直接使用键盘和屏幕 呢?岂不是更简单?
       操作系统为什么要使用标准输入、标准输出与程序交互呢?有 一个很好的原因:
因为这么一来,就可以重定向标准输入、标准输出,让程序从键盘以外的地方读数据、往显示器以外的地方写数据,例如文件。

可以使用 < 操作符重定向标准输入
比如要读取下面的gpsdata.csv中的数据:可以用" 程序名<gpsdata.csv"
     
      
      
  1. 42.3634,-71.098465,Speed = 21
  2. 30.3634,-71.098465,Speed = 21
  3. 42.363327,-71.097588,Speed = 23
  4. 42.363255,-71.09671,Speed = 17
  5. 42.363182,-71.095833,Speed = 22
  6. 423.6311,-71.094955,Speed = 14
  7. 42.363037,-71.094078,Speed = 16
  8. 42.362965,-71.093201,Speed = 18
  9. 42.362892,-71.092323,Speed = 22
  10. 42.36282,-71.091446,Speed = 17
  11. 42.362747,-71.090569,Speed = 23
  12. 42.362675,-71.089691,Speed = 14
  13. 42.362602,-71.088814,Speed = 19
  14. 42.36253,-71.087936,Speed = 16
  15. 42.362457,-71.087059,Speed = 16
  16. 42.362385,-71.086182,Speed = 21
运行结果:
 
用 > 操作符重定向标准输出
比如把上面打印到屏幕的json数据输出到一个文本output.json中,可以用“ 程序名 < gpsdata.csv > output.json ”命令
 
运行结果:
 
将新创建的数据文件在地图上画出坐标

2.把刚才生成的 output.json文件拷贝到ditu_files文件夹中替换掉原有的output.json,然后用浏览器打 开网页。
结果确实标注出来了坐标。
 
一些数据出错了

 刚打开网页会弹出这么一个错误提示,原因是原gpsdata.csv文件中有个latitude数据有问题,
解决这个问题,需要在转换的过程中进行数据检验。
需要注意的是,打印错误如果只用printf,那么错误信息也会被重定向到文件中,可以改用fprintf,
将错误重定向到标准错误stderr中。
标准错误默认的是输出到显示器,当然也可以用 2> 重定向标准错误,如:tojson 2> errors.txt,
这样错误信息就打印到了errors.txt文件中了。
       
        
        
  1. #include <stdio.h>
  2. int main() {
  3. float latitude;
  4. float longitude;
  5. char info[80];
  6. int started = 0;
  7. puts("data=[");
  8. while (scanf("%f,%f,%79[^\n]", &latitude,&longitude, info) == 3) {
  9. if (started)
  10. printf(",\n");
  11. else
  12. started = 1 ;
  13. // 检查输入是否有效
  14. //纬度小于-90或大于 90,退出程序并把错误状态码置为2;
  15. if((latitude<-90.0) || (latitude>90.0)) {
  16. // 如果用这个,打印结果也被重定向了
  17. // printf("invalid latitude: %f\n",latitude);
  18. // 用这个,可以将错误重定向到标准错误中
  19. fprintf(stderr,"invalid latitude: %f\n",latitude);
  20. return 2; // 返回错误码是2
  21. }
  22. //经度小于-180或大于180,退出程序并把错误状态码置为2。
  23. if((longitude<-180.0) || (longitude>180.0)) {
  24. // printf("invalid longitude: %f\n",longitude);
  25. fprintf(stderr,"invalid longitude: %f\n",longitude);
  26. return 2;
  27. }
  28. printf("{latitude: %f, longitude: %f, info: '%s'}", latitude, longitude, info);
  29. }
  30. puts("\n]");
  31. return 0;
  32. }
运行结果:

查看错误状态
程序在数据中发现错误就会退出,并把退出状态置为2。怎么在程序结束后检查错误状态呢?
要看操作系统,如果你的计算机是Mac、Linux、其他UNIX,或你在Windows上使用Cygwin,
可以用以下命令显示错误状态:


如果用的是Windows的命令提示符,则可以输入:
 
灵活的小工具
小工具的优点之一是灵活。如果有一个程序,它很好地完成了一 件事,那么就可以在很多场合用到它。
打个比方,假如你创建了 一个在文件中搜索文本的程序,就可以在很多地方用到它。

切莫修改小工具
因为小工具是做一件事并把它做好。 你不希望修改 tojson 小工具,因为你想让它只做一件事。
如果让程序做了更复杂的事,会给老用户带来麻烦。

一个任务对应一个工具
如果想要跳过百慕大三角以外的数据,应该再创建一个小工 具来做这件事。
这样你将有两个工具,一个是新的 bermuda 工具,它过滤百慕 大三角以外的数据;
另一个是原来的 tojson 工具,它将 剩余数据转化成地图所需要的json格式。
然后将两者数据连接起来即可

用管道连接输入与输出
符号 | 表示管道 (pipe),它能连 接一个进程的标 准输出与另一个 进程的标准输入。
现在要把 bermuda 工具的标准输出连接到to json 工具的标准输入,可以这样做:bermuda | tojson。

制作bermuda工具
           
            
            
  1. #include <stdio.h>
  2. int main() {
  3. float latitude;
  4. float longitude;
  5. char info[80];
  6. while (scanf("%f,%f,%79[^\n]", &latitude,&longitude, info) == 3) {
  7. // 检查输入是否有效
  8. if((latitude>26.0) && (latitude<34.0)) {
  9. if((longitude<-64.0) && (longitude> -76.0)) {
  10. printf("%f,%f,%s", latitude, longitude, info);
  11. printf("\n");
  12. }
  13. }
  14. }
  15. return 0;
  16. }

用管道链接运行两个工具
下面是原始数据spooky.csv
           
            
            
  1. 30.685163,-68.137207,Type=Yeti
  2. 28.304380,-74.575195,Type=UFO
  3. 29.132971,-71.136475,Type=Ship
  4. 28.343065,-62.753906,Type=Elvis
  5. 27.868217,-68.005371,Type=Goatsucker
  6. 30.496017,-73.333740,Type=Disappearance
  7. 26.224447,-71.477051,Type=UFO
  8. 29.401320,-66.027832,Type=Ship
  9. 37.879536,-69.477539,Type=Elvis
  10. 22.705256,-68.192139,Type=Elvis
  11. 27.166695,-87.484131,Type=Elvis
执行下面的命令,注意:括号不能少!

将生成的output.json替换掉ditu_files文件夹中的output.json,重新打开网页,显示结果:
 
 
问: 到底什么是管道?
答: 不同操作系统实现管道的 方法不同,可能用存储器,也可能用 临时文件。我们只要知道它从一端接 收数据,在另一端发送数据就行了

问: 如果两个程序用管道相连,
第二个程序要不要等第一个程序执行
完后才能开始运行?
答: 不需要,两个程序可以同
时运行,第一个程序一发出数据,第
二个程序马上就可以处理。

问: 我能用管道连接多个程序
吗?
答: 能啊,只要在每个程序前
加上一个|就行了,一连串相连的进
程就叫流水线(pipeline)。

问: 当我用管道连接多个进程时,< 与 > 分别重定向哪个进程的标准输入、哪个进程的标准输出?
答: < 会把文件内容发送到流 水线中第一个进程的标准输入, > 会 捕获流水线中最后一个进程的标准输 出。

如何输出多个文件
可以通过创建自己的数据流来完成。
操作系统没有规定只能使用它分配的三条数据流( 标准输入、标 准输出和标准错误 ),你可以 在程序运行时创建自己的数据流。
每条数据流用一个指向文件的指针来表示,可以用 fopen() 函 数创建新数据流。
               
                
                
  1. FILE *in_file = fopen("input.txt", "r"); //r表示“读”(read)模式。
  2. FILE *out_file = fopen("output.txt", "w"); // w表示“写”(write)模式。
fopen() 函数接收两个参数:文件名和模式。共有三种模式:
w (写文件)
r (读文件)
a (在文件末尾追加数据)

创建数据流后,可以用 fprintf() 往数据流中打印数据。如果 想要从文件中读取数据,则可以用 fscanf() 函数:
                
                 
                 
  1. fprintf(out_file, " 不要穿 %s 色的衣服和 %s 色的裤子 , , 绿 );
  2. fscanf(in_file, "%79[^\n]\n", sentence);

注意:当用完数据流,别忘了关闭它
虽然所有的数据流在程 序结束后都会自动关闭,但你仍应该自己关闭它们:
                 
                  
                  
  1. fclose(in_file);
  2. fclose(out_file);
代码示例:
                  
                   
                   
  1. #include <stdio.h>
  2. #include <stdlib.h>
  3. #include <string.h>
  4. int main() {
  5. char line[80];
  6. FILE *in = fopen("spooky.csv","r"); // r 表示读取
  7. FILE *file1 = fopen("ufos.csv","w"); // w 表示写入
  8. FILE *file2 = fopen("disappearances.csv","w");
  9. FILE *file3 = fopen("other.csv","w");
  10. while(fscanf(in,"%79[^\n]\n",line)==1) {
  11. if(strstr(line,"UFO"))
  12. fprintf(file1,"%s\n",line);
  13. else if(strstr(line,"Disappearance"))
  14. fprintf(file2,"%s\n",line);
  15. else
  16. fprintf(file3,"%s\n",line);
  17. }
  18. // 关流
  19. fclose(in);
  20. fclose(file1);
  21. fclose(file2);
  22. fclose(file3);
  23. return 0;
  24. }
运行结果:
 
main()可以做得更多---命令行传递参数给main函数
main() 函数还有下面这种形式,它 能以字符串数组的形式读取命令行参数。
由于C语 言没有内置字符串,所谓的字符串数组其实是一个字符指针数 组。
                    
                     
                     
  1. int main(int argc, char *argv[]) // argc 的值用来记录数组中元素的个数,即argv的长度
  2. {
  3. .... 做事情 ....
  4. }
假如程序名是categorize,那么可以这样传递参数。
"categorize   mermaid   mermaid.csv   Elvis   elvises.csv   the_rest.csv"
   argv[0]         argv[1]       argv[2]            argv[3]     argv[4]          argv[5]
注意:第一个参数是要运行的程序的名字。
也就是说,你在main函数中想拿到mermaid,必须要用argv[1],而不是argv[0]。

修改categorize程序,让它变得更灵活
                 
                  
                  
  1. #include <stdio.h>
  2. #include <stdlib.h>
  3. #include <string.h>
  4. // 命令:appname mermaid mermaid.csv Elvis elvises.csv the_rest.csv
  5. // argv[0] argv[1] argv[2] argv[3] argv[4] argv[5]
  6. int main(int argc, char *argv[]) { // argv是用户输入的程序名+所有参数的字符串数组,argc是参数个数+程序名1个
  7. char line[80];
  8. if(argc != 6) { // 注意这里不能写5
  9. fprintf(stderr,"you need to give 5 params\n");
  10. return 1;
  11. }
  12. // FILE *in = fopen("spooky.csv","r"); // r 表示读取
  13. // 安全检查,防止文件不存在
  14. FILE *in;
  15. if(!(in = fopen("spooky.csv","r"))) {
  16. fprintf(stderr,"can not open this file!\n");
  17. return 1;
  18. }
  19. FILE *file1 = fopen(argv[2],"w"); // w 表示写入
  20. FILE *file2 = fopen(argv[4],"w");
  21. FILE *file3 = fopen(argv[5],"w");
  22. while(fscanf(in,"%79[^\n]\n",line)==1) {
  23. if(strstr(line,argv[1]))
  24. fprintf(file1,"%s\n",line);
  25. else if(strstr(line,argv[3]))
  26. fprintf(file2,"%s\n",line);
  27. else
  28. fprintf(file3,"%s\n",line);
  29. }
  30. // 关流
  31. fclose(in);
  32. fclose(file1);
  33. fclose(file2);
  34. fclose(file3);
  35. return 0;
  36. }
运行结果:
 

设置命令行中的选项
很多程序都会使用命令行选项,因此有一个专门的库函数,
可以用它来简化处理过程。这个库函数叫 getopt() ,每一
次调用都会返回命令行中下一个参数。要使用它,你需要包含头文件
unistd.h ,这个头文件不属于C标
准库,而是
POSIX库中的一
员。POSIX的目标是创建一
套能够在所有主流操作系统
上使用的函数。

举个栗子看它是怎么工作的
假设程序能够接收一组不同的选 项如下:
                         
                          
                          
  1. 命令: rocket_to -e 4 -a Brasilia Tokyo London
程序需要两个选项,一个选项接收值, -e 代表“引擎”;另 一个选项代表了 开 或 关 , -a 代表“无敌模式”。
可以循环调用 getopt() 来处理这两个选项,像这样:
                          
                           
                           
  1. #include <unistd.h> // 需要包含此头文件
  2. ...
  3. while ((ch = getopt(argc, argv, "ae:")) != EOF){ // "ae:"表示选项a和e是有效的,e后面的:表示e选项需要一个参数
  4. switch(ch) {
  5. ...
  6. case 'e': // 在这里读取e选项所带的参数
  7. engine_count = optarg; // 当选项为e时,其参数就是optarg
  8. ...
  9. }
  10. }
  11. //循环结束后,必须用这两行用来跳过已读取的optind个选项。
  12. argc -= optind; // optind保存了“getopt()函数从命令行读取了几个选项”
  13. argv += optind; // 每读完一个选项,argc减一,而argv的首元素指针后移一位。
在循环中,用 switch 语句处理每个有效选项。字符串 ae: 告诉 getopt() 函数 “a和e 是有效选项”, e 后面的冒号表示“ -e 后 面需要再跟一个参数”, getopt() 会用 optarg 变量指向这个参 数。
循环结束以后,为了让程序读取命令行参数,需要调整一下 argv 和 argc 变量,跳过所有选项,最后 argv 数组将变成这样:
Brasilia  Tokyo    London
argv[0]    argv[1]   argv[2]
注意:此时的argv[0]已经不再是程序名,而是指向选项后的第一个命令行参数了。

代码示例
                           
                            
                            
  1. # include <stdio.h>
  2. # include <unistd.h>
  3. int main(int argc,char *argv[]) {
  4. char *delivery = ""; // 用于接收d选项带的参数
  5. int thick = 0 ;
  6. int count = 0 ;
  7. char ch; // 接收 命令行选项名
  8. while((ch = getopt(argc,argv,"d:t"))!=EOF) {
  9. switch(ch) {
  10. case 'd':
  11. delivery = optarg;
  12. break;
  13. case 't':
  14. thick = 1; // 非零即为真
  15. break;
  16. default:
  17. // 如果用户输入未定义的选项名,则打印错误
  18. fprintf(stderr,"Unknown option:%s\n",optarg);
  19. return 1;
  20. }
  21. }
  22. // 这两行用于 跳过已读取的选项
  23. argc -= optind;
  24. argv += optind;
  25. // 如果有t选项 ,那么就说明要thick的
  26. if(thick)
  27. puts("Thick crust.");
  28. // 如果d选项有参数,就打印出来
  29. if(delivery[0])
  30. printf("To be delivered %s.\n",delivery);
  31. // 遍历输出d选项所带的参数
  32. puts("Ingredients:");
  33. for(count = 0; count<argc ; count++)
  34. puts(argv[count]);
  35. return 0;
  36. }
运行结果
 
问: 我能合并两个选项吗?例
如用-td now代替-d now –t。
答: 可以,getopt()函数会全
权处理它们。

问: 我可以改变选项之间的顺序
吗?
答: 可以,因为我们用循环读取
选项,所以-d now -t、-t -d now、
-td now都一样。

问: 也就是说,只要程序在命令行看到一个前缀为-值,就会把它当成选项处理?
答: 是的,前提是它必须在命令 行参数之前出现。

问: 如果我想在命令行参数使用负数怎么办?像set_temperature-c -4,程序会把4当作选项吗?
答: 为了避免歧义,可以用 -- 隔 开参数和选项,比如set_temper- ature -c -- -4。
getopt()看到-- 就会停止读取选项,程序会把后面的 内容当成普通的命令行参数读取。

猜你喜欢

转载自blog.csdn.net/woshiwangbiao/article/details/53513261