Προς το περιεχόμενο

C – Πρόβλημα με την scanf() μέσα σε βρόχο στην περίπτωση λανθασμένης εισαγωγής δεδομένων


cvb~
Μετάβαση στην απάντηση Απαντήθηκε από virxen75,

Προτεινόμενες αναρτήσεις

Δημοσ. (επεξεργασμένο)

    Καλησπέρα! Το παρακάτω μικρό πρόγραμμα μας ζητάει έναν αριθμό που να βρίσκεται μεταξύ δύο άλλων αριθμών. Φαίνεται ότι δουλεύει σωστά όταν εισάγουμε αριθμό. Το πρόβλημα αρχίζει όταν εισάγουμε έναν χαρακτήρα, για παράδειγμα τον ‘a’. Τότε επαναλαμβάνεται διαρκώς στο τερματικό το  μήνυμα “Please give a number between..” και η scanf() δεν δουλεύει. Η scanf() βέβαια περιμένει ακέραιο οριθμό και εμείς της δίνουμε χαρακτήρα. Όμως δεν θα έπρεπε να λειτουργήσει στις επόμενες επαναλήψεις του βρόχου while και να περιμένει για είσοδο δεδομένων; Ευχαριστώ.

#include <stdio.h>

int get_num(const int from, const int to);

int main(void)
{
    int number;
    const int FROM = 1;
    const int TO = 7;

    number = get_num(FROM, TO);
    printf("You gave: %d\n", number);

    return 0;
}

int get_num(const int from, const int to)
{
    int number;

    do {
        printf("Please give a number between %d and %d: ", from, to);
        scanf("%d", &number);
    }
    while (number < from || number > to);

    return number;
}

 

Επεξ/σία από cvb~
Συνδέστε για να σχολιάσετε
Κοινοποίηση σε άλλες σελίδες

Στις 20/10/2022 στις 5:12 ΜΜ, cvb~ είπε

Η scanf() βέβαια περιμένει ακέραιο οριθμό και εμείς της δίνουμε χαρακτήρα. Όμως δεν θα έπρεπε να λειτουργήσει στις επόμενες επαναλήψεις του βρόχου while και να περιμένει για είσοδο δεδομένων; Ευχαριστώ.

8 ώρες πριν, cvb~ είπε

Έχεις δίκιο! Η χρήση της getchar() έλυσε το πρόβλημα. Σ' ευχαριστώ!

Κατάλαβες γιατί η getchar έδωσε τη λύση; (γενικά δεν είναι λύση αλλά στο πλαίσιο της άσκησης αυτής είναι το πιο εύκολο για αυτό και ίσως στο πρότεινε ο virxen75)

Δοκίμασε να περιγράψεις με λόγια τι κάνει ο βρόχος χωρίς και με την getchar.

Συνδέστε για να σχολιάσετε
Κοινοποίηση σε άλλες σελίδες

Να σου πω την αλήθεια με έχει μπερδέψει το γιατί η getchar() έδωσε κάποια λύση στο πρόβλημα του προγράμματος.

Από την μία η scanf() σταματάει την ροή του προγράμματος και περιμένει για είσοδο. Η getchar() κάνει το ίδιο και για την περίπτωση που δοθεί χαρακτήρας και νομίζω πως συλλαμβάνει τα ENTER ('\n').
Η scanf() συμπεριφέραι διαφορετικά όταν περιμένει για είσοδο ακέραιο αριθμό και διαφορετικά όταν περιμένει για είσοδο κάποιον χαρακτήρα.

Συνδέστε για να σχολιάσετε
Κοινοποίηση σε άλλες σελίδες

Δημοσ. (επεξεργασμένο)
2 ώρες πριν, cvb~ είπε

Να σου πω την αλήθεια με έχει μπερδέψει το γιατί η getchar() έδωσε κάποια λύση στο πρόβλημα του προγράμματος.

