C – как обрабатывать ввод пользователя во время цикла

Я новичок в C, и у меня есть простая программа, которая требует некоторого ввода пользователем внутри цикла while и завершает работу, если пользователь нажимает «q»:

while(1) { printf("Please enter a choice: \n1)quit\n2)Something"); *choice = getc(stdin); // Actions. if (*choice == 'q') break; if (*choice == '2') printf("Hi\n"); } 

Когда я запускаю это и нажимаю «q», программа делает это правильно. Однако, если я нажимаю ‘2’, программа сначала выводит «Привет» (как следует), но затем продолжает печатать приглашение «Пожалуйста, выберите вариант» дважды. Если я введю N символов и нажмите Enter, запрос будет распечатан N раз.

Такое же поведение происходит, когда я использую fgets () с лимитом 2.

Как заставить этот цикл работать правильно? Он должен принимать только первый символ ввода, а затем делать что-то один раз в соответствии с тем, что было введено.

РЕДАКТИРОВАТЬ

Таким образом, использование fgets () с большим буфером работает и останавливает повторную подсказку:

 fgets(choice, 80, stdin); 

Это помогло: как очистить входной буфер в C?

Когда вы getc входные данные, важно отметить, что пользователь добавил более одного символа: по крайней мере, stdin содержит 2 символа:

 2\n 

когда getc получает «2», введенный пользователем, конечный символ \n все еще находится в буфере, поэтому вам придется его очистить. Самый простой способ сделать это – добавить это:

 if (*choice == '2') puts("Hi"); while (*choice != '\n' && *choice != EOF)//EOF just in case *choice = getc(stdin); 

Это должно исправить это

Для полноты:
Обратите внимание, что getc возвращает int, а не char . Обязательно скомпилируйте флаги -Wall -pedantic и всегда проверяйте тип возвращаемых функций, которые вы используете.

Заманчиво очистить входной буфер, используя fflush(stdin); , и в некоторых системах это будет работать. Однако: это поведение не определено: в стандарте четко указано, что fflush предназначен для использования в буферах обновления / вывода, а не для входных буферов :

C11 7.21.5.2 Функция fflush, fflush работает только с streamом вывода / обновления, а не с streamом ввода

Однако некоторые реализации (например, Microsoft ) поддерживают fflush(stdin); как расширение. Опираясь на это, однако, идет вразрез с философией, лежащей в основе C. C должен был быть переносимым, и, придерживаясь стандарта, вы уверены, что ваш код переносимый. Опора на определенное расширение отнимает это преимущество.

То, что кажется очень простой проблемой, на самом деле довольно сложно. Корень проблемы заключается в том, что терминалы работают в двух разных режимах: сырые и приготовленные. Режим приготовления, который является значением по умолчанию, означает, что терминал не читает символы, он читает строки. Таким образом, ваша программа никогда не получает никакого ввода вообще, если не введена целая строка (или получен конец символа файла). То, как терминал распознает конец строки, – это получить символ новой строки (0x0A), который может быть вызван нажатием клавиши Enter. Чтобы сделать его еще более запутанным, на машине Windows нажатие Enter вызывает создание двух символов (0x0D и 0x0A).

Итак, ваша основная проблема заключается в том, что вам нужен односимвольный интерфейс, но ваш терминал работает в линейно-ориентированном (приготовленном) режиме.

Правильное решение состоит в том, чтобы переключить терминал в режим raw, чтобы ваша программа могла получать символы, как пользователь их вводит . Кроме того, я бы рекомендовал использовать getchar() вместо getc() в этом использовании. Разница в том, что getc () принимает дескриптор файла в качестве аргумента, поэтому он может читать из любого streamа. Функция getchar() считывает только стандартный ввод, который вы хотите. Поэтому это более конкретный выбор. После того, как ваша программа будет завершена, он должен переключить терминал обратно так, как он был, поэтому перед его модификацией необходимо сохранить текущее состояние терминала.

Кроме того, вы должны обрабатывать случай, когда терминал EOF (0x04) принимается терминалом, который пользователь может сделать, нажав CTRL-D.

Вот полная программа, которая выполняет следующие действия:

 #include  #include  main(){ tty_mode(0); /* save current terminal mode */ set_terminal_raw(); /* set -icanon, -echo */ interact(); /* interact with user */ tty_mode(1); /* restore terminal to the way it was */ return 0; /* 0 means the program exited normally */ } void interact(){ while(1){ printf( "\nPlease enter a choice: \n1)quit\n2)Something\n" ); switch( getchar() ){ case 'q': return; case '2': { printf( "Hi\n" ); break; } case EOF: return; } } } /* put file descriptor 0 into chr-by-chr mode and noecho mode */ set_terminal_raw(){ struct termios ttystate; tcgetattr( 0, &ttystate); /* read current setting */ ttystate.c_lflag &= ~ICANON; /* no buffering */ ttystate.c_lflag &= ~ECHO; /* no echo either */ ttystate.c_cc[VMIN] = 1; /* get 1 char at a time */ tcsetattr( 0 , TCSANOW, &ttystate); /* install settings */ } /* 0 => save current mode 1 => restore mode */ tty_mode( int operation ){ static struct termios original_mode; if ( operation == 0 ) tcgetattr( 0, &original_mode ); else return tcsetattr( 0, TCSANOW, &original_mode ); } в #include  #include  main(){ tty_mode(0); /* save current terminal mode */ set_terminal_raw(); /* set -icanon, -echo */ interact(); /* interact with user */ tty_mode(1); /* restore terminal to the way it was */ return 0; /* 0 means the program exited normally */ } void interact(){ while(1){ printf( "\nPlease enter a choice: \n1)quit\n2)Something\n" ); switch( getchar() ){ case 'q': return; case '2': { printf( "Hi\n" ); break; } case EOF: return; } } } /* put file descriptor 0 into chr-by-chr mode and noecho mode */ set_terminal_raw(){ struct termios ttystate; tcgetattr( 0, &ttystate); /* read current setting */ ttystate.c_lflag &= ~ICANON; /* no buffering */ ttystate.c_lflag &= ~ECHO; /* no echo either */ ttystate.c_cc[VMIN] = 1; /* get 1 char at a time */ tcsetattr( 0 , TCSANOW, &ttystate); /* install settings */ } /* 0 => save current mode 1 => restore mode */ tty_mode( int operation ){ static struct termios original_mode; if ( operation == 0 ) tcgetattr( 0, &original_mode ); else return tcsetattr( 0, TCSANOW, &original_mode ); } 

Как вы можете видеть, сложная задача довольно сложная.

Книга, которую я могу рекомендовать для навигации по этим вопросам, – «Понимание программирования Unix / Linux» Брюса Моле. В главе 6 подробно объясняется все вышеперечисленное.

Причина, по которой это происходит, заключается в том, что stdin буферизуется.

Когда вы попадаете в строку кода * choice = getc (stdin); независимо от того, сколько символов вы набираете, getc (stdin) будет извлекать только первый символ. Поэтому, если вы наберете «foo», он получит «f» и установит * выбор на «f». Символы «оо» все еще находятся во входном буфере. Кроме того, символ возврата каретки, который был вызван тем, что вы ударяете ключ возврата, также находится во входном буфере. Поэтому, поскольку буфер не пуст, при следующем запуске цикла, а не в ожидании ввода чего-либо, getc (stdin); немедленно вернет следующий символ в буфер. Функция getc (stdin) будет продолжать немедленно возвращать следующий символ в буфере, пока буфер не будет пуст. Поэтому, в общем случае, это будет указывать вам N раз, когда вы вводите строку длины N.

Вы можете обойти это, сбросив буфер с помощью fflush (stdin); сразу после строки * choice = getc (stdin);

EDIT: По-видимому, кто-то говорит, что не использовать fflush (stdin); Пойдите с тем, что он говорит.