Почему несколько операторов fgets перезаписывают массивы символов?

Следующий код работает, но если я ввожу более 10 символов (скажем, 10 а), вывод превращается в нечто подобное:

"Кличка собаки? ааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааа за собачья порода? ааааааааааааааааааааааа за порода собаки:"

Почему это? И как я могу это исправить?

#include <stdio.h>
#include <stdlib.h>

typedef struct Dog {
    char name[10];
    char breed[10];
} Dog;

Dog makeDog() {
    Dog dog;

    printf("Dog's name? ");
    fgets(dog.name, 10, stdin);

    printf("Dog's breed? ");
    fgets(dog.breed, 10, stdin);

    return dog;
}

int main() {
    printf("\n");

    Dog dog = makeDog();

    printf("\n");

    printf("Dog's name: %s", dog.name);
    printf("Dog's breed: %s \n", dog.breed);
}

person Yogsoth    schedule 18.06.2019    source источник
comment
Связано: stackoverflow.com/questions/1660228/   -  person Martin Véronneau    schedule 18.06.2019
comment
Есть причина, по которой fgets() сохраняет последнюю новую строку. Если его нет во входных данных, входные данные были усечены, а остальная часть все еще находится во входном буфере (если только это не последняя строка файла и в ней отсутствует новая строка). Кстати, в обычном случае вам нужно будет удалить эту новую строку. Чтобы исправить это, используйте массив большего размера, или динамически выделяйте память, или выйдите из программы, потому что она не может хранить введенные вами данные.   -  person Weather Vane    schedule 18.06.2019
comment
Что ты имеешь в виду под исправлением? Кажется, он работает правильно.   -  person William Pursell    schedule 18.06.2019
comment
Никакие массивы не перезаписываются. Вы ввели 10 'a' и новую строку. 9 из этих «а» были записаны в dog.name, одна из «а» и новая строка были помещены в dog.breed.   -  person William Pursell    schedule 18.06.2019
comment
@WilliamPursell Проблема в том, что он пропускает следующий оператор fgets, если я ввожу более десяти а.   -  person Yogsoth    schedule 18.06.2019
comment
с fgets() будет введено только максимум (в данном случае) 9 символов, а 10-я позиция в массиве будет установлена ​​​​на '\0' Таким образом, в stdin все еще есть символы «a».   -  person user3629249    schedule 18.06.2019
comment
в функции: makeDog() относительно: Dog dog; и return dog; Так как стойка "Собака" находится в стеке и "выйдет за пределы" при выходе из функции. Доступ к нему в вызывающей функции является неопределенным поведением   -  person user3629249    schedule 18.06.2019
comment
Это не пропускает утверждение. Второй вызов fgets считывает оставшуюся часть строки. Если вы хотите отказаться от этих данных, вам нужно написать код для этого.   -  person William Pursell    schedule 18.06.2019
comment
@ user3629249 Спасибо. Как я могу отказаться от символов, оставшихся на стандартном вводе, перед повторным вызовом fgets?   -  person Yogsoth    schedule 18.06.2019
comment
@user3629249 user3629249 Нет. Если возвращается указатель на переменную, это будет проблемой. Но это ничем не отличается от функции, возвращающей int, выполняющей int f; f = 5; return f;.   -  person William Pursell    schedule 18.06.2019
comment
относительно: Как мне удалить символы, оставшиеся на стандартном вводе, перед повторным вызовом fgets? Пожалуйста, прочитайте/поймите мой ответ. Затем, ЕСЛИ прочитанное имя собаки не содержит '\n' (перевод строки), используйте: int ch; while( (ch = getchar()) != EOF && ch != '\n' ){;}   -  person user3629249    schedule 18.06.2019
comment
@WilliamPursell, вы имеете в виду «неопределенное поведение»? индивидуальное/собственное поле будет возвращено в 1 или 2 регистрах ЦП. Однако для возврата более крупных элементов, таких как 20-байтовый struct dog, возникнут проблемы. Вот почему я предлагаю в своем ответе использовать динамическое выделение памяти и возвращать указатель на эту динамическую память.   -  person user3629249    schedule 18.06.2019
comment
чтобы понять, что не так с кодом OP, вам нужно прочитать/понять все детали, касающиеся функции fgets(). Все функции библиотеки C полностью описаны на страницах MAN. Если на вашем компьютере нет страниц MAN, выполните поиск по синтаксису ‹functionName› в c   -  person user3629249    schedule 20.06.2019


