Сохранение ввода пользователя в массив и завершение при значении остановки без сохранения этого значения в C

Вопрос или проблема

Я знаю, как написать цикл while, который считывает ввод пользователя, сохраняет его в массиве и затем заканчивает выполнение при достижении значения остановки. Однако это значение остановки также сохраняется, и я не уверен, как сделать так, чтобы этого не происходило.

Вот что у меня есть:

printf("Введите температуры или -100.0 для завершения: "); // подсказка для ввода пользователя
int i = 0;
while (1) {
  scanf("%f", &temp[i]);     // пользовательский ввод заполняет массив
  if (temp[i] == -100.0) {   // остановить цикл, если ввод = -100.0
    break;
  }
  i++;
}

Ваша немедленная проблема с тем, что “значение остановки” включается в массив temp[], заключается в том, что вы выполняете преобразование с помощью scanf() в &temp[i], что помещает значение в массив вне зависимости от того, является ли оно значением остановки или нет. Чтобы решить эту проблему, просто объявите временную переменную и используйте ее для хранения преобразованного значения. Затем вы можете проверить временное значение по сравнению с вашим значением остановки, прежде чем добавлять его в массив.

В справочной странице scanf(), man 3 scanf предупреждается:

Сложно правильно использовать эти функции, и предпочтительно считывать целые строки с помощью fgets(3) или getline(3) и затем разбирать их с помощью sscanf(3) или более специализированных функций, таких как strtol(3).

Вот почему снова и снова на StackOverflow вы найдете ответы на вопросы о вводе пользователя в C, рекомендующие, что scanf() не является разумным выбором для ввода пользователя, и вам будет лучше использовать fgets(), чтобы прочитать всю строку, а затем обработать любое необходимое значение из считанной строки ввода. Причины этого много, но в основном они сводятся к:

  • если пользователь вводит недопустимый ввод для запрашиваемого типа преобразования, возникает ошибка сопоставления, извлечение символов из stdin прекращается в этот момент, и все символы, вызывающие ошибку, остаются в stdin, не считанные, только и ждущие, чтобы укусить вас при следующем попытке ввода.
  • даже если числовое преобразование проходит успешно, символ '\n' остается в stdin, снова ожидая, чтобы укусить вас при следующем вводе, который не отбрасывает пробелы (scanf() спецификаторы "%c", "%[..]" и "%n" не отбрасывают пробелы в начале, и то же самое касается fgets() или getline() – или любых других системных вызовов, которые выполняют ввод)
  • вы должны проверить возвращаемое значение scanf(), чтобы определить, удался ли ввод и преобразование, или они не удались
  • вы должны очистить входной буфер (stdin) после использования scanf(), чтобы удалить оставшиеся символы. (даже если это просто '\n', оставленное от нажатия пользователем Enter)
  • вы также должны проверять возвращаемое значение на EOF, а также проверять на успех, чтобы позволить пользователю отменить ввод – что также является допустимым вводом.

Считывание всей строки ввода пользователя с fgets() избегает этих проблем, хотя вам все равно нужно выполнить правильную валидацию, проверив возвращаемое значение fgets().

Другая проблема, с которой вы столкнулись, – это сравнение чисел с плавающей запятой для вашего значения остановки, т.е. if (temp[i] == -100.0). Насколько близко к -100.0 это считается? Вы хотите сохранить -100.01? Что насчет -100.04? Что насчет -99.999? Все эти значения могут равняться -100.0.1

В общем, выбор магического числа в качестве вашего значения остановки имеет свои проблемы. fgets() также предлагает простое решение этой проблемы. Вместо использования магического числа, fgets() может проверять на отсутствие ввода (например, только чтение '\n'), позволяя пользователю нажать только Enter, чтобы закончить ввод. Это позволяет всем значениям считаться допустимыми температурами, не выделяя одно магическое.

