Scanf 和一个手动函数一起工作时获取字符串

Scanf and a manual function to get string when they work together

我在使用 scanf 和手动获取输入字符串的函数时遇到问题。

这是我在输入中获取一行字符串的手动函数(我也得到了 [nl] 字符):

void getln(char *a) {
    int i,c;
    i=0;
    do {
        c=getchar();
        a[i]=(char)c;
        i++;
    } while(c!='\n');
}

然后,我这样使用它 (char hs.school[40]; char hs.pc[20]; int hs.age;):

printf("Import age: ");
scanf("%d",&hs.age);
printf("Import personal code: ");
getln(hs.pc);
printf("Import school: ");
getln(hs.school);

输出:

Import age: 18
Import personal code: Import school: Vo Thi Sau

为什么在 scanf 调用之后的 getln 调用被忽略? (不过下一个getln效果不错) 你能解释一下细节并建议我如何修复这个错误吗?谢谢!

已编辑: 这是我的完整代码,接受用户输入并将输入导出回屏幕,这是 运行 在我做了一些小技巧之后,但我决定做一个问题,主要是为了扩充自己的知识^_^ 谢谢大家的解答

#include<stdio.h>

void getln(char *);
void putstr(char *);

int main(void) {
    struct Student {
        struct Fullname {
            char first[10],middle[20],last[10];
        }fu;
        struct Native {
            char social[30],district[30],province[30];
        }na;
        struct Score {
            double maths,physics,chemistry;
        }sc;
        char pc[20],school[40];
        int age;
    }hs;
    printf("Import stage:\n");
    printf("- Import full name:\n");
    printf("++ First name: ");
    getln(hs.fu.first);
    printf("++ Middle name: ");
    getln(hs.fu.middle);
    printf("++ Last name: ");
    getln(hs.fu.last);
    printf("- Import native living place:\n");
    printf("++ Social: ");
    getln(hs.na.social);
    printf("++ District: ");
    getln(hs.na.district);
    printf("++ Province: ");
    getln(hs.na.province);
    printf("- Import school: ");
    getln(hs.school);
    printf("- Import personal code: "); // I have done a little trick
    getln(hs.pc);                       // before I post the question,
    printf("- Import age: ");           // which swaped these two stage,
    scanf("%d",&hs.age);                // but it's works like a charm ^_^
    printf("- Import scores:\n");
    printf("++ Mathematics: ");
    scanf("%lf",&hs.sc.maths);
    printf("++ Physics: ");
    scanf("%lf",&hs.sc.physics);
    printf("++ Chemistry: ");
    scanf("%lf",&hs.sc.chemistry);
    printf("\nExport stage:\n");
    printf("- Full name: ");
    putstr(hs.fu.first);
    printf(" ");
    putstr(hs.fu.middle);
    printf(" ");
    putstr(hs.fu.last);
    printf(".\n");
    printf("- Native living place: ");
    putstr(hs.na.social);
    printf(", ");
    putstr(hs.na.district);
    printf(", ");
    putstr(hs.na.province);
    printf(".\n");
    printf("- School: ");
    putstr(hs.school);
    printf(".\n");
    printf("- Personal code: ");
    putstr(hs.pc);
    printf(".\n");
    printf("- Age: %d.\n",hs.age);
    printf("- Scores (Mathematics, Physics, Chemistry): %.2lf, %.2lf, %.2lf.\n",hs.sc.maths,hs.sc.physics,hs.sc.chemistry);
    return 0;
}

void getln(char *a) {
    int i,c;
    i=0;
    do {
        c=getchar();
        a[i]=(char)c;
        i++;
    } while(c!='\n');
}
void putstr(char *a) {
    int i;
    i=0;
    while(a[i]!='\n') {
        putchar(a[i]);
        i++;
    }
}

您没有清除输入缓冲区。所以在这个换行符将放在给定 scanf 的第一个输入之后。所以 getchar 将获取新行作为输入。所以循环将退出。

scanf之后使用这一行。

int c;
if ( scanf("%d",&hs.age) != 1 ) { 
       printf("Invalid Input\n");retrun 0; }

while((c=getchar()) != '\n' && c != EOF );

它将清除输入缓冲区。然后它会询问用户的第二个输入。

输入 hs.age 后,您按下了 Enter,这是一个 \n 字符。所以你的 getln() 被调用了,但是循环在一次迭代后就被打破了,因为 c 包含 '\n'。如果您打印 hs.pc,您的输出屏幕中将有一个新行。

你的 getln 调用没有被跳过,它把留在 stdin(输入缓冲区)中的 newline 字符作为它的输入,它读取 '\n'作为 c,将其分配给 a[i],检查 c 是否为 '\n' 字符并退出。

要解决最初的问题,您需要在调用getln之前清除输入缓冲区。您可以按照另一个答案中的建议使用 while 循环来执行此操作,或者您可以为 scanf 制作一个适当的格式字符串,它将消耗换行符,清空缓冲区。 (并非万无一失),但替代方案 scanf 是:

