Почему scanf("%d %d") не жалуется на десятичный ввод?

Почему, когда я запускаю этот код:

#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

Формат %d предназначен для чтения целых чисел; . не является допустимой частью любого целого числа в C.

Jonathan Leffler 15.07.2024 16:48

Вероятно, вам понадобится прочитать строку данных с помощью 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-я попытка!)

Jonathan Leffler 15.07.2024 17:09
scanf не очень подходит для проверки ввода. Если вы хотите увидеть функцию с полной проверкой ввода, возможно, вам захочется взглянуть на мою функцию get_int_from_user, которую я разместил в этом моем ответе на другой вопрос. Например, эта функция отклонит ввод с конечными символами без пробелов. Однако эта функция вводит только одно число в строке вместо двух, поэтому она не делает именно то, что вы хотите.
Andreas Wenzel 16.07.2024 05:34
Стоит ли изучать PHP в 2023-2024 годах?
Стоит ли изучать PHP в 2023-2024 годах?
Привет всем, сегодня я хочу высказать свои соображения по поводу вопроса, который я уже много раз получал в своем сообществе: "Стоит ли изучать PHP в...
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
В JavaScript одним из самых запутанных понятий является поведение ключевого слова "this" в стрелочной и обычной функциях.
Приемы CSS-макетирования - floats и Flexbox
Приемы CSS-макетирования - floats и Flexbox
Здравствуйте, друзья-студенты! Готовы совершенствовать свои навыки веб-дизайна? Сегодня в нашем путешествии мы рассмотрим приемы CSS-верстки - в...
Тестирование функциональных ngrx-эффектов в Angular 16 с помощью Jest
В системе управления состояниями ngrx, совместимой с Angular 16, появились функциональные эффекты. Это здорово и делает код определенно легче для...
Концепция локализации и ее применение в приложениях React ⚡️
Концепция локализации и ее применение в приложениях React ⚡️
Локализация - это процесс адаптации приложения к различным языкам и культурным требованиям. Это позволяет пользователям получить опыт, соответствующий...
Пользовательский скаляр GraphQL
Пользовательский скаляр GraphQL
Листовые узлы системы типов GraphQL называются скалярами. Достигнув скалярного типа, невозможно спуститься дальше по иерархии типов. Скалярный тип...
3
3
109
3
Перейти к ответу Данный вопрос помечен как решенный

Ответы 3

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'}

затем преобразует и присваивает 1a.

Второй %d читает и отбрасывает пробел, читает второй '1' и прекращает чтение на ., оставляя его во входном потоке:

{'.', '5', '\n'}

затем преобразует и присваивает 1b.

Что касается 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

Другие вопросы по теме