上一次学C的时候没有看标准库,这一次看了才发现,原来printf实际上是fprintf的简写,相当于自动传入了stdout作为参数。
输入与输出
这一次就借助一个程序来学习一下标准输入输出与重定向吧。
程序来自于Head First C的第三章的示例程序。用格式化的方式读入数据并且转换成JSON格式。
重定向标准输入
一开始的程序很简单,通过读入一行的三个数字,然后以JSON格式输出:
#include <stdio.h>
int parse(void){
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;
}
但是单独运行这个程序的时候,会等待键盘的输入,然后输出一行结果。这是因为标准输入和输出默认情况下是键盘和显示器。
现在我们希望用一个csv文件作为输入,其中的内容是复核程序读取格式的一行一行的数据:
42.363400,-71.098465,Speed = 21
43.363327,-72.097588,Speed = 23
44.363255,-73.096710,Speed = 17
45.363182,-74.095833,Speed = 22
46.363110,-75.094955,Speed = 14
47.363037,-76.094078,Speed = 16
48.362965,-77.093201,Speed = 18
49.362892,-78.092323,Speed = 22
50.362820,-79.091446,Speed = 17
51.362747,-80.090569,Speed = 23
52.362675,-81.089691,Speed = 14
53.362602,-82.088814,Speed = 19
54.362530,-83.087936,Speed = 16
55.362457,-84.087059,Speed = 16
56.362385,-85.086182,Speed = 21
这个时候无需重新编译程序,实际上现在学习的是操作系统的命令,也就是重定向标准输入。
将csv文件和编译出的exe文件放在同一个目录之下,然后输入c < gpsdata.csv,<
表示重定向标准输入到这个文件,即将csv文件的内容输入给c.exe文件。
之后程序成功的输出了结果:
[
{"latitude": 42.363400, "longitude": -71.098465, "info": "Speed = 21"},
{"latitude": 43.363327, "longitude": -72.097588, "info": "Speed = 23"},
{"latitude": 44.363255, "longitude": -73.096710, "info": "Speed = 17"},
{"latitude": 45.363182, "longitude": -74.095833, "info": "Speed = 22"},
{"latitude": 46.363110, "longitude": -75.094955, "info": "Speed = 14"},
{"latitude": 47.363037, "longitude": -76.094078, "info": "Speed = 16"},
{"latitude": 48.362965, "longitude": -77.093201, "info": "Speed = 18"},
{"latitude": 49.362892, "longitude": -78.092323, "info": "Speed = 22"},
{"latitude": 50.362820, "longitude": -79.091446, "info": "Speed = 17"},
{"latitude": 51.362747, "longitude": -80.090569, "info": "Speed = 23"},
{"latitude": 52.362675, "longitude": -81.089691, "info": "Speed = 14"},
{"latitude": 53.362602, "longitude": -82.088814, "info": "Speed = 19"},
{"latitude": 54.362530, "longitude": -83.087936, "info": "Speed = 16"},
{"latitude": 55.362457, "longitude": -84.087059, "info": "Speed = 16"},
{"latitude": 56.362385, "longitude": -85.086182, "info": "Speed = 21"}
]
有了重定向输入,就可以处理以文件形式的数据,而不是每次都一个一个从键盘上输入了。
重定向标准输出
肯定立刻就会想到,那重定向输出应该也可以,是的,可以把上边程序的结果输出成一个.json文件。
输入c < gpsdata.csv >output.json即可。>
是重定向标准输出符号。这里就会将本来应该打印在显示器上的结果重定向输出到output.json文件。执行之后就在同一目录下找到这个文件。
当然,熟悉JSON格式的肯定知道这个输出结果其实并不是一个JSON文件,只需要略微修改一下原来的程序即可:
#include <stdio.h>
int parse(void) {
float latitude;
float longitude;
char info[80];
int started = 0;
puts("[");
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;
}
重定向标准错误输出
现在又有了新需求,我们的程序在处理数据的时候,至少要具备的功能就是对数据进行检测是否符合要求,如果不符合要求,需要提醒使用者,而不是闷头继续处理。从错误的输入得到的结果没有意义。
现在给输入加上一些判断,如果出错的话,就直接返回:
#include <stdio.h>
int parse(void) {
float latitude;
float longitude;
char info[80];
int started = 0;
puts("[");
while (scanf("%f,%f,%79[^\n]", &latitude, &longitude, info) == 3) {
if (started) {
printf(",\n");
} else {
started = 1;
}
if(latitude<-90.0 || latitude> 90.0){
printf("Invalid latitude: %f\n", latitude);
return 2;
}
if(longitude < -180.0 || longitude>180.0){
printf("Invalid longitude: %f\n", longitude);
return 2;
}
printf("{\"latitude\": %f, \"longitude\": %f, \"info\": \"%s\"}", latitude, longitude, info);
}
puts("\n]");
return 0;
}
现在判断经度和纬度超出范围的时候,会停止处理程序并且报错。
在csv文件中插入一条错误的数据:
342.362965,-712.093201,Speed = 18
然后再执行同样的命令,可以看到控制台没有任何错误提示,打开output.json,发现错误信息写在了里边:
[
{"latitude": 42.363400, "longitude": -71.098465, "info": "Speed = 21"},
{"latitude": 43.363327, "longitude": -72.097588, "info": "Speed = 23"},
{"latitude": 44.363255, "longitude": -73.096710, "info": "Speed = 17"},
{"latitude": 45.363182, "longitude": -74.095833, "info": "Speed = 22"},
{"latitude": 46.363110, "longitude": -75.094955, "info": "Speed = 14"},
{"latitude": 47.363037, "longitude": -76.094078, "info": "Speed = 16"},
Invalid latitude: 342.362976
这是因为在重定向标准输出的时候,其实还有一个错误输出,也一并重定向到文件中了。操作系统可以使用>2
来单独指定标准错误输出的重定向。
想重定向这个,就不能使用printf
函数了,而要使用fprintf
函数,printf只是fprintf的一个简化形式。
用fprintf来修改一下程序:
#include <stdio.h>
int parse(void) {
float latitude;
float longitude;
char info[80];
int started = 0;
puts("[");
while (scanf("%f,%f,%79[^\n]", &latitude, &longitude, info) == 3) {
if (started) {
printf(",\n");
} else {
started = 1;
}
if(latitude<-90.0 || latitude> 90.0){
// printf("Invalid latitude: %f\n", latitude);
fprintf(stderr,"Invalid latitude: %f\n", latitude);
return 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>
来重定向,比如:
c < gpsdata.csv >output.json 2>error.txt
这就会将输出写入output.json,将错误信息写入error.txt。
管道符号
假设我们的csv文件是其他程序生成的,不会有错误。现在我们专注于解决问题。
现在需要从结果中过滤出所有的在一定范围内的数据,然后生成对应的JSON文件。考虑一下我们现在的程序,是一个接受CSV然后输出JSON的程序。
我们只要想办法过滤一下所有的csv文件,从中取出符合要求的数据传递给我们现在的程序就可以了。
这个时候就要用到管道符|
,表示将一个程序的标准输出连接到另外一个程序的标准输入。而最开始的输入和最后的输出,可以通过重定向来控制。
先来编写一个过滤CSV的程序:
int filter(void){
float latitude;
float longitude;
char info[80];
while (scanf("%f,%f,%79[^\n]", &latitude, &longitude, info) == 3) {
if(latitude>45.0 && latitude<50.0){
if(longitude<-73.0 && longitude > -80.0){
printf("%f,%f,%s\n", latitude, longitude, info);
}
}
}
return 0;
}
然后把这个编译生成一个filter.exe
文件,刚才的程序我们叫json.exe
文件,将两个文件和csv文件放在同一个目录下,然后执行(windows下):
(filter | json) < gpsdata.csv >output.json
检查生成的JSON文件:
[
{"latitude": 45.363182, "longitude": -74.095833, "info": "Speed = 22"},
{"latitude": 46.363110, "longitude": -75.094955, "info": "Speed = 14"},
{"latitude": 47.363037, "longitude": -76.094078, "info": "Speed = 16"},
{"latitude": 48.362965, "longitude": -77.093201, "info": "Speed = 18"},
{"latitude": 49.362892, "longitude": -78.092323, "info": "Speed = 22"}
]
可以看到成功的过滤出来了结果。这里注意的是无论在windows下还是linux下,都需要用括号包起来管道内的所有程序,这样才能知道是一整个管道,如果不包起来,则认为对每个程序设置重定向。
看来linux的bash编程之后也要学了,学无止境啊。
创建自己的数据流
在一个程序运行的时候,操作系统会为其创建三个数据流,分别是标准输入,标准输出和标准错误输出。
除了重定向标准输入输出,还可以创建自己的数据流,其实就是自定义输入和输出,一般输入没有太多的需要自定义的内容,主要是输出。
所以自定义的数据流可以用一个指向文件的指针来表示:
FILE *in_file = fopen("input.txt","r");
r代表模式,已经很熟悉了。w表示写,r表示读,a表示追加。
创建了这个文件指针之后,就可以和stdio之类的流一样当做fprintf
函数的第一个参数来往里边写东西了,fscanf
也可以从其中进行输入。
只是要记得在完成之后使用fclose(in_file)
来关闭数据流。
来写一个简单的例子,现在有一个文本文件input.txt如下:
a,b,c
d,e,f
g,h,i
想让其中所有逗号分隔的内容,用|分割并且追加到一个output.txt的尾部:
#include <stdio.h>
int custom(void){
char a[5];
char b[5];
char c[5];
FILE *in_file = fopen("input.txt", "r");
FILE *out_file = fopen("output.txt", "a");
//读逗号分隔的字符串真不容易,要先读到逗号之前的部分,之后逗号放回缓冲区。
//再读的时候逗号匹配逗号,再去按照%[^,]来读取
//如果直接要逗号分隔,不加%[^,]是不行的,因为%s会一次读完所有的空白
// fscanf(in_file, "%[^,],%[^,],%[^\n]\n", a, b, c);
// printf("%s|%s|%s|", a, b, c);
//
// fscanf(in_file, "%[^,],%[^,],%[^\n]", a, b, c);
// printf("%s|%s|%s|", a, b, c);
while (fscanf(in_file, "%[^,],%[^,],%[^\n]\n", a, b, c) == 3) {
fprintf(out_file, "%s|%s|%s|", a, b, c);
}
fclose(in_file);
fclose(out_file);
return 0;
}
这里比较搞的是用扫描用逗号分隔的字符串。由于默认情况下%s是会一直扫描到空白字符为止,所以这里要使用类似正则的匹配到逗号为止。
然而注意,逗号匹配不成功后会放回到缓冲区,所以必须添加一个逗号分隔符用于匹配逗号。在最末尾,会在匹配到\n之前,然后把\n放回缓冲区。会匹配到下一行开始的新字符串上,导致使用a模式的时候会换行。所以必须再添加一个\n来匹配掉\n。
这样执行程序的时候,就不会添加上换行字符。
运行程序后产生output.txt文件,结果是:
a|b|c|d|e|f|g|h|i|
再运行一次,变成:
a|b|c|d|e|f|g|h|i|a|b|c|d|e|f|g|h|i|
scanf函数的花头实在太多了,在匹配的时候一定要当心。
还需要注意的是,fopen函数如果打开错误,会返回0,也就是判断是否打开成功:
FILE *in;
if (!(in = fopen("我不存在.txt", "r"))) {
fprintf(stderr, "无法打开文件.\n");
return 1;
}
运行时候获取参数
为了写一个完整的在命令行下可以使用的小程序,还需要知道最后一点,就是可以从命令行中获取参数来执行任务。
之前写main(void)已经形成习惯,其实真正的main()函数要写成如下方式:
int main(int argc, char *argv[]){
}
int argc是记录命令行一共有几个参数,其中自己的可执行文件的名称也算一个参数。
而char *argv[]记录了所有参数的内容,是一个字符串数组或者说是一个char * 指针数组。
这样知道了argc,和数组,就可以遍历其中来获取参数,之后可以进行各种操作。
比如先写一个获取所有参数的命令:
int getparam(int argc, char *argv[]){
int i = 0;
for (i = 0; i < argc; i++) {
printf("顺序为 %d 的参数是 %s\n", i, argv[i]);
}
}
然后在main执行的时候把参数传给这个函数用来打印:
int getparam(int , char *[]);
int main(int argc, char *argv[]) {
getparam(argc, argv);
}
编译之后,在命令行下随便输入一些参数:
c fdsj 98 fdk -312 s9d8
可以看到结果是:
顺序为 0 的参数是 c
顺序为 1 的参数是 fdsj
顺序为 2 的参数是 98
顺序为 3 的参数是 fdk
顺序为 4 的参数是 -312
顺序为 5 的参数是 s9d8
这样就可以根据其中的内容,在程序中进行逻辑处理了,可以规定argc的长度,强制用户输入一些参数。这里边的逻辑就会更复杂了,但是万变不离其宗。
命令行选项库
如果我们的工具写的比较死,比如我们规定argc为3,去掉自己的文件名之后,还有两个参数,必须是源文件和目标文件名,这样是可以的。但是不够灵活。
如果用过一些操作系统工具就会知道,这些工具常常是以提供选项的方式来操作,比如经典的ls命令:
ls -l
就会列出与单独使用ls不同的结果,使用了-l的结果更加详细。
这个通过一个一个搜索数组去进行判断固然可以,不过C语言在unistd.h
标准库中提供了一个函数getopt()
可以方便的来进行操作。
要使用这个库函数,就要把argc和argv交给这个库函数,然后就可以进行操作了。看一个最简单的例子,我们自己编写的程序支持-a name -b age两个选项,如果-a生效会打印名字,-b生效会打印年龄。
int getparam(int , char *[]);
int main(int argc, char *argv[]) {
char ch;
while ((ch = getopt(argc, argv, "a:b:")) != EOF) {
switch (ch) {
case 'a':
printf("Your name is %s\n", optarg);
break;
case 'b':
printf("Your age is %s\n", optarg);
break;
default:
fprintf(stderr, "You don't give name or age!\n");
return 1;
}
}
argc -= optind;
argv += optind;
getparam(argc, argv);
}
这里的核心是使用getopt
函数不断循环来处理所有的选项和传入的值,之后才是剩余的没有处理的普通的参数值。
其中的getopt
的模式很重要,"a:b:"
表示有效的选项是a和b,这两个选项都需要带参数。
注意,我们的程序如果使用这个库,就必须要先输入选项的部分,再输入普通参数,否则不会匹配成功。
在循环结束之后,optind
表示成功匹配的数量,只要跳过匹配的数量,之后就是剩余的参数,所以继续利用上一节里的getparam(argc, argv);
打印出剩余的参数。
在命令行里可以各种实验一下,其中有一点和Head First C上说的不同,不传入任何选项不会报错,只是什么都没有做。
一些尝试和结果如下:
>c -a cony
Your name is cony
>c -b 5
Your age is 5
>c -b 5 -a cony
Your age is 5
Your name is cony
>c -t 32
c: unknown option -- t
You don't give name or age!
>c -a cony -b 5 confidence optimal happy birthday
Your name is cony
Your age is 5
顺序为 0 的参数是 confidence
顺序为 1 的参数是 optimal
顺序为 2 的参数是 happy
顺序为 3 的参数是 birthday
有了输入输出和参数控制,现在就可以写控制台下有用的程序了。
今天还是可爱的女儿5岁生日,超级乐观和自信的小家伙,继续努力成为更厉害的人吧。老爸在此祝你生日快乐。也许某一天你会看到这里哦,那也意外着你应该很厉害啦。