Почему, когда я запускаю этот код:
#include <stdio.h>
int main () {
int a, b;
if (scanf("%d %d", &a, &b) == 2)
printf("%d %d", a, b);
else
printf("Something went wrong");
return 0;
}
и введите, например:
1 1.5
вывод:
1 1
Почему scanf читает оба числа перед '.' и игнорирует «.5»? Как проверить, что последнее число не является числом с плавающей запятой и строка заканчивается?
ОС: MacOS/Linux Компилятор: gcc
Я просто хочу запустить что-то вроде приведенного выше кода
вход:
1 1.5234
(некоторое число с плавающей запятой)
выход:
Something went wrong
Вероятно, вам понадобится прочитать строку данных с помощью fgets() или POSIX getline() , а затем проанализировать данные с помощью sscanf(), используя спецификацию преобразования %n
, чтобы узнать, где закончилось преобразование, и проверьте, что это была новая строка. int n; if (scanf(line, "%d%d%n", &a, &b, &n) != 2 || line[n] != '\n') { …something went wrong… }
. Обратите внимание, что конверсии %n
не учитываются в результате sscanf()
. (3-я попытка!)
scanf
не очень подходит для проверки ввода. Если вы хотите увидеть функцию с полной проверкой ввода, возможно, вам захочется взглянуть на мою функцию get_int_from_user
, которую я разместил в этом моем ответе на другой вопрос. Например, эта функция отклонит ввод с конечными символами без пробелов. Однако эта функция вводит только одно число в строке вместо двух, поэтому она не делает именно то, что вы хотите.
fgets: считывает всю строку ввода, гарантируя, что вы зафиксируете все, что вводит пользователь, пока он не нажмет Enter. Это позволяет избежать ограничения scanf, связанного с остановкой на первом несовпадающем символе.
sscanf: анализирует входные данные, хранящиеся во входных данных. Он пытается прочитать два целых числа (%d %d). После этого он пытается прочитать еще один символ (%c), который должен быть пробелом (например, пробелом или новой строкой).
Проверка:
if (sscanf(input, "%d %d %c", &a, &b, &leftover) == 2)
Проверяет, были ли успешно прочитаны ровно два целых числа.
isspace(leftover)
: Проверяет, является ли символ после второго целого числа пробелом (гарантируя отсутствие дополнительных символов, таких как десятичная точка).
Вывод: если формат ввода совпадает (scanf успешно считывает два целых числа, за которыми следуют пробелы), он печатает целые числа. В противном случае он печатает сообщение об ошибке.
Короткий ответ: вы не можете понять, что происходит со scanf. На его man-странице бесполезно написано:
Правильно использовать эти функции очень сложно, поэтому предпочтительнее читать целые строки с помощью fgets(3) или getline(3), а затем анализировать их с помощью sscanf(3) или более специализированных функций, таких как strtol(3).
Scanf (и fscanf) очень легко рассинхронизироваться с вводом. Я использую fgets и sscanf. С их помощью я могу значительно улучшить вашу проверку ввода следующим образом:
#include <stdio.h>
#include <stdlib.h>
int main () {
char iline[80];
int a, b;
char extra[2];
int result;
if (fgets(iline, sizeof(iline), stdin) == NULL) {
printf("Need an input line.\n"); exit(1);
}
result = sscanf(iline, "%d %d%1s", &a, &b, extra);
if (result == 2) {
printf("%d %d\n", a, b); // Success!
} else if (result == 3) {
printf("Extra stuff starting at '%s'\n", extra);
} else if (result < 2) {
printf("Could not find two integers\n");
}
return 0;
}
Вот несколько тестовых запусков:
$ gcc x.c
$ echo "1" | ./a.out
Could not find two integers
$ echo "1 2" | ./a.out
1 2
$ echo "1 2.5" | ./a.out
Extra stuff starting at '.'
$ echo "1.5 2" | ./a.out
Could not find two integers
$ echo "1 2 5" | ./a.out
Extra stuff starting at '5'
Читая строку отдельно от ее сканирования, вы можете добавить «дополнительную» строку, чтобы проверить, завершилось ли сканирование преждевременно до того, как строка была использована. Если я использую scanf в своем коде выше, он откажется возвращаться, пока не найдет что-то еще для сканирования.
Спецификатор преобразования %d
сообщает scanf
прочитать и отбросить все ведущие пробелы, а затем прочитать до первого нецифрового символа (все, что находится за пределами диапазона ['0'-'9']
.
Предположим, ваш входной поток содержит последовательность:
{'1', ' ', '1', '.', '5', '\n'}
первый %d
читает первым '1'
, прекращает чтение на пробеле, оставляя его во входном потоке:
{' ', '1', '.', '5', '\n'}
затем преобразует и присваивает 1
a
.
Второй %d
читает и отбрасывает пробел, читает второй '1'
и прекращает чтение на .
, оставляя его во входном потоке:
{'.', '5', '\n'}
затем преобразует и присваивает 1
b
.
Что касается scanf
, он успешно прочитал два целочисленных ввода. Спецификатор преобразования %d
не знает и не заботится о том, что 1.5
является допустимой константой с плавающей запятой; его волнует только то, что '.'
не является цифровым символом, поэтому в этот момент он прекращает чтение.
То же самое, если вы введете что-то вроде 12s4
и прочитаете это с помощью %d
-- scanf
успешно преобразует и присвоит 12
и оставит s4
, чтобы испортить следующее чтение.
Есть несколько способов обойти эту проблему, и ни один из них не является особенно элегантным. Мой предпочтительный метод — прочитать входные данные как строку с помощью fgets
, затем токенизировать и преобразовать их с помощью strtol
для целых чисел и strtod
для чисел с плавающей запятой:
#include <stdio.h>
#include <stdlib.h>
#include <ctype.h>
#include <string.h>
#include <limits.h>
#include <errno.h>
/**
* This program demonstrates some of the gymnastics you have to go through
* to validate numeric input.
*
* Validation steps:
*
* 1. Make sure the fgets operation did not see an EOF or
* input error;
*
* 2. Make sure there's a newline character in the input
* buffer, otherwise the input was too long;
*
* 3. Make sure the strtol operation didn't over/underflow;
*
* 4. Make sure that the first character not converted by
* strtol is either whitespace or a terminator;
*
* 5. Make sure the value returned by strtol (which is a long) can be
* stored in a 32-bit int.
*/
int main( void )
{
/**
* Input buffer
*
* Using fixed-size buffers is always risky, but for the purpose of this
* demonstration should be good enough. A 32-bit integer can represent
* up to 10 decimal digits, plus sign, so to store two integer inputs
* we need a buffer that's *at least* 25 elements wide (two integers
* plus space plus newline plus terminator); rounding up
* to the next power of 2.
*/
char buf[32] = {0};
printf( "Gimme two integer values: " );
if ( !fgets( buf, sizeof buf, stdin ) )
{
if ( feof( stdin ) )
fprintf( stderr, "EOF signaled on standard input...\n" );
else
fprintf( stderr, "Error on standard input...\n" );
return EXIT_FAILURE;
}
if ( !strchr( buf, '\n' ) )
{
fprintf( stderr, "Input too long\n" );
return EXIT_FAILURE;
}
int a = 0, b = 0;
char *chk = NULL;
static const char *whitespace = " \n\r\t\f";
/**
* Break the input into tokens separated by whitespace, then
* attempt to convert each token to an integer with strtol.
* The chk parameter will point to the first character *not*
* converted; if that character is anything other than whitespace
* or a terminator, then the input is not a valid integer.
*/
for ( char *tok = strtok( buf, whitespace ), cnt = 0; tok != NULL && cnt < 2; tok = strtok( NULL, whitespace ), cnt++ )
{
errno = 0;
long tmp = strtol( tok, &chk, 10 );
if ( !(isspace( *chk ) || *chk != 0) )
{
fprintf( stderr, "\"%s\" is not a valid integer, exiting...\n", tok );
return EXIT_FAILURE;
}
else if ( (tmp == LONG_MIN || tmp == LONG_MAX) && errno == ERANGE )
{
fprintf( stderr, "Overflow detected while converting \"%s\" to long, exiting...\n", tok );
return EXIT_FAILURE;
}
else if ( tmp > INT_MAX || tmp < INT_MIN )
{
fprintf( stderr, "\"%s\" cannot be represented in an int...\n", tok );
return EXIT_FAILURE;
}
else if ( cnt == 0 )
{
a = tmp;
}
else
{
b = tmp;
}
}
printf( "a = %d, b = %d\n", a, b );
return EXIT_SUCCESS;
}
Ага. Добро пожаловать в программирование на C. И у этого кода есть несколько серьезных недостатков; strtok
деструктивен и не является потокобезопасным, использование буферов фиксированного размера всегда рискованно, я рассчитываю на то, что long
имеет больший диапазон, чем int
(не обязательно верно в некоторых системах) и т. д.
Некоторые примеры запусков:
% ./input5
Gimme two integer values: ^D EOF signaled on standard input...
% ./input5
Gimme two integer values: 1234567890123456789012345678901234567890
Input too long
% ./input5
Gimme two integer values: 1234567890123456789012345
Overflow detected while converting "1234567890123456789012345" to long, exiting...
% ./input5
Gimme two integer values: 12345678901234
"12345678901234" cannot be represented in an int...
% ./input5
Gimme two integer values: 123 4.56
"4.56" is not a valid integer, exiting...
% ./input5
Gimme two integer values: 123 456
a = 123, b = 456
Формат
%d
предназначен для чтения целых чисел;.
не является допустимой частью любого целого числа в C.