Давайте посмотрим на проблему один за другим:
newline Остается в stdin после номера или прочитанных людей
printf("\n\nEnter name: ");
safer_gets(person[i].full_name, 35); /* This is the step being skipped */
Он пропущен, потому что ваш safer_gets()
читает только первый '\n'
( символ новой строки - не возврат каретки , то есть '\r'
). Однако первый символ, который saver_gets()
видит во входном потоке, - это символ '\n'
, который остается в stdin
непрочитанным после вашего вызова scanf
in:
printf("How many people do you want to generate labels for (0-10)? ");
scanf("%i", &x);
Все спецификаторы формата scanf
для преобразования чисел c только для чтения с последним значением di git (или десятичной точки), которое составляет число, оставляя '\n'
, сгенерированный пользователем при нажатии Введите непрочитанные во входном потоке (stdin
здесь). Это одна из основных причин, по которой новым C программистам рекомендуется читать пользовательский ввод с помощью линейно-ориентированной функции ввода, такой как fgets()
(или POSIX getline()
), а затем использовать sscanf
для разбирать значения из заполненного буфера.
Почему линейно-ориентированные функции ввода предпочтительнее для пользовательского ввода
При использовании линейно-ориентированной функции ввода с достаточным буфером, полная используется строка пользовательского ввода (включая '\n'
от пользователя, нажимающего Enter ). Это гарантирует, что stdin
готов к следующему вводу и не содержит непрочитанных символов, оставшихся от предыдущего ввода, ожидающего вас укусить.
Правильное использование всех функций ввода
Если из этого ответа вы больше ничего не поймете, изучите это - вы не сможете правильно использовать любую функцию ввода, если вы не проверите возврат . Это особенно верно для семейства функций scanf
. Почему? Если вы пытаетесь прочитать целое число с scanf
и пользователь вводит вместо него "four"
, то возникает ошибка совпадения , и извлечение символа из вашего входного потока прекращается, когда первый недопустимый символ оставляет все ошибочные символы в ваш входной поток непрочитанный . (просто жду, чтобы вас снова укусить).
Использование scanf Правильно
scanf
можно использовать, если используется правильно. Это означает, что вы несете ответственность за проверку возврата из scanf
каждый раз . Вы должны обработать три условия
(return == EOF)
, пользователь отменил ввод, сгенерировав руководство EOF
, нажав Ctrl + d (или на windows Ctrl + z ); (return < expected No. of conversions)
a соответствие или вход произошла ошибка. Для совпадения вы должны учитывать каждый символ, оставшийся в вашем входном буфере. (сканирование вперед во входном буфере с чтением и отбрасыванием символов до тех пор, пока не будет найдено '\n'
или EOF
); и, наконец, (return == expected No. of conversions)
, указывающий на успешное чтение - тогда вам нужно проверить, соответствует ли входные данные каким-либо дополнительным критериям (например, положительное целое число, положительная плавающая точка, в необходимом диапазоне и т. д. * 1291). * ..).
Вы также должны учитывать, что осталось в вашем входном потоке после успешного чтения с помощью scanf
. Как обсуждалось выше, scanf
оставит '\n'
во входном потоке непрочитанным для ВСЕХ спецификаторов преобразования, если вы специально не учтете его в строке формата (что, если учитывается, обычно приводит к fr agile строка формата ввода, легко смещаемая дополнительными посторонними символами после требуемого ввода, но до '\n'
) При использовании scanf
для ввода вы должны надеть шапку бухгалтера и учесть все символы, которые остаются в потоке ввода и при необходимости очистите поток ввода от любых оскорбительных символов.
Вы можете написать простую функцию empty_stdin()
, которая будет обрабатывать удаление всех посторонних символов, которые остаются после ввода пользователя, просто сканируя вперед, отбрасывая все символы, оставшиеся до '\n'
найдено или EOF
найдено. Вы делаете это в различной степени в своей функции safer_gets()
. Вы можете написать простую функцию как:
void empty_stdin(void)
{
int c = getchar(); /* read character */
while (c != '\n' && c != EOF) /* if not '\n' and not EOF */
c = getchar(); /* repeat */
}
Вы можете сделать то же самое с помощью простого for
l oop inline, например,
for (int c = getchar(); c != '\n' && c != EOF; c = getchar()) {}
Следующая проблема - Попытка записи по неверному адресу
При использовании scanf
, scanf
ожидает, что параметром для соответствующего преобразования будет указатель на соответствующий тип. В:
printf("\nEnter zipcode: ");
scanf("%ld", person[i].zip_code); /* I get a bogus number here */
Вы не можете указать указатель, предоставив вместо него значение long int
. Поскольку person[i].zip_code
имеет тип long int
, чтобы предоставить указатель для scanf
для заполнения, вы должны использовать оператор address-of , например, &person[i].zip_code
, чтобы сообщить scanf
какой адрес заполнить значением, для которого он обеспечивает преобразование.
Подождите? Почему я не должен делать это с массивом? При доступе массив преобразуется в указатель на первый элемент. Поэтому для ввода строки, если для хранения строки используется массив, он автоматически преобразуется в указатель C11 Standard - 6.3.2.1 Другие операнды - L-значения, массивы и указатели функций (p3) .
toupper Работает с символами, а не строками
printf("%s\n", toupper(person[i].state)); /* This is where the error code is occurring */
Как обсуждалось в моем комментарии, toupper
принимает тип int
в качестве параметра, не тип char*
. Чтобы преобразовать строку в верхний / нижний регистр, вам нужно l oop для каждого символа, преобразуя каждый символ в отдельности. Однако в вашем случае с .state
членом вашей структуры нужно беспокоиться только о 2 символах, поэтому просто преобразуйте их оба при чтении, например,
/* just 2-chars to convert to upper - do it here */
person[i].state[0] = toupper (person[i].state[0]);
person[i].state[1] = toupper (person[i].state[1]);
Фундаментальные проблемы в safer_gets ()
Это решает большинство очевидных проблем, но сама функция safer_gets()
имеет несколько фундаментальных проблем. В частности, он не обрабатывает EOF
при возврате getchar()
и не предоставляет пользователю никаких указаний на то, был ли запрошенный пользовательский ввод успешным или нет из-за того, что ничего не возвращалось с типом void
. В любой написанной вами функции, где есть вероятность сбоя внутри функции, вы ДОЛЖНЫ предоставить значимый возврат вызывающей функции, чтобы указать, была ли запрошенная операция функции успешной или неудачной.
Что вы можете сделать с safer_gets()
? Почему бы не вернуть простое значение int
, указывающее количество символов, прочитанных при успехе, или -1
(нормальное значение для EOF
) при ошибке. Вы получаете двойной бонус за то, что теперь можете проверить, был ли ввод успешным, и вы также получите количество символов в строке (ограничено 2147483647
символами). Теперь у вас также есть возможность обрабатывать отмену ввода пользователем, генерируя EOF вручную с помощью Ctrl + d для Linux или Ctrl + z (windows).
Вы также должны очистить stdin
от всех символов, введенных во ВСЕХ случаях, кроме EOF
. Это гарантирует, что после вашего вызова safer_gets()
не останется непрочитанных символов, которые могут укусить вас, если вы позже вызовете другую функцию ввода. Внося эти изменения, вы можете написать safer_gets()
как:
/* always provide a meaninful return to indicate success/failure */
int safer_gets (char *array, int max_chars)
{
int c = 0, nchar = 0;
/* loop while room in array and char read isn't '\n' or EOF */
while (nchar + 1 < max_chars && (c = getchar()) != '\n' && c != EOF)
array[nchar++] = c; /* assing to array, increment index */
array[nchar] = 0; /* nul-terminate array on loop exit */
while (c != EOF && c != '\n') /* read/discard until newline or EOF */
c = getchar();
/* if c == EOF and no chars read, return -1, otherwise no. of chars */
return c == EOF && !nchar ? -1 : nchar;
}
( примечание: над тестом на nchar + 1 < max_chars
гарантирует, что символ остается для nul- завершающий символ , и это просто более безопасная перестановка nchar < max_chars - 1
)
Общий подход к проверке ввода
Теперь у вас есть функция ввода, которую вы можете использовать это указывает на успех / неудачу ввода, позволяя вам проверить ввод обратно в вызывающей функции (main()
здесь). Возьмем, к примеру, чтение элемента .full_name
с использованием safer_gets()
. Вы не можете просто слепо вызвать safer_gets()
и не знать, был ли отменен ввод или произошел преждевременный EOF
, и затем используйте строку, заполненную с уверенностью в вашем коде. * Проверять, подтверждать, проверять каждое выражение. Вернувшись в main()
, вы можете сделать это, вызвав safer_gets()
следующим образом: .full_name
(и любая другая строковая переменная):
#define NAMELEN 35 /* if you need a constant, #define one (or more) */
#define ADDRLEN 50 /* (don't skimp on buffer size) */
...
for (;;) { /* loop continually until valid name input */
fputs ("\nEnter name : ", stdout); /* prompt */
int rtn = safer_gets(person[i].full_name, NAMELEN); /* read name */
if (rtn == -1) { /* user canceled input */
puts ("(user canceled input)");
return 1; /* handle with graceful exit */
}
else if (rtn == 0) { /* if name empty - handle error */
fputs (" error: full_name empty.\n", stderr);
continue; /* try again */
}
else /* good input */
break;
}
( note: return safer_gets()
фиксируется в переменной rtn
и затем оценивается для -1
(EOF
), 0
пустая строка или больше 0
, хороший ввод)
Вы можете сделать это для каждой строковой переменной, которую вам нужно использовать, а затем использовать те же принципы, которые обсуждались выше, для чтения и проверки .zip_code
. В целом, в коротком примере, вы можете сделать:
#include <stdio.h>
#include <ctype.h>
#define NAMELEN 35 /* if you need a constant, #define one (or more) */
#define ADDRLEN 50 /* (don't skimp on buffer size) */
#define CITYLEN 25
#define STATELEN 3
#define PERSONS 10
struct information {
char full_name[NAMELEN],
address[ADDRLEN],
city[CITYLEN],
state[STATELEN];
long int zip_code;
};
/* always provide a meaninful return to indicate success/failure */
int safer_gets (char *array, int max_chars)
{
int c = 0, nchar = 0;
/* loop while room in array and char read isn't '\n' or EOF */
while (nchar + 1 < max_chars && (c = getchar()) != '\n' && c != EOF)
array[nchar++] = c; /* assing to array, increment index */
array[nchar] = 0; /* nul-terminate array on loop exit */
while (c != EOF && c != '\n') /* read/discard until newline or EOF */
c = getchar();
/* if c == EOF and no chars read, return -1, otherwise no. of chars */
return c == EOF && !nchar ? -1 : nchar;
}
int main (void) {
/* declare varaibles, initialize to all zero */
struct information person[PERSONS] = {{ .full_name = "" }};
int i = 0, x = 0;
puts ("\nWelcome to the mailing label generator program.\n"); /* greeting */
for (;;) { /* loop continually until a valid no. of people entered */
int rtn = 0; /* variable to hold RETURN from scanf */
fputs ("Number of people to generate labels for? (0-10): ", stdout);
rtn = scanf ("%d", &x);
if (rtn == EOF) { /* user generated manual EOF (ctrl+d [ctrl+z windows]) */
puts ("(user canceled input)");
return 0;
}
else { /* either good input or (matching failure or out-of-range) */
/* all required clearing though newline - do that here */
for (int c = getchar(); c != '\n' && c != EOF; c = getchar()) {}
if (rtn == 1) { /* return equals requested conversions - good input */
if (0 <= x && x <= PERSONS) /* validate input in range */
break; /* all checks passed, break read loop */
else /* otherwise, input out of range */
fprintf (stderr, " error: %d, not in range 0 - %d.\n",
x, PERSONS);
}
else /* matching failure */
fputs (" error: invalid integer input.\n", stderr);
}
}
if (!x) { /* since zero is a valid input, check here, exit if zero requested */
fputs ("\nzero persons requested - nothing further to do.\n", stdout);
return 0;
}
/* Begin loop for individual information */
for (i = 0; i < x; i++) { /* loop until all person filled */
/* read name, address, city, state */
for (;;) { /* loop continually until valid name input */
fputs ("\nEnter name : ", stdout); /* prompt */
int rtn = safer_gets(person[i].full_name, NAMELEN); /* read name */
if (rtn == -1) { /* user canceled input */
puts ("(user canceled input)");
return 1; /* handle with graceful exit */
}
else if (rtn == 0) { /* if name empty - handle error */
fputs (" error: full_name empty.\n", stderr);
continue; /* try again */
}
else /* good input */
break;
}
for (;;) { /* loop continually until valid street input */
fputs ("Enter street address : ", stdout); /* prompt */
int rtn = safer_gets(person[i].address, ADDRLEN); /* read address */
if (rtn == -1) { /* user canceled input */
puts ("(user canceled input)");
return 1; /* handle with graceful exit */
}
else if (rtn == 0) { /* if address empty - handle error */
fputs ("error: street address empty.\n", stderr);
continue; /* try again */
}
else /* good input */
break;
}
for (;;) { /* loop continually until valid city input */
fputs ("Enter city : ", stdout); /* prompt */
int rtn = safer_gets(person[i].city, CITYLEN); /* read city */
if (rtn == -1) { /* user canceled input */
puts ("(user canceled input)");
return 1; /* handle with graceful exit */
}
else if (rtn == 0) { /* if city empty - handle error */
fputs ("error: city empty.\n", stderr);
continue; /* try again */
}
else /* good input */
break;
}
for (;;) { /* loop continually until valid state input */
fputs ("Enter state : ", stdout); /* prompt */
int rtn = safer_gets(person[i].state, STATELEN); /* read state */
if (rtn == -1) { /* user canceled input */
puts ("(user canceled input)");
return 1; /* handle with graceful exit */
}
else if (rtn == 0) { /* if state empty - handle error */
fputs ("error: state empty.\n", stderr);
continue; /* try again */
}
else { /* good input */
/* just 2-chars to convert to upper - do it here */
person[i].state[0] = toupper (person[i].state[0]);
person[i].state[1] = toupper (person[i].state[1]);
break;
}
}
/* read/validate zipcode */
for (;;) { /* loop continually until valid zipcode input */
fputs ("Enter zipcode : ", stdout); /* prompt */
int rtn = scanf ("%ld", &person[i].zip_code); /* read zip */
if (rtn == EOF) { /* user pressed ctrl+d [ctrl+z windows] */
puts ("(user canceled input)");
return 1;
}
else { /* handle all other cases */
/* remove all chars through newline or EOF */
for (int c = getchar(); c != '\n' && c != EOF; c = getchar()) {}
if (rtn == 1) { /* long int read */
/* validate in range */
if (1 <= person[i].zip_code && person[i].zip_code <= 99999)
break;
else
fprintf (stderr, " error: %ld not in range of 1 - 99999.\n",
person[i].zip_code);
}
else /* matching failure */
fputs (" error: invalid long integer input.\n", stderr);
}
}
}
/* Output individual information in mailing format, condition for 0 individuals */
for(i = 0; i < x; i++)
/* you only need a single printf */
printf ("\n%s\n%s\n%s, %s %ld\n", person[i].full_name, person[i].address,
person[i].city, person[i].state, person[i].zip_code);
fputs ("\nThank you for using the program.\n", stdout);
}
( note: , используя #define
для создания необходимых констант, если вам нужно настроить число, вы у вас есть одно место для внесения изменений, и вам не придется выбирать, хотя каждое объявление переменной и l oop ограничивают, чтобы попытаться внести изменения)
Пример использования / Вывод
Когда вы закончите писать какую-либо подпрограмму ввода - go попробуйте разбить ее! Найдите неудачные угловые случаи и исправьте их. Продолжайте пытаться сломать его, намеренно вводя неправильный / недействительный ввод, пока он больше не исключает ничего, кроме того, что требуется пользователю для ввода. Выполните свои процедуры ввода, например,
$ ./bin/nameaddrstruct
Welcome to the mailing label generator program.
Number of people to generate labels for? (0-10): 3
Enter name : Mickey Mouse
Enter street address : 111 Disney Ln.
Enter city : Orlando
Enter state : fL
Enter zipcode : 44441
Enter name : Minnie Mouse
Enter street address : 112 Disney Ln.
Enter city : Orlando
Enter state : Fl
Enter zipcode : 44441
Enter name : Pluto (the dog)
Enter street address : 111-b.yard Disney Ln.
Enter city : Orlando
Enter state : fl
Enter zipcode : 44441
Mickey Mouse
111 Disney Ln.
Orlando, FL 44441
Minnie Mouse
112 Disney Ln.
Orlando, FL 44441
Pluto (the dog)
111-b.yard Disney Ln.
Orlando, FL 44441
Thank you for using the program.
Уважая пользователей wi sh, чтобы отменить ввод в любой момент, когда они генерируют EOF вручную с Ctrl + d на Linux или Ctrl + z (windows), вы сможете справиться с этим из любой точки вашего кода.
При первом запросе:
$ ./bin/nameaddrstruct
Welcome to the mailing label generator program.
Number of people to generate labels for? (0-10): (user canceled input)
Или в любое последующее приглашение:
$ ./bin/nameaddrstruct
Welcome to the mailing label generator program.
Number of people to generate labels for? (0-10): 3
Enter name : Mickey Mouse
Enter street address : 111 Disney Ln.
Enter city : (user canceled input)
Обработка запроса для нулевого лица:
$ ./bin/nameaddrstruct
Welcome to the mailing label generator program.
Number of people to generate labels for? (0-10): 0
zero persons requested - nothing further to do.
(** лично я просто изменил бы тест ввода и попросил бы ввести значение от 1-10
вместо)
Неверный ввод:
$ ./bin/nameaddrstruct
Welcome to the mailing label generator program.
Number of people to generate labels for? (0-10): -1
error: -1, not in range 0 - 10.
Number of people to generate labels for? (0-10): 11
error: 11, not in range 0 - 10.
Number of people to generate labels for? (0-10): banannas
error: invalid integer input.
Number of people to generate labels for? (0-10): 10
Enter name : (user canceled input)
Вы получаете точку ... Итог, вы должны проверить каждый ввод пользователя и знать, что он действителен, прежде чем использовать ввод в вашем программа. Вы не можете проверить любой ввод из любой функции, если вы не проверили возврат . Если ты ничего не уберешь, кроме этого, обучение стоило того.
Посмотри и дай мне знать, если у тебя есть дополнительные вопросы. (и спросите своего профессора, как safer_gets()
обрабатывает EOF
и как вы должны проверить, успешно ли функционировала функция)