scanf(" %d%*c",&hs.age);

这将跳过数字前的所有空格(包括任何换行符),读取十进制值,然后读取并丢弃换行符。 注意:这仅适用于没有尾随字符的数字。输入 13abc 将在 stdin 中留下 bc\n。这种情况下的 while 循环更灵活,因为它读取所有字符直到遇到换行符,这可能是更好的选择:

scanf(" %d",&hs.age);
while ((c = getchar()) != '\n' && c != EOF);

至于你的getln函数,只需要将每个字符读入a[i]即可。没有真正需要 c。您还需要对您的输入进行相同的检查,以便不留下换行符。您还需要根据 a 减 1 的最大长度检查 i。我建议您输入的最大字符串长度为 #define MAXS 128。这将允许测试 i 以防止写入超出字符串末尾的内容。

这是您的 getln 的替代方案。 注意:它是 int 类型,允许它 return 读取的行的长度,因此您可以确定在达到 MAXS 时要做什么(因为那时 stdin 中仍然会有字符)。作为一般规则,如果您在可能出错的情况下在函数中执行某些操作,最好 return 一个指示 success/failure/problem:

的值
#define MAXS 128
...
int getln (char *a) 
{
    size_t i = 0;
    while ((a[i] = getchar()) != '\n' && a[i] != EOF)
    {
        i++;
        if (i == MAXS - 1)
        {
            a[i] = 0;
            break;
        }
    }

    return i;
}

Can you explain me the details ...

我会尽量不使用混淆术语,例如 buffer

您可能已经知道 "%d" 对应于一组转换为 int 的十进制数字字符。当您按照其他人的建议按 'Enter' 时,'\n' 字符将通过 stdin 传输。 '\n' 不是十进制数字字符,因此它会放回流中供您的 getln 函数稍后发现...

实际上,您的 "getln call right after scanf call" 可能 没有被忽略; 可能 只是阅读尾随 '\n' 并看到一个空行。

这是假设其他问题没有出现。 getln 看不到 a 指向多少字节,所以它无法判断它何时会溢出,因此没有尝试防止缓冲区溢出......你基本上重写了 gets。如果您的输入足够长,那么我想这也可能导致您的问题......缓冲区溢出是未定义的行为,使用未定义行为的后果是 undefined.

关于未定义行为的主题,由于 getln 在技术上不生成字符串,我希望您以后不要将其用作标准字符串函数的输入...

关于未定义行为的主题,如果用户输入的不是一组十进制数字,您认为会发生什么? scanf 通过 return 值传达输入错误...所以 永远不要忽略 return 值 。您可以(并且应该在某个时候)在 the scanf manual.

中找到更多相关信息

... and suggest me how to fix this bug.

丢弃用户输入没有多大意义,但不幸的是,您不能指望一种解决方案可以在不使代码大小(和此解释)超出比例的情况下带来更好的用户体验。


您可以使用 scanf 丢弃十进制数字集后面的行的其余部分(可能只是 '\n'),如下所示:scanf("%*[^\n]"); getchar();... "%d" scanf 调用,当然......你甚至可以将两者合并在一起,如下所示:

if (scanf("%d%*[^\n]", &hs.age) != 1) {
    puts("ERROR: EOF or file access error.");
    exit(0);
}
getchar();

不幸的是,如果您的用户使用空格键而不是输入键,他或她可能不会发现这方面的问题,直到为时已晚...


关于缓冲区溢出问题,我建议使用fgets而不是getsfgets 也有失败模式,通过 return 值和数组的内容传达。 return 值用于传达 EOF 和文件访问错误,return 值中是否存在 '\n' 用于传达何时输入行太大,无法存储在数组中。我们可以通知用户溢出(我相信他们会很感激)并使用之前使用的相同 scanf 技巧丢弃多余的...

if (fgets(hs.pc, sizeof hs.pc, stdin) == NULL) {
    puts("ERROR: EOF or file access error.");
    exit(0);
}

size_t size = strcspn(hs.pc, "\n");
if (hs.pc[size] != '\n') {
    printf("WARNING: MAXIMUM SIZE OF %zu EXCEEDED! LINE TRUNCATED.\n", sizeof hs.pc - 1);
    scanf("%*[^\n]");
    getchar();
}

hs.pc[size] = '[=11=]';

我认为将这些解决方案包装到函数中是有意义的,只是这些函数随后会促进用户输入的丢弃。尽管如此,后一个足够长,您很可能会从抽象中受益...

void getln(char *a, size_t a_size) {
    if (fgets(a, a_size, stdin) == NULL) {
        puts("ERROR: EOF or file access error.");
        exit(0);
    }

    size_t size = strcspn(a, "\n");
    if (a[size] != '\n') {
        printf("WARNING: MAXIMUM SIZE OF %zu EXCEEDED! LINE TRUNCATED.\n", a_size - 1);
        scanf("%*[^\n]");
        getchar();
    }

    a[size] = '[=12=]';
}

... 现在您可以像这样使用它:getln(hs.pc, sizeof hs.pc);