从scanf的学习接口设计
对大多数程序员来说scanf可以能是最熟悉,也是陌生的工具。在学习C语言时,大家一定没少用它,但是对它也知道不多。比如说,它有哪些可能的返回值?又比如怎么样才能跳过回车,读一个字符?我们可以一起来研究一下,为什么scanf会设计成这样子,我们如何更好的使用它?如何扩展它?
处理好IO不容易--scanf的返回值设计
如果我们有这样一个函数int readInt()是不是比scanf更好用呢?一切正常时OK,但有些情况下不一定。
int readInt();
比如要1 2 3 4 5这样的数据,开始它很好用,但是如何决定已经结束了呢?按照C语言的惯例,我们用返回值来表示出错,接口变成int readInt(int *data)
int readInt(int *data);
当函数遇到文件结束时,返回EOF,值可能是-1。
当成功时,返回0,虽然一般是用0表示失败,不过因为出错已经返回-1,这里定义为0。
我们很容易注意到scanf可能有多个数据,当scanf返回,是否每个数据都已经赋值了?也许是,也许不是,这时我们访问data的数据就会得到上一次的结果。
如果只是数据不足或是没有适合的数据,可能我们返回已经赋值的数量也许是一个选择。对于以下代码,我们可能返回2。
ret = sscanf("1 2 3 a", "%d%d%d", &a, &b, &c);
如果是真的出错了,比如读到了文件结尾,这时我们只能返回出错,如EOF,可能就无法知道在出错前正确处理了几个数据。
不过,通常一组数据如果有一个无效,我们不关心其它几个正常的数据,所以标准库中还是把它处理成了这样。这个返回值的设计相当不完美,但体现了实用性原则。
组合爆炸—转型
如果只是简单读取整数、浮点数和字符串,我们完全可以设计一组接口,比如:
int readDecimal (int *d);
int readFlt(int *f);
int readStr(char *s);
int readChar(char *s);
这样我们的代码就很难看了,完成同样的功能可能会比scanf多很多代码。C语言选择另一种方式来定义接口,一个小型的说明性语言。
上面的接口中,除了返回值外,只有函数名和参数类型不同,如果用一个字符代表来参数类型,我们可能定义这样一个接口。
int readX (char x, void*d);
因为C语言中没有通用类型,我们使用void类型指针来定义数据。在实现时,可以根据x的值转成相应的类型。
if (x == 'd') return readDecimal((int*)d);
if (x == 'f') return readFlt((float*)d);
if (x == 's') return readStr((char*)d);
其实C语言中并不需要这个转型,因为void*可以赋值任何类型的指针,这里强制转型只是为了明确。
注意,整型可能有八进制、十进制、甚至十六进制的表示法,也就是说,对于int*类型,我们需要用不同的函数来读取不同的表示法。当然,我们可再引入一些其它字符来区分不同的表示,比如o, d, x分别表示八进制、十进制和十六进制。
另一个问题是,整型还有short/long的区别,我们不得不再引入一个字符来表示比如h和l分别表示short int和long int,这时接口已经变成了下面这样,相当接近scanf了。X可能有时可能是两个字符,有时可能是一个。
int readX (char* x, void*d);
想想如果用函数接口,我们需要多少个接口?事实上,我们还有更复杂的情况,如有符号和无符号整数,不同的浮点数表示法,这就是一个组合爆炸。
下表解析了标准库中已经定义的格式转换,外部表示法指示不同的外部数据形式,数据类型说明了保存数据需要的指针类型,空白表示没有这个组合。hh,ll,j,z,t都是C99才引入的。
|
外部表示法 |
||||||
|
数据类型 |
d i |
u o x |
f e g a |
c s [] [^] |
p |
n |
|
(none) |
int* |
unsigned int* |
float* |
char* |
void** |
int* |
|
hh |
signed char* |
unsigned char* |
signed char* |
|||
|
h |
short int* |
unsigned short int* |
short int* |
|||
|
l |
long int* |
unsigned long int* |
double* |
wchar_t* |
long int* |
|
|
ll |
long long int* |
unsigned long long int* |
long long int* |
|||
|
j |
||||||
|
z |
||||||
|
t |
||||||
|
L |
long double* |
|||||
灵活性不易得
有几个常见的情况我们不得不注意,一是数据前后的空白字符,二是分隔符问题。空白字符其实很好解决,只要在读取数据前跳过空白的字符就好了,但是当空白字符也是数据时,这个问题就难办了。这时就需要一个明确的说明,哪此空白字符是需要跳过,哪些需要读取。从可读性来说,我们可以用一个空格来表示,这里要跳过空白字符,用c来表示,这里要读取字符,无论有多少个。例如,"ldc hd"可以表示,先读取一个long int,再读取一个字符,跳过后面的空白,再读取一个short int。为了方便发现我们到底要传入几个数据指针,我们用%和*来标记,%要保存到数据,*不保存数据,上面的例子变成%ld*c %hd。
经过对空白字符的处理另一个问题也很好解决了, 我们只要把分隔符原样写出来,让它们表示这里要读取对应的符号就可以,如以逗号分隔,即可以写成"ld,"。不过,对于cdfosx这几个已经有含义的字符同,我们需要特殊处理,可以通过前面的%来区别,因此也需要为*c加上一个前缀%。
有了这个模式后,我们再加入一些新的特性就比较简单了,如我们可以要求一个数据只解析一定长度,即宽度限制,这个写在%以后即可。如%3d%3d可以解析315248为315和248。
不过在C语言的标准定义中,数值会自动跳过前导空白字符,但不会跳过数值后面的空白。
最特别是的,如果以%为分隔符,我们需要在格式中说明%%。
解决了空白字符后,对于字符串中的空白我们还不好解决,这时通过引入一个新的外部表示,表示可以包括空白的字符串。事实上,标准中定义了两种特别字符串,一是只能包含一些字符的字符串,另一种是除了一些字符不包含外,其它字符都可以的字符串,分别是[]和[^]。使用时在中括号中写上可以包含或需要禁止的那些字符。
超越C语言标准
特别说明,以下不是C语言标准实现,不能直接使用。需要使用xscanf这个库。
沿着说明式接口这条路,我们还可以走的更远一点,比如引入数组。数组是重复同一个说明符号,简单的方式我们引入#,如#%d,即可完成读取一串整数,直到行尾,这个最常用的情况。
int n, a[32];
xscanf("#32%d", &n, a);
上面的代码,会读取一行上最多32个整型,实际读取了多个数保存在n中。
下面是完成同样功能的代码
char line[];
int off = , read; fgets(line, sizeof(line), stdin); *n = ;
while (sscanf(line + off, "%d%n", a + *n, &read) > ) {
++*n; off+=read;
}
从scanf的学习接口设计的更多相关文章
- RESTful接口设计原则/最佳实践(学习笔记)
RESTful接口设计原则/最佳实践(学习笔记) 原文地址:http://www.vinaysahni.com/best-practices-for-a-pragmatic-restful-api 1 ...
- Verilog学习笔记简单功能实现(七)...............接口设计(并行输入串行输出)
利用状态机实现比较复杂的接口设计: 这是一个将并行数据转换为串行输出的变换器,利用双向总线输出.这是由EEPROM读写器的缩减得到的,首先对I2C总线特征介绍: I2C总线(inter integra ...
- Web API接口设计(学习)
1.在接口定义中确定MVC的GET或者POST方式 由于我们整个Web API平台是基于MVC的基础上进行的API开发,因此整个Web API的接口,在定义的时候,一般需要显示来声明接口是[HttpG ...
- 优秀的API接口设计原则及方法(转)
一旦API发生变化,就可能对相关的调用者带来巨大的代价,用户需要排查所有调用的代码,需要调整所有与之相关的部分,这些工作对他们来说都是额外的.如果辛辛苦苦完成这些以后,还发现了相关的bug,那对用户的 ...
- WCF从零学习之设计和实现服务协定2
WCF从零学习之设计和实现服务协定(二) 在创建服务协定之前,有很多WCF术语,比如: 消息.服务.终结点 创建协定 类或接口都可以定义服务协定,建议使用接口,因为接口可以直接对服务协定建模 服务 ...
- Spring+SpringMVC+MyBatis+easyUI整合进阶篇(二)RESTful API实战笔记(接口设计及Java后端实现)
写在前面的话 原计划这部分代码的更新也是上传到ssm-demo仓库中,因为如下原因并没有这么做: 有些使用了该项目的朋友建议重新创建一个仓库,因为原来仓库中的项目太多,结构多少有些乱糟糟的. 而且这次 ...
- 【老司机经验】CC2530&STM8S105二合一嵌入式学习板设计思路与经验分享
CC2530&STM8S105二合一嵌入式学习板设计思路与经验分享 1.缘起 这些年来一直在其他公司的实验箱和别人的开发板上进行教学与开发工作,总是觉得功能设计不那么合意.心里突然冒出个 ...
- 开源IM项目-InChat登录接口设计与实现(基于Netty)
- RESTful API实战笔记(接口设计及Java后端实现)
写在前面的话 原计划这部分代码的更新也是上传到ssm-demo仓库中,因为如下原因并没有这么做: 有些使用了该项目的朋友建议重新创建一个仓库,因为原来仓库中的项目太多,结构多少有些乱糟糟的. 而且这次 ...
随机推荐
- 《深入浅出嵌入式底层软件开发》—1. ARM汇编编程基础
1.1 ARM CPU寄存器 ARM的汇编编程,本质上就是针对CPU寄存器的编程,所以要搞清楚ARM有哪些寄存器:ARM寄存器分为两类:普通寄存器和状态寄存器:普通寄存器一共有16个,分别为R0——R ...
- python练习程序(c100经典例4)
题目: 输入某年某月某日,判断这一天是这一年的第几天? def judge_run(year): a=year/4.0; b=year/100.0; c=year/400.0; if a==int(a ...
- Android Studio 学习 - 基本控件的使用;Intent初学
Android Studio学习第三天. 今天主要学习 1. RadioButton.CheckBox.RatingBar.SeekBar等基础控件的使用. 结合Delphi中相类似的控件,在这些基本 ...
- HDU 4741
获得 新的模板了/// 此模板 有线段和线段的最短距离方法,同时包含线段与线段的最短距离:#include<iostream> #include<stdio.h> #inclu ...
- Android 网络流量监听开源项目-ConnectionClass源码分析
很多App要做到极致的话,对网络状态的监听是很有必要的,比如在网络差的时候加载质量一般的小图,缩略图,在网络好的时候,加载高清大图,脸书的android 客户端就是这么做的, 当然伟大的脸书也把这部分 ...
- 更新Code First生成的数据库
1,首次访问时会自动生成数据库 2,某个Model增加一个字段后,再次访问会报,数据库不是最新 操作 1,Enable-Migrations 注意选择Default project为Star.Core ...
- myeclipse9 struts2配置
引用struts2所用到的jar web.xml配置如下 <?xml version="1.0" encoding="UTF-8"?> <we ...
- WPF:百度百科
WPF http://baike.baidu.com/view/292311.htm
- linux实现自动远程备份(scp+ssh)
刚上线的服务器需要备份日志,要备份到另一台服务器上去,为了减少工作量,采用linux的定时任务去自动执行.因服务器都是linux的,因此采用linux的远程复制scp命令.但这里涉及到一个问题,就是s ...
- Markdown使用记录
1,打开Markdown文件夹(已经解压好了) 2,运行markdownPad2.exe. 3,然后工具-选项-程序语言 改为中文. 4,帮助-Markdown激活. 5,打开激活工具,KG_tt7z ...