Από την μία η scanf() σταματάει την ροή του προγράμματος και περιμένει για είσοδο. Η getchar() κάνει το ίδιο και για την περίπτωση που δοθεί χαρακτήρας και νομίζω πως συλλαμβάνει τα ENTER ('\n').
Η scanf() συμπεριφέραι διαφορετικά όταν περιμένει για είσοδο ακέραιο αριθμό και διαφορετικά όταν περιμένει για είσοδο κάποιον χαρακτήρα.

Για να μην μπλέξουμε με υλοποιήσεις standard input / output, ungetc, και γενικά δύσκολες έννοιες, θα αλλάξω τον κώδικα σου ώστε να διαβάζει από αρχείο. Η συμπεριφορά είναι ίδια απλά θα έχουμε fscanf αντί για scanf.

int get_num(const int from, const int to)
{
    int number, res = 0;
    FILE *f;

    f = fopen("data", "r");
    res = fscanf(f, "%d", &number);
    printf("res = %d, file pos = %ld\n", res, ftell(f));
    if (res == 0) {
	    res = fscanf(f, "%d", &number);
	    printf("res = %d, file pos = %ld\n", res, ftell(f));
    }
    fclose(f);

    return number;
}

Ο κώδικας είναι παρόμοιος με τον δικό σου απλά θα κάνει πιο εύκολο να δεις την συμπεριφορά. Αντί λοιπόν να διαβάζει από το standard input, διαβάζει από το αρχείο data (χάριν ευκολίας δεν κάνω ελέγχους αν υπάρχει το αρχείο, κτλ).

% echo "5" > data 
% hexdump -C data  
00000000  35 0a                                             |5.|
% ./getnum 
res = 1, file pos = 1
You gave: 5

Έγραψα την τιμή "5" σε ένα αρχείο ώστε να εξομοιώσω ότι την έδωσες από το πληκτρολόγιο. Όπως, ανέφερες και εσύ, το αρχείο έχει το 5 + το newline. Τρέχοντας την (f)scanf με format specifier το %d, διαβάζεται σωστά ένας αριθμός και το "input" προχωράει μία θέση. Όπως βλέπεις, η μεταβλητή res έχει τιμή 1 (το οποίο σημαίνει ότι η scanf διάβαζε σωστά μία μεταβλητή) και το file pos έχει επίσης τιμή 1 δηλαδή δείχνει πλέον στην 2η θέση του αρχείου (το newline).

% echo "a" > data 
% ./getnum 
res = 0, file pos = 0
res = 0, file pos = 0
You gave: 0

Σε αυτή την περίπτωση, η scanf δεν μπόρεσε να βρει ένα αριθμό οπότε δεν προχωράει την θέση του αρχείου (ή αν θες την προχωράει αναγκαστικά για να επιχειρήσει το διάβασμα και μετά την ξαναγυρνάει) οπότε το "input" σου δείχνει πάντα στον χαρακτήρα a. Και 500 φορές να τρέξεις την scanf, 500 φορές θα επιστρέψει λάθος γιατί διαβάζει το a συνέχεια, για αυτό και βλέπεις το file pos να έχει τιμή 0. Το ίδιο γίνεται και στην περίπτωση του standard input και της σκέτης scanf.

Edit: Αυτό που είπες δηλαδή ότι η scanf σταματά την ροή και περιμένει να δώσεις χαρακτήρες δεν γίνεται πάντα αλλά μόνο όταν το standard input είναι άδειο. Στην δική σου περίπτωση δεν είναι άδειο αλλά περιέχει το "a\n" οπότε δεν περιμένει να δώσεις χαρακτήρες αλλά επεξεργάζεται συνέχεια την ίδια είσοδο.

Τρέχοντας την getchar, όπως σου είπε ο virxen75, "τρως" τον χαρακτήρα a (και την newline) οπότε το standard input είναι ξανά "άδειο" και έτσι η scanf περιμένει ξανά να δώσεις χαρακτήρες.

Επεξ/σία από imitheos
  • Like 1
  • Thanks 1
Συνδέστε για να σχολιάσετε
Κοινοποίηση σε άλλες σελίδες

8 ώρες πριν, παπι είπε

scanf("%d\n", &number); ή scanf("%d\r\n", &number); 

 

