对大多数程序员来说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

intmax_t*

uintmax_t*

     

intmax_t*

z

size_t*

size_t*

     

size_t*

t

ptrdiff_t*

ptrdiff_t*

     

ptrdiff_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的学习接口设计的更多相关文章

  1. RESTful接口设计原则/最佳实践(学习笔记)

    RESTful接口设计原则/最佳实践(学习笔记) 原文地址:http://www.vinaysahni.com/best-practices-for-a-pragmatic-restful-api 1 ...

  2. Verilog学习笔记简单功能实现(七)...............接口设计(并行输入串行输出)

    利用状态机实现比较复杂的接口设计: 这是一个将并行数据转换为串行输出的变换器,利用双向总线输出.这是由EEPROM读写器的缩减得到的,首先对I2C总线特征介绍: I2C总线(inter integra ...

  3. Web API接口设计(学习)

    1.在接口定义中确定MVC的GET或者POST方式 由于我们整个Web API平台是基于MVC的基础上进行的API开发,因此整个Web API的接口,在定义的时候,一般需要显示来声明接口是[HttpG ...

  4. 优秀的API接口设计原则及方法(转)

    一旦API发生变化,就可能对相关的调用者带来巨大的代价,用户需要排查所有调用的代码,需要调整所有与之相关的部分,这些工作对他们来说都是额外的.如果辛辛苦苦完成这些以后,还发现了相关的bug,那对用户的 ...

  5. WCF从零学习之设计和实现服务协定2

    WCF从零学习之设计和实现服务协定(二)   在创建服务协定之前,有很多WCF术语,比如: 消息.服务.终结点 创建协定 类或接口都可以定义服务协定,建议使用接口,因为接口可以直接对服务协定建模 服务 ...

  6. Spring+SpringMVC+MyBatis+easyUI整合进阶篇(二)RESTful API实战笔记(接口设计及Java后端实现)

    写在前面的话 原计划这部分代码的更新也是上传到ssm-demo仓库中,因为如下原因并没有这么做: 有些使用了该项目的朋友建议重新创建一个仓库,因为原来仓库中的项目太多,结构多少有些乱糟糟的. 而且这次 ...

  7. 【老司机经验】CC2530&STM8S105二合一嵌入式学习板设计思路与经验分享

    CC2530&STM8S105二合一嵌入式学习板设计思路与经验分享 1.缘起    这些年来一直在其他公司的实验箱和别人的开发板上进行教学与开发工作,总是觉得功能设计不那么合意.心里突然冒出个 ...

  8. 开源IM项目-InChat登录接口设计与实现(基于Netty)

  9. RESTful API实战笔记(接口设计及Java后端实现)

    写在前面的话 原计划这部分代码的更新也是上传到ssm-demo仓库中,因为如下原因并没有这么做: 有些使用了该项目的朋友建议重新创建一个仓库,因为原来仓库中的项目太多,结构多少有些乱糟糟的. 而且这次 ...

随机推荐

  1. 【Android】SDK工具学习 - Traceview 和 dmtracedump

    dmtracedump官方文档 Traceview 根据程序的log,形成图形 dmtracedump [-ho] [-s sortable] [-d trace-base-name] [-g out ...

  2. 剑指offer—算法之位运算(二进制中1的个数)

    位运算: 左移:m<<n将m左移n位,左移后低位补充0: 右移:m>>n将m右移n位,右移后高位补充的是符号位,负数补充1,整数补充0.(正数的边界值为(1,ox7FFFFFF ...

  3. When not to automate 什么时候不进行自动化

    The cornerstone of test automation is the premise that the expected application behavior is known. W ...

  4. jquery元素定位方法

    用chrome浏览器打开页面,按f12调出开发者调试模式,查看elements,部分代码如下图所示,注意红框部分 假设我们要查找某些元素的位置,用鼠标移到那部分元素,调试器会自动用蓝颜色标示选中部分的 ...

  5. linux_2015_0827_linux中一些常用词的发音and…

    linux相关 Unix: [ ju:niks ] 发音 (yew-nicks) 尤里克斯 GNU [ gəˈnju: ] 发音 (guh-noo) 葛扭 Linux: [ 'li:nэks ] 里那 ...

  6. 精品手游《里奥的财富》高清版逆向移植家用机与PC平台(转)

    冒险动作游戏<里奥的财富>于去年10月登陆移动平台,曾荣获App Store“年度优秀游戏”.开发商宣布将推出其HD版本,近期会陆续登陆PS4.PC.MAC.Xbox One平台. 由瑞典 ...

  7. 一步步实现Promise

    最近在https://github.com/ThoughtWorksInc/rest-rpc上工作,遇到了一个scala隐式转换的问题,简单的说是要实现这么个东西: implicit def json ...

  8. c语言的几个重要知识点

      内存结构 这是核心中的核心,请仔细看完,充分理解,否则请不要看下一节内容. 每个程序一启动都有一个大小为4GB的内存,这个内存叫虚拟内存,是概念上的,真正能用到的,只是很小一部分,一般也就是在几百 ...

  9. [转] Web前端优化之 Javascript篇

    原文链接: http://lunax.info/archives/3099.html Web 前端优化最佳实践之 JavaScript 篇,这部分有 6 条规则,和 CSS 篇 重复的有几条.前端优化 ...

  10. hadoop的ganglia数据监控

    如果我们想知道当前运行的hadoop集群的状态,可以通过hadoop的客户端和web页面来获得,但是如果我们想知道当前集群的繁忙程度,如读写次数,这些工具就办不到了.幸运的是hadoop提供了一种ga ...