В предыдщуем примере мы уже сталкивались с использованием функции 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()
– рассмотрим в следующей части.
Продолжение.