Δοκίμασε το και θα δεις ότι δεν θα παίξει. Στην περίπτωση που δώσει μη-αριθμό θα πάρει πάλι το ίδιο κατεβατό με το "δώσε αριθμό" και στην περίπτωση που δώσει σωστά αριθμό, θα πρέπει να δώσει τουλάχιστον ένα κενό και κάτι ακόμη (δηλαδή πχ "3\na\n" ή "3 5\n").

% ./a.out 
Please give a number between 1 and 7: 5 3
You gave: 5

% ./a.out 
Please give a number between 1 and 7: 3
ghjgjkg                               <---- Εδώ η ροή σταματάει και περιμένει να δώσουμε κάτι ακόμη
You gave: 3

Το συγκεκριμένο format specifier λέει στην scanf να διαβάσει ένα αριθμό και μετά ένα χαρακτήρα newline. Πώς όμως ξέρει η scanf ότι ο αριθμός που διάβασε τελείωσε (πχ διαβάζοντας το 3 ότι δεν έχει μετά και ακόμη ένα ψηφίο); Το κάνει όταν "δει" χαρακτήρα "κενού" δηλαδή space, tab, newline, κτλ. Επειδή όμως του είπες ότι το format string πρέπει να περιέχει και ένα newline, θα πρέπει να ψάξει να το βρει.

Στην περίπτωση που έδωσα "5 3" σε μία σειρά, διάβασε κανονικά το 5 επειδή υπήρχε το κενό, μετά αγνόησε το 3 και βρήκε το newline. Στην δεύτερη  περίπτωση που έδωσα "ένα αριθμό" και πάτησα enter, κατάλαβε ότι τελείωσε το 3 επειδή ήρθε το newline. Αυτό όμως το newline μπορούμε καταχρηστικά να πούμε ότι είναι μέρος του αριθμού οπότε θα σταματήσει και θα περιμένει να βρει το newline που του είπες. Έτσι πρέπει να γράψω κάτι ακόμα (οτιδήποτε), το οποίο θα αγνοηθεί, και μετά να πατήσω enter για να βρει το newline που ψάχνει.

Ποτέ δεν βάζουμε \n στην scanf. Υπάρχουν πιο "εσωτερικά / weird" format specifiers που μπορούν να  κάνουν αυτό που θέλουμε αλλά πάντα υπάρχουν περιπτώσεις εισόδων που μπορούν να οδηγήσουν σε αποτυχία και επίσης είναι δυσανάγνωστα οπότε το getchar που πρότεινε ο virxen75 είναι το καλύτερο, ειδικά στο πλαίσιο μιας άσκησης. Βασικά αν δεν μιλούσαμε για άσκηση δεν νομίζω να μιλούσαμε για scanf :)

  • Like 1
Συνδέστε για να σχολιάσετε
Κοινοποίηση σε άλλες σελίδες

Είναι αλήθεια ότι η συνάρτηση scanf() είναι αποδοκιμασμένη, αλλά και η χρήση της συνάρτησης getchar() πρόχειρη, αν και όπως είπε ο imitheos για το συγκεκριμένο παράδειγμα είναι μία λύση.

Νομίζω πως είναι πιο σωστό να παίρναμε την είσοδο με τη συνάρτηση fgets() και να μετατρέπαμε μετά αυτή την είσοδο σε ακέραιο μεγάλο αριθμό με τη συνάρτηση strtol(). Αν και σε αυτή την περίπτωση μπορεί να φαίνεται λίγο υπερβολικό όλο αυτό (over engineered).

Οπότε με βάση αυτό το σκεπτικό ο κώδικας θα μπορούσε να γίνει όπως πιο κάτω που δεν χρησιμοποιούμε πια τη συνάρτηση scanf() αλλά ούτε και τη συνάρτηση getchar(). Διορθώστε με αν κάνω κάπου λάθος.

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

// Asks and returns an long integer number from from to to
long int get_num(const long int from, const long int to);

int main(void)
{
    long int number;
    const long int FROM = 1;
    const long int TO = 7;

    number = get_num(FROM, TO);
    printf("You gave: %ld\n", number);

    return 0;
}

