Εργαστήριο Τεχνολογίας Λογισμικού
0 votes
3.3k views

Προσπαθώ να διαβάσω την είσοδο στην άσκηση με τα παλίνδρομα. Χρησιμοποιώ <cstdio> και scanf και ο αντίστοιχος κώδικάς μου είναι αυτός:

#include <cstdio>

int main() {
  int N;
  char s[100];

  scanf("%d", &N);
  for (int i = 0; i < N; ++i) {
    scanf("%s", s);
    printf("This is the string I read: %s\n", s);
  }
}

Δε δουλεύει σωστά. Δίνοντας το input της εκφώνησης:

5
madam
this

-0123+ab##ba+3210-
abcdefghijklmnopqrstuvwxyz

(προσέξτε ότι η τέταρτη γραμμή είναι κενή) το αποτέλεσμα του προγράμματός μου είναι:

This is the string I read: madam
This is the string I read: this
This is the string I read: -0123+ab##ba+3210-
This is the string I read: abcdefghijklmnopqrstuvwxyz
This is the string I read: abcdefghijklmnopqrstuvwxyz

Προφανώς κάτι δεν πηγαίνει καλά με την κενή γραμμή. Τι κάνω λάθος και πώς μπορώ να το διορθώσω;

in progintro by (9.5k points) | 3.3k views

2 Answers

+3 votes

TL;DR: Δεν είναι καλή ιδέα να χρησιμοποιήσετε το scanfγια να διαβάσετε το input σε αυτή την άσκηση. Θα παιδευτείτε χωρίς λόγο.


Η λειτουργία του scanf περιγράφεται με μεγάλη λεπτομέρεια εδώ. Αντιγράφω δύο σημεία που είναι ενδιαφέροντα για τη συζήτησή μας:

Conversion specifier %s: "matches a sequence of non-whitespace characters (a string)."

και μετά:

All conversion specifiers other than [, c, and n consume and discard all leading whitespace characters (determined as if by calling isspace) before attempting to parse the input.

Δηλαδή, το scanf("%s", s) ξεκινάει προσπερνώντας τυχόν λευκούς χαρακτήρες στην αρχή του input. Στη συνέχεια, διαβάζει και αποθηκεύει στον πίνακα s μία συμβολοσειρά, μέχρι να βρει τον πρώτο λευκό χαρακτήρα (μη συμπεριλαμβανομένου).

Για τις ανάγκες της άσκησης που συζητάμε, αυτό δεν είναι αυτό που θέλουμε, για δύο λόγους:

  1. Η εκφώνηση δε λέει ότι οι προς εξέταση συμβολοσειρές δεν περιέχουν λευκούς χαρακτήρες, π.χ. κενά διαστήματα. Για παράδειγμα, η συμβολοσειρά "hello olleh" (με ένα ή περισσότερα κενά μεταξύ των λέξεων) είναι παλινδρομική. Τα testcases στον grader δεν έχουν τέτοιες συμβολοσειρές, όμως θα μπορούσαν να έχουν!

  2. Στην περίπτωση των κενών γραμμών, δεν υπάρχει τίποτα για να διαβάσει η scanf σε μια κενή γραμμή, οπότε συνεχίζει να διαβάζει από την επόμενη γραμμή. Αυτός είναι ο λόγος που οι συμβολοσειρές "-0123+ab##ba+3210-" και "abcdefghijklmnopqrstuvwxyz" (4η και 5η στο input, αντίστοιχα) φαίνεται να διαβάζονται πριν την ώρα τους από το πρόγραμμα της ερώτησης. (Αναζητήστε το λόγο που η δεύτερη συμβολοσειρά επαναλαμβάνεται δύο φορές! Σκεφτείτε τι κάνει η scanf όταν τελειώσει το input.)

Άρα τι κάνουμε τώρα;

Ξεχνάμε το (scanf("%s", ...) για αυτή την άσκηση! Δε βολεύει. Αν θέλετε <cstdio>, χρησιμοποιήστε το gets ή καλύτερα το fgets. Διαφορετικά, είτε χρησιμοποιήστε το <iostream> και την cin.getline(), είτε διαβάστε τους χαρακτήρες της γραμμής με τα χεράκια σας, με ένα loop που κάνει getchar(), και αποθηκεύστε τους έναν-έναν στον πίνακά σας. Το τελευταίο είναι πολύ εύκολο!

by (9.5k points)
edited by
+2 votes

Σύμφωνα με το standard της c++ (https://en.cppreference.com/w/cpp/io/c/fscanf) η συνάρτηση scanf όταν πάει να διαβάσει μια συμβολοσειρά προσπερνά πρώτα όλους τους "κενούς" χαρακτήρες (whitespaces) και μετά ξεκινάει να τοποθετεί τους χαρακτήρες στην συμβολοσειρά. Επειδή το newline θεωρείται "κενός" χαρακτήρας η scanf τον προσπερνά και περιμένει να εισάγετε κάποιον μη-κενό χαρακτήρα για να ξεκινήσει να τοποθετεί χαρακτήρες στην συμβολοσειρά.

Συνεπώς με τον τρόπο scanf("%s", s) δεν είναι δυνατόν να ανιχνεύσουμε την κένη σειρά. Μια πρόταση θα ήταν να διαβάσουμε την είσοδο του χρήστη γραμμή-γραμμή. Δηλαδή να διαβάζουμε χαρακτήρες μέχρι να πετύχουμε newline και οτιδήποτε βρούμε να το καταχωρούμε στην συμβολοσειρά (φροντίζοντας στο τέλος να βάλουμε τον null χαρακτήρα '\0' ).

Η ανάγνωση γραμμή-γραμμή μπορεί είτε να υλοποιηθεί από τον γράφοντα είτε να χρησιμοποιηθεί η έτοιμη συνάρτηση της C fgets (https://en.cppreference.com/w/cpp/io/c/fgets).

Στην περίπτωση της συνάρτησης fgets:
Για να ανιχνεύσουμε την κενή σειρά θα ελέγχουμε αν ο πρώτος χαρακτήρας της συμβολοσειράς ειναι το newline. Για να ανιχνεύσουμε αν ο χρήστης εισήγαγε πλήθος χαρακτήρων μεγαλύτερο από τον επιτρεπόμενο ελέγχουμε τον προτελευταίο χαρακτήρα αν είναι newline (εννοώντας ως προτελευταίο τον χαρακτήρα που βρίσκεται ακριβώς πριν το '\0'. Μια ιδέα για να αποφύγουμε τον έλεγχο χαρακτήρα-χαρακτήρα θα ήταν να αρχικοποιήσουμε την συμβολοσειρά με μηδενικά, οπότε να μπορούμε να ελεγξούμε άμεσα τον προτελευταίο χαρακτήρα αν είναι διάφορος από μηδέν και newline)

Για να λειτουργήσει αυτό καλό θα ήταν να είμαστε συνεπείς σε όλη την είσοδο και να διαβάσουμε και τον αρχικό αριθμό με αυτόν τρόπο. Για να μετατρέψουμε την 1η γραμμή (τον αριθμό) σε αριθμό μπορούμε να χρησιμοποιήσουμε την συνάρτηση sscanf (https://en.cppreference.com/w/cpp/io/c/fscanf).

EDIT: Όσο δακτυλογραφούσα την απάντηση, ο κύριος Παπασπύρου (@nickie) έστειλε απάντηση για την ερώτηση. Ο μελλοντικός ανάγνωστης καλύτερα να μελετήσει την απάντησή του, εφόσον είναι ο καθηγητής του μαθήματος.

by (2.9k points)

301 questions

289 answers

288 comments

909 users