Строка, прочитанная fgets, не записывает новую строку с помощью fputs

Я пишу программу, которая принимает пользовательский ввод данных о человеке, который состоит из имени, идентификатора и номера телефона. Я ограничиваю свой идентификатор и номер телефона максимум 14 и 13 символами соответственно (исключая символ новой строки) - например. 010203-1234567 для ID и 010-1111-2222 для номера телефона. По праву, если я правильно понял, fgets должен автоматически добавлять байт новой строки после того, как он читает строку из пользовательского ввода, а fputs должен записывать новую строку в конце строки, хранящейся в id и phoneNum, но при попытке ввести больше символов, чем разрешено, напр. 010203-12345678 для идентификатора следующий fputs не выводит новую строку, как следует. Как я могу убедиться, что даже если пользователь вводит больше символов, чем разрешено, новая строка записывается fputs?

Ниже приведен мой код:

#include <stdio.h>

void FlushInputBuf(void)
{
    while (getchar() != '\n');
}

int main(void)
{
    FILE *fp = fopen("details.txt", "wt");
    if (fp == NULL)
    {
        puts("File open failed.\n");
        return -1;
    }
    char name[20];
    char id[15];
    char phoneNum[14];

    printf("Enter name: ");
    fgets(name, sizeof(name), stdin);
    printf("Enter ID: ");
    fgets(id, sizeof(id), stdin);
    FlushInputBuf();
    printf("Enter phone number: ");
    fgets(phoneNum, sizeof(phoneNum), stdin);

    fputs("#Name: ", fp);
    fputs(name, fp);
    fputs("#SSID: ", fp);
    fputs(id, fp);
    fputs("#Phone number: ", fp);
    fputs(phoneNum, fp);
  
    fclose(fp);

    return 0;
}

Я предполагаю, что ввод для имени не будет длиннее 20. Когда я ввожу идентификатор как 010203-1234567 и номер телефона как 010-1111-2222, я получаю следующий результат в файле details.txt:

#Name: John
#SSID: 010203-1234567#Phone number: 010-1111-2222

Как мне получить код для записи в details.txt следующим образом:

#Name: John
#SSID: 010203-1234567
#Phone number: 010-1111-2222

Последним символом будет \0. И он перезапишет \n ввода. только что сделал идентификатор на 1 символ больше.

S_IROTH 04.04.2023 04:01
char id[15]; может содержать 14 символов и 0 терминатора. Там нет места для новой строки. Обычно вы все равно удаляете новую строку из любой строки, прочитанной fgets, а затем форматируете вывод, предполагая, что ее там нет.
Retired Ninja 04.04.2023 04:01
fgets() не «автоматически добавляет символ новой строки» — если он читает новую строку до того, как закончится место для хранения данных, он будет включать новую строку, которую он прочитал, но если места недостаточно, он прекратит чтение, когда места не осталось — возможно, во вводе осталась новая строка (возможно, с другими непрочитанными символами перед концом строки).
Jonathan Leffler 04.04.2023 04:44

Что произойдет, когда getchar() вернется EOF?

tadman 04.04.2023 05:08
Стоит ли изучать 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 называются скалярами. Достигнув скалярного типа, невозможно спуститься дальше по иерархии типов. Скалярный тип...
2
4
57
2
Перейти к ответу Данный вопрос помечен как решенный

Ответы 2

Ответ принят как подходящий

По праву, если я правильно понял, fgets должен автоматически добавлять символ новой строки после того, как он читает строку из пользовательского ввода.

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

fputs должен записывать символ новой строки в конце строки, хранящейся в id и phoneNum.

Да, fputs запишет все новые строки, появляющиеся в указанной строке. То, что ни один из них не печатается, является убедительным доказательством того, что ни один из них не считывается в ваш буфер idphoneNum).

при попытке ввести больше символов, чем разрешено, например. «010203-12345678» для идентификатора, следующие fputs не записывают новую строку, как следует.

Он не печатает новую строку, как вы ожидали. Но он делает именно то, что должен.

