先写个小工具
这个工具能从命令行读取用逗号分隔的数据,然后以JSON格式显示
#include <stdio.h>
int main() {
float latitude;
float longitude;
char info[80];
int started = 0;
puts("data=[");
while (scanf("%f,%f,%79[^\n]", &latitude,&longitude, info) == 3) {
if (started)
printf(",\n");
else
started = 1 ;
printf("{latitude: %f, longitude: %f, info: '%s'}", latitude, longitude, info);
}
puts("\n]");
return 0;
}
运行结果:
程序是工作了,但是输入和输出数据混作一团
。
对于大量数据来说,如果不用手工输入,而是能
从文件中直接读取,那就事半功倍了。
标准输入和标准输出
在用 scanf() 从键盘读取数据、printf() 向显示器写数据时,
这两个函数其实并没有直接使用键盘、显示器,而是用了标
准输入和标准输出。程序运行时,操作系统会创建标准输入
和标准输出。
操作系统控制数据如何进出标准输入、标准输出。如果在命令
提示符或终端运行程序,操作系统会把所有键盘输入都发送
到标准输入;默认情况下,如果操作系统从标准输出中读到
数据,就发送到显示器。
scanf() 和 printf() 函数并不知道数据从哪里来,也不知道
数据要到哪里去,它们也不关心这点,它们只管从标准输入
读数据,
向标准输出写数据。
听起来有些故弄玄虚,为什么不让程序直接使用键盘和屏幕
呢?岂不是更简单?
操作系统为什么要使用标准输入、标准输出与程序交互呢?有
一个很好的原因:
因为这么一来,就可以重定向标准输入、标准输出,让程序从键盘以外的地方读数据、往显示器以外的地方写数据,例如文件。
可以使用 < 操作符重定向标准输入
比如要读取下面的gpsdata.csv中的数据:可以用" 程序名<gpsdata.csv"
42.3634,-71.098465,Speed = 21
30.3634,-71.098465,Speed = 21
42.363327,-71.097588,Speed = 23
42.363255,-71.09671,Speed = 17
42.363182,-71.095833,Speed = 22
423.6311,-71.094955,Speed = 14
42.363037,-71.094078,Speed = 16
42.362965,-71.093201,Speed = 18
42.362892,-71.092323,Speed = 22
42.36282,-71.091446,Speed = 17
42.362747,-71.090569,Speed = 23
42.362675,-71.089691,Speed = 14
42.362602,-71.088814,Speed = 19
42.36253,-71.087936,Speed = 16
42.362457,-71.087059,Speed = 16
42.362385,-71.086182,Speed = 21
运行结果:
用 > 操作符重定向标准输出
比如把上面打印到屏幕的json数据输出到一个文本output.json中,可以用“
程序名 < gpsdata.csv > output.json
”命令
运行结果:
将新创建的数据文件在地图上画出坐标
1.打开这个地图网页:
http://chengyichao.info/hfc/ditu.html ,保存网页到本地
2.把刚才生成的
output.json文件拷贝到ditu_files文件夹中替换掉原有的output.json,然后用浏览器打
开网页。
结果确实标注出来了坐标。
刚打开网页会弹出这么一个错误提示,原因是原gpsdata.csv文件中有个latitude数据有问题,
解决这个问题,需要在转换的过程中进行数据检验。
需要注意的是,打印错误如果只用printf,那么错误信息也会被重定向到文件中,可以改用fprintf,
将错误重定向到标准错误stderr中。
标准错误默认的是输出到显示器,当然也可以用 2> 重定向标准错误,如:tojson 2> errors.txt,
这样错误信息就打印到了errors.txt文件中了。
#include <stdio.h>
int main() {
float latitude;
float longitude;
char info[80];
int started = 0;
puts("data=[");
while (scanf("%f,%f,%79[^\n]", &latitude,&longitude, info) == 3) {
if (started)
printf(",\n");
else
started = 1 ;
// 检查输入是否有效
//纬度小于-90或大于 90,退出程序并把错误状态码置为2;
if((latitude<-90.0) || (latitude>90.0)) {
// 如果用这个,打印结果也被重定向了
// printf("invalid latitude: %f\n",latitude);
// 用这个,可以将错误重定向到标准错误中
fprintf(stderr,"invalid latitude: %f\n",latitude);
return 2; // 返回错误码是2
}
//经度小于-180或大于180,退出程序并把错误状态码置为2。
if((longitude<-180.0) || (longitude>180.0)) {
// printf("invalid longitude: %f\n",longitude);
fprintf(stderr,"invalid longitude: %f\n",longitude);
return 2;
}
printf("{latitude: %f, longitude: %f, info: '%s'}", latitude, longitude, info);
}
puts("\n]");
return 0;
}
运行结果:
查看错误状态
程序在数据中发现错误就会退出,并把退出状态置为2。怎么在程序结束后检查错误状态呢?
要看操作系统,如果你的计算机是Mac、Linux、其他UNIX,或你在Windows上使用Cygwin,
可以用以下命令显示错误状态:
如果用的是Windows的命令提示符,则可以输入:
灵活的小工具
小工具的优点之一是灵活。如果有一个程序,它很好地完成了一
件事,那么就可以在很多场合用到它。
打个比方,假如你创建了
一个在文件中搜索文本的程序,就可以在很多地方用到它。
切莫修改小工具
因为小工具是做一件事并把它做好。
你不希望修改 tojson 小工具,因为你想让它只做一件事。
如果让程序做了更复杂的事,会给老用户带来麻烦。
一个任务对应一个工具
如果想要跳过百慕大三角以外的数据,应该再创建一个小工
具来做这件事。
这样你将有两个工具,一个是新的 bermuda 工具,它过滤百慕
大三角以外的数据;
另一个是原来的 tojson 工具,它将
剩余数据转化成地图所需要的json格式。
然后将两者数据连接起来即可
用管道连接输入与输出
符号 | 表示管道
(pipe),它能连
接一个进程的标
准输出与另一个
进程的标准输入。
现在要把 bermuda 工具的标准输出连接到to
json 工具的标准输入,可以这样做:bermuda | tojson。
制作bermuda工具
#include <stdio.h>
int main() {
float latitude;
float longitude;
char info[80];
while (scanf("%f,%f,%79[^\n]", &latitude,&longitude, info) == 3) {
// 检查输入是否有效
if((latitude>26.0) && (latitude<34.0)) {
if((longitude<-64.0) && (longitude> -76.0)) {
printf("%f,%f,%s", latitude, longitude, info);
printf("\n");
}
}
}
return 0;
}
下面是原始数据spooky.csv
30.685163,-68.137207,Type=Yeti
28.304380,-74.575195,Type=UFO
29.132971,-71.136475,Type=Ship
28.343065,-62.753906,Type=Elvis
27.868217,-68.005371,Type=Goatsucker
30.496017,-73.333740,Type=Disappearance
26.224447,-71.477051,Type=UFO
29.401320,-66.027832,Type=Ship
37.879536,-69.477539,Type=Elvis
22.705256,-68.192139,Type=Elvis
27.166695,-87.484131,Type=Elvis
执行下面的命令,注意:括号不能少!
将生成的output.json替换掉ditu_files文件夹中的output.json,重新打开网页,显示结果:
答: 不同操作系统实现管道的
方法不同,可能用存储器,也可能用
临时文件。我们只要知道它从一端接
收数据,在另一端发送数据就行了
问: 如果两个程序用管道相连,
第二个程序要不要等第一个程序执行
完后才能开始运行?
答: 不需要,两个程序可以同
时运行,第一个程序一发出数据,第
二个程序马上就可以处理。
问: 我能用管道连接多个程序
吗?
答: 能啊,只要在每个程序前
加上一个|就行了,一连串相连的进
程就叫流水线(pipeline)。
问: 当我用管道连接多个进程时,< 与 > 分别重定向哪个进程的标准输入、哪个进程的标准输出?
答: < 会把文件内容发送到流
水线中第一个进程的标准输入, > 会
捕获流水线中最后一个进程的标准输
出。
如何输出多个文件
可以通过创建自己的数据流来完成。
操作系统没有规定只能使用它分配的三条数据流(
标准输入、标
准输出和标准错误
),你可以
在程序运行时创建自己的数据流。
每条数据流用一个指向文件的指针来表示,可以用 fopen() 函
数创建新数据流。
FILE *in_file = fopen("input.txt", "r"); //r表示“读”(read)模式。
FILE *out_file = fopen("output.txt", "w"); // w表示“写”(write)模式。
fopen() 函数接收两个参数:文件名和模式。共有三种模式:
w (写文件)
r (读文件)
a (在文件末尾追加数据)
创建数据流后,可以用 fprintf() 往数据流中打印数据。如果
想要从文件中读取数据,则可以用 fscanf() 函数:
fprintf(out_file, " 不要穿 %s 色的衣服和 %s 色的裤子 ” , ” 红 ” , ” 绿 ” );
fscanf(in_file, "%79[^\n]\n", sentence);
注意:当用完数据流,别忘了关闭它。
虽然所有的数据流在程
序结束后都会自动关闭,但你仍应该自己关闭它们:
fclose(in_file);
fclose(out_file);
代码示例:
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
int main() {
char line[80];
FILE *in = fopen("spooky.csv","r"); // r 表示读取
FILE *file1 = fopen("ufos.csv","w"); // w 表示写入
FILE *file2 = fopen("disappearances.csv","w");
FILE *file3 = fopen("other.csv","w");
while(fscanf(in,"%79[^\n]\n",line)==1) {
if(strstr(line,"UFO"))
fprintf(file1,"%s\n",line);
else if(strstr(line,"Disappearance"))
fprintf(file2,"%s\n",line);
else
fprintf(file3,"%s\n",line);
}
// 关流
fclose(in);
fclose(file1);
fclose(file2);
fclose(file3);
return 0;
}
运行结果:
main() 函数还有下面这种形式,它
能以字符串数组的形式读取命令行参数。
由于C语
言没有内置字符串,所谓的字符串数组其实是一个字符指针数
组。
int main(int argc, char *argv[]) // argc 的值用来记录数组中元素的个数,即argv的长度
{
.... 做事情 ....
}
假如程序名是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程序,让它变得更灵活
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
// 命令:appname mermaid mermaid.csv Elvis elvises.csv the_rest.csv
// argv[0] argv[1] argv[2] argv[3] argv[4] argv[5]
int main(int argc, char *argv[]) { // argv是用户输入的程序名+所有参数的字符串数组,argc是参数个数+程序名1个
char line[80];
if(argc != 6) { // 注意这里不能写5
fprintf(stderr,"you need to give 5 params\n");
return 1;
}
// FILE *in = fopen("spooky.csv","r"); // r 表示读取
// 安全检查,防止文件不存在
FILE *in;
if(!(in = fopen("spooky.csv","r"))) {
fprintf(stderr,"can not open this file!\n");
return 1;
}
FILE *file1 = fopen(argv[2],"w"); // w 表示写入
FILE *file2 = fopen(argv[4],"w");
FILE *file3 = fopen(argv[5],"w");
while(fscanf(in,"%79[^\n]\n",line)==1) {
if(strstr(line,argv[1]))
fprintf(file1,"%s\n",line);
else if(strstr(line,argv[3]))
fprintf(file2,"%s\n",line);
else
fprintf(file3,"%s\n",line);
}
// 关流
fclose(in);
fclose(file1);
fclose(file2);
fclose(file3);
return 0;
}
运行结果:
设置命令行中的选项
很多程序都会使用命令行选项,因此有一个专门的库函数,
可以用它来简化处理过程。这个库函数叫 getopt() ,每一
次调用都会返回命令行中下一个参数。要使用它,你需要包含头文件
unistd.h ,这个头文件不属于C标
准库,而是
POSIX库中的一
员。POSIX的目标是创建一
套能够在所有主流操作系统
上使用的函数。
举个栗子看它是怎么工作的
假设程序能够接收一组不同的选
项如下:
命令: rocket_to -e 4 -a Brasilia Tokyo London
程序需要两个选项,一个选项接收值, -e 代表“引擎”;另
一个选项代表了 开 或 关 , -a 代表“无敌模式”。
可以循环调用
getopt() 来处理这两个选项,像这样:
#include <unistd.h>
// 需要包含此头文件...
while ((ch = getopt(argc, argv, "ae:")) != EOF){ // "ae:"表示选项a和e是有效的,e后面的:表示e选项需要一个参数
switch(ch) {
...
case 'e': // 在这里读取e选项所带的参数
engine_count = optarg; // 当选项为e时,其参数就是optarg
...
}
}
//循环结束后,必须用这两行用来跳过已读取的optind个选项。
argc -= optind; // optind保存了“getopt()函数从命令行读取了几个选项”
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]已经不再是程序名,而是指向选项后的第一个命令行参数了。
代码示例
# include <stdio.h>
# include <unistd.h>
int main(int argc,char *argv[]) {
char *delivery = ""; // 用于接收d选项带的参数
int thick = 0 ;
int count = 0 ;
char ch; // 接收 命令行选项名
while((ch = getopt(argc,argv,"d:t"))!=EOF) {
switch(ch) {
case 'd':
delivery = optarg;
break;
case 't':
thick = 1; // 非零即为真
break;
default:
// 如果用户输入未定义的选项名,则打印错误
fprintf(stderr,"Unknown option:%s\n",optarg);
return 1;
}
}
// 这两行用于 跳过已读取的选项
argc -= optind;
argv += optind;
// 如果有t选项 ,那么就说明要thick的
if(thick)
puts("Thick crust.");
// 如果d选项有参数,就打印出来
if(delivery[0])
printf("To be delivered %s.\n",delivery);
// 遍历输出d选项所带的参数
puts("Ingredients:");
for(count = 0; count<argc ; count++)
puts(argv[count]);
return 0;
}
运行结果
问: 我能合并两个选项吗?例
如用-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()看到--
就会停止读取选项,程序会把后面的
内容当成普通的命令行参数读取。