scanf()函数的真实含义及其正确用法

本文章翻译自公开外文,请谨慎转载。

0. scanf()函数有什么问题?

规则0: 不要使用scanf()(除非你知道你在干什么)

1. 我想从用户那读入一个数

下面是一个常见的用法:

#include 

int a;
scanf("%d", &a);
printf("你输入的数字是:%d\n", a);

你在写这段代码时大概应该知道“%d”是表示把输入内容转换为整数,所以你或许认为这个代码是没有问题的。确实,假如你输入42,输出结果的确为“你输入的数字是:42”;但假如你输入的是“abcdefgh”呢?你会发现结果居然是“38”。

38是从tm哪来的?答案是:如果你输入非数字的话,那么输出的内容可能是任意值,甚至程序可能会直接崩溃掉。因为scanf()是用于转换一个数字,但用户输入的却不是数字,所以它实质上没有转换任何东西。

因此,变量'a'压根就没有被执行写操作,而我们在printf中试图读取一个没有被写入初值的变量,这种行为显然是非法的。

现在我们尝试来修复上述错误。我们知道scanf()函数的返回值是被成功转换的对象的个数,所以我们的第一个想法是加入一个复审的环节来防止用户输入一些别的东西。

while(scanf("%d", &a) != 1){
    //输入不是数字,请重新输入
    printf("enter a number: ");
}
printf("你输入的数字是 %d", a);

运行起来试一下,输入abc:结果是“enter a number: enter a number: enter a number: enter a number: enter a number: enter a number: enter a number: enter a number: enter a number: enter a number: enter a number: enter a number: enter a number: enter a..."

吓得赶紧用ctrl+c停止程序。为什么会出现这种结果呢?

请记住下面这条规则:

规则1: scanf()不是用于读入输入的,而是用于解析输入的。

scanf()的第一个参数是一个string,用来告诉scanf()应该解析什么样的东西。重点在于:scanf()不会读入任何它无法解析的东西。在我们的例子中,我们使用了“%d”来告诉scanf去解析一个数字。显然,abc不是数字,因此,abc压根没有被读入,仍然保留在输入缓冲中。下一次循环,scanf()仍然能访问到我们的未被读入的abc,但再一次无法解析。

那么你又想了,我们能否将未被读入的数据先清零呢?这样下一次循环时scanf()就无法读到内容了,就必须等待用户直到用户输入。所以你试图采用下面的做法:

fflush(stdin);

求求你,千万别试图采用这种做法!在c语言中,flushing一个输入流是未定义的行为。所以,唯一能清除一个未读数据的方法就是把它读掉。当然,我们可以采用一个读入任意内容的scanf()来把它读掉,这应该很简单。

2. 我想从用户那读入一个字符串

另一种常见用法:

char name[12];
printf("what is your name?");
scanf("%s", name);
printf("your name is %s\n", name);

%s用于解析字符串,所以输入任何内容应该都奏效。

但上述代码的问题在于容易造成溢出。scanf()并不知道什么时候应该停止读入,所以只要它还能解析,就会一直读入并写到name中,哪怕超出了声明的name的大小也不停下。

因此,请记住下面这条规则:

规则2: 使用scanf()解析string时,请指定字串宽度。

char name[40];
scanf("%39s", name);

%39s会告诉scanf()至多从输入中解析39个字符。注意,还得留一个位置来存入\0字符,它表示字符串结束。当scanf()结束解析时,会在被写入的变量尾部自动加上\0。

再试一下,输入Matin Brown,结果为:“your name is Matin”