long int get_num(const long int from, const long int to)
{
    long int number;

    do {
        const int BASE = 10;        // Numbering system for strtol()
        const int BUF_SIZE = 10;    // Buffer size for fgets()
        char str_buf[BUF_SIZE];     // Buffer to put data from fgets()

        printf("Please give a number between %ld and %ld: ", from, to);
        if(fgets(str_buf, BUF_SIZE, stdin) != NULL);
            number = strtol(str_buf, NULL, BASE);
    }
    while (number < from || number > to);

    return number;
}

 

Συνδέστε για να σχολιάσετε
Κοινοποίηση σε άλλες σελίδες

26 λεπτά πριν, cvb~ είπε

Είναι αλήθεια ότι η συνάρτηση scanf() είναι αποδοκιμασμένη, αλλά και η χρήση της συνάρτησης getchar() πρόχειρη, αν και όπως είπε ο imitheos για το συγκεκριμένο παράδειγμα είναι μία λύση.

Νομίζω πως είναι πιο σωστό να παίρναμε την είσοδο με τη συνάρτηση fgets() και να μετατρέπαμε μετά αυτή την είσοδο σε ακέραιο μεγάλο αριθμό με τη συνάρτηση strtol(). Αν και σε αυτή την περίπτωση μπορεί να φαίνεται λίγο υπερβολικό όλο αυτό (over engineered).

Είναι όντως over-engineered αλλά όταν δεν μπορείς να ελέγξεις την είσοδο, η μόνη λύση (σε c) είναι να δέχεσαι την είσοδο σαν string και μετά να τρέξεις είτε strtoX, όπως είπες, ή sscanf / strtok / whatever αν θέλεις να κάνεις parse κάτι πιο πολύπλοκο. Αυτό γίνεται γιατί έχεις την ευχέρεια να πραγματοποιήσεις ελέγχους για το αν είναι σωστή η είσοδος.

26 λεπτά πριν, cvb~ είπε
long int get_num(const long int from, const long int to)
{
    do {
        Αν και ο compiler θα κάνει σωστά τη δουλειά του,
        αυτά μπορούν να μπουν έξω από το do
        const int BASE = 10;        // Numbering system for strtol()
        const int BUF_SIZE = 10;    // Buffer size for fgets()
        char str_buf[BUF_SIZE];     // Buffer to put data from fgets()

        Εδώ έχεις ένα ερωτηματικό στο τέλος οπότε θα τρέξει μεν η fgets
        αλλά το if θα είναι άχρηστο και το strtol θα τρέχει πάντα
        Αν αφαιρέσεις το ερωτηματικό και τρέχει σωστά το if,
        τότε ενδεχομένως το number να μην έχει σωστή τιμή
        και δεν το ελέγχεις πριν το επιστρέψεις
        if(fgets(str_buf, BUF_SIZE, stdin) != NULL);
            number = strtol(str_buf, NULL, BASE);
    }
}

 

Από εκεί και πέρα, άλλα πράγματα που, για λόγους πληρότητας, καλό είναι να γίνουν (και προσθέτουν στο over-engineering του πράγματος σε σημείο να γελάνε αυτοί που προγραμματίζουν σε άλλες γλώσσες :P) είναι να  πρέπει να αφαιρεθεί το newline από το τέλος του str_buf (εδώ φυσικά δεν χρειάζεται) και να εκμεταλλευτείς το γεγονός ότι η strtoX σου επιτρέπει να κάνεις ελέγχους.

% ./a.out 
Please give a number between 1 and 7: 5
You gave: 5
% ./a.out 
Please give a number between 1 and 7: 5 dkfkgk
You gave: 5

Για παράδειγμα δες το παραπάνω. Επειδή έβαλες NULL και δεν χρησιμοποίησες την δυνατότητα ελέγχου που σου παρέχει η strtol, το πρόγραμμα δέχεται σαν σωστή την είσοδο αν περιέχει ένα αριθμό ακόμη και αν μετά έχει "σκουπίδια".

