Как да използвате fgets() за безопасно обработване на въвеждане от потребителя повече от веднъж

Съжалявам, ако дублирам, но опитах ВСИЧКО и не мога да разбера защо този код продължава да се поврежда. Целта с най-висок приоритет беше този код да обработва безопасно въвеждането или просто всичко, което потребителят може да въведе в конзолата, без да се счупи. Но също така ми трябва, за да мога да стартирам повече от веднъж. fgets() няма да ми позволи да направя това, тъй като продължава да чете '\n' някъде и ми пречи да въвеждам повече от веднъж, когато достигне края на do/while цикъла. Опитах fflushing stdin, опитах scanf("%d *[^\n]"); и просто обикновен scanf("%d *[^\n]");, но нито едно от тях не работи и всъщност те нарушават кода! Използвах този уебсайт, за да се опитам да получа кода „Безопасно боравене с въвеждане“ да работят, но не разбирам напълно какво правят. Опитах се да го подправя (правопис?) възможно най-добре, но не съм сигурен дали го направих правилно. Пропуснах ли нещо? Не мислех, че проблем, който изглежда толкова прост, може да бъде толкова голямо главоболие! >_‹

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

using namespace std;

#define BUF_LEN 100
#define SPACE 32
#define SPCL_CHAR1F 33
#define SPCL_CHAR1L 47
#define SPCL_CHAR2F 58
#define SPCL_CHAR2L 64
#define SPCL_CHAR3F 91
#define SPCL_CHAR3L 96
#define NUMF  48
#define NUML 57
#define UC_CHARF 65
#define UC_CHARL 90
#define LC_CHARF 97
#define LC_CHARL 122

void main ()
{
    char* buffer;
    int SpcCounter=0, SpclCounter=0, NumCounter=0,LcCounter=0, UcCounter=0;
    char line[BUF_LEN],response[4];
    char*input="";
    bool repeat=false;

    do
    {
        for(int i=0;i<BUF_LEN;i++)
        {
            line[i]=NULL;
        }
        buffer=NULL;
        printf("Enter your mess of characters.\n");

        buffer=fgets(line,BUF_LEN,stdin);

        //To handle going over the buffer limit: BROKEN
        if(buffer!=NULL)
        {
            size_t last=strlen(line)-1;

            if(line[last]=='\n')

                line[last]='\0';
            else
            {
                fscanf(stdin,"%c *[^\n]");
            }
        }


        for(int i=0;i<BUF_LEN;i++)
        {
            char temp=buffer[i];
            if(temp==SPACE||temp==255)
                SpcCounter++;
            else if((temp >= SPCL_CHAR1F && temp <= SPCL_CHAR1L)||/*Special  characters*/
                (temp >= SPCL_CHAR2F && temp <= SPCL_CHAR2L)||
                (temp >= SPCL_CHAR3F && temp <= SPCL_CHAR3L))
                SpclCounter++;
            else if (temp >=NUMF && temp <= NUML)/*Numbers*/
                NumCounter++;
            else if (temp >= UC_CHARF && temp <= UC_CHARL)/*Uppercase letters*/
                UcCounter++;
            else if (temp >= LC_CHARF && temp <= LC_CHARL)/*Lowercase letters*/
                LcCounter++;
        }

        printf("There were %i space%s, %i special character%s, %i number%s, and %i letter%s,\n"
            "consisting of %i uppercase letter%s and %i lowercase.\n",
            SpcCounter,(SpcCounter==1?"":"s"),SpclCounter,(SpclCounter==1?"":"s"), NumCounter,(NumCounter==1?"":"s"),UcCounter+LcCounter,
            (UcCounter+LcCounter==1?"":"s"), UcCounter,(UcCounter==1?"":"s"), LcCounter);
        printf("Would you like to do this again? (yes/no)");

        input=fgets(response,4,stdin);
        /*
        ALL BROKEN
        if(input!=NULL)
        {
            size_t last=strlen(response)-1;

            if(response[last]=='\n')
                response[last]='\0';
            else
            {
                fscanf(stdin,"%*[^\n]");
                fscanf(stdin,"%c");
            }
        }
        */

        //To capitalize the letters
        for(int i=0;i<4;i++)
        {
            char* temp=&response[i];
            if (*temp >= LC_CHARF && *temp <= LC_CHARL)
                *temp=toupper(*temp);//Capitalize it
        }
        //To set repeat: WORKS, BUT WEIRD
        repeat=!strncmp(input,"YES",4);
    }
    while(repeat);
}

