Ενότητα 3 Πράξεις, Τελεστές & Σταθερές Πράξεις, Τελεστές & Σταθερές Καλώς ήλθατε στη 3 η παρουσίαση του μαθήματος ηλεκτρονικής διδασκαλίας «Εισαγωγή στη γλώσσα C & Εφαρμογές» της Digital Academy. Εισαγωγή Στην ενότητα αυτή θα ασχοληθούμε με την υλοποίηση των πράξεων (αριθμητικών και άλλων) και τη χρήση των τελεστών (operators) στη C. Θα παρουσιάσουμε το σύνολο σχεδόν των τελεστών της γλώσσας μέσα από αναλυτικά παραδείγματα κώδικα, τονίζοντας, παράλληλα, τα σημεία στα οποία πρέπει να δώσετε ιδιαίτερη προσοχή. Επιπλέον, θα παρουσιάσουμε το τρόπο ορισμού και χρήσης σταθερών σε ένα πρόγραμμα C, ενώ θα δώσουμε και ορισμένες διευκρινήσεις που αφορούν στη χρήση της scanf() για την ανάγνωση χαρακτήρων από το χρήστη και τα προβλήματα που μπορούν να δημιουργηθούν. Πράξεις, Τελεστές & Σταθερές 1
Τελεστές (Operators) Έχοντας καθορίσει τον τρόπο με τον οποίο μπορούμε να αναπαραστήσουμε, να αποθηκεύσουμε και να εμφανίσουμε δεδομένα χρησιμοποιώντας μεταβλητές, το επόμενο βήμα είναι να παρουσιάσουμε τους τρόπους με τους οποίους μπορούμε να επεξεργαστούμε τα δεδομένα αυτά. Ορολογία & Γενική Μορφή Όπως οι περισσότερες γλώσσες προγραμματισμού, η C παρέχει έναν αριθμό από ειδικά σύμβολα προκειμένου να μας επιτρέπει να εκτελέσουμε πράξεις (operations) και να τροποποιήσουμε τα δεδομένα μας. Τα σύμβολα αυτά, ονομάζονται τελεστές (operators), ενώ τα δεδομένα πάνω στα οποία επενεργούν, ονομάζονται τελεστέοι (operands). Κατηγορίες Ανάλογα με τον αριθμό των τελεστέων στους οποίους επενεργούν, οι τελεστές διακρίνονται σε: Unary (μοναδιαίοι): Πρόκειται για τελεστές που επενεργούν πάνω σε ένα μόνο τελεστέο (π.χ. ο τελεστής ~ ή ο τελεστής πρόσημου +). Binary (δυαδικοί): Πρόκειται για τελεστές που επενεργούν πάνω σε δύο τελεστέους και είναι οι πιο κοινοί (π.χ. οι τελεστές των κλασικών αριθμητικών πράξεων +, -, κ.λπ.). Ternary (τριαδικοί): Πρόκειται για τελεστές που επενεργούν πάνω σε τρεις τελεστέους. Υπάρχει μόνο ένας τέτοιος τελεστής (υπό-έλεγχο-ανάθεση). Επίσης, και ανάλογα με τη σημασία τους, χωρίζονται σε: Ανάθεσης (Assignment) Αριθμητικούς (Arithmetic) Αντικατάστασης (Compound Assignment) Αύξησης & Μείωσης (Increment & Decrement) Συγκριτικούς-Σχεσιακούς (Comparison-Relational) Λογικούς (Logical) Επιπέδου bit (Bitwise) Εδικής Χρήσης o Έχουν ειδική σημασία και θα τους παρουσιάσουμε κατά περίπτωση. Γενική Μορφή Όταν θέλουμε να αναπαραστήσουμε κανόνες που ισχύουν για μια οικογένεια πράξεων, θα χρησιμοποιούμε το σύμβολο op για την αναπαράσταση του τελεστή, και τα a, b, c για την αναπαράσταση των τελεστέων. Έτσι, για παράδειγμα, η γενική μορφή των μοναδιαίων τελεστών είναι op a, ενώ η γενική μορφή των δυαδικών τελεστών είναι a op b. Στη συνέχεια, θα παρουσιάσουμε αναλυτικά τους τελεστές της C. Για τα αποσπάσματα κώδικα που δίνονται, μπορείτε να θεωρήσετε ότι είναι δηλωμένες οι ακέραιες μεταβλητές (int) a, b, c, οι μεταβλητές χαρακτήρα (char) d, e, f και οι μεταβλητές κινητής υποδιαστολής (double) x, y, z. Πράξεις, Τελεστές & Σταθερές 2
Τελεστής Ανάθεσης (Assignment) Όπως είδαμε και στην προηγούμενη ενότητα, η C χρησιμοποιεί το σύμβολο =, ως τελεστή ανάθεσης. Να υπενθυμίσουμε ότι ο τελεστής έχει το νόημα της ανάθεσης τιμής και όχι της μαθηματικής ισότητας ή ταυτότητας. Έτσι λοιπόν, όλες οι εντολές που φαίνονται στο παρακάτω απόσπασμα κώδικα είναι έγκυρες. (Δίπλα σε κάθε γραμμή, σε σχόλια, εμφανίζεται το αποτέλεσμα της αντίστοιχης εντολής.) a = 4; // [01]: a is now 4 b = a; // [02]: b is now 4 c = b = 3; // [03]: both c & b are now 3 x = a; // [04]: x is now 4 (or 4.0 to be more exact) Listing 1 Τελεστής Ανάθεση Παρατηρήσεις Στη C όλες οι εκφράσεις έχουν τιμή. Έτσι, η έκφραση b = 3 έχει τιμή που είναι ίση με τη τιμή του b. Αυτό είναι που επιτρέπει σε εντολές όπως αυτή της γραμμής [03] να λειτουργούν σωστά, καθώς, η C υπολογίζει τις αναθέσεις από τα δεξιά προς τα αριστερά. o Καλό βέβαια είναι, τέτοιου είδους κώδικας να αποφεύγεται γιατί δημιουργεί προβλήματα αναγνωσιμότητας και δυσκολεύει το debugging. Η C δεν είναι ιδιαίτερα αυστηρή σχετικά με τη συμφωνία των τύπων δεδομένων ανάμεσα στους τελεστέους μιας πράξης ανάθεσης. Έτσι, επιτρέπει αναθέσεις όπως αυτή στη γραμμή [04] προσπαθώντας να μετατρέψει κατάλληλα την τιμή. o Ανάλογα με τον compiler που χρησιμοποιείτε, μπορεί να σας εμφανίσει λάθος, προειδοποίηση ή και απολύτως τίποτα. o Η συγκεκριμένη ανάθεση είναι γενικά ασφαλής αφού στις περισσότερες υλοποιήσεις ένας double, είναι αρκετά μεγάλος για να αποθηκεύσει οποιοδήποτε int, ωστόσο καλό είναι να αποφεύγεται. o Θα δούμε αργότερα ότι αν προκύψει ανάγκη να γίνουν τέτοιου είδους αναθέσεις, η C παρέχει ένα τελεστή (casting operator) που κάνει τις μετατροπές αυτές ρητές (explicit) προς όφελος τόσο του compiler, όσο και αυτού που διαβάζει τον κώδικα. Αριθμητικοί Τελεστές (Arithmetic Operators) Στην κατηγορία αυτή, ανήκουν οι κλασικοί τελεστές των αριθμητικών πράξεων. Οι τελεστές + και - μπορούν να χρησιμοποιηθούν είτε ως δυαδικοί (υποδηλώνοντας την αντίστοιχη πράξη), είτε ως μοναδιαίοι (υποδηλώνοντας το πρόσημο). Στον παρακάτω πίνακα φαίνονται οι αριθμητικοί τελεστές που υποστηρίζει η C: Σύμβολο - Όνομα & Περιγραφή Παράδειγμα Τελεστής + Θετικό Πρόσημο +5 + Πρόσθεση 2+3 - Αρνητικό Πρόσημο -5 - Αφαίρεση a-3 * Πολλαπλασιασμός a*b Πράξεις, Τελεστές & Σταθερές 3
Σύμβολο - Όνομα & Περιγραφή Τελεστής / Διαίρεση Όταν και οι δύο τελεστέοι είναι ακέραιοι, εφαρμόζεται ακέραια διαίρεση. Αν ένας από τους δύο είναι κινητής υποδιαστολής, εφαρμόζεται διαίρεση πραγματικών αριθμών. % Υπόλοιπο Διαίρεσης (Modulo) Εφαρμόζεται μόνο σε ακέραιους. Πίνακας 1 Αριθμητικοί Τελεστές Παράδειγμα a/b x/2 a/2.0 a%b Στο παρακάτω απόσπασμα κώδικα φαίνονται παραδείγματα χρήσης των αριθμητικών τελεστών με σχολιασμό για την περιγραφή του αποτελέσματος. a = -5; // [05]: a is now -5 a = 3 + 2; b = a - 3; // [06]: a is now 3+2=5 // [07]: b is now 5-3=2 c = a * b; // [08]: c is now 5*2=10 c = a / b; // [09]: c is now 5/2=2 c = a % b; // [10]: c is now 5%2=1 y = a / 2; // [11]: y is now 2 (or 2.0 to be more exact) x = a; y = x / 2; // [12]: x is now 5 (or 5.0 to be more exact) // [13]: y is now 2.5 z = a / 2.0; // [14]: z is now 2.5 Listing 2 - Αριθμητικοί Τελεστές Ακέραια Διαίρεση & Υπόλοιπο Ίσως οι δύο τελεστές με τους οποίους αξίζει να ασχοληθούμε λίγο παραπάνω είναι αυτός της διαίρεσης και του υπολοίπου (modulo). Όταν και οι δύο τελεστέοι είναι ακέραιοι, και ανεξάρτητα με το που ανατίθεται το αποτέλεσμα, η C υλοποιεί ακέραια διαίρεση, υπολογίζοντας πρακτικά το πηλίκο. Έτσι, τόσο στη γραμμή [09], όσο και στην [11], η διαίρεση 5/2 θα δώσει αποτέλεσμα 2. Αν επιθυμούμε να κάνουμε διαίρεση πραγματικών, θα πρέπει να εξασφαλίσουμε ότι τουλάχιστον ο ένας από τους δύο τελεστέους θα είναι πραγματικός (κινητής υποδιαστολής), είτε αναθέτοντας προσωρινά την ακέραια τιμή σε έναν πραγματικό κι εκτελώντας τη διαίρεση με αυτόν (γραμμές [12-13]), είτε δηλώνοντας ρητά στη C ότι θέλουμε να αντιμετωπίσει τους αριθμούς ως πραγματικούς (γραμμή [14]). Σημείωση: Η χρήση της ακέραιας διαίρεσης, δημιουργεί επιπλέον σύγχυση όταν χρησιμοποιούνται αρνητικοί αριθμοί. Έτσι, για παράδειγμα στη διαίρεση -7/4 (=-1.75) κάποιες υλοποιήσεις επέλεγαν κατ αντιστοιχία με τους θετικούς αριθμούς να δώσουν ως αποτέλεσμα τον αμέσως μικρότερο ακέραιο (-2), ενώ κάποιες άλλες επέλεγαν απλά να απορρίψουν το δεκαδικό μέρος (-1). Προκειμένου να μην υπάρχουν διαφοροποιήσεις ανάμεσα στις υλοποιήσεις, το πρότυπο C99 ξεκαθαρίζει ότι η υλοποίηση της ακέραιας διαίρεσης πρέπει να γίνεται με το δεύτερο τρόπο (αποκοπή truncate toward zero). Ο τελεστής υπολοίπου (modulo %) αποτελεί το λογικό συμπλήρωμα της ακέραιας διαίρεσης, υπολογίζοντας το υπόλοιπο της διαίρεσης δύο αριθμών. Ο τελεστής μπορεί να εφαρμοστεί μόνο ανάμεσα σε ακέραιους αριθμούς, αν και υπάρχουν εξωτερικές βιβλιοθήκες που επιτρέπουν τον υπολογισμό του υπολοίπου κατά τη διαίρεση και πραγματικών αριθμών, χωρίς ωστόσο να χρησιμοποιείται ο συγκεκριμένος τελεστής, αλλά ειδικές συναρτήσεις. Πράξεις, Τελεστές & Σταθερές 4
Προτεραιότητα Τελεστών Η C επιτρέπει την εκτέλεση πολλών πράξεων σε μια εντολή. Έτσι, γίνεται αναγκαία η εισαγωγή της έννοιας της προτεραιότητας τελεστών που καθορίζει με ποια σειρά θα εκτελεστούν οι πράξεις. Στον παρακάτω πίνακα φαίνεται η σειρά προτεραιότητας των τελεστών (από τη μεγαλύτερη προς τη μικρότερη - πρακτικά η ίδια με την κλασική προτεραιότητα μαθηματικών πράξεων), καθώς και η κατεύθυνση με την οποία συσχετίζεται ένας τελεστής με τον ή τους τελεστέους. Προσέξτε ότι τα μοναδιαία (unary) + και - έχουν υψηλότερη προτεραιότητα από τα αντίστοιχα δυαδικά (binary). Τελεστές Κατεύθυνση Συσχέτισης () Αριστερά Προς Τα Δεξιά + - (unary) Δεξιά Προς Τα Αριστερά * / Αριστερά Προς Τα Δεξιά + - (binary) Αριστερά Προς Τα Δεξιά = Δεξιά Προς Τα Αριστερά Πίνακας 2 - Προτεραιότητα Αριθμητικών Τελεστών Αν και οι κανόνες είναι σχετικά απλοί, δεν υπάρχει κανένας λόγος να μπαίνετε στη διαδικασία να δοκιμάζετε τις γνώσεις σας σε αυτούς, ειδικά καθώς θα αρχίσετε να χρησιμοποιείτε κι επιπλέον τελεστές πέρα από τους αριθμητικούς. Αποκτήστε, λοιπόν, τη συνήθεια, να χρησιμοποιείτε παρενθέσεις για ο,τιδήποτε πέρα από τις πιο τετριμμένες περιπτώσεις. Για παράδειγμα, οι δύο γραμμές στο παρακάτω απόσπασμα κώδικα είναι εντελώς ισοδύναμες. a = -2 * 3 + 4 / -2 + 1; // [15]: practically the same as [16] a = (-2 * 3) + (4 / -2) + 1; // [16]: a is now (-6)+(-2)+1=-7 Listing 3 Προτεραιότητα Αριθμητικών Πράξεων Τελεστές Αντικατάστασης (Compound Assignment Operators) Μια από τις πιο κοινές τεχνικές που εμφανίζονται στην επίλυση σχεδόν όλων των προβλημάτων είναι η ανανέωση μιας μεταβλητής όχι με μια αυθαίρετη τιμή, αλλά με μια τιμή που σχετίζεται με το προηγούμενο περιεχόμενό της. Για παράδειγμα αύξησε την τιμή της a κατά 5 ή διπλασίασε τη b, τα οποία (σύμφωνα με όσα έχουμε δει μέχρι τώρα) θα υλοποιούνταν ως a=a+5 και b=b*2 αντίστοιχα. Προκειμένου για τη διευκόλυνση της συγγραφής τέτοιων εκφράσεων, η C παρέχει τους τελεστές αντικατάστασης (compound assignment operators) οι οποίοι με ένα συνδυασμό συμβόλων εκφράζουν τόσο την αντίστοιχη πράξη όσο και τον τελεστή ανάθεσης. Έτσι, για παράδειγμα οι προηγούμενες εντολές θα μπορούσαν να γραφτούν a+=5 και b*=2 αντίστοιχα. Πρακτικά, για κάθε αριθμητικό τελεστή (και όχι μόνο, όπως θα δούμε αργότερα) υπάρχει ένας αντίστοιχος τελεστής αντικατάστασης που συνδυάζει την αριθμητική πράξη με τη διαδικασία της ανάθεσης τιμής. Στο παρακάτω απόσπασμα κώδικα δίνονται παραδείγματα χρήσης των τελεστών αντικατάστασης. Πράξεις, Τελεστές & Σταθερές 5
a = 7; // [15]: Initialization, a is now 7 b = 3; // [16]: Initialization, b is now 3 a += 5; // [17]: Same as a = a + 5 --> 7 + 5 = 12 b *= 2; // [18]: Same as b = b * 2 --> 3 * 2 = 6 a -= 8; // [19]: Same as a = a - 8 --> 12-8 = 4 b /= a; // [20]: Same as b = b / a --> 6 / 5 = 1 a %= 2; // [21]: Same as a = a % 2 --> 4 % 2 = 0 Listing 4 Τελεστές Αντικατάστασης (Αριθμητικοί) Παρατηρήσεις Ο αριστερός τελεστέος πρέπει πάντα να είναι μεταβλητή, γιατί σε αυτόν θα ανατεθεί το αποτέλεσμα της πράξης. Για παράδειγμα το 7+=2 θα οδηγήσει σε συντακτικό λάθος. Πολλοί θεωρούν ότι οι τελεστές αντικατάστασης είναι δυσνόητοι και περιπλέκουν τον κώδικα. Αν σας μπερδεύουν, δεν είναι ανάγκη να τους χρησιμοποιείτε (μπορείτε να χρησιμοποιείστε την εκτεταμένη μορφή a = a op b), αλλά σε κάθε περίπτωση θα πρέπει να γνωρίζετε τη λειτουργία τους γιατί είναι σίγουρο ότι θα τους συναντήσετε στον κώδικα άλλων προγραμματιστών. Τελεστές Αύξησης & Μείωσης (Increment & Decrement Operators) Οι τελεστές αύξησης και μείωσης αποτελούν κατά κάποιο τρόπο μια ειδική περίπτωση των τελεστών αντικατάστασης όταν επιθυμούμε να αυξήσουμε (ή να μειώσουμε αντίστοιχα) την τιμή μιας μεταβλητής κατά 1. Γι αυτές λοιπόν τις πολύ συνηθισμένες περιπτώσεις, η C παρέχει τους μοναδιαίους τελεστές ++ και -- που επιτρέπουν την υλοποίηση της αντίστοιχης λειτουργικότητας με έναν πολύ σύντομο τρόπο γραφής. Prefix & Suffix Form (Προθεματική & Επιθεματική Μορφή) Σε αντίθεση με τους περισσότερους μοναδιαίους τελεστές, οι οποίοι τοποθετούνται αποκλειστικά πριν από τον αντίστοιχο τελεστέο (prefix), οι τελεστές αύξησης και μείωσης επιτρέπεται να τοποθετηθούν και μετά από αυτόν (suffix). Έτσι λοιπόν, και τα δύο statements a++ και ++a, είναι έγκυρα. Επιπλέον, αν τα statements εμφανίζονται ως μέρος μιας εντολής ανάθεσης, υπάρχει μια μικρή αλλά σημαντική διαφορά που αφορά στη σειρά διαβάσματος και αύξησης (ή μείωσης) της τιμής της μεταβλητής και γι αυτό πρέπει να χρησιμοποιούνται με ιδιαίτερη προσοχή. Συγκεκριμένα, και θεωρώντας ότι τόσο το b όσο και το c αρχικά έχουν την τιμή 5: a = ++b; Πρώτα, ο τελεστής εφαρμόζεται στη μεταβλητή b και στη συνέχεια η τιμή της b διαβάζεται και ανατίθεται στη μεταβλητή a. Έτσι, μετά την ολοκλήρωση της εντολής, τόσο η a, όσο και η b, περιέχουν την τιμή 6. a = c++; Πρώτα, η τιμή της c διαβάζεται και ανατίθεται στη μεταβλητή a και στη συνέχεια ο τελεστής εφαρμόζεται στη μεταβλητή c. Έτσι, μετά την ολοκλήρωση της εντολής, η a είναι ίση με 5, ενώ η c είναι ίση με 6. Πράξεις, Τελεστές & Σταθερές 6
Στο παρακάτω απόσπασμα κώδικα δίνονται παραδείγματα χρήσης των τελεστών αύξησης και μείωσης και στις δύο μορφές. a = 7; // [22]: Initialization, a is now 7 a++; // [23]: Same as a = a + 1 --> a is now 8 ++a; // [24]: Same as a = a + 1 --> a is now 9 a--; // [25]: Same as a = a - 1 --> a is now 8 // CAUTION: Prefix vs. Suffix Form b = 5; // [26]: Initialization, b is now 7 c = 5; // [27]: Initialization, c is now 7 a = ++b; // [28]: Prefix Form - a and b are both 6 a = c++; // [29]: Suffix Form - a is 5 and c is 6 Listing 5 Τελεστές Αύξησης & Μείωσης Συγκριτικοί-Σχεσιακοί Τελεστές (Comparison-Relational Operators) Πρόκειται για τους γνωστούς τελεστές ισότητας και ανισότητας και χρησιμοποιούνται για να εκφράσουν τη σχέση ανάμεσα σε δύο τελεστέους. Μια έκφραση που περιλαμβάνει ένα συγκριτικό τελεστή χαρακτηρίζεται ως λογική παράσταση. Η τιμή (αποτέλεσμά) μιας λογικής παράστασης μπορεί να είναι είτε αληθής - true (π.χ. 7 > 5), είτε ψευδής - false (π.χ. 7 < 5). Στον παρακάτω πίνακα δίνεται μια σύνοψη των συγκριτικών τελεστών, ενώ στο απόσπασμα κώδικα που ακολουθεί παρουσιάζονται παραδείγματα χρήσης τους. Τελεστής Σύμβολο Παράδειγμα Περιγραφή - Σημειώσεις ισότητας == a == 5 a ίσο με 5 ανισότητας!= not_eq a!= 5 a not_eq 5 a διάφορο του 5 Η μορφή not_eq μπορεί να χρειάζεται τη συμπερίληψη του αρχείου iso646.h. Καλό είναι να την αποφεύγετε. μεγαλύτερο > a > 5 a μεγαλύτερο του 5 μικρότερο < a < b a μικρότερο του b μεγαλύτερο-ή-ίσο >= a >= 5 a μεγαλύτερο ή ίσο με 5 μικρότερο-ή-ίσο <= a <= b a μικρότερο ή ίσο με b Πίνακας 3 - Συγκριτικοί-Σχεσιακοί Τελεστές a = 5; // [30]: Initialization, a is now 5 b = 8; // [31]: Initialization, b is now 8 c = (a == 5); // [32]: Is a equal to 5? True --> c is now 1 c = (a!= 5); // [33]: Is a not equal to 5? False --> c is now 0 c = (a > 5); // [34]: Is a greater than 5? False --> c is now 0 c = (a < b); // [35]: Is a less than b? True --> c is now 1 c = (a >= 5); // [36]: Is a greater than or equal to 5? True --> c is now 1 c = (a <= b); // [37]: Is a less than or equal to b? True --> c is now 1 c = (a >= 5) + (a < b); /* [38]: (a >= 5) is true (1), and so is (a < b). Therefore c is now 1 + 1 = 2. */ Listing 6 - Συγκριτικοί-Σχεσιακοί Τελεστές Παρατηρήσεις Δώστε ιδιαίτερη προσοχή στον τελεστή ισότητας. Χρησιμοποιεί 2 =. Οι συγκριτικοί τελεστές χρησιμοποιούνται κατά κόρον στις εντολές ελέγχου και στις εντολές επανάληψης που θα δούμε σε επόμενες ενότητες. Υπενθυμίζουμε ότι στη C, το λογικό αληθές εκφράζεται με την τιμή 1, ενώ το λογικό ψευδές με την τιμή 0. Κάτι τέτοιο επιτρέπει την υποστήριξη αριθμητικών πράξεων Πράξεις, Τελεστές & Σταθερές 7
ανάμεσα στις λογικές τιμές (όπως φαίνεται στη γραμμή [38]) αν και κάτι τέτοιο γενικά είναι καλό να αποφεύγεται. Ισότητα & Αριθμοί Κινητής Υποδιαστολής (Πραγματικοί) Η αναπαράσταση των πραγματικών αριθμών στους υπολογιστές (θυμηθείτε ότι τα πάντα καταλήγουν στη μνήμη ως σειρές από bits) παρουσιάζει αρκετές ιδιοτροπίες. Μια αρκετά ενδιαφέρουσα (αλλά και αρκετά επικίνδυνη) ιδιοτροπία σχετίζεται με την αδυναμία ακριβούς αναπαράστασης απλών δεκαδικών αριθμών (π.χ. 0.2). Αυτό, σε συνδυασμό με το γεγονός ότι οι τύποι δεδομένων στη C είναι περιορισμένου μήκους, σημαίνει ότι η εσωτερική δυαδική αναπαράσταση του 0.2 είναι μια προσέγγιση της τιμής αυτής. Η προσέγγιση αυτή είναι αρκετά ακριβής για να μη δημιουργεί προβλήματα κατά τη χρήση των αριθμών στις περισσότερες πρακτικές εφαρμογές, ωστόσο μπορεί σε ορισμένες περιπτώσεις να δημιουργήσει μερικά μη αναμενόμενα αποτελέσματα κατά τη σύγκριση αριθμών που θεωρητικά θεωρούμε ίσους. Έτσι, για παράδειγμα, στο παρακάτω απόσπασμα κώδικα ενώ η τιμή του x θα έπρεπε να είναι ακριβώς 0.2 (0.08/0.4), στην πράξη (και στη συγκεκριμένη υλοποίηση) η σύγκριση στη γραμμή [40], δίνει ψευδές αποτέλεσμα. Γι αυτό το λόγο, όταν χειριζόμαστε πραγματικούς αριθμούς και θέλουμε να ελέγξουμε για ισότητα ανάμεσα σε δύο τιμές, είναι καλύτερο να ελέγχουμε αν οι δύο τιμές είναι αρκετά κοντά. Δηλαδή, όπως φαίνεται στη γραμμή [41], αν η απόλυτη τιμή της διαφοράς τους fabs(x 0.2) είναι αρκετά μικρή. Προφανώς, το αρκετά μικρή είναι ένας σχετικός όρος κι εξαρτάται κάθε φορά από την εφαρμογή που υλοποιούμε. // CAUTION: Limited floating point precision x = 0.08 / 0.4; // [39]: x should be 0.08/0.4 = 0.2 c = ( x == 0.2 ); // [40]: According to my implementation this is NOT true!! c = ( fabs(x - 0.2) < 0.000001 ); /* [41]: Better check if the result and the expected result are close enough (i.e. their absolute difference is very small). In my implementation this IS true. Listing 7 Ισότητα Αριθμών Κινητής Υποδιαστολής (Πραγματικών) Το θέμα της αναπαράστασης των πραγματικών αριθμών είναι τεράστιο και προφανώς ξεφεύγει από τα πλαίσια της τρέχουσας παρουσίασης. Αν κάποιος ενδιαφέρεται περισσότερο για το θέμα και θέλει να ανακαλύψει και μερικές επιπλέον (πιθανότατα καλύτερες) μεθόδους για τη σύγκριση πραγματικών αριθμών, μπορεί να δει εδώ Comparing Floating Point Numbers, 2012 Edition (http://randomascii.wordpress.com/2012/02/25/comparing-floatingpoint-numbers-2012-edition/) και σε άλλες παρόμοιες σελίδες. Πράξεις, Τελεστές & Σταθερές 8
Λογικοί Τελεστές Οι λογικοί τελεστές χρησιμοποιούνται για το συνδυασμό και τη σύνθεση πιο πολύπλοκων λογικών παραστάσεων. Υπάρχουν 3 λογικοί τελεστές, από τους οποίους οι δύο (AND, OR) είναι δυαδικοί, ενώ ο τρίτος (ΝΟΤ) είναι μοναδιαίος. Στον παρακάτω πίνακα δίνεται μια σύνοψη των λογικών τελεστών, ενώ στο απόσπασμα κώδικα που ακολουθεί παρουσιάζονται παραδείγματα χρήσης τους. Τελεστής Σύμβολο Παράδειγμα Περιγραφή - Σημειώσεις NOT! not!(a > 0) not (a > 0) Λογική άρνηση, το αντίθετο το τελεστέου. Αν ο τελεστέος είναι true η λογική του άρνηση είναι false και το αντίστροφο. AND && and OR or (a >= 0) && (a <= 5) (a >= 0) and (a <= 5) Λογική σύζευξη. Το αποτέλεσμα είναι true, μόνο αν και οι δύο τελεστέοι είναι true. b (a > 0) Λογική διάζευξη. b or (a > 0) Το αποτέλεσμα είναι true, αν τουλάχιστον ένας από τους τελεστέους είναι true. Πίνακας 4 - Λογικοί Τελεστές a = 1; // [41]: Initialization, a is now 1 (i.e. true) b =!a; // [42]: b is now 'not a' --> false (i.e. 0) c = (a >= 0) && (a <= 5); /* [43]: (a >= 0) is true, and so is (a <= 5). Since both are true, so is the result (c). Practically this statement checks if a is between 0 and 5. */ c = b (a > 0); /* [44]: b is false, but (a > 0) is true. Since one of them is true, so is the result (c). */ Listing 8 Λογικοί Τελεστές Παρατηρήσεις Όπως και για τον τελεστή ανισότητας, οι μορφές not, and και or μπορεί να χρειάζονται τη συμπερίληψη του αρχείου iso646.h και καλό είναι να τις αποφεύγετε. Επίσης, όπως και με τους συγκριτικούς τελεστές, θα συναντήσετε τους λογικούς τελεστές κυρίως στις εντολές ελέγχου και στις εντολές επανάληψης. Εκτίμηση Τιμής Λογικής Παράστασης Λόγω της φύσης των λογικών τελεστών, η τιμή μιας λογικής παράστασης, μπορεί να υπολογιστεί χωρίς να είναι απαραίτητο να εκτιμηθούν και οι δύο τελεστέοι. Για παράδειγμα στην παράσταση a AND b, αν το a είναι false, τότε και η τιμή όλης της παράστασης θα είναι false, ανεξάρτητα από την τιμή του b. Παρόμοια στην παράσταση a OR b, αν το a είναι true, τότε και η τιμή όλης της παράστασης θα είναι true, ανεξάρτητα από την τιμή του b. Έτσι λοιπόν, οι περισσότεροι compilers κατά τη διάρκεια της μεταγλώττισης ελέγχουν για τέτοιου είδους περιπτώσεις και ανάλογα με τις ρυθμίσεις τους μπορεί να παράγουν βελτιστοποιημένο (optimized) κώδικα μηχανής ο οποίος θα εκμεταλλεύεται αυτή την ιδιότητα προκειμένου να αποφύγει την εκτίμηση του δεύτερου τελεστέου όταν κάτι τέτοιο δεν είναι απαραίτητο. Τυπικά, κάτι τέτοιο δεν αποτελεί πρόβλημα, αλλά μπορεί να οδηγήσει σε αναπάντεχες καταστάσεις, όταν η εκτίμηση του δεύτερου τελεστέου έχει side effects. Όταν δηλαδή η Πράξεις, Τελεστές & Σταθερές 9
εκτίμηση περιλαμβάνει εντολές που έχουν σαν αποτέλεσμα την αλλαγή της τιμής των μεταβλητών. Για παράδειγμα, στο παρακάτω απόσπασμα κώδικα, η τιμή του c στη γραμμή [46] είναι δυνατό να υπολογιστεί με βάση μόνο τον πρώτο τελεστέο (b) ο οποίος είναι false. Έτσι λοιπόν, και ανάλογα με τις ρυθμίσεις βελτιστοποίησης του compiler, ο κώδικας που θα παραχθεί, μπορεί να εκτιμήσει το δεύτερο τελεστέο (a++) ή όχι. Όπως είναι προφανές, στην πρώτη περίπτωση η τιμή του a θα αυξηθεί και θα γίνει 2, ενώ στη δεύτερη θα παραμείνει 1. Μια τέτοια κατάσταση προφανώς και δεν είναι επιθυμητή και γι αυτό προτείνεται (ή μάλλον επιβάλλεται) το σπάσιμο της γραμμής σε δυο ξεχωριστά statements όπως φαίνεται στις γραμμές [48-49]. Εδώ, καθώς η εκτίμηση του δεύτερου τελεστέου δεν περιλαμβάνει side effects, δε μας ενδιαφέρει αν εν τέλει θα γίνει ή όχι η εκτίμηση, και είναι εξασφαλισμένο ότι η τιμή του a θα είναι αυτή που σκόπευε ο προγραμματιστής. // CAUTION: Compiler Optimization & Side Effects a = 1, b = 0; // [45]: Initialization, a is 1 (true) and b is 0 (false) c = b && (a++); /* [46]: Since b is false, the result will always be false no matter what the second operand is. Therefore, and depending on compiler optimization level, the second operand may or may-not be evaluated. Which means, that we don't know what the value of a will be after the statement is complete (might be either 1 or 2). DON'T USE. Better to do it, like in the following lines which always have predictable results. */ a = 1, b = 0; // [47]: Initialization, a is 1 (true) and b is 0 (false) a++; // [48]: Increase the value of a. It is now 2. c = b && a; /* [49]: Since b is false, the code generated by the compiler may or may-not evaluate the second operand, but this will have no effect on its value and therefore is totally safe. */ Listing 9 Λογικοί Τελεστές & Side Effects Τελεστές Επιπέδου bit (Bitwise Operators) Οι τελεστές επιπέδου bit αντιμετωπίζουν τους τελεστέους όχι σαν μια τιμή, αλλά ως μια συλλογή από bits (όπως δηλαδή αναπαρίστανται εσωτερικά) κι εκτελούν λειτουργίες πάνω σε κάθε ένα από αυτά. Περιλαμβάνουν τις βασικές πράξεις της άλγεβρας Boole (που μοιάζουν τόσο στη λογική όσο και στο συμβολισμό με τους λογικούς τελεστές), καθώς και τους τελεστές ολίσθησης. Στον παρακάτω πίνακα δίνεται μια σύνοψη των λογικών τελεστών, ενώ στο απόσπασμα κώδικα που ακολουθεί παρουσιάζονται παραδείγματα χρήσης τους. Στον κώδικα δίπλα σε κάθε εντολή με σχόλια δίνεται η εσωτερική (δυαδική) αναπαράσταση κάθε μεταβλητής και συγκεκριμένα τα τελευταία 8 bits αυτής (όλα τα υπόλοιπα είναι 0). Τελεστής Σύμβολο Παράδειγμα Περιγραφή - Σημειώσεις Bitwise NOT ~ compl ~a not a Άρνηση σε επίπεδο bit, συμπλήρωμα. Αντιστρέφει τα bits του τελεστέου, δηλαδή αν είναι 0 τα κάνει 1 και αν είναι 1 τα κάνει 0. Bitwise AND & bitand a & b a bitand b Σύζευξη σε επίπεδο bit. Στο αποτέλεσμα, ένα bit είναι 1, μόνο αν τα αντίστοιχα bits στους τελεστέους είναι και τα δύο 1. Πράξεις, Τελεστές & Σταθερές 10
Τελεστής Σύμβολο Παράδειγμα Περιγραφή - Σημειώσεις Bitwise OR bitor a b a bitor b Διάζευξη σε επίπεδο bit. Στο αποτέλεσμα, ένα bit είναι 1, αν τουλάχιστον ένα από τα αντίστοιχα bits στους τελεστέους είναι 1. Bitwise XOR Bitwise Left Shift Bitwise Right Shift ^ xor a ^ b a xor b Αποκλειστική διάζευξη σε επίπεδο bit. Στο αποτέλεσμα, ένα bit είναι 1, αν ακριβώς ένα (όχι και τα δύο) από τα αντίστοιχα bits στους τελεστέους είναι 1. << a << 2 Αριστερή ολίσθηση. Μετατοπίζει τα bits του αριστερού τελεστέου προς τα αριστερά. Ο αριθμός των θέσεων μετατόπισης καθορίζεται από το δεξιό τελεστέο. Οι κενές θέσεις που προκύπτουν στα δεξιά γεμίζουν με μηδενικά, ενώ τα αριστερότερα bits που πέφτουν έξω από τον αριθμό χάνονται. >> b >> 1 Δεξιά ολίσθηση. Μετατοπίζει τα bits του αριστερού τελεστέου προς τα δεξιά. Ο αριθμός των θέσεων μετατόπισης καθορίζεται από το δεξιό τελεστέο. Οι κενές θέσεις που προκύπτουν στα αριστερά γεμίζουν με μηδενικά, ενώ τα δεξιότερα bits που πέφτουν έξω από τον αριθμό χάνονται. Πίνακας 5 - Τελεστές Επιπέδου bit (Bitwise Operators) a = 25; // [50]: a is 25 --> 0001 1001 b = 35; // [51]: b is 35 --> 0010 0011 c = ~a; /* [52]: complement of a --> 0001 1001 Therefore, c --> 1110 0110 */ c = a & b; /* [53]: bitwise AND between a --> 0001 1001 and b --> 0010 0011 Therefore, c --> 0000 0001 --> 1 */ c = a b; /* [54]: bitwise OR between a --> 0001 1001 and b --> 0010 0011 Therefore, c --> 0011 1011 --> 59 */ c = a ^ b; /* [55]: bitwise XOR between a --> 0001 1001 and b --> 0010 0011 Therefore, c --> 0011 1010 --> 58 */ c = a << 2; /* [56]: left shift by 2 bits a --> 0001 1001 Therefore, c --> 0110 0100 --> 100 */ c = b >> 1; /* [57]: right shift by 1 bit a --> 0010 0011 Therefore, c --> 0001 0001 --> 17 */ Listing 10 Τελεστές Επιπέδου bit (Bitwise Operators) Παρατηρήσεις Προσέξτε τη διαφορά ανάμεσα στους bitwise τελεστές AND και OR (μονό σύμβολο) και στους αντίστοιχους λογικούς τελεστές (διπλό σύμβολο). Επιλέξτε και χρησιμοποιήστε το σωστό ανά περίπτωση. Οι τελεστές αριστερής και δεξιάς ολίσθησης αποτελούν έναν απλό (και πολύ γρήγορο) τρόπο για την υλοποίηση του πολλαπλασιασμού και της διαίρεσης ακεραίων με δυνάμεις του 2. Παρατηρήστε στο παραπάνω παράδειγμα ότι στη γραμμή [56] η αριστερή ολίσθηση κατά 2 θέσεις έχει ως αποτέλεσμα των τετραπλασιασμό (2 2 =4) του αριθμού, ενώ αντίστοιχα στη γραμμή [57] η δεξιά ολίσθηση κατά 1 θέση έχει ως αποτέλεσμα τη διαίρεση του αριθμού με το 2 (2 1 =2). Πράξεις, Τελεστές & Σταθερές 11
Λόγω του τρόπου με τον οποίο αναπαρίστανται στη μνήμη οι ακέραιοι, όπου στην ουσία το αριστερότερο bit υποδηλώνει κατά κάποιο τρόπο το πρόσημο, οι πράξεις της ολίσθησης μπορεί να έχουν ως αποτέλεσμα την αλλαγή πρόσημου του αριθμού. Οι bitwise τελεστές ήταν αρκετά κοινοί παλιότερα όπου οι προγραμματιστές εκμεταλλευόντουσαν τις ιδιότητές τους για να κάνουνε γρήγορα και αποτελεσματικά μερικές απλές πράξεις. Ωστόσο, σήμερα και λόγω της εξέλιξης του υλικού που υλοποιεί τις αριθμητικές πράξεις, η χρήση τους σπανίζει. Τελεστές Αντικατάστασης (Compound Assignment Operators) Όπως και για τους αριθμητικούς τελεστές, έτσι και για όλους τους δυαδικούς bitwise τελεστές, η C παρέχει αντίστοιχούς τελεστές αντικατάστασης, που επιτρέπουν με πιο σύντομο τρόπο τη συγγραφή statements της μορφής a = a op b. Στον παρακάτω πίνακα παρουσιάζονται συνοπτικά οι bitwise τελεστές αντικατάστασης. Τελεστής Σύμβολο Παράδειγμα Περιγραφή - Σημειώσεις Bitwise AND Assignment &= and_eq a &= b a and_eq b Σύζευξη σε επίπεδο bit και ανάθεση στον αριστερό τελεστέο. Bitwise OR Assignment = or_eq a = b a or_eq b Διάζευξη σε επίπεδο bit και ανάθεση στον αριστερό τελεστέο. Bitwise XOR Assignment ^= xor_eq a ^= b a xor_eq b Αποκλειστική διάζευξη σε επίπεδο bit και ανάθεση στον αριστερό τελεστέο. Bitwise Left Shift Assignment <<= a <<= 2 Αριστερή ολίσθηση και ανάθεση στον αριστερό τελεστέο. Bitwise Right Shift Assignment >>= b >>= 1 Δεξιά ολίσθηση και ανάθεση στον αριστερό τελεστέο. Πίνακας 6 - Bitwise Τελεστές Αντικατάστασης Ειδικής Χρήσης Τελεστές (Special Use Operators) Τέλος, η C παρέχει μια σειρά από ειδικής χρήσης τελεστές, των οποίων ο σκοπός δεν είναι τόσο η υλοποίηση πράξεων, όσο η υποστήριξη ειδικών λειτουργιών και δομών της γλώσσας. Τέτοιοι τελεστές είναι οι τελεστές μετατροπής τύπου (casting), ο τελεστής sizeof(), ο τελεστής δεικτοδότησης πίνακα, οι τελεστές διεύθυνσης και από-αναφοροποίησης, οι τελεστές πρόσβασης στα πεδία μιας δομής και άλλοι. Στη συνέχεια θα παρουσιάσουμε μερικούς από αυτούς τους τελεστές τους οποίους μπορούμε να εκμεταλλευτούμε άμεσα, ενώ τους υπόλοιπους θα τους εξετάζουμε κατά περίπτωση στην αντίστοιχη ενότητα. Πράξεις, Τελεστές & Σταθερές 12
Τελεστής, Ο τελεστής κόμμα (,) διαχωρίζει πολλές δευτερεύουσες εκφράσεις οι οποίες εκτελούνται διαδοχικά από αριστερά προς τα δεξιά, όπως φαίνεται στο παρακάτω απόσπασμα κώδικα. Ωστόσο, η χρήση του οδηγεί σε δυσανάγνωστο κώδικα και γι αυτό δεν χρησιμοποιείται παρά μόνο στις δηλώσεις μεταβλητών. int a, b, c; a = 10, b = a + 5, printf("a + b = %d.\n", a + b); Listing 11 Ο Τελεστής Κόμμα (,) // [70]: Comma operator. Casting Μετατροπή Τύπου Όπως αναφέρθηκε και νωρίτερα στην παρουσίαση του τελεστή ανάθεσης, η C δεν είναι ιδιαίτερα αυστηρή σχετικά με τη συμφωνία των τύπων δεδομένων ανάμεσα στους τελεστέους μιας πράξης ανάθεσης. Όταν οι τύποι δεδομένων δε συμφωνούν, αυτόματα ο compiler θα προσπαθήσει να προβεί στις κατάλληλες μετατροπές προκειμένου να είναι δυνατή η ανάθεση. Ωστόσο, εν τέλει θα υπάρξουν περιπτώσεις όπου είτε ο compiler δε μπορεί αυτόματα να κάνει τις απαραίτητες μετατροπές, ή οι μετατροπές στις οποίες προβαίνει δεν είναι οι επιθυμητές. Για τις περιπτώσεις αυτές, η C παρέχει τους τελεστές μετατροπής τύπου (casting). Οι τελεστές μετατροπής τύπου επιτρέπουν στον προγραμματιστή να δηλώσει ρητά το είδος της μετατροπής που επιθυμεί και πρακτικά να εξαναγκάσει τον compiler να παράγει τον απαραίτητο κώδικα, ακόμα κι αν δεν είναι ασφαλής. Είναι μοναδιαίοι τελεστές και η γενική μορφή τους είναι (επιθυμητός_τύπος_δεδομένων) τελεστέος_άλλου_τύπου όπου επιθυμητός_τύπος_δεδομένων είναι ένας από τους υποστηριζόμενους τύπους δεδομένων της C. Στο παρακάτω απόσπασμα κώδικα παρουσιάζονται μερικά παραδείγματα χρήσης του τελεστή casting. a = 10; // [80]: Initialization, a is now 10 b = 1000; // [81]: Initialization, b is now 1000 x = 12.000000001; // [82]: Initialization, x is now 12.000000001 c = (char)a; // [83]: c is now 10. OK. c = (char)b; // [84]: c is now -24. char too small for 1000 c = (int)x; y = (float)x; // [85]: c is now 12. Truncation // [86]: y is now 12.0. float not precise enough Listing 12 Τελεστής Μετατροπής Τύπου (Casting) Όπως είναι προφανές, οι μετατροπές τύπου δεν είναι ασφαλείς λειτουργίες και μπορεί να μην έχουν πολλές φορές τα αναμενόμενα αποτελέσματα. Επιπλέον, η σωστή λειτουργία τους μπορεί να εξαρτάται όχι μόνο από τον αρχικό και τον τελικό τύπο δεδομένων, αλλά και από τη τιμή που τυγχάνει να έχει εκείνη τη στιγμή η μεταβλητή. Έτσι για παράδειγμα ενώ η μετατροπή (από int σε char) στη γραμμή [83] έχει τα αναμενόμενα αποτελέσματα αφού μια μεταβλητή τύπου char μπορεί να αποθηκεύσει τη τιμή 10, η ίδια μετατροπή στη γραμμή [84] δε δουλεύει σωστά, αφού το char δεν έχει αρκετό εύρος τιμών προκειμένου να αποθηκέυσει τη τιμή 1000. Ομοίως, στις γραμμή [85] η μετατροπή από double σε int έχει ως αποτέλεσμα την απώλεια του δεκαδικού μέρους (σχετικά αναμενόμενο), ενώ στη γραμμή [86] η μετατροπή από double σε float έχει ως αποτέλεσμα τη μειωμένη ακρίβεια και τη συνεπακόλουθη απώλεια πληροφορίας. Πράξεις, Τελεστές & Σταθερές 13
Από όλα τα παραπάνω γίνεται προφανές ότι η χρήση των τελεστών μετατροπής τύπου πρέπει να γίνεται με εξαιρετική προσοχή. sizeof() Ο τελεστής sizeof() είναι ένας μοναδιαίος τελεστής που χρησιμοποιείται για να υπολογίσει το απαιτούμενο μέγεθος στη μνήμη για την αναπαράσταση-αποθήκευση του τελεστέου του. Ο τελεστέος μπορεί να είναι μια κυριολεκτική τιμή (literal), μια μεταβλητή ή ένας τύπος δεδομένων. Μόνο στη τελευταία περίπτωση, είναι υποχρεωτική η χρήση παρενθέσεων, αλλά γενικά καλό είναι για ομοιομορφία και καλύτερη αναγνωσιμότητα να τις χρησιμοποιείτε παντού. Ο τελεστής sizeof() χρησιμοποιείται κυρίως στα προγράμματα που κάνουν δυναμική διαχείριση μνήμης στα οποία πρέπει κατά τη διάρκεια της εκτέλεσής τους να υπολογιστεί το ακριβές μέγεθος μνήμης που πρέπει να δεσμευθεί. Στο παρακάτω απόσπασμα κώδικα δίνονται μερικά απλά παραδείγματα χρήσης του. Να σημειωθεί, ότι καθώς η C για τους περισσότερους τύπους δεδομένων δεν καθορίζει ακριβώς το μέγεθός τους, τα αποτελέσματα που θα πάρετε από την εκτέλεση του κώδικα εξαρτώνται από τη συγκεκριμένη υλοποίηση. a = sizeof(10); // [90]: representation size of 10 --> 4 a = sizeof(a); // [91]: storage size of a --> 4 a = sizeof(double); // [92]: storage size of double variables --> 8 Listing 13 Τελεστής sizeof() Address & Pointer Dereference Οι συμπληρωματικοί τελεστές διεύθυνσης (address - &) και αποαναφοροποίησης (dereference - *) χρησιμοποιούνται κυρίως σε συνεργασία με τους δείκτες (pointers). Στην πράξη, μας επιτρέπουν να δουλέψουμε όχι με τα ίδια τα δεδομένα αλλά με τις διευθύνσεις μνήμης στα οποία αυτά αποθηκεύονται. Αυτό, όσο και αν ακούγεται πρωτόγονο (και κατά κάποιο τρόπο είναι), στην πράξη αποδεικνύεται εξαιρετικά χρήσιμο και αποτελεσματικό. Θα δούμε τους τελεστές αυτούς αναλυτικά, όταν θα παρουσιάσουμε τους δείκτες. Προς το παρόν, να υπενθυμίσουμε ότι ο τελεστής διεύθυνσης χρησιμοποιείται υποχρεωτικά στις μεταβλητές που περνιούνται ως παράμετροι στη scanf(). Πράξεις, Τελεστές & Σταθερές 14
Extras Στη συνέχεια παρουσιάζουμε μερικά επιπλέον χαρακτηριστικά της γλώσσας τα οποία θα σας επιτρέψουν να δημιουργήσετε ορθότερα και ασφαλέστερα προγράμματα. Σταθερές (Constants) Οι σταθερές αποτελούν ένα εργαλείο της C που μας επιτρέπει τον καθορισμό φιλικών προς το χρήστη ονομάτων για κυριολεκτικές τιμές (literals) που έχουν ειδική σημασία για το πρόγραμμα. Όπως φανερώνει και το όνομά τους, η τιμή των σταθερών δεν αλλάζει στη διάρκεια του προγράμματος μετά την αρχική τους δήλωση. Υπάρχουν δύο τρόποι να δηλωθούν σταθερές σε ένα πρόγραμμα C: Με την οδηγία προεπεξεργαστή (preprocessor directive) #define που είναι ο πιο κλασικός τρόπος. Σαν τυπική μεταβλητή με τον επιπλέον προσδιοριστή const. Ο τρόπος αυτός επιτρέπει και τον καθορισμό του τύπου δεδομένων της σταθεράς. Για τα ονόματα των σταθερών, ισχύουν οι ίδιοι κανόνες με τα ονόματα των μεταβλητών, με την επιπλέον σύμβαση ότι για να διαχωρίζονται οπτικά από τις τελευταίες συνήθως χρησιμοποιούμε αμιγώς κεφαλαίους χαρακτήρες. Στη συνέχεια θα παρουσιάσουμε και τους δύο τρόπους με παραδείγματα που θα κάνουν καλύτερα κατανοητούς και τους λόγους για τους οποίους χρησιμοποιούμε σταθερές. #define #include <stdio.h> #define PI 3.14159 int main(void) { double radius, periphery, area; // Get the radius. printf("please enter the radius of the circle: "); scanf("%lf", &radius); // Calculate and display periphery and area. periphery = 2 * PI * radius; area = PI * radius * radius; printf("the periphery of the circle is %.2f and its area is %.2f.\n\n", periphery, area); } return 0; Listing 14 Υπολογισμός Περιφέρειας κι Εμβαδού Κύκλου Ο παραπάνω κώδικας είναι ένα απλό πρόγραμμα που ζητάει από το χρήστη την ακτίνα ενός κύκλου και με βάση αυτό, υπολογίζει κι εμφανίζει την περιφέρειά του και το εμβαδό του. Όπως είναι γνωστό, στον υπολογισμό αυτών των τιμή υπεισέρχεται η τιμή π. Τυπικά θα μπορούσαμε στις γραμμές υπολογισμού της περιφέρειας και του εμβαδού να Πράξεις, Τελεστές & Σταθερές 15
χρησιμοποιήσουμε την κυριολεκτική τιμή 3.14159. Ωστόσο, ο καθορισμός και η χρήση της σταθεράς PI, κάνει τον κώδικα πιο ευανάγνωστο και κατανοητό. Το #define είναι όπως και το #include μια οδηγία προεπεξεργαστή. Συντάσσεται ως #define ΟΝΟΜΑ_ΣΤΑΘΕΡΑΣ ΤΙΜΗ_ΑΝΤΙΚΑΤΑΣΤΑΣΗΣ και στην ουσία καθορίζει ότι καθώς ο προπεξεργαστής σαρώνει τον κείμενο του κώδικα, όπου συναντάει το ΟΝΟΜΑ_ΣΤΑΘΕΡΑΣ θα το αντικαθιστά με την ΤΙΜΗ_ΑΝΤΙΚΑΤΑΣΤΑΣΗΣ. Συνήθως, οι σταθερές αυτής της μορφής δηλώνονται στην αρχή του κώδικα αμέσως πριν ή μετά από τις οδηγίες #include. Προσοχή: Η οδηγία #define όπως και η #include δεν είναι εντολή της C και δεν παίρνει ερωτηματικό στο τέλος. Αν τοποθετήσετε ερωτηματικό, αυτό θα αποτελέσει μέρος της τιμής της σταθεράς. const #include <stdio.h> int main(void) { const double VAT = 0.23; double retail, vatvalue, final; // Get the retail price. printf("please enter the retail price of the item: "); scanf("%lf", &retail); // Calculate and display the vat value and the final price. vatvalue = VAT * retail; final = (1 + VAT) * retail;; printf("the vat value of the item is %.2f and its final price is %.2f.\n\n", vatvalue, final); } return 0; Listing 15 Υπολογισμός Φ.Π.Α. και Τελικής Τιμής Ο παραπάνω κώδικας είναι ένα απλό πρόγραμμα που ζητάει από το χρήστη τη τιμή εμπορίου ενός προϊόντος και με βάση αυτό, υπολογίζει το Φ.Π.Α. και τη τελική του τιμή. Προφανώς, σε αυτούς τους υπολογισμούς υπεισέρχεται η τιμή του καθορισμένου από το κράτος ποσοστού Φ.Π.Α. Αντί λοιπόν να χρησιμοποιούμε συνέχεια την κυριολεκτική τιμή 0.23 (23%) ορίζουμε μια σταθερά (VAT) και χρησιμοποιούμε αυτή. Σε αυτήν την περίπτωση ο καθορισμός και η χρήση σταθεράς, πέρα από το ότι κάνει τον κώδικα πιο ευανάγνωστο και κατανοητό, παρέχει ένα επιπλέον πλεονέκτημα. Αν η κυβέρνηση αποφασίσει να αλλάξει τη τιμή του Φ.Π.Α. δε χρειάζεται να ψάξουμε όλο τον κώδικα για να βρούμε τα σημεία που πρέπει να εφαρμόσουμε την αλλαγή, αλλά αρκεί να αλλάξουμε την τιμή, μόνο σε ένα σημείο, στη δήλωση της σταθεράς. Η δήλωση μιας σταθεράς με τη χρήση του const είναι ίδια με μιας τυπικής μεταβλητής με τη διαφορά ότι: Εμφανίζεται ο προσδιοριστής const πριν από τον τύπο δεδομένων. Η σταθερά πρέπει οπωσδήποτε να αρχικοποιηθεί στο σημείο δήλωσής της. Πράξεις, Τελεστές & Σταθερές 16
scanf() & Διάβασμα Χαρακτήρων Στην προηγούμενη ενότητα παρουσιάσαμε την εντολή scanf(), η οποία επιτρέπει την ανάγνωση δεδομένων από το πληκτρολόγιο, τη μετατροπή τους (με τη βοήθεια ειδικών format specifiers) στον κατάλληλο τύπο και την αποθήκευσή τους στις παρεχόμενες μεταβλητές. Όπως ήδη αναφέρθηκε, η scanf() δεν είναι ιδιαίτερα ασφαλής συνάρτηση και πρέπει να αποφεύγεται σε περιβάλλοντα παραγωγής. Πέρα όμως από το θέμα της ασφάλειας, παρουσιάζει και αρκετές ιδιοτροπίες. Μια από αυτές τις ιδιοτροπίες έγκειται στον τρόπο που χειρίζεται το χαρακτήρα αλλαγής γραμμής που προκύπτει στην είσοδο όταν ο χρήστης πατήσει το πλήκτρο Enter. Συγκεκριμένα, η scanf() παρότι χρησιμοποιεί το συγκεκριμένο χαρακτήρα για να αναγνωρίσει το τέλος των δεδομένων, δεν τον απορρίπτει αλλά τον αφήνει στην ουρά εισόδου. Σε πολλές περιπτώσεις κάτι τέτοιο δεν αποτελεί πρόβλημα. Ο χαρακτήρας θα διαβαστεί από την επόμενη κλήση της scanf() (αν υπάρχει), θα ανιχνευτεί ότι πρόκειται για το χαρακτήρα αλλαγής γραμμής και θα απορριφθεί, με τη scanf() να διαβάζει τον επόμενο χαρακτήρα από την ουρά εισόδου. Το πρόβλημα προκύπτει, όταν η επόμενη κλήση της scanf() αφορά στο διάβασμα όχι κάποιου αριθμού, αλλά χαρακτήρα (specifier %c). Σε αυτή την περίπτωση, κι επειδή ο χαρακτήρας αλλαγής γραμμής (όπως είδαμε και στις ακολουθίες διαφυγής) αποτελεί έναν έγκυρο χαρακτήρα, δε θα απορριφθεί, αλλά αντίθετα θα αποθηκευτεί στην αντίστοιχη μεταβλητή. Για να γίνει αυτό πιο κατανοητό ας δούμε το επόμενο παράδειγμα #include <stdio.h> int main(void) { int age; char gender; // Ask the user for information. printf("please enter your age: "); scanf("%d", &age); printf("are you (M)ale or (F)emale? "); scanf("%c", &gender); // Display info. printf("your age is %d and your gender is '%c'.\n\n", age, gender); } return 0; Listing 16 Λανθασμένη Ανάγνωση Χαρακτήρα Ο παραπάνω κώδικας είναι ένα απλό πρόγραμμα που ζητάει από το χρήστη να εισάγει την ηλικία του κι ένα χαρακτήρα που καθορίζει αν είναι άντρας η γυναίκα και στη συνέχεια εμφανίζει τις πληροφορίες αυτές. (Στην ουσία πρόκειται για μια απλοποιημένη κι ελαφρώς αντεστραμμένη έκδοση του παραδείγματος που είχαμε δει στην παρουσίαση της scanf()). Αν εκτελέσετε το παραπάνω πρόγραμμα θα παρατηρήσετε ότι δε δίνεται στο χρήστη η δυνατότητα να εισάγει το χαρακτήρα καθορισμού του φύλου, αλλά αντίθετα η εκτέλεση συνεχίζεται κατευθείαν στην printf(), η οποία μάλιστα εκεί που θα έπρεπε να εκτυπώσει το φύλο, αλλάζει γραμμή. Στην ουσία αυτό που έχει γίνει είναι ότι η δεύτερη scanf(), Πράξεις, Τελεστές & Σταθερές 17
διαβάζει το Enter που πατάει ο χρήστης μετά την πρώτη ερώτηση και αναθέτει το χαρακτήρα αλλαγής γραμμής στη μεταβλητή gender. Εικόνα 1 - Έξοδος με Λανθασμένο Διάβασμα Χαρακτήρα Αλλαγή Σειράς Μια πρώτη λύση για το πρόβλημα, θα ήταν η αλλαγή σειράς διαβάσματος των δεδομένων, ώστε να διαβάζεται πρώτα το φύλο και μετά η ηλικία. Αν και στη συγκεκριμένη περίπτωση κάτι τέτοιο είναι εφικτό, δεν ισχύει το ίδιο πάντα, οπότε δε μπορεί να χρησιμοποιηθεί ως γενική λύση. fflush(stdin) Μια άλλη λύση που συστήνεται σε πολλά forum και βιβλία είναι η προσθήκη της εντολής fflush(stdin) ακριβώς πριν από την εντολή scanf(), όπως φαίνεται στο παρακάτω απόσπασμα κώδικα. fflush(stdin); // Empty the input stream. scanf("%c", &gender); Listing 17 Χρήση του fflush(stdin) Η εντολή fflush() καθαρίζει το ρεύμα δεδομένων που της δίνεται ως παράμετρος πετώντας στην ουσία τον εναπομείναντα χαρακτήρα αλλαγής γραμμής. Στις περισσότερες υλοποιήσεις το trick θα δουλέψει, ωστόσο το πρότυπο C καθορίζει τη λειτουργία της fflush() μόνο με ρεύματα εξόδου και όχι με ρεύματα εισόδου όπως το stdin. Συνεπώς, η ορθή λειτουργία του παραπάνω κώδικά εξαρτάται κάθε φορά από τη συγκεκριμένη υλοποίηση. Πράξεις, Τελεστές & Σταθερές 18
Format String Εν τέλει, υπάρχει μια σχετικά απλή λύση που έγκειται στην προσθήκη ενός κενού στο format string της δεύτερης scanf(), ακριβώς πριν από το %c, όπως φαίνεται στο παρακάτω (ολοκληρωμένο) πρόγραμμα. #include <stdio.h> int main(void) { int age; char gender; // Ask the user for information. printf("please enter your age: "); scanf("%d", &age); printf("are you (M)ale or (F)emale? "); // It's better to simply add a space before the %c specifier. scanf(" %c", &gender); // Display info. printf("your age is %d and your gender is '%c'.\n\n", age, gender); } return 0; Listing 18 Ορθή Ανάγνωση Χαρακτήρα Ο κενός χαρακτήρας, καθοδηγεί τη scanf() να απορρίψει οποιοδήποτε χαρακτήρα νέας γραμμής έχει πιθανώς παραμείνει στο ρεύμα εισόδου και μετά να διαβάσει την τιμή που θα αποθηκεύσει στη μεταβλητή. Αν εκτελέσουμε το πρόγραμμα θα πάρουμε την αναμενόμενη έξοδο. Εικόνα 2 - Έξοδος με Σωστό Διάβασμα Χαρακτήρα Whitespace Characters Ένα πιθανό πρόβλημα σε αυτή την τελευταία λύση, είναι ότι το κενό στο format string δεν καθοδηγεί τη scanf() να απορρίψει απλώς και μόνο τους χαρακτήρες νέας γραμμής, αλλά οποιοδήποτε χαρακτήρα λευκού κενού (whitespace), όπως κενό, tab, κ.λπ. Στις περισσότερες περιπτώσεις πάντως, κάτι τέτοιο είναι επιθυμητό αφού σπανίως έχει νόημα να διαβάσουμε από το χρήστη ένα κενό χαρακτήρα. Πράξεις, Τελεστές & Σταθερές 19