Ваш массив id имеет длину 15 char, что достаточно для 14 символов идентификатора и одного нулевого терминатора. Соответственно, fgets прекращает чтение после копирования 14-го символа, «7», и завершает строку последним байтом буфера. Затем ваша функция FlushInputBuf() читает и отбрасывает конец строки, включая следующую новую строку. Новая строка не сохраняется. То же самое произойдет и с записью, состоящей ровно из 14 символов. И если введенный идентификатор короче этого, то вызов FlushInputBuf() заставит вас ввести вторую новую строку, прежде чем он запросит номер телефона (и отбросит все, что вы наберете до этого).

Хорошим способом сделать это было бы

  1. Обеспечьте буфер, по крайней мере, достаточно длинный, чтобы вместить ожидаемый ввод, а также новую строку и признак конца строки.

    #define MAX_ID_LENGTH 14
    // ...
    char id[MAX_ID_LENGTH + 2];
    

    Иногда полезно позволить себе немного больше, чем вы на самом деле ожидаете.

  2. После вызова fgets() проверьте наличие новой строки в буфере. Используйте хвост входной строки, только если он есть, то есть если новая строка еще не была прочитана.

    size_t newline_pos = strcspn(id, "\n");
    if (id[newline_pos] != '\0') {
        assert(id[newline_pos] == '\n');
        FlushInputBuf();
    }
    
  3. Подтвердите или, по крайней мере, исправьте ввод. В частности, удалите символ новой строки, если он есть, так как он фактически не является частью идентификатора.

    id[newline_pos] = '\0';
    

    Отклоняйте или усекайте слишком длинные идентификаторы.

    // maybe:
    id[MAX_ID_LENGTH] = '\0';
    

    Выполните любые другие необходимые проверки.

  4. Печатайте новые строки на выходе вручную, где вы хотите. В вашем конкретном случае я бы использовал один вызов fprintf() вместо каждой пары вызовов fputs() с правильным размещением новых строк в форматах.

    fprintf(fp, "#SSID: %s\n", id);
    

    Но если вы должны использовать только fputs(), вы можете вместо этого добавить fputs("\n", fp);.

    Если бы вы не беспокоились о последствиях коротких записей (а вы должны были бы), то этого шага было бы достаточно.

В дополнение к отличному ответу @JohnBollinger, вот модифицированная версия вашего кода, использующая вспомогательную функцию для контролируемого пользовательского ввода:

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

int input_string(const char *prompt, char *dest, int size) {
    int c;      // byte read from stdin
    int i = 0;  // index into dest
    int n = 0;  // number of bytes read excluding the newline

    printf("%s", prompt);
    /* ensure prompt is visible (necessary on some older systems) */
    fflush(stdout);

    /* read all bytes from the user input line */
    while ((c = getchar()) != EOF && c != '\n') {
        if (i + 1 < size) {
            /* append the character to dest, space permitting */
            dest[i++] = (char)c;
        }
        n++;
    }
    if (size > 0) {
        // set the null terminator
        dest[i] = '\0';
    }
    if (n >= size) {
        printf("discarded %d extra characters\n", n - size - 1);
    }
    if (c == EOF) {
        if (i == 0)
            printf("premature end of file\n");
            return EOF;
        } else {
            printf("missing newline at end of file\n");
        }
    }
    /* return the number of characters stored into dest */
    return i;
}

int main(void) {
    FILE *fp = fopen("details.txt", "w");
    if (fp == NULL) {
        fprintf(stderr, "Failed to open %s: %s\n", "details.txt",
                strerror(errno));
        return 1;
    }
    char name[20];
    char id[15];
    char phoneNum[14];

    if (input_string("Enter name: ", name, sizeof(name)) < 0
    ||  input_string("Enter ID: ", id, sizeof(id)) < 0
    ||  input_string("Enter phone number: ", phoneNum, sizeof(phoneNum)) < 0) {
        fprintf(stderr, "Missing input\n");
        fclose(fp);
        return 1;
    }
    fprintf(fp, "#Name: %s\n", name);
    fprintf(fp, "#SSID: %s\n", id);
    fprintf(fp, "#Phone number: %s\n", phoneNum);

    fclose(fp);
    return 0;
}

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