person Skello    schedule 06.01.2014    source източник
comment
stackoverflow.com/questions/4023895/   -  person paxdiablo    schedule 07.01.2014
comment
Това не беше достатъчно. Имах проблем да разбера как работи всяка от различните части, а не просто да получа отговора. Ако си спомняте от публикацията, имах отговора, но не го разбирах, поради което дойдох тук. Освен това вашата справка не покрива възможността да можете да управлявате цикъл while, използвайки същите fgets, което с вашия код изглежда не работи.   -  person Skello    schedule 07.01.2014
comment
За да бъда по-точен, трябва да знам защо трябва да използвам fflush за каквото и да било, как използването на getchar() изчисти нещо от буфера, какво всъщност прави fflush(stdin) (не е в моя публикуван код, но го имах след всяка заявка за въвеждане ), и как да се отърва от нещата, които по някаква причина ВСЕ ОЩЕ СА В БУФЕРА, дори след като го изчистя, изчистя буферите и направя цялото scanf("%*[^\n]") нещо.   -  person Skello    schedule 07.01.2014
comment
Sage, промиването на входните потоци е недефинирано поведение. В допълнение, животът ви ще бъде много по-лесен, ако не се опитвате да смесвате базирано на ред и символно въвеждане. Този отговор, към който се свързах, предоставя всичко, от което се нуждаете за въвеждане на базата на ред, сериозно тествано в продължение на десетилетия.   -  person paxdiablo    schedule 07.01.2014
comment
И работи с непрекъснати повиквания, освен ако не се забърквате с въвеждане на знаци между тях. Беше нещо, което трябваше да разгледате като възможност, докато чакахте по-пълен отговор (затова беше коментар).   -  person paxdiablo    schedule 07.01.2014
comment
Извинявам се, че съм толкова откровен. Цял ден се боря с този проблем, без никакво разбиране какво правя. Всички тези неща, които опитвам, изглеждат или безполезни, или си противодействат едно на друго. Освен това ми е трудно да формулирам въпроса си. Имате ли нещо против, ако просто избухна за секунда: да ви разкажа целия проблем или просто да го публикувам в отделна тема?   -  person Skello    schedule 07.01.2014
comment
позволете ми да разгледам по-подробно въпроса довечера (на път съм за вкъщи). Ще видя дали мога да отговоря подробно. И не се притеснявайте, че ще ме обидите, кожата ми е достатъчно дебела, за да спре неутрино :-)   -  person paxdiablo    schedule 07.01.2014
comment
Sage, добавих отговор, който (надявам се) може да бъде разбран. Разгледайте го и не се колебайте да задавате въпроси в коментарите, а аз ще се опитам да разширя всички области, които смятате, че са недостатъчни.   -  person paxdiablo    schedule 07.01.2014


Отговори (1)


За безопасно, сигурно потребителско въвеждане в C (и в C++, ако използвам низове в стил C), обикновено се връщам към моя стара любима функция getLine:

// Use stdio.h and string.h for C.
#include <cstdio>
#include <cstring>

#define OK       0
#define NO_INPUT 1
#define TOO_LONG 2
static int getLine (char *prmpt, char *buff, size_t sz) {
    int ch, extra;

    // Output prompt then get line with buffer overrun protection.
    if (prmpt != NULL) {
        printf ("%s", prmpt);
        fflush (stdout);
    }
    if (fgets (buff, sz, stdin) == NULL)
        return NO_INPUT;

    // If it was too long, there'll be no newline. In that case, we flush
    // to end of line so that excess doesn't affect the next call.
    if (buff[strlen(buff)-1] != '\n') {
        extra = 0;
        while (((ch = getchar()) != '\n') && (ch != EOF))
            extra = 1;
        return (extra == 1) ? TOO_LONG : OK;
    }

    // Otherwise remove newline and give string back to caller.
    buff[strlen(buff)-1] = '\0';
    return OK;
}

Тази функция:

  • може да изведе подкана, ако желаете.
  • използва fgets по начин, който избягва препълването на буфера.
  • открива края на файла по време на въвеждане.
  • открива дали редът е твърде дълъг, като открива липсата на нов ред в края.
  • премахва новия ред, ако има.
  • "изяжда" знаци до следващия нов ред, за да гарантира, че няма да останат във входния поток за следващото извикване на тази функция.

Това е доста солидна част от кода, който е тестван в продължение на много години и е добро решение на проблема с въвеждането от потребителя.

По отношение на начина, по който го наричате за целите на вашия въпрос, бих добавил нещо много подобно на това, което имате, но използвайки функцията getLine вместо директно извикване на fgets и ръчкане с резултатите. Първо няколко заглавия и същите дефиниции:

#include <iostream>
#include <cstdlib>
#include <cctype>

#define BUF_LEN 100
#define SPACE 32
#define SPCL_CHAR1F 33
#define SPCL_CHAR1L 47
#define SPCL_CHAR2F 58
#define SPCL_CHAR2L 64
#define SPCL_CHAR3F 91
#define SPCL_CHAR3L 96
#define NUMF 48
#define NUML 57
#define UC_CHARF 65
#define UC_CHARL 90
#define LC_CHARF 97
#define LC_CHARL 122

След това първата част от main събира валиден ред (с помощта на функцията), който да бъде оценен:

int main () {
    int SpcCounter, SpclCounter, NumCounter, LcCounter, UcCounter;
    char line[BUF_LEN], response[4];
    bool repeat = false;

    do {
        SpcCounter = SpclCounter = NumCounter = LcCounter = UcCounter = 0;

        // Get a line until valid.

        int stat = getLine ("\nEnter a line: ", line, BUF_LEN);
        while (stat != OK) {
            // End of file means no more data possible.

            if (stat == NO_INPUT) {
                cout << "\nEnd of file reached.\n";
                return 1;
            }

            // Only other possibility is "Too much data on line", try again.

            stat = getLine ("Input too long.\nEnter a line: ", line, BUF_LEN);
        }

Имайте предвид, че промених мястото, където броячите са настроени на нула. Вашият метод ги накара да натрупват стойности всеки път през цикъла, вместо да ги нулират до нула за всеки входен ред. Това е последвано от вашия собствен код, който присвоява всеки символ на клас:

        for (int i = 0; i < strlen (line); i++) {
            char temp=line[i];
            if(temp==SPACE||temp==255)
                SpcCounter++;
            else if((temp >= SPCL_CHAR1F && temp <= SPCL_CHAR1L)||
                (temp >= SPCL_CHAR2F && temp <= SPCL_CHAR2L)||
                (temp >= SPCL_CHAR3F && temp <= SPCL_CHAR3L))
                SpclCounter++;
            else if (temp >=NUMF && temp <= NUML)
                NumCounter++;
            else if (temp >= UC_CHARF && temp <= UC_CHARL)
                UcCounter++;
            else if (temp >= LC_CHARF && temp <= LC_CHARL)
                LcCounter++;
        }

        printf("There were %i space%s, %i special character%s, "
               "%i number%s, and %i letter%s,\n"
               "consisting of %i uppercase letter%s and "
               "%i lowercase.\n",
            SpcCounter,  (SpcCounter==1?"":"s"),
            SpclCounter, (SpclCounter==1?"":"s"),
            NumCounter, (NumCounter==1?"":"s"),
            UcCounter+LcCounter, (UcCounter+LcCounter==1?"":"s"),
            UcCounter, (UcCounter==1?"":"s"),
            LcCounter);

След това накрая, подобен начин като по-горе за запитване дали потребителят иска да продължи.

        // Get a line until valid yes/no, force entry initially.

        *line = 'x';
        while ((*line != 'y') && (*line != 'n')) {
            stat = getLine ("Try another line (yes/no): ", line, BUF_LEN);

            // End of file means no more data possible.

            if (stat == NO_INPUT) {
                cout << "\nEnd of file reached, assuming no.\n";
                strcpy (line, "no");
            }

            // "Too much data on line" means try again.

            if (stat == TOO_LONG) {
                cout << "Line too long.\n";
                *line = 'x';
                continue;
            }

            // Must be okay: first char not 'y' or 'n', try again.

            *line = tolower (*line);
            if ((*line != 'y') && (*line != 'n'))
                cout << "Line doesn't start with y/n.\n";
        }
    } while (*line == 'y');
}   

По този начин вие изграждате вашата програмна логика въз основа на солидна рутина за въвеждане (която се надяваме да разберете като отделна единица).

Можете допълнително да подобрите кода, като премахнете изричните проверки на диапазона и използвате подходящи класове знаци с cctype(), като isalpha() или isspace(). Това би го направило по-преносим (за не-ASCII системи), но ще оставя това като упражнение за по-късно.

Примерно изпълнение на програмата е:

Enter a line: Hello, my name is Pax and I am 927 years old!
There were 10 spaces, 2 special characters, 3 numbers, and 30 letters,
consisting of 3 uppercase letters and 27 lowercase.
Try another line (yes/no): yes

Enter a line: Bye for now
There were 2 spaces, 0 special characters, 0 numbers, and 9 letters,
consisting of 1 uppercase letter and 8 lowercase.
Try another line (yes/no): no
person paxdiablo    schedule 07.01.2014
comment
Благодаря за изчерпателния отговор. Това беше много полезно. Ще разгледам и тези cctype(), isalpha() и isspace() методи, тъй като не знаех, че съществуват. В момента имам само един въпрос: какво точно направи scanf("%*[^\n])? Не разбирам от това, което прочетох на cplusplus.com. - person Skello; 07.01.2014
comment
Търся цялостна реализация за четене на ред повече от половин час и това е най-доброто засега - person sosmo; 28.04.2015
comment
C getline е разширение на GCC. Не е много полезно за Solaris дори с -std=c99 -D_GNU_SOURCE. - person jww; 13.04.2019
comment
@jww, getLine, за което се позовавам (обърнете внимание на главната L), е мое лично и е предоставено в отговора. Той няма връзка (освен подобно име) с функцията getline, предоставена от gcc (или glibc). Трябва да работи на всеки стандартен компилатор. - person paxdiablo; 13.04.2019