Также обратите внимание, что независимо от того, какой метод вы выберете, вы не можете просто циклически выполнять while(1) без какого-либо учета границ вашего массива temp[]. Вместо этого, как проверка внутри цикла, так и тест самого цикла, вам нужно принимать ввод только при наличии места в массиве для его хранения. Например, если у вас массив из 100 элементов, вы захотите сделать что-то вроде while (i < 100) { ... }, чтобы убедиться, что вы не пытаетесь добавить больше значений, чем у вас есть элементов. (конечно, используйте правильный #define, чтобы предоставить константу)

Использование scanf()

Даже с недостатками, если вы проверяете каждое необходимое условие и заботитесь о извлечении лишних символов, оставшихся в stdin, вы можете сделать scanf() работоспособным для ввода. Это не рекомендуется, но вы можете сделать что-то вроде:

#include <stdio.h>
#include <math.h>

#define NTEMPS    100   /* если вам нужна константа, #define одну (или более) */
#define STOPV    -100
#define ELIMIT    1.e-4

/* простая функция для очистки оставшихся символов во входном потоке */
void empty_stdin (void)
{
  int c = getchar();

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

int main (void) {

  float temp[NTEMPS] = { 0 };     /* массив для хранения температур */
  int i = 0;                      /* переменная-счетчик */

  /* цикл только пока остается место в массиве */
  while (i < NTEMPS) {
    float tmp = 0;                /* временное значение для преобразования */

    printf ("\nВведите температуры или %d для остановки: ", STOPV);
    fflush (stdout);              /* избежать проблем с буферизацией строк */

    if (scanf("%f", &tmp) != 1) { /* преобразование в float не удалось */

      if (feof (stdin)) {         /* проверяем на ручной EOF */
        puts ("  пользователь отменил ввод.");
        return 0;
      }
      else {  /* в противном случае очищаем оставшиеся символы в stdin */
        fputs ("  ошибка: недопустимый ввод с плавающей точкой.\n", stderr);
        empty_stdin();
      }
      continue;                   /* получить следующий ввод */
    }

    /* проверка условия выхода с магическим числом */
    if (fabs(tmp - STOPV) < ELIMIT) {
      empty_stdin();
      break;
    }

    temp[i] = tmp;                /* сохраняем преобразованное значение в массив */
    i++;                          /* увеличиваем счетчик */
  }

  /* показываем результат */
  puts ("\nЗаписанные температуры:\n");
  for (int j = 0; j < i; j++) {
    printf ("temp[%2d] : %.2f\n", j, temp[j]);
  }
}

замечание: использование диапазона вокруг -100 (в пределах 1.e-4), который считается вашим магическим значением остановки.

Использование fgets()

Использование fgets() рекомендовано не только для ввода пользователя, но на самом деле это значительно проще, чем проходить через все различные проверки и управление входным буфером, которые вам нужно выполнить с scanf(). Вместо того чтобы использовать scanf() напрямую для ввода, вы просто объявляете массив символов для хранения ввода пользователя, а затем передаете массив в sscanf() вместо того, чтобы выполнять ввод напрямую с помощью scanf(). Это значительно упрощает задачу (и делает код короче), например:

#include <stdio.h>
#include <string.h>

#define MAXC    1024    /* если вам нужна константа, #define одну (или более) */
#define NTEMPS   100

int main (void) {

  char buf[MAXC] = "";            /* массив для хранения строки ввода пользователя */
  int i = 0;                      /* переменная-счетчик */
  float temp[NTEMPS] = { 0 };     /* массив для хранения температур */

  /* цикл постоянно считывает ввод */
  while (i < NTEMPS) {
    float tmp;                    /* временное значение для преобразования */

    /* подсказка и очистка вывода (без завершающего '\n' в подсказке) */
    fputs ("\nВведите температуры (пустой ввод для остановки) : ", stdout);
    fflush (stdout);

    /* считываем ввод, проверяем на ручной EOF */
    if (fgets (buf, MAXC, stdin) == NULL) {
      puts ("  пользователь отменил ввод.");
      return 0;
    }

    if (buf[0] == '\n') {         /* если нажата клавиша Enter без ввода */
      break;                      /* готово! */
    }

    /* проверяем преобразование в float */
    if (sscanf (buf, "%f", &tmp) == 1) {
      temp[i] = tmp;              /* добавляем значение в массив температур */
      i++;                        /* увеличиваем счетчик */
    }
    else {  /* в противном случае преобразование не удалось, показываем недопустимый ввод */
      buf[strcspn (buf, "\n")] = 0;   /* обрезаем '\n' из строки */
      fprintf (stderr, "  ошибка: недопустимый ввод с плавающей точкой '%s'.\n",
                buf);
    }
  }

  /* показываем результат */
  puts ("\nЗаписанные температуры:\n");
  for (int j = 0; j < i; j++) {
    printf ("temp[%2d] : %.2f\n", j, temp[j]);
  }
}

Пример использования/выход

$ ./read-float-arr-fgets

Введите температуры (пустой ввод для остановки) : 10.8

Введите температуры (пустой ввод для остановки) : -20.9

Введите температуры (пустой ввод для остановки) : 59.1

Введите температуры (пустой ввод для остановки) : 37.4

Введите температуры (пустой ввод для остановки) : бананы и соус барбекю
  ошибка: недопустимый ввод с плавающей точкой 'бананы и соус барбекю'.

Введите температуры (пустой ввод для остановки) : -100.2

Введите температуры (пустой ввод для остановки) : -99.99

Введите температуры (пустой ввод для остановки) :

Записанные температуры:

temp[ 0] : 10.80
temp[ 1] : -20.90
temp[ 2] : 59.10
temp[ 3] : 37.40
temp[ 4] : -100.20
temp[ 5] : -99.99

Попробуйте ввести "бананы и соус барбекю" в вашу программу и посмотрите, что произойдет (намек: держите ctrl + c под рукой)

Для реализации scanf() выше, единственное отличие будет в строке значения остановки, как-то так:

Введите температуры или -100 для завершения: -100.0001

Потратьте время, чтобы пройти через каждый пример и изучить каждую использованную функцию и понять, почему это было необходимо для создания разумно надежной вводной рутины. Если у вас есть дальнейшие вопросы, просто оставьте комментарий ниже, и я с радостью помогу дальше.

Примечания:

  1. Сломана ли арифметика с плавающей запятой? и Почему числа с плавающей запятой неточные? и Сравнение с плавающей запятой a != 0.7

Ответ или решение

Хранение пользовательского ввода в массив с прекращением ввода при указании значения-стопа без его хранения в C

При написании программы на C, которая должна принимать пользовательский ввод и хранить данные в массиве, стоит учитывать несколько важных аспектов. В ситуации, когда необходимо завершить ввод при получении определённого "значения-стопа" (например, -100.0), важно не сохранять это значение в массив данных. В этом ответе будет представлено, как правильно реализовать такую логику, минимизируя количество ошибок и улучшая читаемость кода.

Основная проблема

Ваша исходная реализация использует цикл while, который продолжается до тех пор, пока не будет ввёдено значение -100. Однако в этом варианте при условии, что пользователь ввёл -100, эта величина всё равно записывается в массив. Для того чтобы избежать такой ситуации, необходимо использовать временную переменную для хранения ввёденного значения, проверять его на соответствие значению-стопу и лишь потом сохранять в массив.

Рекомендации к использованию scanf и fgets

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

Подход 1: Использование scanf

Пример кода с использованием функции scanf():

#include <stdio.h>
#include <math.h>

#define NTEMPS 100   /* Максимальное количество температур */
#define STOPV -100.0 /* Значение-стоп для завершения ввода */
#define ELIMIT 1.e-4 /* Допуск для сравнения чисел с плавающей точкой */

/* Функция для очистки ввода */
void empty_stdin(void) {
    int c;
    while ((c = getchar()) != '\n' && c != EOF);
}

int main(void) {
    float temp[NTEMPS];
    int i = 0;

    while (i < NTEMPS) {
        float tmp;

        printf("Введите температуру или %f для завершения: ", STOPV);
        fflush(stdout);

        if (scanf("%f", &tmp) != 1) { // Проверка успешности ввода
            if (feof(stdin)) {
                puts("Ввод отменён пользователем.");
                return 0;
            }
            puts("Ошибка: неверный ввод.");
            empty_stdin();
            continue;
        }

        // Проверка на значение-стоп
        if (fabs(tmp - STOPV) < ELIMIT) {
            break; // Завершение цикла
        }

        temp[i++] = tmp; // Сохранение значения
    }

    // Вывод результатов
    puts("Записанные температуры:");
    for (int j = 0; j < i; j++) {
        printf("temp[%2d]: %.2f\n", j, temp[j]);
    }

    return 0;
}

Подход 2: Использование fgets

Пример кода с использованием fgets() для более безопасного получения вводимых данных:

#include <stdio.h>
#include <string.h>

#define MAXC 1024    // Размер буфера для ввода
#define NTEMPS 100   // Максимальное количество температур

int main(void) {
    char buf[MAXC];           // Буфер для ввода строки
    float temp[NTEMPS];      // Массив для хранения температура
    int i = 0;               // Индекс для хранения

    while (i < NTEMPS) {
        float tmp;

        printf("Введите температуру (пустой ввод для завершения): ");
        fflush(stdout);

        if (fgets(buf, MAXC, stdin) == NULL) {
            puts("Ввод отменён пользователем.");
            return 0;
        }

        // Проверка на пустую строку
        if (buf[0] == '\n') {
            break; // Завершение цикла
        }

        // Преобразование строки в число
        if (sscanf(buf, "%f", &tmp) == 1) {
            temp[i++] = tmp; // Сохранение температуры
        } else {
            fprintf(stderr, "Ошибка: неверный ввод '%s'.\n", buf);
        }
    }

    // Вывод записанных температур
    puts("Записанные температуры:");
    for (int j = 0; j < i; j++) {
        printf("temp[%2d]: %.2f\n", j, temp[j]);
    }

    return 0;
}

Заключение

Использование fgets() является предпочтительным способом работы с пользовательским вводом в C, так как он значительно упрощает обработку ошибок и избежание неявного сохранения нежелательных значений. Независимо от выбранного метода, обязательно обращайте внимание на возможности выхода за пределы массива, производя соответствующую проверку на каждой итерации ввода.

Этот код создан для демонстрации примера обработки ввода и может быть дополнен в соответствии с конкретными требованиями вашей программы.

Оцените материал
Добавить комментарий

Капча загружается...