Ответы (2)


следующий предлагаемый код:

  1. чисто компилирует
  2. выполняет желаемую функцию
  3. Выходит, если имя собаки слишком длинное
  4. удаляет новую строку в конце имени собаки
  5. удаляет новую строку в конце породы собак
  6. не проверяет правильность длины породы собаки
  7. правильно очищает после использования динамической памяти
  8. избегает использования «магических» чисел
  9. правильно выделяет динамическую память и проверяет/обрабатывает любые ошибки
  10. использует: «максимум 8 символов», чтобы оставить место для завершающей новой строки и завершающего символа NUL

и теперь предлагаемый код:

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

#define MAX_NAME_LEN  10
#define MAX_BREED_LEN 10

typedef struct Dog 
{
    char name[ MAX_NAME_LEN ];
    char breed[ MAX_BREED_LEN ];
} Dog;

Dog * makeDog() 
{
    Dog *dog = malloc( sizeof( Dog ) );
    if( !dog )
    {
        perror( "malloc for struct Dog failed" );
        exit( EXIT_FAILURE );
    }

    printf("Dog's name? max 8 characters ");
    if( !fgets(dog->name, MAX_NAME_LEN, stdin) )
    {
        perror( "fgets for dog name failed" );
        exit( EXIT_FAILURE );
    }

    if( dog->name[ strlen( dog->name ) -1 ] != '\n' )
    {
        puts( "dog name too long, aborting" );
        exit( EXIT_FAILURE );
    }

    // remove trailing newline
    dog->name[ strcspn( dog->name, "\n" ) ] = '\0';

    printf("Dog's breed? max 8 characters");
    if( !fgets(dog->breed, MAX_BREED_LEN, stdin) )
    {
        perror( "fgets for dog breed failed" );
        exit( EXIT_FAILURE );
    }

    // remove trailing newline
    dog->breed[ strcspn( dog->name, "\n" ) ] = '\0';

    return dog;
}

int main() {
    printf("\n");

    Dog *dog = makeDog();

    printf("\n");

    printf("Dog's name: %s\n", dog->name);
    printf("Dog's breed: %s\n", dog->breed);

    free( dog );
}
person user3629249    schedule 18.06.2019
comment
Спасибо за усилия, хотя это слишком сложно для меня на моем нынешнем уровне C. Я сделаю все возможное, чтобы попытаться понять это. - person Yogsoth; 18.06.2019

Массив символов name объявлен с 10 элементами

char name[10];

Если вы используете следующий вызов fgets

fgets(dog.name, 10, stdin);

после ввода 10 символов 'a' вызов fgets считывает только 9 символов из входного буфера и добавляет к массиву завершающий нулевой символ '\0'.

Таким образом, массив будет содержать строку "aaaaaaaaa". Это то же самое, что инициализировать массив следующим образом

char name[10[ = { 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', '\0' };

После этого входной буфер будет содержать один символ 'a' и новый символ строки '\n'. Эти символы будут прочитаны при следующем вызове

fgets(dog.breed, 10, stdin);

В результате массив bread будет содержать строку "a\n".

Это то же самое, что инициализировать массив следующим образом

char bread[10[ = { 'a', '\n', '\0' };

Если вы хотите хранить в массивах строки с большим количеством символов, вы должны увеличить массивы.

Например, если вы хотите ввести в качестве имени массива строку из 10 символов 'a', вы должны объявить массив как имеющий 12 элементов. Почему 12? Потому что помимо 10 символов 'a' и завершающего нулевого символа функция fgets также попытается прочитать символ новой строки '\n' из входного буфера. В противном случае этот символ будет прочитан вторым вызовом fgets.

Чтобы удалить символ новой строки из массива, вы можете использовать следующий подход

#include <string.h>

//...

fgets( dog.name, 12, stdin );
dog.name[strcspn( dog.name, "\n" )] = '\0';
person Vlad from Moscow    schedule 18.06.2019