Συνδέστε για να σχολιάσετε
Κοινοποίηση σε άλλες σελίδες

Νομίζω πως έχεις δίκιο σ' αυτά που αναφέρεις. Για το ερωτηματικό ειδικά, ήταν αβλεψία μου, η πρόθεση μου ήταν να μην υπάρχει, δηλαδή να εκτελεστεί η γραμμή κώδικα κατώ απ' το if εφόσον ο έλεγχος στο if ισχύει.

Συνδέστε για να σχολιάσετε
Κοινοποίηση σε άλλες σελίδες

14 ώρες πριν, imitheos είπε

 

Ποτέ δεν βάζουμε \n στην scanf. Υπάρχουν πιο "εσωτερικά / weird" format specifiers που μπορούν να  κάνουν αυτό που θέλουμε αλλά πάντα υπάρχουν περιπτώσεις εισόδων που μπορούν να οδηγήσουν σε αποτυχία και επίσης είναι δυσανάγνωστα οπότε το getchar που πρότεινε ο virxen75 είναι το καλύτερο, ειδικά στο πλαίσιο μιας άσκησης. Βασικά αν δεν μιλούσαμε για άσκηση δεν νομίζω να μιλούσαμε για scanf :)

Μια χαρα μπαινει το \n :P ειναι και κατι regexοδειδες που παιρνει με [] αλλα οπως λες "βασικα αν δε μιλουσαμε για ασκηση δεν νομιχω να μιλουσαμε για scanf"

 

Ετσι για το τζερτζελο, ποιος εχει τι πιο ευστοχη απαντηση

εγω που εβαλα οτι θα διαβασει αριθμο και newline που μπορει να δουλεψει μπορει και οχι

ο virxen75 που εβαλε ενα ασχετο/σχετικο getchar που θα μαζεψει το newline και αυτο μπορει να δουλεψει μπορει και οχι

εσυ που εκανες ενα δικο σου implementation το οποιο θα δουλεψει

ή ο ts που εβαλε μια συναρτηση που λεει οτι διβαζει εναν αριθμο αλλα τελικα δεν διαβαζει εναν αριθμο και δεν δουλευει.

 

Προσωπικα πιστευω οτι η πιο σωστη απαντηση ειναι του TS. Ασχετα που δε δουλευει.

Συνδέστε για να σχολιάσετε
Κοινοποίηση σε άλλες σελίδες

Στο βιβλίο “Η γλώσσα προγραμματισμού C” (K&R, 2η εκδ. εκδ. Κλειδάριθμος), στο κεφάλαιο “7.4 Μορφοποιημένη είσοδος – scanf”, στη σελίδα 219, ανάμεσα σε άλλα αναφέρει: “Η scanf σταματάει όταν εξαντλήσει το αλφαριθμητικό μορφοποίησης ή αν κάποια είσοδος δεν συμφωνήσει με την προδιαγραφή της.”

Εγώ δίνω στη scanf χαρακτήρα αλλά αυτή ζητάει ακέραιο, οπότε η scanf σταματάει να δουλεύει, άρα έτσι εξηγείται που χωρίς την getchar επαναλαμβάνεται συνέχεια ο βρόχος.

Τώρα η getchar (όπως έδειξε ο virxen75), φαίνεται πως “τρώει” (όπως είπε ο imitheos) τον χαρακτήρα που δώσαμε με αποτέλεσμα η scanf πλέον να δουλεύει σωστά στο βρόχο ότι δεδομένα και να δώσουμε.

Συνδέστε για να σχολιάσετε
Κοινοποίηση σε άλλες σελίδες

Δημιουργήστε ένα λογαριασμό ή συνδεθείτε για να σχολιάσετε

Πρέπει να είστε μέλος για να αφήσετε σχόλιο

Δημιουργία λογαριασμού

Εγγραφείτε με νέο λογαριασμό στην κοινότητα μας. Είναι πανεύκολο!

Δημιουργία νέου λογαριασμού

Σύνδεση

Έχετε ήδη λογαριασμό; Συνδεθείτε εδώ.

Συνδεθείτε τώρα
  • Δημιουργία νέου...