C programming для начинающих: часть 15 – проблемы gets() и fgets()

 

Предыдущая часть

В предыдщуем примере мы уже сталкивались с использованием функции gets():

...
int main(int argc, char **argv) {
        char agestring[10];
        int age;
        int bonus;

        printf("Enter your age : ");
        gets(agestring);
...

Тут мы получаем данные от пользователя из STDIN, и вписываем их в массив agestring.

Однако – тут имеется серьёзная проблема: gets() примет любое количество символов, которые введёт пользователь программы, несмотря на то, что сам массив объявлен длиной в 10 символов:

...
        char agestring[10];
...

Что произойдёт, если ввести большее их количество?

Давайте рассмотрим это на следующей программе:

#include <stdio.h>

void flush_input(){
        int ch;
        while ((ch = getchar()) != '\n' && ch != EOF);
}

void getinput_with_gets() {
        char firstname[5];
        char lastname[5];
        printf("Enter your first name:");
        gets(firstname);
        printf("Enter your last name:");
        gets(lastname);
        printf("Hello, %s, %s\n", firstname, lastname);
}


void getinput_with_fgets() {
        char firstname[5];
        char lastname[5];
        printf("Enter your first name:");
        fgets(firstname, 5, stdin);
        printf("Enter your last name:");
        // fflush(stdin);       // This function may not (invariably) work with input!
        flush_input();
        fgets(lastname, 5, stdin);
        flush_input();
        printf("Hello, %s, %s\n", firstname, lastname);
}

int main(int argc, char **argv) {
        getinput_with_gets();
        // getinput_with_fgets();
        return 0;
}

Собираем её:

[simterm]

$ gcc getinput.c -o getinput
getinput.c: In function ‘getinput_with_gets’:
getinput.c:12:9: warning: implicit declaration of function ‘gets’; did you mean ‘fgets’? [-Wimplicit-function-declaration]
         gets(firstname);
         ^~~~
         fgets
/tmp/ccZ17mWS.o: In function `getinput_with_gets':
getinput.c:(.text+0x43): warning: the `gets' function is dangerous and should not be used.

[/simterm]

Обратите внимание на предупреждение:

warning: the `gets’ function is dangerous and should not be used.

Сейчас мы увидим – почему оно появляется.

В программе выше мы повторяем те же действия – создаём два массива с длиной в 5 элементов:

..
        char firstname[5];
        char lastname[5];
...

После чего в функции getinput_with_gets() мы используем gets() для получения данных от пользователя.

В данном случае – у нас всё сработает как положено если мы введём 4 символа (5-ый – null-терминатор, символ завершения строки, \0):

[simterm]

$ ./getinput 
Enter your first name:1234
Enter your last name:1234
Hello, 1234, 1234

[/simterm]

Но если вы введёте большее количество – то gets() примет всю строку ввода, начнёт вносить данные в массив, и т.к. длина массива будет меньше, чем строка – то эта строка выйдет за пределы массива, в другой, непредсказуемый, участок памяти, что может привести к тому что эта строка перезапишет данные, уже имеющиеся в этом участке памяти.

Проверим:

[simterm]

$ ./getinput
Enter your first name:1234
Enter your last name:1234
Hello, 1234, 1234

[/simterm]

Можно продемонстрировать происходящее так: в getinput_with_gets() добавим цикл, который будет выводить все элементы массива и их индексы:

...
void getinput_with_gets() {
        char firstname[5];
        char lastname[5];
        printf("Enter your first name:");
        gets(firstname);
        printf("Enter your last name:");
        gets(lastname);
        printf("Hello, %s, %s\n", firstname, lastname);

        int i;
        for (i=0;i < (sizeof (firstname) /sizeof (firstname[0]));i++) {
            printf("Element: %d, value: %c\n", i, firstname[i]);
        }
}
...

Результат:

[simterm]

$ ./getinput                                                                                                                                                                                
Enter your first name:123456                                                                                                                                                                                                                  
Enter your last name:123456                                                                                                                                                                                                                   
Hello, 6, 123456                                                                                                                                                                                                                              
Element: 0, value: 6                                                                                                                                                                                                                          
Element: 1, value: 
Element: 2, value: 3
Element: 3, value: 4
Element: 4, value: 5

[/simterm]

Тут хорошо видно, что gets() просто начал перезаписывать массив сначала: вместо 1 в первом (нулевом) элементе мы получаем последний символ – 6.

Решение этой проблемы – использовать альтернативную функцию fgets() вместо gets(), что сделано в функции getinput_with_fgets() нашего примера выше:

...
void getinput_with_fgets() {
        char firstname[5];
        char lastname[5];
        printf("Enter your first name:");
        fgets(firstname, 5, stdin);
        printf("Enter your last name:");
        // fflush(stdin);       // This function may not (invariably) work with input!
        flush_input();
        fgets(lastname, 5, stdin);
        flush_input();
        printf("Hello, %s, %s\n", firstname, lastname);
}
...

Обновим main(), меняем используемую функцию:

...
int main(int argc, char **argv) {
//         getinput_with_gets();
        getinput_with_fgets();
        return 0;
}
...

В отличии от gets()fgets() принимает три аргумента:

...
fgets(firstname, 5, stdin)
...

Первым указывается имя массива, в который мы будем вносить данные, вторым – максимальное количество символов, которые fgets() примет на входе. Третьим указывается источник данных, в данном случае – обычный STDIN.

Итак – второй аргумент указывает fgets() принять только указанное кол-во символов -1 (завершение строки).

Таким образом, если вы укажете вторым аргументом строку “abcde” – только первые 4 символа abcd будут внесены в массив firstname + последний символ \0. Следовательно – firstname будет одержать строку “acbd\0“.

Проверяем:

[simterm]

$ ./getinput 
Enter your first name:abcde
Enter your last name:acbde
Hello, abcd, acbd

[/simterm]

Казалось бы – всё работает, как и ожидается. Но давайте внимательнее рассмотрим код:

...
        fgets(firstname, 5, stdin);
        printf("Enter your last name:");
        // fflush(stdin);       // This function may not (invariably) work with input!
        flush_input();
        fgets(lastname, 5, stdin);
        flush_input();
...

Обратите внимание на функцию flush_input():

...
void flush_input(){
        int ch;
        while ((ch = getchar()) != '\n' && ch != EOF);
}
...

Зачем она нужна? Давайте закомментируем её вызов, и проверим:

...
void getinput_with_fgets() {
        char firstname[5];
        char lastname[5];
        printf("Enter your first name:");
        fgets(firstname, 5, stdin);
        printf("Enter your last name:");
        // fflush(stdin);       // This function may not (invariably) work with input!
//        flush_input();
        fgets(lastname, 5, stdin);
//        flush_input();
        printf("Hello, %s, %s\n", firstname, lastname);
}
...

Запускаем:

[simterm]

$ ./getinput 
Enter your first name:abcde
Enter your last name:Hello, abcd, e

[/simterm]

Упс… Наша программа теперь даже не вызывает fgets(lastname, 5, stdin), и вместо этого срабатывает printf() сразу же после fgets(firstname, 5, stdin), при этом присваивая “остаток” первой строки во второй массив lastname[].

Почему так происходит, для чего потребовалось явно создавать и вызывать функцию flush_input() – рассмотрим в следующей части.

Продолжение.