• 从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;
        }
  • 相关阅读:
    VC combobox
    myitoa()函数
    什么是虚拟显示
    一些itoa()函数
    C语言运算符优先级
    一些小问题
    return 语句会发生的错误
    Sizeof与Strlen的区别与联系
    用异或运算符实现不用第三个临时变量交换两个变量的值
    字符串化运算符#
  • 原文地址:https://www.cnblogs.com/ahuangliang/p/scanf_api.html
Copyright © 2020-2023  润新知