为什么没有读入Brown呢?我们翻一下手册,会发现:%s其实只能解析一个word,而不是一个string。从输入中解析一个“string”的问题在于,它不知道string结束的标志是什么。如果使用%s的话,其实是告诉scanf()一旦遇到空白就停止解析。如果你想达到别的效果的话,请使用%[:

* %[a-z]:只要输入位于a-z就继续解析

*%[^.]:只要输入中没有.就继续解析

所以记住下面这条:

规则3: 虽然scanf()的格式字串和printf()的很像,但常常有不同的含义(请阅读其手册)

所以你可以这样写:

scanf("%39[^\n]", name);

这样,告诉scanf,只要输入不是\n就继续解析。所以只有输入了回车才会停止解析。

但遗憾的是,上述代码仍然有bug。假如你直接输入回车键,会产生什么效果呢?你会发现输出结果是:“your name is y|n”,输出了一个莫名其妙的名字。

原因:手册规定,使用了[ 指定转换后,scanf()不会跳过前导空白,所以不会跳过我们输入的\n符号;而它又不匹配我们指定的格式(我们的格式是:只解析非回车的字符)。所以最终的结果是,scanf()没有解析任何东西,没有向name中写入任何内容。

一种解决办法是,告诉scanf()跳过空白。

char name[40];
scanf(" %39[^\n]", name);    //注意%39前的空白,它可以匹配任何空白

注:上面这段我不是很理解,我在mac上测试了一下,直接输入回车并没有发生错误。可能跟具体库函数有关。

3. 好吧,我只是想从用户那读入一些东西而已

在C中用于读入数据的函数有几个,最常用的函数:fgets()

fgets()的功能很简单,它读入指定长度的字符,或者在遇到新行的时候停止(注意:会读入换行符)。换言之,它读入一行内容。

char* fgets(char* str, int n, FILE* stream)

这个函数有几点好处:

1. 最大字符长度参数已经考虑了需要添加的\0,所以我们只需传入我们的变量的长度40即可

2. 返回值是个指向str的指针,或指向NULL的指针(如果因某种原因没有读入任何东西)

现在可以重写上面的代码了:

char name[40];
if(fgets(name, 40, stdin)){
    printf("your name is %s!\n", name);
}

但上述代码还有点小问题,它在输入!前会先换行。这其实是因为fgets()把换行符一起读进来了。

我们可以使用string.h提供的strcspn()函数来获取换行符在字串中的索引并用0来覆盖。

if(fgets(name, 40, stdin)){
    name[strcspn(name, '\n')]=0;
    printf("your name is %s!\n", name);
}

4. 如果不使用scanf()的话该怎么读入数字?

在C中有很多函数可用于将字符转换为数字。最常用的一个是atoi()函数,名字的含义是anything to integer。它在转换失败时会返回0.

现在可以重写之前的函数了:

int main(){
    int a;
    char buf[1024];
    do{
        printf("enter a number: ");
        if(!fgets(buf, 1024, stdin)){
            //读入数据失败,放弃
            return 1;
        }
        //有一些输入, 将其转换为整数
        a = atoi(buf);
    } while(a == 0);    //重复,直到我们得到一个有效的数字
}

但atoi()的问题在于:

1)假如我们想输入的就是0数字呢?我们无法区分atoi()返回的0到底是因为它就是读入了0,还是因为它转换失败了返回的0.

2)假如输入的是15xsd,那么它会转换得到数字15,而忽略掉其他的字符,这可能不是我们想达到的效果。

如果你想规避这种错误,那么有个更好的选择:strtol()函数:

long int strtol(const char* nptr, char** endptr, int base);

参数:

1)endptr:它被设置为指向第一个不能被转换的字符。

2)base:用于指定读入数字的基,多数情况下是10,但如果你想读入二进制数据的话,那么应该给2

3)这个函数甚至提供了errno,所以你可以检查一下待转换的数字是不是太长或太短

改一下上面的代码:

#include 
#include 
#include 

long a;
char buf[1024];
int success;
do{
    printf("enter a number: ");
    if(!fgets(buf, 1024, stdin)){
        return 1;
    }
    //有一些输入,将其转换为整数 
    char* endptr;
    errno = 0;
    a = strtol(buf, &endptr, 10);
    if(errno == ERANGE){
        printf("sorry, this number is too small or too large");
        success = 0;
    }
    else if(endptr == buf){
        //没有字符读入    
        success = 0;
    }
    else if(*endptr && *endptr != '\n'){
        //既不是字符结束也不是新行,所以我们没有转换"全部"输入
        success = 0;
    }
    else
        success = 1;
}while(!success)

printf("your entered %ld", a);

5. 但我能不能修复之前使用scanf()的例子呢?

是的,你可以,这里是最后一个规则:

规则 4: scanf()是个非常强大的函数

下面直接给出例子: 

int main(){
    int a;
    int rc;
    printf("enter a number: ");
    while((rc = scanf("%d", &a)) == 0){    //既没有成功(1),也不是EOF
        //清除掉剩下的内容,*表示匹配并抛弃
        scanf("%*[^\n]");
        //输入不是数字,再次询问
        printf("enter a number: ");
    }
    if(rc == EOF){
        printf("没有更多可读内容,且没有找到任何数字");
    }
    else{
        printf("你输入的是:%d", a);
    }
}
int main(){
    char name[40];
    if(scanf("%39[^\n]%*c", name) == 1)    //只希望有一次转换
        printf("your name is %s", name);
}

 

你可能感兴趣的:(scanf()函数的真实含义及其正确用法)