C Для l oop пропускает первую итерацию и поддельное число из l oop scanf - PullRequest
0 голосов
/ 01 марта 2020

Я создаю генератор почтовых меток для школы и у меня возникла проблема с несколькими проблемами. Моя программа состоит в том, чтобы взять полное имя, адрес, город, штат и почтовый индекс для людей от 0 до 10. При запуске моей программы у меня возникают две основные проблемы. Функция for l oop пропускает полное имя safergets () и переходит к адресам safergets. Я перешел, чтобы посмотреть, все ли работает, но моя проверка почтового индекса не будет работать правильно. Я добавил printf, чтобы увидеть, был ли ввод тем же номером, и нашел его ложным. Кроме того, я получаю код ошибки для моей строки, пытающейся использовать вывод состояния. Я уверен, что я использую toupper неправильно. Ниже приведен мой код, код ошибки и выходные данные.

#include <stdio.h>
#include <ctype.h>

/* Define structure */

struct information
{
    char full_name[35], address[50], city[25], state[3];
    long int zip_code;
};

/* Function safer_gets */
/* ------------------- */

void safer_gets (char array[], int max_chars)
{
  /* Declare variables. */
  /* ------------------ */

  int i;

  /* Read info from input buffer, character by character,   */
  /* up until the maximum number of possible characters.    */
  /* ------------------------------------------------------ */

  for (i = 0; i < max_chars; i++)
  {
     array[i] = getchar();


     /* If "this" character is the carriage return, exit loop */
     /* ----------------------------------------------------- */

     if (array[i] == '\n')
        break;

   } /* end for */

   /* If we have pulled out the most we can based on the size of array, */
   /* and, if there are more chars in the input buffer,                 */
   /* clear out the remaining chars in the buffer.                      */
   /* ----------------------------------------------------------------  */

   if (i == max_chars )

     if (array[i] != '\n')
       while (getchar() != '\n');

   /* At this point, i is pointing to the element after the last character */
   /* in the string. Terminate the string with the null terminator.        */
   /* -------------------------------------------------------------------- */

   array[i] = '\0';


} /* end safer_gets */

/* Begin main */

int main()
{
    /* Declare variables */

    struct information person[10];
    int x, i;

    /* Issue greeting */

    printf("Welcome to the mailing label generator program.\n\n");

    /* Prompt user for number of individuals between 0 - 10. If invalid, re-prompt */

    do
    {
        printf("How many people do you want to generate labels for (0-10)? ");
        scanf("%i", &x);

        if(x<0 || x>10)
        printf("Invalid number. Please re-enter number. Must be from 0 to 10.\n");

    }while(x<0 || x>10);

    /* Begin loop for individual information */

    for(i = 0; i < x; i++)
    {
        printf("\n\nEnter name: ");
        safer_gets(person[i].full_name, 35); /* This is the step being skipped */

        printf("\nEnter street address: ");
        safer_gets(person[i].address, 50);

        printf("\nEnter city: ");
        safer_gets(person[i].city, 25);

        printf("\nEnter state: ");
        gets(person[i].state);

        /* Begin loop to verify correct zipcode */

        do
        {
            printf("\nEnter zipcode: ");
            scanf("%ld", person[i].zip_code); /* I get a bogus number here */

            if(person[i].zip_code<00001 || person[i].zip_code>99999)
            {
                printf("\nInvalid zipcode. Must be from 00001 to 99999.");
            }
        }while(person[i].zip_code<00001 || person[i].zip_code>99999);
        /* end loop */

    }/* end of loop */

    /* Output individual information in mailing format, condition for 0 individuals */
    if(x>0 && x<10)
    {
    printf("\n\nBelow are your mailing labels:\n\n");
    }

    /* Begin loop for outputting individual(s) mailing labels */

    for(i = 0; i < x; i++)
    {
        printf("%s\n",person[i].full_name);
        printf("%s\n",person[i].address);
        printf("%s\n",person[i].city);

        /* Output state in all uppercase */

        printf("%s\n", toupper(person[i].state)); /* This is where the error code is occurring */

        printf("%.5ld\n\n", person[i].zip_code);
    } /* end of loop */

    printf("Thank you for using the program.\n");

}/*end of main */

Код ошибки: 142: предупреждение: передача аргумента 1 из `toupper 'делает целое число из указателя без приведения.

Вывод :

Welcome to the mailing label generator program.

How many people do you want to generate labels for (0-10)? 1


Enter name:
Enter street address: 100 Needhelp Ave.

Enter city: Gardner

Enter state: NY

Enter zipcode: 01420

Invalid zipcode. Must be from 00001 to 99999.
Enter zipcode:

Я рассмотрел несколько вопросов здесь, чтобы попытаться понять, в чем я ошибаюсь, но, если почувствуете, что это может быть несколько проблем, влияющих на мою программу. Кроме того, наш преподаватель дал функцию safergets моему классу, чтобы гарантировать, что пользователь не вводит больше символов, чем может вместить массив. Спасибо за вашу помощь и терпимость, помогая мне понять мои ошибки!

1 Ответ

4 голосов
/ 01 марта 2020

Давайте посмотрим на проблему один за другим:

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 каждый раз . Вы должны обработать три условия

  1. (return == EOF), пользователь отменил ввод, сгенерировав руководство EOF, нажав Ctrl + d (или на windows Ctrl + z );
  2. (return < expected No. of conversions) a соответствие или вход произошла ошибка. Для совпадения вы должны учитывать каждый символ, оставшийся в вашем входном буфере. (сканирование вперед во входном буфере с чтением и отбрасыванием символов до тех пор, пока не будет найдено '\n' или EOF); и, наконец,
  3. (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 и как вы должны проверить, успешно ли функционировала функция)

...