从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

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[1024];
    int off = 0, read;

    fgets(line, sizeof(line), stdin);

    *n = 0;
    while (sscanf(line + off, "%d%n", a + *n, &read) > 0) {
        ++*n; off+=read;
    }

 

posted on 2014-04-02 23:04  Anthony-黄亮  阅读(1018)  评论(1编辑  收藏  举报

导航