goto
Δεν πρόκειται όμως εδώ ούτε να καλύψω όλα τα σχετικά με το συγκεκριμένο μάθημα, ούτε να περιοριστώ μόνο σε αυτά. Πρόθεσή μου είναι αυτό το κείμενο να έχει μια κάπως αυτόνομη ύπαρξη, και μέσα σε αυτό θα βάζω οτιδήποτε εγώ θεωρώ χρήσιμο, ενδιαφέρον ή απλώς όμορφο.
Κάτι άλλο που δε πρόκειται να κάνω είναι να ψάχνω να βρώ μεταφράσεις στα Ελληνικά για όλους τους όρους που χρησιμοποιούνται σήμερα σε σχέση με υπολογιστές. Η γλώσσα που θα χρησιμοποιώ θα είναι λίγο πολύ αυτή που χρησιμοποιείται σε μια τυπική συνομιλία ανάμεσα σε δυο συνεργαζόμενους προγραμματιστές (μείον κάποιες αθυροστομίες που φαίνεται να είναι απαραίτητες για την επιτυχή εκτέλεση ενός προγράμματος). Έτσι θα αναμειγνύω ελεύθερα Ελληνικά, Αγγλικά και δε θα διστάσω ακόμη και να κλίνω με τον Ελληνικό τρόπο Αγγλικές λέξεις. Αμαρτία εξομολογημένη ...
Το κείμενο αυτό είναι φτιαγμένο για να διαβάζεται online με ένα τυπικό web browser (π.χ. netscape, internet explorer, κλπ) αλλά η μορφή του θα προσπαθήσω να είναι τέτοια ώστε να μπορεί κανείς πάντα να εκτυπώσει όλο ή μέρος του κειμένου. Επίσης το κείμενο αυτό θα είναι ένα ``δυναμικό'' (δηλ. όχι στατικό στο χρόνο) κείμενο και μπορεί ανά πάσα στιγμή να αλλάξει μορφή και περιεχόμενο.
Σχόλια κάθε είδους είναι ευπρόσδεκτα και επιθυμητά από όλους, και όχι μόνο απ' όσους παίρνουν το μάθημά μου το επόμενο εξάμηνο, και μπορούν να στέλνονται με e-mail στη διεύθυνση mk@fourier.math.uoc.gr.
Σε κάθε κύκλο του ο υπολογιστής (για την ακρίβεια ο μικροεπεξεργαστής που είναι η καρδιά κάθε υπολογιστικού συστήματος) λαμβάνει μια εντολή και την εκτελεί. Μια τέτοια εντολή δεν είναι κατ' ανάγκη αριθμητικής φύσης, και εκεί ακριβώς έγκειται η ισχύς του υπολογιστή. Υπάρχουν, π.χ., εντολές που λένε στο μικροεπεξεργαστή να πάρει ως επόμενη εντολή που θα εκτελέσει την εντολή Α ή την εντολή Β, ανάλογα με το αν η τιμή που υπολόγισε στο αμέσως προηγούμενο βήμα είναι θετική ή αρνητική. Είναι ακριβώς τέτοιες εντολές που δίνουν τη δυνατότητα στον υπολογιστή να εκτελεί περίπλοκα ``προγράμματα'', όπως το επόμενο.
Διάβασε τις τιμές των φυσικών αριθμών a και b x <- 0 test: Αν a >= b τότε { x <- x+1 a <- a - b Πήγαινε στο test } Τύπωσε "Το πηλίκο της διαίρεσης ", a, "δια ", b, "είναι ", x
Πρέπει να τονίσουμε εδώ ότι το σύνολο εντολών που αναγνωρίζει και μπορεί να εκτελεί κάθε υπολογιστής είναι ένα μικρό σχετικά σύνολο εντολών, οι οποίες έχουν τέτοια δυσανάγνωστη μορφή που είναι σχεδόν αδύνατο να γράψει κανείς κάποιο μεγάλο πρόγραμμα χρησιμοποιώντας τις.
Σε αυτή ακριβώς τη δυσκολία οφείλεται, κατά κύριο λόγο, η ύπαρξη των γλωσσών προγραμματισμού υψηλού επιπέδου όπως οι Fortran (χρονικά η πρώτη τέτοια γλώσσα), BASIC, C, Pascal, Lisp και δεκάδες άλλες.
Όπως θα δούμε παρακάτω οι γλώσσες αυτές παρέχουν σημαντικές ευκολίες στον προγραμματιστή, οι οποίες δεν υφίστανται στη γλώσσα μηχανής, ή έστω στη γλώσσα assembly που αποτελεί ένα ενδιάμεσο σημείο ανάμεσα στη γλώσσα μηχανής και στις γλώσσες υψηλού επιπέδου (πολύ πλησιέστερα όμως στη γλώσσα μηχανής απ' ότι στις τελευταίες).
Επειδή όμως κάθε υπολογιστής μια γλώσσα μόνο ``καταλαβαίνει'' κάθε πρόγραμμα που έχει γραφεί σε μια γλώσσα προγραμματισμού (C, BASIC, Fortran κλπ, ή ακόμη και σε γλώσσα assembly) πρέπει πρώτα να μεταγλωττιστεί σε γλώσσα μηχανής προτού μπορέσει να εκτελεστεί. Η διαδικασία αυτή της μεταγλώττισης συναντάται σε τρεις, λίγο-πολύ, διαφορετικές μορφές:
Το bytecode interpretation προσφέρει μια ενδιάμεση αποδοτικότητα (προγράμματα που έχουν γίνει fully compiled και έχουν μετατραπεί σε εκτελέσιμο κώδικα σε γλώσσα μηχανής είναι πολύ ταχύτερα από προγράμματα που διαβάζονται και εκτελούνται ταυτόχρονα από interpreters, είναι όμως καταδικασμένα να τρέχουν μόνο στον τύπο μηχανής (η έννοια ``τύπος της μηχανής'' εδώ περιλαμβάνει το είδος του hardware αλλά και το είδος και τις διάφορες παραμέτρους του λειτουργικού συστήματος) για τον οποίο έχουν μεταγλωττιστεί. Αντίθετα, ένα πρόγραμμα που τρέχει μέσα από interpreter τρέχει οπουδήποτε υπάρχει εγκατεστημένος ένας interpreter για τη γλώσσα στην οποία έχει το πρόγραμμα γραφεί. Είναι όμως πολύ πιο αργό. Το bytecode προσφέρει μια μέση οδό ανάμεσα στο portability (μεταφερσιμότητα) και στο efficiency, που είναι μάλιστα ιδιαίτερα σημαντική στην εκτέλεση προγραμμάτων διά μέσου του Internet για τον απλούστατο λόγο ότι θέλει κανείς να κάνει κλικ σε ένα πρόγραμμα που βρίσκεται στο τάδε site και αυτό να τρέχει τοπικά, στη μηχανή του. Εκεί έχει σημασία τα προγράμματα να είναι αποθηκευμένα σε γλώσσα ανεξάρτητη μηχανής, μια και δεν μπορεί κανείς να υποθέσει κάτι για ένα remote site, αλλά είναι επίσης σημαντικό να είναι αποθηκευμένα σε μια μορφή που να γίνεται interpreted αποτελεσματικά. Το bytecode προσφέρει μια καλή μέση οδό.
Το πρόγραμμα που περιγράφτηκε αμέσως προηγούμενα δεν είναι γραμμένο σε κάποια γλώσσα προγραμματισμού. Οι γλώσσες αυτές έχουν πολύ αυστηρό συντακτικό το οποίο είναι αναγκαστικά περιοριστικό στο προγραμματιστή ακριβώς για να είναι ερμηνεύσιμο από μια μηχανή.
Για παράδειγμα, στη γλώσσα προγραμματισμού BASIC1.1, μια από τις απλούστερες ίσως γλώσσες, το παραπάνω πρόγραμμα μπορεί να γραφεί ως εξής:
10 print "Give a: " 20 input a 30 print "Give b: " 40 input b 50 x = 0 60 if a<b then goto 100 70 x = x + 1 80 a = a - b 90 goto 60 100 print "The quotient a/b is "; xΗ γλώσσα BASIC, στη μορφή αυτή που φαίνεται στο πιο πάνω πρόγραμμα συναντάται πλέον σπάνια. Υπάρχουν πιο εξελιγμένες μορφές της γλώσσας που χρησιμοποιούνται σήμερα.
Παρ' όλα αυτά, στο παραπάνω πρόγραμμα, όπου υπολογίζεται το πηλίκο της διαίρεσης του ακεραίου a διαιρούμενου από τον ακέραιο b (θετικοί ακέραιοι κι οι δυο), εμφανίζονται τα βασικότερα στοιχεία ενός προγράμματος και θα το χρησιμοποιήσουμε ως το βασικό παράδειγμα για την εισαγωγή τους.
Η γλώσσα BASIC, λόγω της απλότητάς της, είναι μάλλον προσφορότερη για την εισαγωγή αυτών των βασικών εννοιών από τη γλώσσα C στην οποία θα μεταπηδήσουμε σύντομα. Προχωρούμε λοιπόν τώρα να δούμε πώς δουλεύει το παραπάνω πρόγραμμα και καθ' οδόν θα δούμε ποια είναι τα βασικά στοιχεία ενός προγράμματος γενικά.
Ας κάνουμε όμως πρώτα μια περιγραφή της στρατηγικής που χρησιμοποιείται στον αλγόριθμο που υλοποιείται στο παραπάνω πρόγραμμα BASIC. Πώς δηλ. υπολογίζεται το πηλίκο της διαίρεσης a/b;
Το πηλίκο k της διαίρεσης του a διά του b είναι ο μέγιστος ακέραιος τέτοιος ώστε k . ba. Ο τρόπος που έχουμε επιλέξει εδώ για να υπολογίσουμε το k είναι το να αρχίσουμε να προσθέτουμε το b στον εαυτό του μέχρι αυτό το άθροισμα να ξεπεράσει το a. Το πλήθος των φορών που προσθέσαμε πριν ξεπεραστεί το a είναι το πηλίκο k.
a, b, x
,
που και οι τρεις είναι τύπου ακέραιου.
Οι δύο μεταβλητές a
και b
χρησιμοποιούνται κατ' αρχήν ως μεταβλητές εισόδου:
εκεί κρατάει το πρόγραμμα, στην αρχή τουλάχιστον, τους δύο ακεραίους των οποίων
το πηλίκο θέλουμε να υπολογίσουμε. Αντιθέτως η μεταβλητή x
είναι μεταβλητή που χρησιμοποιείται και για να κρατήσει μερικά ενδιάμεσα
αποτελέσματα, αλλά είναι και η μεταβλητή που στο τέλος των
βασικών υπολογισμών κρατάει το επιθυμητό αποτέλεσμα.
x
ίση με 0,
η 70 αυξάνει την τιμή της ίδιας μεταβλητής κατά 1
και η 80 αφαιρεί από τη μεταβλητή a
την τιμή
της μεταβλητής b
.
Η χρήση του συμβόλου = που μοιάζει κατ' αρχήν αντιφατική
σε σχέση με όσα έχει κανείς συνηθίσει από την αλγεβρική
του χρήση, πρέπει στη γλώσσα BASIC (όπως και στη γλώσσα
C) να ερμηνεύεται ως εξής: αριστερά του = υπάρχει πάντα
μια μεταβλητή και δεξιά του υπάρχει πάντα μια παράσταση.
Το πρόγραμμα πρώτα κάνει όσους υπολογισμούς χρειάζεται
για να υπολογίσει την τιμή της παράστασης (ενδεχομένως
χρησιμοποιώντας και την μεταβλητή που υπάρχει αριστερά
του =, και το αποτέλεσμα αυτό εκχωρεί στη
μεταβλητή που βρίσκεται αριστερά.
a<b
) και μια εντολή (goto 100
) που εκτελείται
μόνο αν ισχύει τη συγκεκριμένη εκείνη χρονική στιγμή
εκτέλεσης του προγράμματος η συνθήκη.
goto
<line number> που εμφανίζεται στις γραμμές 60 και 90
είναι μια εντολή που δεν κάνει τίποτε άλλο από το να μεταφέρει
τον έλεγχο ροής του προγράμματος σε μια συγκεκριμένη γραμμή.
Αμέσως μετά τη γραμμή 90, για παράδειγμα, εκτελείται η γραμμή 60
και ποτέ η 100.
Ας δώσουμε τώρα ένα παράδειγμα της ροής του προγράμματος όταν το πρόγραμμα τρέξει με a ίσο με 13, και b ίσο με 10 (εδώ εννοούμε ότι ο χρήστης πληκτρολογεί τους αριθμούς 13 και 10 στο πληκτρολόγιο και με αυτή τη σειρά). Η ροή λοιπόν του προγράμματος σε αριθμούς γραμμών είναι η εξής:
The quotient a/b is 1
).
Αυτό το χαρακτηριστικό του λειτουργικού συστήματος Unix βοήθησε τρομερά στο να λειτουργήσει αυτό σε όλων των ειδών τις μηχανές (υπολογιστές διαφορετικών κατασκευαστών) με σχετικά μεγάλη ευκολία, αφού το machine-dependent κομμάτι του ήταν περιορισμένο στο ελάχιστο δυνατό. Έτσι, όταν κάποιος ήθελε να μεταφέρει (port) το Unix σε μια νέα μηχανή, αρκούσε να γράψει ένα μικρό κομμάτι του λειτουργικού συστήματος. Για το υπόλοιπο αρκούσε να έχει ένα C compiler για τη συγκεκριμένη μηχανή. (Είναι σημαντικό να ξεχωρίσουμε εδώ ότι ένας compiler για μια γλώσσα που τρέχει σε μια μηχανή Α μπορεί κάλλιστα να παράγει κώδικα ο οποίος να είναι εκτελέσιμος σε μια μηχανή Β. Αυτοί λέγονται cross-compilers.)
Από τη δεκαετία του 70 και πέρα η C έχει γίνει μια από τις πιο ευρέως διαδεδομένες γλώσσες, ξεφεύγοντας από τα στενά όρια του system programming για το οποίο είχε επινοηθεί. Ήταν μια γλώσσα θεμελιωδώς απλή που παρείχε μεν τη δυνατότητα στον προγραμματιστή για το λεγόμενο δομημένο προγραμματισμό (structured programming) αλλά δεν του έδενε ταυτόχρονα τα χέρια (όπως κάνει π.χ. μια γλώσσα όπως η Pascal, της οποίας οι compilers επιμένουν, κατά παράδοση, στην εφαρμογή ενός στυλ προγραμματισμού και δεν επιτρέπουν παρεκκλίσεις από αυτό.)
Εξ αρχής η C είχε συνδυαστεί με το λεγόμενο C preprossesor (που συνήθως καλείται cpp στο Unix). Αυτός είναι μια ιδιότυπη μορφή προεπεξεργασίας του προγράμματος, που θα δούμε σε λεπτομέρεια αργότερα, και που είναι κατ' ουσίαν ανεξάρτητη της γλώσσας C, και επιτρέπει μια προεπεξεργασία του προγράμματος, σε επίπεδο κειμένου και μόνο, προτού αυτό πάει για μεταγλώττιση στον compiler. Πρέπει δηλ. ο cpp να θεωρείται ένα είδος φίλτρου: το C πρόγραμμα μπαίνει ως είσοδος στον cpp και ένα άλλο C πρόγραμμα βγαίνει ως έξοδος το οποίο στη συνέχεια οδηγείται στον compiler. Αυτός, μετά από ορισμένα ακόμη στάδια επεξεργασίας, θα παραγάγει τον εκτελέσιμο κώδικα. Η προεπεξεργασία με τον cpp παρέχει μια τεράστια ευελιξία στον προγραμματιστή και δύσκολα μπορεί κανείς να φανταστεί πως η C θα είχε πετύχει τόσο πολύ ως γλώσσα προγραμματισμού αν δεν είχε εξ αρχής συνοδευτεί από τον cpp. Θα δούμε αργότερα αρκετά παραδείγματα χρήσης του cpp, πολλά από αυτά καθόλου τετριμμένα.
Στη διεύθυνση http://cm.bell-labs.com/cm/cs/who/dmr/chist.html μπορείτε να διαβάσετε ένα ενδιαφέρον άρθρο του D. Ritchie, που μαζί με τον B. Kernighan, δημιούργησε τη γλώσσα C στα Bell Laborataries. Αφορά την ιστορία της ανάπτυξης της γλώσσας κι ίσως είναι λίγο τεχνικό σε ορισμένα σημεία του.
Για παράδειγμα, αν καθήσετε μπροστά σε ένα υπολογιστή με γραφικό περιβάλλον και παράθυρα, όπως τα Miscrosoft Windows ή τα X Windows (το standard γραφικό περιβάλλον για Unix συστήματα) θα δείτε ότι όλα τα αντικείμενα που βρίσκονται πάνω στην οθόνη, είτε παράθυρα είτε διαφόρων τύπων εικονίδια, υποστηρίζουν την έννοια του ``ανοίγματος'' (open). Όταν κάνετε open σε ένα παράθυρο που είναι ελαχιστοποιημένο αυτό μεγαλώνει, όταν κάνετε open σε ένα εικονίδιο που αντιπροσωπεύει ένα modem2.1τότε το modem ενεργοποιείται και παίρνει τηλέφωνο, κλπ. Τα διάφορα εικονίδια δηλ. ανταποκρίνονται στο ίδιο ``σήμα'' (open) με διαφορετικό τρόπο, που εν γένει δεν είναι γνωστός στο περιβάλλον σύστημα αλλά υλοποιείται από το κάθε αντικείμενο (παράθυρο, modem, πίνακα ελέγχου, κλπ) με δικό του ξεχωριστό τρόπο. Εναλλακτικά θα έπρεπε το περιβάλλον σύστημα να γνωρίζει τα ``εσωτερικά'' του κάθε αντικειμένου που ``φιλοξενεί'', πράγμα ανέφικτο. Αυτού του είδους η προσέγγιση είναι χαρακτηριστική των object oriented συστημάτων.
Hello, world!στην οθόνη του υπολογιστή.
1 #include <stdio.h> 2 3 main() 4 { 5 printf("Hello, world!\n"); 6 }
Οι αριθμοί που εμφανίζονται μπροστά από κάθε γραμμή δεν υπάρχουν στην πραγματικότητα αλλά έχουν προστεθεί για να μας διευκολύνουν να αναφερόμαστε στις διάφορες γραμμές του κώδικα, κατά την ανάλυση που κάνουμε εδώ.
Ότι πούμε παρακάτω ισχύει στο λειτουργικό σύστημα Unix. Σε άλλα λειτουργικά συστήματα (π.χ. MS Windows) και ανάλογα με το software που χρησιμοποιείται, δηλ. ανάλογα με τον ποιόν compiler χρησιμοποιούμε, η διαδικασία μπορεί να είναι από αρκετά δαφορετική έως πολύ όμοια.
Κατ' αρχήν, το C πρόγραμμα (το κείμενο που βλέπετε παραπάνω) φτιάχνεται
και αποθηκεύεται σε ένα αρχείο (file), στο οποίο δίνουμε ένα περιγραφικό
όνομα και την κατάληξη2.2.c
(στο συγκεκριμένο πρόγραμμα δώσαμε το
όνομα hello.c
, για παράδειγμα).
Το κείμενο δημιουργήθηκε χρησιμοποιώντας ένα text editor.
Υπάρχουν πολλοί text editors στο Unix, με αρκετά διαφορετικά χαρακτηριστικά
ο καθένας, και προτείνεται, για αρχή τουλάχιστον, να χρησιμοποιείτε τον
pico
, ο οποίος είναι πολύ εύχρηστος. Για να δημιουργήσετε
λοιπόν το αρχείο hello.c
θα ξεκινήσετε δίνοντας την εντολή
(από δω και πέρα οι γραμμές που αρχίζουν με %
θα παριστάνουν εντολές
που δίνετε στο command line - γραμμή εντολών - του Unix):
% pico hello.cΑφού τελειώσετε με το γράψιμο του κειμένου το αποθηκεύετε και βγαίνετε από το περιβάλλον του
pico
.
Αν τα κάνετε όλα σωστά υπάρχει πλέον στην περιοχή σας ένα αρχείο
με όνομα hello.c
.
Προσέξτε γιατί στο Unix και στη C τα κεφαλαία και τα μικρά γράμματα
θεωρούνται διαφορετικά, για παράδειγμα τα ονόματα hello.c
και Hello.c
θεωρούνται διαφορετικά από το Unix και μπορεί κάλλιστα
να έχετε δυο διαφορετικά αρχεία με αυτά τα ονόματα.
Για να βεβαιωθείτε ότι έχετε όντως το αρχείο στην περιοχή σας δώστε
την εντολή2.3
% cat hello.cκαι, αν όλα έχουν πάει καλά, θα δείτε τα περιεχόμενα του αρχείου να τυπώνονται στην οθόνη σας.
Έχοντας βεβαιωθεί πια ότι το αρχείο hello.c
υπάρχει και έχει τα σωστά περιεχόμενα, μπορούμε να προχωρήσουμε
στη φάση του compilation, ώστε να παραχθεί έτσι ένα εκτελέσιμο
πρόγραμμα.
Δώστε την εντολή2.4
% gcc hello.cΑν δεν εκτυπωθούν error messages (αν έχετε το σωστό αρχείο δε θα εκτυπωθούν) τότε μετά από το πέρας αυτής της εντολής θα υπάρχει στην περιοχή σας ένα αρχείο με όνομα
a.out
,
που είναι κατά παράδοση το όνομα που παίρνουν τα εκτελέσιμα αρχεία
στο Unix, αν ο χρήστης δεν κάνει τον κόπο να δηλώσει ότι επιθυμεί ένα
άλλο όνομα.
Αν ο χρήστης θέλει το εκτελέσιμο αρχείο να μην έχει το
καθιερωμένο όνομα a.out (για παράδειγμα αν θέλει να έχει
παραπάνω από ένα εκτελέσιμα αρχεία στην περιοχή του) τότε
μπορεί να πεί στον compiler τι όνομα να δώσει σε αυτό ως εξής:
% gcc hello.c -o helloΑυτό φτιάχνει ένα εκτελέσιμο αρχείο με όνομα hello (είναι σύνηθες στη C το όνομα του εκτελέσιμοθ να είναι το ίδιο με το όναμα του αρχείου του προγράμματος χωρίς κατάληξη, αν πρόκειται για το Unix, ή με κατάληξη .exe στα MS Windows).
Για να βεβαιωθείτε ότι υπάρχει όντως το αρχείο a.out
στην περιοχή σας δώστε την εντολή
% lsη οποία θα τυπώσει στην οθόνη σας τα ονόματα όλων των υπαρχόντων αρχείων (ανάμεσά τους θα πρέπει να είναι τα
hello.c
και a.out
).
Η εντολή
% ls -Fθα πρέπει να τυπώσει κι ένα αστερίσκο (*) δίπλα στο
a.out
το οποίο υποδηλώνει ότι αυτό είναι ένα εκτελέσιμο αρχείο.
Τέλος, εκτελούμε το πρόγραμμά μας δίνοντας την εντολή
% a.out Hello, world! %(Οι γραμμές παραπάνω που δεν αρχίζουν με % υποδηλώνουν το output του προγράμματος που μόλις εκτελέσαμε.)
hello.c
γραμμή προς γραμμή για να
δούμε πως δουλεύει.
Κατ' αρχήν, στη γραμμή 1, βρίσκεται μια εντολή που απευθύνεται στον
cpp, τον C preprossesor, και λέει στον cpp να τοποθετήσει τα περιεχόμενα
του αρχείου stdio.h στη θέση της γραμμής #include
.
Το αρχείο stdio.h (standard input/output header file) δεν
περιέχει κάποιο εκτελέσιμο κώδικα, αλλά κάποιες δηλώσεις σταθερών
και υποπρογραμμάτων (για παράδειγμα, τι είδους παραμέτρους
περιμένει να λάβει το υποπρόγραμμα printf που χρησιμοποιείται
μέσα στο hello.c.
Για να είμαστε ακριβείς, η πρώτη γραμμή δεν είναι απολύτως απαραίτητη
για την επιτυχή μετάφραση και εκτέλεση του συγκεκριμένου προγράμματος,
κι αν σβήσετε την πρώτη γραμμή και επαναλάβετε τον κύκλο μεταγλώττισης
και εκτέλεσης θα δείτε πως το πρόγραμμα θα εκτελεστεί κανονικά.
Είναι όμως λάθος πρακτική να μην συμπεριλαμβάνονται τα απαραίτητα header files (αυτά που κατά παράδοση έχουν την κατάληξη .h) στο υπό μεταγλώττιση πρόγραμμα, μια και περιέχουν πληροφορίες για τα χρησιμοποιούμενα υποπρογράμματα (πληροφορίες και οδηγίες για τον compiler και όχι για τον προγραμματιστή, ο οποίος σπανίως χρειάζεται να εξετάσει τα περιεχόμενα ενός header file εκτός ίσως από αυτά που ο ίδιος έχει γράψει) που είναι πιο συχνά απαραίτητες για να δουλέψει το πρόγραμμα σωστά παρά όχι.
#include
;
Η πληροφορία αυτή βρίσκεται στο manual της γλώσσας, ή,
για να είμαστε πιο ακριβείς, στο manual της standard library
της C, μέρος της οποίας βιβλιοθήκης είναι και η συνάρτηση
(συνάρτηση, υποποπρόγραμμα και υπορουτίνα είναι συνώνυμες λέξεις
στη C) printf.
Στο Unix το manual αυτό βρίσκεται (ή, καλύτερα, πρέπει να βρίσκεται,
μια και δεν είναι όλα τα Unix συστήματα το ίδιο καλά φτιαγμένα ή,
το σημαντικότερο, συντηρημένα) και σε ηλεκτρονική μορφή.
Αν δώσετε την εντολή
% man 3 printfτο σύστημα θα σας δείξει μερικές σελίδες με πληροφορίες για τη συνάρτηση printf και άλλες σχετικές συναρτήσεις. Ανάμεσα στις πληροφορίες αυτές είναι και η συμβουλή να κάνετε
#include
το stdio.h προκειμένου να
χρησιμοποιείσετε την printf.
Τι είναι ο αριθμός 3 αμέσως μετά το όνομα του προγράμματος man; Είναι το τμήμα του manual του συστήματος όπου βρίσκονται οι πληροφορίες για τη standard C library. Αν είχαμε παραλείψει τον αριθμό 3 και είχαμε απλώς γράψει man printf, το σύστημα θα μας τύπωνε πληροφορίες για το πρόγραμμα printf, που είναι ένα αυτοδύναμο πρόγραμμα που υπάρχει στο Unix και που εκτελεί μια παρόμοια δουλειά με αυτή που κάνει η συνάρτηση printf της standard C library. Και πώς θα καταλαβαίναμε τότε σε ποιο τμήμα του manual είναι η printf που μας ενδιαφέρει εμάς; Υπάρχει το option -k (από το keyword) για το πρόγραμμα man που λέει στο πρόγραμμα να τυπώσει μια γραμμή με λίγη πληροφορία για όλα τα σχετικά προγράμματα. Για παράδειγμα, στο Unix σύστημα που δουλεύω αυτή τη στιγμή ιδού το αποτέλεσμα του man -k printf:
% man -k printf fprintf (3) - formatted output conversion gl_printf (3) - write formatted output in graphic mode printf (1) - format and print data printf (3) - formatted output conversion printftest (6) - tests the vgagl gl_printf function snprintf (3) - formatted output conversion sprintf (3) - formatted output conversion vfprintf (3) - formatted output conversion vprintf (3) - formatted output conversion vsnprintf (3) - formatted output conversion vsprintf (3) - formatted output conversion %Το output έχει από μια γραμμή για κάθε τι που βρήκε το man σχετικό με τη λέξη printf. Η πρώτη λέξη είναι το όνομα της σχετικής ``σελίδας'' του manual και εντός παρενθέσεως βρίσκεται το section του manual. Προσέξτε ότι εμφανίζονται δύο printf, ένα στο section 1 κι ένα στο section 3. Κάνοντας man 1 printf και man 3 printf θα σας δείξει πληροφορίες για δύο διαφορετικά πράγματα. Αντίθετα για το vprintf που εμφανίστηκε μόνο του αρκεί να κάνει κανείς man vprintf για να πάρει όλες τις πληροφορίες που υπάρχουν.
Το συντακτικό της εντολής man μπορεί να διαφέρει από σύστημα σε σύστημα. Για παράδειγμα σε ορισμένα συστήματα της Sun χρειάζεται κανείς να γράψει man -s 3 printf αντί για man 3 printf. Δώστε απλώς
% man manγια να βεβαιωθείτε πώς περιμένει τις παραμέτρους του το δικό σας man.
#include <stdio.h> main() { printf("Hello, world!\n"); }Με λίγα λόγια, οι κενές γραμμές και η στοίχιση δεν επηρεάζουν το πως ο compiler αντιλαμβάνεται το πρόγραμμα και άρα είμαστε ελεύθεροι να στοιχίσουμε το κείμενο όπως εμάς εξυπηρετεί. (Οι εντολές όμως προς τον cpp , τα
#include
δηλ. και
τα #define
, που θα τα δούμε αργότερα, υπακούουν σε αρκετά
αυστηρότερους κανόνες στοίχισης, και θα τα γράφουμε πάντα σε μια
γραμμή το καθένα - αν δε ``χωράνε'' θα δούμε αργότερα τι θα κάνουμε -
αρχίζοντας από τα αριστερά της γραμμής.)
Οι γραμμές 3-6 στο πρόγραμμα παριστάνουν την περιγραφή λειτουργίας μιας συνάρτησης (υπορουτίνα, υποπρόγραμμα). Ένα C πρόγραμμα αποτελείται από ένα σύνολο συναρτήσεων, γραμμένες συνήθως η μια μετά την άλλη (δεν επιτρέπονται, όπως στην Pascal, για παράδειγμα, οι δηλώσεις υπορουτινών μέσα σε υπορουτίνες). Από αυτές τις συναρτήσεις ακριβώς μία φέρει το όνομα main και είναι, κατά σύμβαση, η υπορουτίνα που εκτελείται πρώτη, και που είναι άρα υπεύθυνη για να ``καλέσει'' οποιαδήποτε άλλη συνάρτηση απαιτείται. Μετά το πέρας της main το πρόγραμμα έχει τελειώσει.
Στο πρόγραμμα αυτό εμφανίζονται δυο συναρτήσεις:
η main και η printf.
Η πρώτη από αυτές ορίζεται μέσα στο πρόγραμμα ενώ η δεύτερη
όχι, είναι δηλ. η printf μια συνάρτηση βιβλιοθήκης,
και ο compiler (για την ακρίβεια, ο linker2.5ή loader, όπως
λέγεται στο Unix) ξέρει που θα την αναζητήσει για να συμπεριλάβει
τον κώδικα για την printf στο τελικό εκτελέσιμο
πρόγραμμα2.6.
Η main καλείται από το λειτουργικό σύστημα όταν δώσουμε
εμείς στο command line την εντολή a.out, και με τη σειρά της
καλεί την printf ``περνώντας'' της, όπως λέμε, κάποιες
παραμέτρους. Στη συγκεκριμένη περίπτωση η παράμετρος είναι μία,
η λέξη (string) "Hello, world!\n"
.
Και η printf καλεί κι αυτή με τη σειρά της κάποιες συναρτήσεις
βιβλιοθήκης, αλλά αυτές δεν είμαστε σε θέση να τις γνωρίζουμε,
και δε χρειάζεται συνήθως να τις γνωρίζουμε. Είμαστε απλοί
χρήστες ενός ``μαύρου κουτιού'' που λέγεται printf
και χρειάζεται να ξέρουμε μόνο τη λειτουργική του
συμπεριφορά, δηλ. τι αποτέλεσμα θα έχει αν του δώσουμε το τάδε
όρισμα, κι όχι το πώς είναι φτιαγμένο.
Στο πρόγραμμα αυτό δεν υπάρχουν μεταβλητές.
Υπάρχει μόνο μια σταθερά, το string "Hello, world!\n"
.
Ποιο το νόημα του περίεργου \n
στο τέλος
του string;
Το backslash (\
) παίζει ένα ειδικό ρόλο στη C, και στο Unix γενικότερα.
Χρησιμοποιείται για να υποδηλώσει ότι ο επόμενος χαρακτήρας (δηλ. γράμμα)
που ακολουθεί στο string δεν αντιπροσωπεύει το γράμμα το οποίο φαίνεται,
αλλά κάποιο άλλο χαρακτήρα, για τον οποίο συνήθως δεν υπάρχει σύμβολο
να τον γράψουμε.
Έτσι στη C ο συνδυασμός \n
παριστάνει το χαρακτήρα LF (line feed)
ο οποίος χρησιμοποιείται για να αλλάξει γραμμή το τερματικό στο οποίο
τυπώνουμε.
Αν η σταθερά ήταν η "Hello, world!" (χωρίς το LF στο τέλος)
τότε το output του προγράμματος θα ήταν ως εξής:
% a.out Hello, world!%Δε θα άλλαζε δηλαδή γραμμή το τερματικό μετά την εκτύπωση του κειμένου και το prompt του επόμενου command line θα τυπωνόταν αμέσως μετά το θαυμαστικό.
Για να είμαστε λίγο πιο ακριβείς, ο κώδικας ASCII μόνο αντιστοιχεί χαρακτήρες στα νούμερα από 0 έως 127 (αριθμοί που μπορούν να παρασταθούν με 7 δυαδικά ψηφία). Πολλά σύμβολα, όπως για παράδειγμα αυτά της Ελληνικής αλφαβήτου μένουν απ' έξω, και το κομμάτι από 128 έως 255 χρησιμοποιείται, μεταξύ άλλων, και για την παράσταση αλφαβήτων διαφορετικών από το λατινικό (το οποίο παρίσταται από το standard ASCII κάτω από το 128).
Μπορείτε να δείτε στον παρακάτω πίνακα τους χαρακτήρες για τα ASCII codes από 0 έως 127.
Dec Hx Oct Char Dec Hx Oct Char Dec Hx Oct Char Dec Hx Oct Char --------------- --------------- --------------- --------------- 0 0 000 NUL (null) 32 20 040 SPACE 64 40 100 @ 96 60 140 ` 1 1 001 SOH (start of heading) 33 21 041 ! 65 41 101 A 97 61 141 a 2 2 002 STX (start of text) 34 22 042 " 66 42 102 B 98 62 142 b 3 3 003 ETX (end of text) 35 23 043 # 67 43 103 C 99 63 143 c 4 4 004 EOT (end of transmission) 36 24 044 $ 68 44 104 D 100 64 144 d 5 5 005 ENQ (enquiry) 37 25 045 % 69 45 105 E 101 65 145 e 6 6 006 ACK (acknowledge) 38 26 046 & 70 46 106 F 102 66 146 f 7 7 007 BEL (bell) 39 27 047 ' 71 47 107 G 103 67 147 g 8 8 010 BS (backspace) 40 28 050 ( 72 48 110 H 104 68 150 h 9 9 011 TAB (horizontal tab) 41 29 051 ) 73 49 111 I 105 69 151 i 10 A 012 LF (NL line feed, new line) 42 2A 052 * 74 4A 112 J 106 6A 152 j 11 B 013 VT (vertical tab) 43 2B 053 + 75 4B 113 K 107 6B 153 k 12 C 014 FF (NP form feed, new page) 44 2C 054 , 76 4C 114 L 108 6C 154 l 13 D 015 CR (carriage return) 45 2D 055 - 77 4D 115 M 109 6D 155 m 14 E 016 SO (shift out) 46 2E 056 . 78 4E 116 N 110 6E 156 n 15 F 017 SI (shift in) 47 2F 057 / 79 4F 117 O 111 6F 157 o 16 10 020 DLE (data link escape) 48 30 060 0 80 50 120 P 112 70 160 p 17 11 021 DC1 (device control 1) 49 31 061 1 81 51 121 Q 113 71 161 q 18 12 022 DC2 (device control 2) 50 32 062 2 82 52 122 R 114 72 162 r 19 13 023 DC3 (device control 3) 51 33 063 3 83 53 123 S 115 73 163 s 20 14 024 DC4 (device control 4) 52 34 064 4 84 54 124 T 116 74 164 t 21 15 025 NAK (negative acknowledge) 53 35 065 5 85 55 125 U 117 75 165 u 22 16 026 SYN (synchronous idle) 54 36 066 6 86 56 126 V 118 76 166 v 23 17 027 ETB (end of trans. block) 55 37 067 7 87 57 127 W 119 77 167 w 24 18 030 CAN (cancel) 56 38 070 8 88 58 130 X 120 78 170 x 25 19 031 EM (end of medium) 57 39 071 9 89 59 131 Y 121 79 171 y 26 1A 032 SUB (substitute) 58 3A 072 : 90 5A 132 Z 122 7A 172 z 27 1B 033 ESC (escape) 59 3B 073 ; 91 5B 133 [ 123 7B 173 { 28 1C 034 FS (file separator) 60 3C 074 < 92 5C 134 \ 124 7C 174 | 29 1D 035 GS (group separator) 61 3D 075 = 93 5D 135 ] 125 7D 175 } 30 1E 036 RS (record separator) 62 3E 076 > 94 5E 136 ^ 126 7E 176 ~ 31 1F 037 US (unit separator) 63 3F 077 ? 95 5F 137 _ 127 7F 177 DEL
1 #include <stdio.h> 2 3 main() 4 { 5 int a, b; 6 double mean; 7 8 printf("Give the integer a: "); 9 scanf("%d", &a); 10 printf("Give the integer b: "); 11 scanf("%d", &b); 12 mean = 0.5*(a+b); 13 printf("The mean of %d and %d is %g\n", a, b, mean); 14 }
Τα νέα στοιχεία που συναντά κανείς στο mean.c είναι τα ακόλουθα:
&a
και &b
στις γραμμές
9 και 11.
Όνομα τύπου | Περιγραφή | Μέγεθος σε bytes |
char | Χαρακτήρας | 1 |
int | Προσημασμένος Ακέραιος | 4 |
float | Αριθμός Κινητής Υποδιαστολής (πραγματικός αριθμός) | 4 |
double | Αριθμός Κινητής Υποδιαστολής (πραγματικός αριθμός) | 8 |
void | Κενός τύπος | - |
Για το ξεκαθάρισμα τυχόν τέτοιων αμφιβολιών
υπάρχει ο τελεστής sizeof
στη C, που συντάσσεται ως
sizeof(type)και επιστρέφει το μέγεθος του τύπου type σε bytes. (Αυτό γίνεται φυσικά at compilation time κι όχι at run time.) Έτσι
sizeof(int)
είναι συνήθως 4, sizeof(double)
είναι συνήθως 8, sizeof(char)
είναι 1, κλπ.
Ο τύπος int παίρνει τις ακέραιες τιμές από -2147483648 ( = -231) έως 2147483647 ( = 231 - 1), ο τύπος char παίρνει τις ακέραιες τιμές από -128 ( = -27) έως 127 ( = 27 - 1), ενώ οι τύποι float και double παριστάνουν πραγματικούς αριθμούς, με ακρίβεια η οποία είναι μεταβλητή ανάλογα με το μέγεθος των αριθμών αυτών, οπότε δεν έχει νόημα να προσδιορίσουμε κάποιο έυρος τιμών τους. Βέβαια, επειδή ο τύπος double έχει 64 bits (= 8 bytes) στη διάθεσή του για να παραστήσει ένα πραγματικό αριθμό ενώ ο float χρησιμοποιεί μόνο 32 bits ( = 4 bytes), είναι ευνόητο ότι οι πράξεις με double είναι ακριβέστερες απ' ότι με float και συνιστάται να χρησιμοποιείται ο τύπος double και όχι ο float, παρά μόνο αν υπάρχει πραγματική ανάγκη εξοικονόμησης χώρου.
Υπάρχει επίσης και ο τύπος short int ο οποίος είναι προσημασμένος ακέραιος με 2 bytes μόνο, και εύρος τιμών από -32768 (= -215) έως 32767 (= 215 - 1).
Τέλος σε όλους τους ακέραιους τύπους (δηλ. char, int και short int) μπορεί να προστεθεί το πρόθεμα unsigned, που υποδηλώνει ότι ο τύπος δεν παίρνει αρνητικές τιμές και παριστά λοιπόν όλους τους ακεραίους από 0 έως 2n - 1, όπου n είναι το πλήθος των bits που χρησιμοποιεί ο τύπος. Για παράδειγμα, ο unsigned char παριστά τους ακεραίους από 0 έως 255.
Τέλος, ο τύπος void πρέπει προς το παρόν να θεωρείται σαν ο πιο γενικός τύπος: ονομάζουμε κάτι void αν δεν έχει νόημα να προσδιορίσουμε τον τύπο του. Παραδείγματα της χρήσης τους είναι ουσιαστικά δύο: (α) δίνουμε τύπο void σε μια συναρτηση που δε θέλουμε να επιστρέφει τίποτα, και (β) δίνουμε τύπο void * σε μια τελείως γενική διεύθυνση μνήμης, που δεν ξέρουμε δηλ. σε τι τύπο αντικειμένου ``δείχνει'' (αυτό θα γίνει πιο καθαρό παρακάτω).
true
(αληθής)
είτε false
(ψευδής).
Στη C η σύμβαση είναι ότι οποιαδήποτε μη μηδενική ακέραια τιμή
(και συνηθέστατα το 1) θεωρείται αληθής και το 0 μόνο ψευδής.
Όταν αργότερα θα δούμε την εντολή if
, για παράδειγμα,
θα δούμε ότι αυτή συντάσσεται με μια λογική συνθήκη και κάποιες
εντολές που εκτελούνται αν και μόνο αν η λογική αυτή συνθήκη είναι
αληθής. Αυτό σημαίνει ότι η συνθήκη αυτή δεν είναι τίποτε
άλλο από μια έκφραση ακέραιου τύπου (δηλ. int
ή
short
ή char
κλπ) και για να εκτελεστούν
οι εντολές που ακολουθούν πρέπει η έκφραση αυτή να μην είναι ίση
με 0.
Για παράδειγμα, θα δούμε αργότερα, ο δυαδικός τελεστής &&
είναι
το λογικό και.
Αυτό σημαίνει ότι η τιμή του
expr1&& expr2
είναι 1 αν και μόνο αν δεν είναι και οι δυο εκφράσεις
expr1 και expr2 ίσες με 0,
και είναι ίση με 0 αλλιώς.
Έτσι, η έκφραση 5 && 3 είναι απολύτως
θεμιτή στη C και είναι ίση με 1.
;
(semicolon) όπως σχεδόν και όλες
οι άλλες εντολές στη C.
Που μπορούν να γραφτούν οι δηλώσεις μεταβλητών; Κατ' αρχήν μπορούν να γραφτούν στο χώρο του κειμένου εκτός συναρτήσεων. Π.χ. ένα πρόγραμμα που είναι λειτουργικά ισοδύναμο με το mean.c είναι το ακόλουθο:
#include <stdio.h> int a; double mean; main() { int b; double useless_variable_1; printf("Give the integer a: "); scanf("%d", &a); printf("Give the integer b: "); scanf("%d", &b); mean = 0.5*(a+b); printf("The mean of %d and %d is %g\n", a, b, mean); }Επίσης μπορεί μια δήλωση μεταβλητών να υπάρχει στην αρχή ενός block εντολών. Ένα block εντολών είναι μια λίστα εντολών στη C η οποία βρίσκεται μέσα σε άγκιστρα <λίστα εντολών> . Για παράδειγμα, η δήλωση της συνάρτησης main αποτελείται από το όνομα της συνάρτησης και τα ονόματα και τύπους των παραμέτρων της κι ένα block. Blocks εντολών όμως μπορούν να υπάρχουν και σε άλλα σημεία μέσα στο κείμενο της συνάρτησης, όπως θα δούμε παρακάτω, και σε κάθε τέτοιο μπορεί κανείς να δηλώσει μεταβλητές, οι οποίες όμως πρέπει να θεωρούνται ότι υπάρχουν μόνο μέσα στο block στο οποίο είναι δηλωμένες. Το παρακάτω σύντομο πρόγραμμα δείχνει μια τέτοια δήλωση μέσα σε block:
#include <stdio.h> main() { int t=1; {int t=2; printf("t=%d\n", t);} printf("t=%d\n", t); }Το ouput του προγράμματος αυτού είναι
% a.out 2 1Υπάρχουν σε αυτό δυο δηλώσεις μεταβλητής
int t
,
μια στο block της συνάρτησης main
και άλλη μια
μέσα σε ένα block εσωτερικό της main
.
Η δήλωση της μεταβλητής που γίνεται μέσα σ' ένα block έχει
εμβέλεια (scope) μόνο μέσα σε αυτό το block.
Έτσι, κάθε αναφορά στην t
μέσα στο εσωτερικό block
αναφέρεται στη μεταβλητή που ορίστηκε μέσα στο block αυτό,
η οποία επισκιάζει κάθε άλλο πρότερο ορισμό μεταβλητής με το ίδιο όνομα.
Μόλις βγούμε από το block, η δήλωση που έχει γίνει μέσα στο block παύει
να ισχύει και η μόνη ισχύουσα δήλωση είναι η προηγούμενη.
Τι είδους όνομα μπορεί να χρησιμοποιηθεί για μια μεταβλητή;
Λίγο-πολύ οτιδήποτε μπορεί κανείς να φτιάξει με μικρά ή κεφαλαία
λατινικά γράμματα, με ψηφία και το σύμβολο _
(underscore), φτάνει
η λέξη να μην αρχίζει με ψηφίο.
%
που βρίσκονται
μέσα στο format string)
παριστάνουν η καθεμία από μια θέση μνήμης2.8,
την πρώτη θέση
μνήμης που καταλαμβάνει η μεταβλητή την οποία θέλουμε
να ``γεμίσει'' η scanf.
Στη C για να πάρουμε αυτή τη θέση μνήμης για μια μεταβλητή
με όνομα x γράφουμε απλά &x
.
Αυτή η θέση μνήμης λέγεται και διεύθυνση (address) του
x.
Ας έρθουμε τώρα στο format string. Η γραμμή
scanf("%d", &a);διαβάζει ένα ακέραιο από το πληκτρολόγιο και τον αποθηκεύει στη μεταβλητή a. Το format string είναι το "Το νόημα του "διαβάσει από το πληκτρολόγιο ένα προσημασμένο δεκαδικό (εξ ού και το
d
, από το decimal) ακέραιο, και να τον τοποθετήσει στη θέση
που λέει η επόμενη διεύθυνση μνήμης που έχει περαστεί ως όρισμα
στη scanf.
Πώς θα διαβάσουμε τους αριθμούς a, b και x που έχουν δηλωθεί ως εξής;
int a, b; double x;Η απάντηση είναι:
scanf("%d%d%lf", &a, &b, &x);Η scanf έχει πολύ περισσότερες δυνατότητες απ' ότι έχουμε δει μέχρι στιγμής. Μπορείτε να συμβουλευτείτε το αντίστοιχο manual page, αλλά θα δούμε μερικές από τις δυνατότητές της παρακάτω, και μερικές δυσκολίες που παρουσιάζει η χρήση της.
mean = 0.5*(a+b);Στο δεξιό μέλος του συμβόλου εκχώρησης (=) βρίσκεται μια αριθμητική έκφραση. Τα σύμβολα των πράξεων που υπάρχουν στη C είναι τα +, -, *, /, που είναι οι συνηθισμένες τέσσερεις αριθμητικές πράξεις. Η χρήση των παρενθέσεων είναι τέτοια ώστε να υποδηλώνει ποιες πράξεις γίνονται πρώτα: το αποτέλεσμα θα ήταν πολύ διαφορετικό αν η γραμμή είχε γραφεί
mean = 0.5*a+b;
.
Υπάρχουν και άλλα σύμβολα διμελών πράξεων στη C, αλλά τα παραπάνω είναι
τα μόνο που είναι αριθμητικά, έχουν δηλ. νόημα και για
αριθμούς κινητής υποδιαστολής (doubles και floats).
Το σύμβολο της διαίρεσης / έχει όμως το νόημα του πηλίκου διαίρεσης
ακεραίων αν και τα δυο ορίσματά της είναι ακέραιοι, και το αποτέλεσμα
που επιστρέφει μια τέτοια πράξη είναι πάλι τύπου ακεραίου.
Με a%b
συμβολίζουμε το υπόλοιπο της διαίρεσης του ακεραίου
a
δια του ακεραίου b
(αυτό φυσικά έχει νόημα, όπως
και το a/b
μόνο όταν το b
δεν είναι το 0.
2.9
Οι αριθμητικές πράξεις έχουν ως τύπο αποτελέσματος ακέραιο αν όλα τα συμμετέχοντα μέρη είναι ακέραια, αλλιώς έχουν τύπο double. Αυτός ο κανόνας θέλει μεγάλη προσοχή γιατί μπορεί να δημιουργήσει προβλήματα που μπορεί να ταλαιπωρήσουν πολυύ τον προγραμματιστή μέχρι να τα λύσει. Το παρακάτω πρόγραμμα expr.c είναι ενδεικτικό του τι μπορεί να συμβεί.
1 #include <stdio.h> 2 3 main() 4 { 5 int a, b; 6 double x, y; 7 8 a = 2; 9 b = 3; 10 x = a/b; 11 y = 2.0/3; 12 printf("a=%d, b=%d, x=%g, y=%g\n", a, b, x, y); 13 }
Ιδού το output αυτού:
% a.out a=2, b=3, x=0, y=0.666667Το πρόβλημα εδώ είναι στη γραμμή 10, όπου το x παίρνει την τιμή 0 αντί για 0.66... Ο λόγος είναι ο εξής. Ο C compiler όταν βλέπει την έκφραση a/b την κατανοεί, ως οφείλει, ως το πηλίκο της διαίρεσης του ακεραίου a δια του ακεραίου b, και φτιάχνει κώδικα μηχανής για ακριβώς αυτή την πράξη, της οποίας το αποτέλεσμα μετά (που είναι ένας ακέραιος) το μετατρέπει στον αντίστοιχο double και εκχωρεί αυτό στο x. Φυσικά το πηλίκο της διαίρεσης 2/3 είναι 0, άρα ορθώς το x παίρνει τελικά την τιμή 0.
Γιατί δε συμβαίνει το ίδιο και στη γραμμή 11, όπου βλέπουμε ότι το y παίρνει τη σωστή του τιμή τελικά; Ο λόγος είναι ότι η προς υπολογισμό έκφραση 2.0/3 έχει τον ένα από τους δύο συμμετέχοντες αριθμούς να μην είναι ακέραιος (από τη στιγμή που χρησιμοποιούμε τη δεκαδική υποδιαστολή η σταθερά 2.0 αποθηκεύεται σε μορφή double και όχι int, όπως θα μπορούσε μια και το δεκαδικό της μέρος είναι 0. Ο μόνος τρόπος για να έχει νόημα μια αριθμητική πράξη ανάμεσα σε ένα ακέραιο κι ένα δεκαδικό αριθμό είναι να υποθέσουμε ότι, στη γενικότητα του πράγματος, το αποτέλεσμα είναι δεκαδικό. Αυτό ακριβώς κάνει ο C compiler και συγκεκριμένα μετατρέπει και τον άλλο operand (το 3) στον αντιστοιχο δεκαδικό, και κάνει διαίρεση ανάμεσα σε δυο δεκαδικούς αριθμούς (δεν παίρνει πηλίκο ακεραίων δηλαδή) της αποτέλεσμα είναι 0.666.. που τελικά εκχωρείται στο y.
printf("a=%d, b=%d, x=%g, y=%g\n", a, b, x, y);χρησιμοποιούμε τη ρουτίνα
printf
για να τυπώσουμε
στην οθόνη τέσσερεις αριθμούς, δύο int και δύο double,
και κάποιο επεξηγηματικό κείμενο.
Η printf παίρνει, όπως και η scanf, ένα πρώτο όρισμα που είναι το format string και το οποίο προσδιορίζει πώς θα τυπωθούν με τη σειρά τα υπόλοιπα ορίσματα της printf.
Μέσα στο format string γράφουμε σε ελεύθερη μορφή το κείμενο
που θέλουμε να τυπωθεί (π.χ. το ``a=
'' είναι τέτοιο κείμενο,
όπως και το ``, b=
'' μετά το πρώτο %d
).
Στις θέσεις που θέλουμε να τυπωθεί μια έκφραση ή μια μεταβλητή
τοποθετούμε το χαρακτήρα %
ακολουθούμενο από κάποιους
κωδικούς χαρακτήρες ή και αριθμούς (κάντε man 3 printf),
οι οποίοι δηλώνουν τον τύπο της προς τύπωση έκφρασης και
διάφορες άλλες παραμέτρους. Μπορεί κανείς π.χ. να προσδιορίσει
πόσα δεκαδικά ψηφία θα χρησιμοποιηθούν όταν τυπώνεται ένας
double.
Μερικές από τις δυνατές επιλογές για το κείμενο που ακολουθεί
το σύμβολο %
είναι οι ακόλουθες:
Κείμενο στο format string | Τι τυπώνεται |
%d |
int, δεκαδική αναπαράσταση |
%u |
unsigned int, δεκαδική αναπράσταση |
%x |
unsigned int, δεκαεξαδική αναπαράσταση |
%o |
unsigned int, οκταδική αναπαράσταση |
%f |
float/double, συνήθης αναπαράσταση με υποδιαστολή |
%e |
float/double, εκθετική μορφή |
%g |
float/double, επιλέγεται αυτόματα %f ή %e ανάλογα με τον αριθμό |
%c |
int ή char, τυπώνεται το ASCII σύμβολο που αντιστοιχεί στον αριθμό, π.χ. αν η έκφραση είναι ίση με 65 θα τυπωθεί το A κεφαλαίο λατινικό |
%s |
διεύθυνση χαρακτήρα (char *), τυπώνεται η λέξη που έχει αποθηκευτεί να ξεκινά από εκείνη τη διεύθυνση χαρακτήρα |
%p |
διεύθυνση μνήμης |
Στο παρακάτω πρόγραμμα
1 #include <stdio.h> 2 3 main() 4 { 5 printf("[%d]\n", 106); 6 printf("[%x]\n", 106); 7 printf("[%o]\n", 106); 8 printf("[%6d]\n", 106); 9 printf("[%-6d]\n", 106); 10 printf("[%06d]\n", 106); 11 printf("[%c]\n", 106); 12 13 printf("\n"); 14 15 printf("[%f]\n", 3.14159); 16 printf("[%e]\n", 3.14159); 17 printf("[%g]\n", 3.14159); 18 printf("[%.3f]\n", 3.14159); 19 printf("[%10.3f]\n", 3.14159); 20 printf("[%-10.3f]\n", 3.14159); 21 }
δοκιμάζουμε διάφορους τρόπους να μορφοποιηθεί η εκτύπωση για τον ακέραιο 106 και τον δεκαδικό αριθμό 3.14159. Ιδού το output του προγράμματος format.c
% a.out [106] [6a] [152] [ 106] [106 ] [000106] [j] [3.141590] [3.141590e+00] [3.14159] [3.142] [ 3.142] [3.142 ] %Σε κάθε μια από τις γραμμές του output όπου τυπώνεται ένας αριθμός (ακέραιος ή δεκαδικός) τυπώνεται μέσα σε ένα ζεύγος αγκυλών, ώστε να είναι φανερό ακριβώς πόσες θέσεις πιάνει. Αλλιώς οι λευκοί χαρακτήρες (blanks) δε θα φαίνονταν.
Στις πρώτες 3 γραμμές τυπώνεται ο ακέραιος 106 σε μορφή δεκαδική, δεκαεξαδική και οκταδική. Στις επόμενες δύό γραμμές τυπώνεται ο ίδιος ακέραιος έτσι ώστε να καταλαμβάνει 6 θέσεις και στοιχισμένος αριστερά (που είναι η default στοίχιση) και στοιχισμένος δεξιά. Στην επόμενη γραμμή καταλμβάνει πάλι 6 θέσεις αλλά συμπληρώνεται με μηδενικά προς τα αριστερά. Η τελευταία γραμμή του πρώτου block γραμμών είναι ίσως η πιο ενδιαφέρουσα και μη ασυνήθιστη σε άλλες γλώσσες προγραμματισμού. Σε αυτήν ο ακέραιος 106 τυπώνεται ως χαρακτήρας: δηλ. τυπώνεται το σύμβολο που του αντιστοιχεί στον πίνακα ASCII που είναι το λατινικό γράμμα j.
Στις επόμενες δύό γραμμές τυπώνεται ο πραγματικός αριθμός 3.14159 σε συνηθισμένη μορφή και σε εκθετική. Στην επόμενη γραμμή επιλέγεται αυτόματα ο βέλτιστος τρόπος εκτύπωσης ανάλογα με το ποιος είναι ο αριθμός. Στην επόμενη τυπώνεται σε συνηθισμένη μορφή και με ακριβώς 3 δεκαδικά ψηφία. Στην επόμενη και μεθεπόμενη γραμμή τυπώνεται πάλι στη συνηθισμένη μορφή και με 3 δεκαδικά ψηφία αλλά έτσι ώστε να καταλαμβάνει ακριβώς 10 θέσεις, στιοχισμένος πρώτα δεξιά (default) και μετά αριστερά.
Θα πρέπει να είναι ήδη φανερό πως οι δυνατότητες της printf όπως και της scanf είναι πάρα πολλές. Δεν έχει νόημα να είμαστε εξαντλητικοί εδώ στην αναφορά μας προς αυτές. Ο χρήστης πρέπει να αμαφερθεί στο αντίστοιχο manual page για να δεί ποιο ακριβώς format string να χρησιμοποιήσει κάθε φορά που θέλει να μορφοποιήσει το output με κάποιο ιδιαίτερο τρόπο. Και με λίγο πειραμτισμό, γράφοντας μικρά προγραμματάκια όπως το format.c μπορεί κάθε φορά να βρίσκει λύση στο πρόβλημά του. Αυτή η λύση βέβαια σπανίως είναι η μόνη δυνατή.
1 /* 2 This program computes the letter grade for a certain class 3 (A, B, C or F) given the student's numeric grades for the 4 midterm exam, the homework and the final exam 5 */ 6 #include <stdio.h> 7 #include <stdlib.h> 8 9 #define GRADE_A_MIN 0.9 10 #define GRADE_B_MIN 0.7 11 #define GRADE_C_MIN 0.5 12 13 #define MIDTERM_COEFF 0.4 14 #define HOMEWORK_COEFF 0.2 15 #define FINAL_COEFF 0.4 16 17 int check_number(double grade) 18 { 19 char message[100]="You gave me a wrong number. Bye."; 20 21 if((grade<0) || (grade>10)) { 22 printf("%s\n", message); 23 exit(1); 24 } 25 return 0; 26 } 27 28 main() 29 { 30 double mid, hm, fin, numeric_grade; 31 char letter_grade; 32 int ret; 33 34 printf("Give grade for midterm (in scale from 0 to 10): "); 35 scanf("%lf", &mid); 36 check_number(mid); 37 38 printf("Give grade for homework (same scale): "); 39 scanf("%lf", &hm); 40 check_number(hm); 41 42 printf("Give grade for final (same scale): "); 43 scanf("%lf", &fin); 44 check_number(fin); 45 46 numeric_grade = MIDTERM_COEFF*mid + 47 HOMEWORK_COEFF*hm + 48 FINAL_COEFF*fin; 49 50 // if you did better on the final than on the average 51 // your grade will be that of the final 52 if(fin>numeric_grade) 53 numeric_grade = fin; 54 55 // bring the grade to the scale 0 to 1 56 numeric_grade /= 10.; 57 58 // numeric_grade is now in the range from 0 to 1 59 if(numeric_grade >= GRADE_A_MIN) 60 letter_grade = 'A'; 61 else 62 if(numeric_grade >= GRADE_B_MIN) 63 letter_grade = 'B'; 64 else 65 if(numeric_grade >= GRADE_C_MIN) 66 letter_grade = 'C'; 67 else 68 letter_grade = 'F'; 69 70 printf("Your grade is %c (numeric = %.1f)\n", 71 letter_grade, numeric_grade*10); 72 73 ret = 0; 74 // Now print a sensible comment along with the grade 75 switch(letter_grade) { 76 case 'A': 77 printf("You did an excellent job. Congratulations!\n"); 78 break; 79 case 'B': 80 printf("You did very well. Keep trying to improve.\n"); 81 break; 82 case 'C': 83 printf("Well, you passed the class, but you should really\ 84 do better next time.\n"); 85 break; 86 case 'F': 87 printf("Sorry, you failed. More luck next time.\n"); 88 break; 89 default: 90 printf("Internal error, I have not seen this kind of\ 91 grade before.\n"); 92 ret = 1; 93 } 94 95 return ret; 96 }
Παρακάτω δείχνουμε δυο διαφορετικά τρεξίματα του προγράμματος:
% a.out Give grade for midterm (in scale from 0 to 10): 5 Give grade for homework (same scale): 6 Give grade for final (same scale): 7.5 Your grade is B (numeric = 7.5) You did very well. Keep trying to improve. % a.out Give grade for midterm (in scale from 0 to 10): 9.5 Give grade for homework (same scale): 8.0 Give grade for final (same scale): 6 Your grade is B (numeric = 7.8) You did very well. Keep trying to improve.Ο τρόπος που δουλεύει το πρόγραμμα σε γενικές γραμμές είναι ότι διαβάζει από το πληκτρολόγιο τα τρία νούμερα που αντιπροσωπεύουν τους βαθμούς σε midterm, homework και final, υπολογίζει το μέσο όρο τους με τα βάρη που προαναφέραμε, και, αντικαθιστά αυτόν με το final αν το final είναι μεγαλύτερο. Έπειτα υπολογίζεται το γράμμα που αντιστοιχεί στον αριθμητικό βαθμό που έχουμε το οποίο και τυπώνεται μαζί με ένα μήνυμα που εξαρτάται από το ποιος είναι ο βαθμός.
/*
και */
.
Ανάμεσα στα δύο αυτά σύμβολα μπορούμε να βάλουμε οτιδήποτε
κείμενο θέλουμε.
Επίσης, μέσα στη συνάρτηση main παρατηρείστε ότι υπάρχει
ελεύθερο κείμενο μετά το σύμβολο //
.
Αυτά τα σχόλια σκοπό έχουν να δώσουν τη δυνατότητα στον προγραμματιστή να συμπεριλαμβάνει κάποιες επεξηγήσεις δίπλα στον, συνήθως, αρκετά κρυπτικό κώδικα σε C. Αυτές οι επεξηγήσεις βοηθούν πάρα πολύ στο να μπορεί αργότερα ένας άλλος (ή και ο ίδιος) προγραμματιστής να καταλάβει πώς λειτουργεί ένα πρόγραμμα και να κάνει αλλαγές ή διορθώσεις. Συνιστάται ιδιαίτερα να συμπεριλαμβάνει κανείς αρκετά σχόλια στα προγράμματά του, αν μη τι άλλο, για να τα καταλαβαίνει αργότερα ο ίδιος.
#define name textείναι ότι ζητείται από τον cpp οποτεδήποτε βλέπει το string name στο κείμενο του προγράμματος να αντικαθιστά αυτό με το κείμενο text. Το string name πρέπει να είναι του ίδιου τύπου με αυτά που χρησιμοποιούνται για μεταβλητές, δηλαδή να αρχίζει με γράμμα ή underscore ( _ ) και να περιέχει μόνο γράμματα, αριθμούς και underscores, αλλά το κείμενο text είναι τελείως ελεύθερο και αποτελείται από ολόκληρο το υπόλοιπο κομμάτι της γραμμής του #define. Ο cpp λοιπόν, θα αντικαταστήσει μέσα στο πρόγραμμα οποιαδήποτε εμφάνιση του identifier GRADE_A_MIN με το κείμενο 0.9. Γιατί το κάνουμε αυτό και δε γράφουμε κατ' ευθείαν το 0.9 εκεί που το χρειαζόμαστε; Η απάντηση είναι τόσο απλή όσο και σημαντική: γιατί ο identifier GRADE_A_MIN παριστάνει μια έννοια που μπορεί να χρησιμοποιείται σε πάρα πολλά σημεία μέσα στο πρόγραμμα. Στο συγκεκριμένο πρόγραμμα δε συμβαίνει αυτό αλλά θα μπορούσε κάλλιστα το πρόγραμμα να περιλαμβάνει μερικές δεκάδες αναφορές στο GRADE_A_MIN. Αν αντί για GRADE_A_MIN εμείς έιχαμε γράψει 0.9 και ξαφνικά αποφάσιζε ο καθηγητής που κάνει το μάθημα να ελαττώσει το κατώφλι του βαθμού A σε 0.85 εμείς θα έπρεπε να ψάξουμε να δούμε που μέσα στο πρόγραμμα έχουμε κάνει μια αναφορά στην έννοια αυτή και να αντικαταστήσουμε το 0.9 με 0.85. Ενώ τώρα το μόνο που χρειζεται να κάνουμε είναι να αλλάξουμε το αντίστοιχο #define σε
#define GRADE_A_MIN 0.85Γίνεται έτσι φανερό πόσο το να τηρεί κανείς κάποιους κανόνες σωστού σχεδιασμού για το πρόγραμμά του μπορεί να βοηθήσει στο γράψιμο και τη συντήρηση αυτού (τυχόν αλλαγές που ζητούνται στη διάρκεια του χρόνου, λάθη που ανακαλύπτονται και πρέπει να διορθωθούν, κλπ).
Η εντολή #define του cpp έχει όμως άλλη χρήση κυρίως, την κατασκευή των macros, τα οποία θα δούμε αργότερα.
int check_number(double grade)Είναι μια συνάρτηση που πάιρνει ως όρισμα ένα double και επιστρέφει ένα int. Η λειτουργία που πραγματοποιεί η συνάρτηση αυτή είναι ότι ελέγχει το όρισμά της (grade κι αν αυτό είναι εκτός του διαστήματος [0, 10] τότε τυπώνει ένα μήνυμα και σταματά την εκτέλεση του προγράμματος.
Αντίθετα με γλώσσες όπως η Pascal και η Fortran, όπου τα υποπρογράμματα είναι δύο ειδών, αυτά που επιστρέφουν κάτι (functions στην Pascal) και αυτά που δεν επιστρέφουν τίποτα (procedures στην Pascal), στη C όλα τα υποπρογράμματα επιστρέφουν μια τιμή (σχεδόν - εξαιρούνται αυτά που είναι δηλωμένα ότι επιστρέφουν void - κενό). Δεν είναι υποχρεωμένος όμως ο χρήστης να χρησιμοποιήσει αυτή την τιμή, ούτε καν ο προγραμματιστής της συνάρτησης είναι υποχρεωμένος να ορίσει κώδικα για αυτό που επιστρέφεται (αν και μερικοί compilers θα φροντίσουν να του τονίσουν πως δεν κάνει καλά να αφήσει κάτι τέτοιο στην τύχη του, κανείς compiler δε θα επιμείνει σε αυτή του τη θέση).
Ο ορισμός λοιπόν μιας συνάρτησης στη C αποτελείται από μια δήλωση
τύπου (τι ορίσματα παίρνει και τι τύπου είναι αυτό που επιστρέφει)
ο οποίος ακολουθείται από ένα block, δηλ. μια σειρά από
εντολές που περικλείονται μέσα σε δύο άγκιστρα ({ ... }
).
Μέσα σε κάθε block μπορούν να περιέχονται κι άλλα blocks.
char message[100]="You gave me a wrong number. Bye.";Το νόημα της δήλωσης char message[100] είναι ότι λέει στον compiler να κρατήσει μνήμη για 100 αντικείμενα τύπου char στα οποία το πρόγραμμα αναφέρεται με το όνομα message. (Επειδή ένα char καταλαμβάνει ένα byte στη μνήμη, αυτό ισοδυναμεί με τη δέσμευση 100 διαδοχικών bytes στη μνήμη.) Επίσης η δήλωση της μεταβλητής αυτής ακολουθείται από μια δήλωση αρχικής τιμής, την
="You gave me a wrong number. Bye."
,
ένα initialization.
Αυτό δηλώνει στον compiler ότι από τις 100 θέσεις χαρακτήρων
που θα δεσμεύσει οι πρώτες 32 θα περιέχουν τους χαρακτήρες του
initialization string με αυτή τη σειρά (δηλ. ο πρώτος χαρακτήρας
θα είναι ο 'Y', ο δεύτερος ο 'o', ο 32ος ο '.').
Επίσης ο χαρακτήρας με ASCII code 0 (λέγεται συνήθως NUL)
τοποθετείται αμέσως μετά τον τελευταίό χαρακτήρα του
initialization string. Αυτή είναι η σύμβαση που χρησιμοποιεί η C
για να ξέρει που τελειώνει το string.
if ( condition ) blockΤο νόημα αυτής της εντολής είναι φυσικά ότι το block εκτελείται αν και μόνο αν η συνθήκη που αναφέρεται μέσα στο if ισχύει. Στη συγκεκριμένη περίπτωση η συνθήκη που ελέγχει την εκτέλεση είναι η
(grade<0) || (grade>10)Ο τελεστής (operator)
||
είναι το διαζευκτικό ή,
και η συνολική συνθήκη είναι αληθής αν και μόνο αν μια τουλάχιστον
από τις (grade<0)
και (grade>10)
είναι αληθής.
Το block που εκτελείται υπό συνθήκη είναι το
{ printf("%s\n", message); exit(1); }Τυπώνεται δηλ. το μήνυμα που έχει αποθηκευτεί στη μεταβλητή message και καλείται η συνάρτηση exit με όρισμα τον ακέραιο 1. (Είναι λόγω της κλήσης στην exit που έχουμε συμπεριλάβει τη γραμμή
#include <stdlib.h>
μια και η συνάρτηση αυτή εκεί
είναι δηλωμένη - κάντε man 3 exit.)
Η συνάρτηση exit δεν επιστρέφει ποτέ. Διακόπτει
το πρόγραμμα κι επιστρέφει στο λειτουργικό σύστημα,
που είναι το πρόγραμμα που εκτέλεσε το πρόγραμμά μας, τον
αριθμό που λέει το όρισμά της.
Είθισται να επιστρέφουν τα προγράμματα 0 αν όλα έχουν πάει
καλά και κάτι μη μηδενικό αλλιώς, γι' αυτό επιλέξαμε να επιστρέψουμε
1 εδώ.
Φυσικά το τι επιστρέφει αυτό το πρόγραμμα μάλλον λίγη διαφορά θα
κάνει, εκτός αν το λειτουργικό σύστημα είναι προγραμματισμένο να
κάνει κάτι που το πρόγραμμά μας τελειώνει με 1 κι όχι με 0,
για παράδειγμα θα μπορούσε να ξανακαλεί το πρόγραμμά μας μέχρις
ότου εκτελεστεί επιτυχώς.
Η εντολή if έχει και μια δεύτερη μορφή στην οποία συναντάται και στο πρόγραμμα cond.c.
if ( condition ) statement1 else statement2όπου statement1 και statement2 είναι είτε μια απλή εντολή της C (π.χ.
a=1;
) είτε ένα block εντολών,
όπως φαίνεται στο επόμενο παράδειγμα, όπου ανάλογα με το αν
ισχύει η συνθήκη a == b
(είναι η μεταβλητή
a ίση με τη μεταβλητή b;) ή όχι η μεταβλητή x παίρνει
την τιμή 1 ή 2 και τυπώνεται και ένα αντίστοιχο μήνυμα.
if(a == b) { x = 1; printf("a equals b\n"); } else { x = 2; printf("a is different from b\n"); }(Η στοίχιση εδώ είναι φυσικά δικαίωμά μας να την επιλέξουμε και η επιλογή μας είναι τέτοια που να δείχνει, γράφοντας μια θέση δεξιότερα, ποιές εντολές του προγράμματος ``ελέγχονται'' από ποιες άλλες.)
Παρακάτω στο cond.c βλέπουμε μια αλληλοδιαδοχή από if (..) .. else ..
// numeric_grade is now in the range from 0 to 1 if(numeric_grade >= GRADE_A_MIN) letter_grade = 'A'; else if(numeric_grade >= GRADE_B_MIN) letter_grade = 'B'; else if(numeric_grade >= GRADE_C_MIN) letter_grade = 'C'; else letter_grade = 'F';Σε ποιο if ανήκει ένα συγκεκριμένο από τα else; Η απάντηση είναι στο πλησιέστερο προς τα πίσω. Για παράδειγμα, ο κώδικας
if(a == b) x=0; else if(a<b) x= -1; else if(a>b+1) x=2; else x=1;έχει ακριβώς το ίδιο αποτέλεσμα με τον κώδικα, στον οποίο έχουμε φτιάξει τη στοίχιση κι έχουμε προσθέσει περιττά blocks για να τον κάνουμε πιο ευανάγνωστο:
if(a==b) x=0; else { if(a<b) x= -1; else { if(a>b+1) x=2; else x=1; } }Πρέπει να είναι ήδη προφανές πως μια σωστή στοίχιση καθιστά ένα πρόγραμμα σημαντικά πιο ευανάγνωστο από μια στοίχιση που δεν υπακούει σε κάποιους κανόνες, κι αυτό βοηθά τα μέγιστα στην κατανόηση ενός προγράμματος από τρίτους (ή και από τον ίδιο το συγγραφέα του μετά από λίγο καιρό) και στο να αποφεύγονται επίσης λάθη κατά το γράψιμο του προγράμματος.
return 0;
.
Οποτεδήποτε μέσα στη συνάρτηση εκτελεσεί η εντολή
return;ή
return value;σταματάει η εκτέλεση του κώδικα της συνάρτησης και επιστρέφει ο έλεγχος στο κομμάτι του προγράμματος που είχε ενεργοποιήσει (καλέσει) το υποπρόγραμμα. Στην πρώτη περίπτωση η τιμή επιστροφής είναι αόριστη (με άλλα λόγια δεν μπορούμε να πούμε οτιδήποτε γι' αυτήν - μας είναι άχρηστη) ενώ στη δεύτερη περίπτωση επιστρέφεται η τιμή value το οποίο μπορεί να είναι οποιαδήποτε έφραση του τύπου που επιστρέφει η συνάρτηση.
Στη συνάρτηση check_number επιστρέφουμε (όταν επιστρέφουμε, αφού η ίδια συνάρτηση μπορεί να τερματίσει το όλο πρόγραμμα, αν ο έλεγχος μπει μέσα στο if) τον ακέραιο 0. Ακολουθούμε έτσι τη σύμβαση που θέλει τις συναρτήσεις που δεν έχουν τίποτε χρήσιμο να επιστρέψουν, να γυρνούν το 0, αν όλα έχουν πάει καλά (δηλ. δε διαπιστώθηκε κάποιο σφάλμα) ενώ επιστρέφουν κάποια μη μηδενική τιμή όταν έχει ευρεθεί κάποιο σφάλμα, και μάλιστα φροντίζουμε ώστε το ποια μη μηδενική τιμή θα επιστρέψουν να είναι επεξηγηματικό του τι είδους σφάλμα υπήρξε.
Στο αντίστοιχο manual page για μια συνάρτηση δηλώνεται επίσης και το πώς πρέπει να ερμηνευτεί η επιστρεφόμενη τιμή. Για παράδειγμα, αν κάνετε man 3 printf θα δείτε ότι η συνάρτηση αυτή επιστρέφει τον συνολικό αριθμό χαρακτήρων που γράφτηκαν στο output κατά την εκτέλεσή της.
Μόλις διαβαστεί, κάθε μια από αυτές τις τρεις μεταβλητές δίνεται ως όρισμα στη ρουτίνα check_number η οποία ελέγχει αν είναι στο σωστό εύρος 0 έως 10, κι αν δεν είναι διακόπτει το πρόγραμμα.
Στη συνέχεια η μεταβλητή numeric_grade υπολογίζεται ως ο ζυγισμένος μέσος όρος των mid, hm και fin. Βλέπουμε εδώ πως μπορεί κάλλιστα μια εντολή να καταλαμβάνει κάμποσες γραμμές στο κείμενο και δεν είναι ανάγκη να χωράει σε μια και μόνη γραμμή, και ο τρόπος που επιλέγουμε να τη γράψουμε είναι τέτοιος που να την κάνει ευανάγνωστη.
Η επόμενη εντολή if ελέγχει αν το fin είναι μεγαλύτερο του numeric_grade κι αν είναι τότε αντικαθιστά την τιμή του numeric_grade με αυτή του fin.
numeric_grade /= 10.;βλέπουμε για πρώτη φορά ένα τέτοιου είδους τελεστή. Το νόημα του
a /= expressionόπου το a είναι μεταβλητή αριθμητικού τύπου (δηλ. ακέραια ή δεκαδική) και το expression είναι τυχαία έκφραση, είναι ότι το expression υπολογίζεται, η τιμή που έχει το a διαιρείται με την τιμή που υπολογίστηκε για το expression, και το αποτέλεσμα της διαίρεσης εκχωρείται στο a.
Υπάρχουν κι άλλοι τελεστές με παρόμοιο νόημα. Το τι κάνουν οι
a += expressionπρέπει ήδη να είναι ξεκάθαρο.
a -= expression
a *= expression
Πρέπει επίσης να αναφέρουμε τους τελεστές που φαίνονται παρακάτω
a++Ο τελεστής a++ αυξάνει την τιμή της μεταβλητής a κατά 1 κι ο ++a κάνει το ίδιο. Ομοίως οι τελεστές
++aa--
--a
a--
και --a
μειώνουν την τιμή
της μεταβλητής a κατά 1
(συμβαίνει τόσο συχνά σον προγραμματισμό αυτή η πράξη της
αυξομείωσης κατά 1, ιδίως σε loops, που στη C επέλεξαν
οι συγγραφείς της να έχουν ειδική συντομογραφίά γι' αυτήν).
Ποια η διαφορά τότε ανάμεσα στα a++ και ++a; Η απάντηση σε αυτό είναι μια πολύ σημαντική και χρήσιμη ιδιορρυθμία της γλώσσας C, σε σχέση με άλλες γλώσσες. Στη C δεν υπάρχει καμιά διαφορά (σε επίπεδο γλώσσας, όχι περιεχομένου) ανάμεσα στις εξής δύο εντολές, για παράδειγμα:
a = 5;και
a + 5;Η δεύτερη εντολή δεν υφίσταται σε γλώσσες όπως η Pascal και η Fortran. Στη C είναι απολύτως επιτρεπτή. Οι τελεστές
=
και +
είναι αμφότεροι
δυαδικοί τελεστές.
Έχουν βέβαια πολύ διαφορετική σημασιολογία.
Η σημασία του
a = bείναι ότι η έκφραση b υπολογίζεται και ανατίθεται η τιμή της στο a που πρέπει άρα να είναι μεταβλητή (ακριβέστερα είναι αυτό που λέγεται lvalue, δηλ. μια έκφραση στην οποία αντιστοιχεί μια θέση μνήμης). Το
a = b
όμως είναι κι αυτό μια έφραση της οποίας η τιμή είναι,
εξ ορισμού στη C, η τιμή που τελικά ανατίθεται στο a,
δηλ. αυτό που υπολογίστηκε στην έκφραση b.
Η σημασία του
a + bόπου τώρα το a δε χρειάζεται να είναι lvalue αλλά μπορεί να είναι οποιαδήποτε (αριθμητικού τύπου) έκφραση, είναι ότι υπολογίζεται η έκφραση a, μετά υπολογίζεται η έκφραση b, και τέλος τα δύο αποτελέσματα προστίθενται και επιστρέφεται, ως τιμή της έκφρασης
a+b
, το άθροισμά τους.
Το ακόλουθο κομμάτι κώδικα είναι επιτρεπτό, για παράδειγμα:
int a, b, c; c = 1; a = b = c++;Στο τέλος αυτού του κώδικα τα a, b, c έχουν τις τιμές 1, 1, και 2 αντίστοιχα. Γιατί;
Οι τελεστές που εμφανίζονται στη μαγική τελευταία γραμμή είναι
οι =
και ++
.
Ο πρώτος είναι δυαδικός (binary) τελεστής και ο δεύτερος είναι μονικός
(unary). Αυτό σημαίνει ότι ο πρώτος απαιτεί δύο εκφράσεις για να
συνταχθεί σωστά, μια αριστερά του και μια δεξιά του, ενώ ο δεύτερος
θέλει μόνο μια, από τα αριστερά του.
Η τελευταία γραμμή λοιπόν ερμηνεύεται από τον compiler ως εξής
a = ( b = (c++) );Το γιατί μπήκαν οι παρενθέσεις εκεί που μπήκαν δεν είναι τυχαίο και έχει να κάνει με την προσεταιριστικότητα από δεξιά του δυαδικού τελεστή
=
και την σχετική ισχύ (precedence) των δύο
τελεστών =
και ++
.
Ο τελεστής ++
έχει μεγαλύτερη ισχύ (higer precedence)
από τον τελεστή =
κι άρα κολλάει πρώτος στα ορίσματά του.
Αν ο ++
είχε μικρότερη ισχύ από τον =
τότε
η γραμμή θα ερμηνευόταν ως
(a = (b = c))++ ;Αυτή η, συντακτικά ορθή, ερμηνεία έχει όμως σημασιολογικό πρόβλημα μια και ο τελεστής
++
καλείται να τελέσει στο όρισμα
(a = (b = c))
που δεν είναι όμως lvalue.
Ένας compiler που καλείται να μεταφράσει την παραπάνω γραμμή
κώδικα δίνει ένα σφάλμα του τύπου
invalid lvalue in incrementΈνα πιο γνωστό παράδειγμα που ίσως ξεκαθαρίσει την έννοια της ισχύος των τελεστών είναι η σχετική ισχύς του
+
και του *
(επί) στην γλώσσα της αριθμητικής.
Η παράσταση 1*2+3*4
ερμηνεύεται ως
(1*2)+(3*4)
ακριβώς επειδή η δυαδικός τελεστής *
έχει μεγαλύτερη ισχύ από τον +
.
Έχοντας δει το ότι η εντολή a = b = c++;
ερμηνεύεται ως a = ( b = (c++) );
είναι πια εύκολο
να δούμε πως θα λειτουργήσει.
Για να υλοποιηθεί η πρώτη πράξη, δηλ. το a = (...);
υπολογίζεται πρώτα η τιμή της παράστασης στο δεξί μέλος,
δηλ. της b = (c++)
.
Για να γίνει αυτή η πράξη πρέπει να υπολογιστεί πρώτα η παράσταση
στο δεξί μέλος, δηλ. η (c++)
.
Για να υπολογιστεί τέλος η παράσταση c++
επιστρέφεται
η υπάρχουσα τιμή του c
(το οποίο αυξάνεται κατά 1 αφού
έχει επιστραφεί η τιμή του, δηλ. το 1, και προτού συνεχίσει ο
υπόλοιπος υπολογισμός.
Έτσι το b
παίρνει την τιμή 2 και η έκφραση
b = (c++)
επιστρέφει την τιμή που ανατέθηκε στο
b
η οποία με τη σειρά της, στην εντολή
a = (...)
ανατίθεται στο a
.
Το a
έτσι γίνεται 2 και 2 είναι λοιπόν κι η τιμή που επιστρέφει
η έκφραση a = ( b = (c++) )
, η οποία όμως δε χρησιμοποιείται
περαιτέρω.
Επίσης για κάθε τελεστή περιγράφουμε και ποια είναι η
προσεταριστικότητά του. Για τον υποθετικό τελεστή
με σύμβολο T
λέμε ότι είναι προσεταιριστικός αριστερά προς δεξιά
(ΑπΔ) αν η έκφραση a T b T c
ερμηνεύεται ως
(a T b) T c
(παράδειγμα
ο τελεστής της πρόσθεσης +
), ενώ λέμε ότι είναι από τα δεξιά προς
τα αριστερά (ΔπΑ) αν ερμηνεύεται ως a T (b T c)
(παράδειγμα
ο τελεστής της εκχώρησης =
).
Προσεταιριστικότητα | Τελεστής | Επεξήγηση |
ΑπΔ | () [], ->, . |
Παρενθέσεις, πίνακες, πεδίο αντικειμένου μέσω διεύθυνσης, πεδίο αντικειμένου |
ΔπΑ | !, ~, ++, --, -, *, &, sizeof |
Λογικό όχι, bitwise άρνηση, αύξηση κατά 1, μείωση κατά 1, μονικό πλην, pointer dereferencing (αντικείμενο του οποίου έχουμε τη διεύθυνση), διεύθυνση αντικειμένου, μέγεθος κάποιου τύπου σε bytes |
ΔπΑ | (type) | casting (μετατροπή σε άλλο τύπο) |
ΑπΔ | *, /, % |
επί, διά, υπόλοιπο διαίρεσης (mod) |
ΑπΔ | -, + |
πλην, συν |
ΑπΔ | <<, >> |
bitwise shift αριστερά κατά 1 και δεξιά κατά ένα |
ΑπΔ | <, <=, >, >= |
μικρότερο, μικρότερο ή ίσο, μεγαλύτερο, μεγαλύτερο ή ίσο |
ΑπΔ | ==, != |
έλεγχος ισότητας, έλεγχος ανισότητας |
ΑπΔ | &, | |
bitwise και, bitwise ή |
ΑπΔ | ^ |
bitwise xor (αποκλειστικό ή) |
ΑπΔ | && |
Λογικό και |
ΑπΔ | || |
Λογικό ή |
ΔπΑ | ?: |
επιλογή τιμής χρησιμοποιώντας μια συνθήκη |
ΔπΑ | =, +=, -=, /=, %=, >>=, &= |
εκχώρηση τιμής, πρόσθεση δεξιού μέλους στο αριστερό, αφαίρεση δεξιού μέλους από αριστερό, διαίρεση αριστερού μέλους με δεξιό, υπόλοιπου αριστερού μέλους με δεξιό ανατίθεται στο αριστερό, αριστερό μέλος γίνεται bitwise shifted δεξιά τόσες θέσεις όσες λέει το αριστερό μέλος, αριστερό μέλος γίνεται bitwise and με το δεξιό μέλος |
ΑπΔ | , |
επιστρέφεται το δεξιό μέλος |
Πολλούς από αυτούς τους τελεστές δεν τους έχουμε ακόμη συναντήσει και πρέπει να περιμένουμε άρα μέχρι αργότερα για να γίνει αυτός ο πίνακας πλήρως κατανοητός.
Μετά αναθέτουμε στη μεταβλητή ret την τιμή 0. Η ret είναι η μεταβλητή που κρατά τον ακέραιο που θα επιστρέψει η main (στο λειτουργικό σύστημα που την κάλεσε). Κατά παράδοση, επιστρέφει 0 αν όλα πάνε καλά και δεν έχει τίποτε άλλο πιο χρήσιμο να επιστρέψει. Αναθέτουμε λοιπόν στην ret την τιμή 0 αρχικά με την πρόθεση να αλλάξουμε από δω και πέρα την τιμή της μόνο αν εμφανιστεί κάποιο πρόβλημα. Αυτό ακριβώς γίνεται στο default κομμάτι του switch statement αμέσως παρακάτω.
Η main, τέλος, επιστρέφει το ret.
switch ( expression ) {Με τη switch υπολογίζεται η έκφραση expression και αν η τιμή της είναι ίση με value1 τότε εκτελούνται οι εντολές που βρίσκονται μετά το case value1: και μέχρι το επόμενο break;, αν η τιμή είναι value2 εκτελούνται οι εντολές που βρίσκονται μετά το case value2:, κλπ. Αν η τιμή δεν ισούται με καμιά από τις τιμές που έχουν απαριθμηθεί στα διάφορα case τότε εκτελούνται οι εντολές που βρίσκονται μετά το default, και μέχρι το τέλος του switch statement.
case value1:
statement1; ... statementN; break;
...
case valueK:
statement1; ... statementM; break;
default:
statement1; ... statementL;
}
Η έκφραση expression όπως και όλα τα value1 έως valueN πρέπει να είναι ακέραιου τύπου (δηλ. int, char, unsigned int, κλπ). Δεν μπορούν να είναι double ή float.
Στη main το switch επιλέγει με βάση την έκφραση letter_grade, που είναι τύπου char (αν η μεταβλητή letter_grade είχε δηλωθεί int αντί για char το πρόγραμμα θα εξακολουθούσε να δουλεύει κανονικά). Ανάλογα με την τιμή του letter_grade τυπώνεται και το αντίστοιχο μήνυμα. Αν το letter_grade δεν έχει καμιά από τις επιτρεπτές τιμές του, A, B, C και F τότε κάτι έχει πάει πολύ στραβά. Κάτι τέτοιο δε δικαιολογείται αν το πρόγραμμα δουλεύει όπως εμείς πιστεύουμε ότι δουλεύει, και, υποθέτοντας ότι ο compiler και το hardware έχουν κάνει σωστά τη δουλειά τους, πρέπει να σηκώσουμε τα χέρια ψηλά και να παραδεχτούμε πως το πρόγραμμά μας δε δουλεύει όπως εμείς πιστεύουμε ότι δουλεύει. Γι' αυτό και τυπώνουμε ένα μήνυμα ότι έχει συμβεί κάποιο εσωτερικό λάθος (δεν έκανε δηλ. ο χρήστης κάτι στραβό, αλλά κάτι πάει στραβά με το πρόγραμμα), θέτουμε τη μεταβλητή ret (που επιστρέφεται από τη main) ίση με 1, και τελειώνουμε το switch.
Να πούμε εδώ ότι ο αλγόριθμος που χρησιμοποιούμε για να βρούμε όλους τους πρώτους μέχρι το N είναι πολύ απλός: απλώς παίρνουμε κάθε i = 2, 3,...N και εξετάζουμε αν αυτό είναι πρώτος. Ελέγχουμε δηλ. αν υπάρχει φυσικός αριθμός μεγαλύτερος ή ίσος του 2 και μικρότερος από i ο οποίος να διαιρεί το i.
Το μόνο έξυπνο που έχει το πρόγραμμα που ακολουθεί είναι ότι δε χρειάζεται να δοκιμάσει κανείς όλους τους φυσικούς τους μικρότερους από i για να δει αν το i είναι πρώτος αριθμός, αλλά μόνο αυτούς τους φυσικούς που είναι . Ο λόγος είναι απλός. Αν ο i είναι σύνθετος, δηλ. γράφεται ως i = a . b, για δυό φυσικούς a και b, τότε δεν μπορεί και ο a και ο b να είναι μεγαλύτεροι του , γιατί τότε το γινόμενό τους θα ήταν αυστηρά μεγαλύτερο του i και όχι ίσο με i, όπως μόλις υποθέσαμε. Άρα, κάθε σύνθετος αριθμός έχει αναγκαστικά κάποιο διαιρέτη μικρότερο ή ίσο από την τετραγωνική του ρίζα.
Αυτή η παρατήρηση επιταχύνει πάρα πολύ το πρόγραμμα. Μπορείτε να το διαπιστώσετε αν φτιάξετε άλλο ένα πρόγραμμα που να κάνει την ίδια δουλειά αλλά να κάνει όλους του ελέγχους για διαιρετότητα, με όλους τους αριθμούς από 2 έως i - 1. Δοκιμάστε μετά και τα δύο προγράμματα με λίγο μεγάλο N (Ν = 1000 θα πρέπει ήδη να σας δείξει πολύ μεγάλη διαφορά στο χρόνο εκτέλεσης) και συγκρίνετε την ταχύτητά τους. Παρακάτω θα δούμε ένα ακόμη πιο έξυπνο τρόπο για να βρούμε τους πρώτους αριθμούς από 2 έως N, το λεγόμενο κόσκινο του Ερατοσθένη.
1 // Read a positive integer N and print all primes up to N 2 3 #include <stdio.h> 4 #include <math.h> 5 6 #define DIVIDES(x, y) (!((y)%(x))) 7 8 // The following function decides if its argument is a prime number 9 int is_prime(int n) 10 { 11 int i; 12 double s=sqrt(n); 13 14 for(i=2; i<=s; i++) 15 if(DIVIDES(i, n)) 16 return 0; // n is not a prime 17 return 1; // n is a prime 18 } 19 20 main() 21 { 22 int N, i, ret, k=0; 23 char str[300]; 24 25 // Read an integer N from the keyboard 26 read_int: 27 printf("Give a positive integer: "); 28 fgets(str, 300, stdin); // this reads a whole line of input 29 // in the variable 'str' 30 ret = sscanf(str, "%d", &N); // reads integer from string variable 31 if((ret != 1) || (N<=0)) { 32 printf("Oops! Bad input.\n"); 33 goto read_int; 34 } 35 36 // Print all primes from 2 up to N in 10 positions each 37 // 7 numbers per screen line 38 printf("The primes up to %d are the following:\n", N); 39 for(i=2; i<=N; i++) 40 if(is_prime(i)) { 41 k++; // increment counter for primes by 1 42 printf("%10d", i); 43 if(DIVIDES(7, k)) printf("\n"); // change line every 7 primes 44 } 45 printf("\n%d primes were found from 2 to %d\n", k, N); 46 }
Δείχνουμε παρακάτω ένα τυπικό τρέξιμο του προγράμματος:
% a.out Give a positive integer: qwerty Oops! Bad input. Give a positive integer: 200 The primes up to 200 are the following: 2 3 5 7 11 13 17 19 23 29 31 37 41 43 47 53 59 61 67 71 73 79 83 89 97 101 103 107 109 113 127 131 137 139 149 151 157 163 167 173 179 181 191 193 197 199 46 primes were found from 2 to 200
% gcc primes.c -lmΤο -lm στην εντολή κλήσης του compiler gcc οφείλεται στο ότι το πρόγραμμα καλεί μια συνάρτηση, την sqrt (τετραγωνική ρίζα) η οποία δεν είναι μέρος του standard library της C, και ως εκ τούτου, ο compiler (πάλι, για να είμαστε πιο ακριβείς, ο linker) δεν ξέρει που να βρεί τον κώδικα για την ρουτίνα αυτή. Αν στον compiler περάσουμε (κάντε man gcc) το όρισμα -lxxx τότε ο compiler ψάχνει για οποιαδήποτε άγνωστη σε αυτόν ρουτίνα και στη βιβλιοθήκη2.10libxxx.a. Έτσι, η βιβλιοθήκη libm.a είναι απλά η βιβλιοθήκη της C με τις ρουτίνες μαθηματικής φύσης, όπως η sqrt. (Αυτό είναι απλά ένα αρχείο στο directory /usr/lib, συνήθως.) Το γιατί δεν είναι οι ρουτίνες μαθηματικής φύσης στο standard library δεν οφείλεται σε κάποιο ουσιώδη λόγο, αλλά αποκλειστικά και μόνο στην παράδοση. Πολλοί compilers, ειδικά εκτός Unix, δεν ακολουθούν αυτή τη σύμβαση.
Η sqrt είναι και ο λόγος που κάνουμε στην αρχή #include το αρχείο math.h, όπου βρίσκεται κι η περιγραφή του τύπου της sqrt, ο οποίος είναι ο
double sqrt(double x)Αυτό σημαίνει ότι η sqrt είναι μια ρουτίνα που παίρνει ένα όρισμα, τύπου double, και επιστρέφει τιμή τύπου double. Αυτή η πληροφορία για μια συνάρτηση είναι απαραίτητη για ένα σωστά γραμμένο πρόγραμμα. Αν αυτή πληροφορία δε δοθεί στον compiler κάνοντας #include το math.h ή απλά γράφοντας σε μια γραμμή
double sqrt(double x);τότε ο compiler, όταν δει μια κλήση στην sqrt, θα προσπαθήσει να μαντέψει τι τύπου είναι τα ορίσματα (ανάλογα με το τι έχουμε περάσει στη ρουτίνα στην κλήση αυτή) και θα θεωρήσει ότι η sqrt επιστρέφει int. Αυτό είναι συνήθως καταστροφικό για το πρόγραμμα.
Το παρακάτω πρόγραμμα, για παράδειγμα, έχει απρόβλεπτη2.11συμπεριφορά:
#include <stdio.h> main() { printf("The square root of 4 is %g\n", sqrt(4)); }Ο λόγος είναι ο εξής: η ρουτίνα sqrt είναι έτσι γραμμένη που περιμένει να της δώσουμε ένα double και μας επιστρέφει επίσης ένα double. Επίσης, η αριθμός 4, έχει πολύ διαφορετική μορφή αν αποθηκευτεί ως int από το να αποθηκευτεί ως double. Κατ' αρχήν ως int καταλαμβάνει 4 bytes (συνήθως) μνήμης ενώ ως double 8 bytes (συνήθως). ακόμη όμως κι αν καταλάμβαναν τον ίδιο αριθμό bytes πάλι ο τρόπος που χρησιμοποιούνταν αυτά τα 8 bytes για να αναπαρασταθεί ο ακέραιος αριθμός θα ήταν διαφορετικός απ' ότι για να αναπαρασταθεί ο αριθμός κινητής υποδιαστολής.
Για να δουλέψει σωστά η sqrt πρέπει συνεπώς, στη συγκεκριμένη κλήση να της περάσουμε ως όρισμα τον αριθμό 4 αποθηκευμένο σε μορφή double, πράγμα που δε συμβαίνει, μια και ο compiler αυτόματα όταν βλέπει μια σταθερά ακέραια την αποθηκεύει ως int. Ο τρόπος να γίνει αυτό, να περαστεί δηλ. η σταθερά 4 ως double είναι γράψουμε sqrt(4.0), αναγκάζοντας έτσι τον compiler να αποθηκεύσει τη σταθερά 4.0 ως double.
Κι αν είχαμε μια μεταβλητή int x και θέλαμε να πάρουμε την τετραγωνική της ρίζα; Τότε θα έπρεπε (εξακολουθούμε να υποθέτουμε εδώ ότι ο compiler δε γνωρίζει τον τύπο της sqrt) να γράψουμε sqrt( (double)x ). Η έκφραση (αυτό λέγεται cast στη C)
(type) expressionμετατρέπει την έκφραση expression σε μια άλλη ``ισοδύναμη'' τύπου type. Το τι σημαίνει ισοδύναμη έκφραση εξαρτάται από τους τύπους του expression και του type.
Για παράδειγμα το (double)x είναι μιά έκφραση τύπου double με τιμή την τιμή του x, ενώ το
int x; double y=1.2; x = (int)y;θα δώσει στο x την τιμή 1, δηλ. το casting σε int από double κόβει το δεκαδικό μέρος του αριθμού.
Είδαμε πώς γίνεται λοιπόν να περάσουμε σε μια συνάρτηση ένα όρισμα με το σωστό τύπο. Αυτό όμως δε λύνει το πρόβλημα του τι παίρνουμε πίσω. Η sqrt γυρνάει ένα double, κι αν εμείς δε γνωστοποιήσουμε αυτό το γεγονός στον compiler, δηλώνοντας τον τύπο της, τότε αυτός, by default, θα θεωρήσει ότι γυρνάει int, πράγμα που θα δημιουργήσει πρόβλημα. Ο μόνος τρόπος να αποφευχθεί αυτό είναι κάπως να δηλώσουμε στον compiler τον τύπο που επιστρέφει η sqrt. Η δήλωση double sqrt(); κάνει ακριβώς αυτό, χωρίς να λέει τίποτα για τους τύπους των ορισμάτων της sqrt. Το παρακάτω τροποποιημένο πρόγραμμα είναι εντάξει:
#include <stdio.h> main() { double sqrt(); printf("The square root of 4 is %g\n", sqrt(4.0)); }Έχοντας πει όλα αυτά, να επαναλάβουμε ότι το σωστότερο είναι να δοθεί στον compiler πριν από κάθε χρήση μιας συνάρτησης ο πλήρης τύπος της, πράγμα που για συναρτήσεις βιβλιοθήκης επιτυγχάνεται συνήθως κάνοντας
#include
το αντίστοιχο
header file.
#include
υπάρχει η γραμμή
#define DIVIDES(x, y) (!((y)%(x)))Αυτή είναι μια χρήση του
#define
διαφορετική απ' ότι έχουμε
δει μέχρι τώρα στο ότι έχει κάποιες παραμέτρους x
και y
οι οποίες υπάρχουν και στο δεξί μέρος της δήλωσης.
Έχει την εξής έννοια: ο cpp όταν συναντήσει στο κείμενο
κάτι της μορφής
DIVIDES(expr1,expr2)όπου expr1 και expr2 είναι δύο οποιεσδήποτε εκφράσεις, θα το αντικαταστήσει με το
(!((expr2)%(expr1)))Έτσι όταν στο πρόγραμμα primes.c γράφουμε
if(DIVIDES(i, n))
στη ρουτίνα is_prime
ο compiler αυτό που πραγματικά θα δεί είναι
if((!((n)%(i))))
.
Θυμίζουμε εδώ ότι ο (μονικός) τελεστής !
είναι η λογική άρνηση.
Γιατί έχουμε βάλει τόσες πολλές παρενθέσεις στον ορισμό του
macro DIVIDES
;
Αυτή είναι μια πολλή φυσιολογική ερώτηση, μια και είναι
φανερό ότι !(y%x)
είναι ακριβώς το ίδιο πράγμα
(δηλ. 1 αν το υπόλοιπο της διαίρεσης y
δια x
είναι 0, δηλ. αν ο x
διαιρεί τον y
, και 0 αν
ο x
δεν διαιρεί τον y
) με το
(!((y)%(x)))
.
Η απάντηση είναι ότι εν γένει τα x
και y
δε θα είναι
μεταβλητές.
Αν είχαμε ορίσει
#define DIVIDES(x, y) !(y%x)και είχαμε χρησιμοποιήσει το κείμενο
DIVIDES(5+1, 12)αυτό θα αναπτυσσόταν απο τον cpp σε
!(12%5+1)το οποίο υπολογίζεται σε
!(2+1) = !3 = 0
,
(αφού το %
έχει μεγαλύτερο precedence από το +
)
πράγμα που είναι φυσικά λάθος.
Αν υπήρχαν οι παρενθέσεις στον ορισμό, τότε δε θα υπήρχε κανένα
πρόβλημα.
Οι παρενθέσεις δηλ. πρέπει να χρησιμοποιούνται στον ορισμό των
macros για να ``προστατεύουν'' τα ορίσματα των.
Να υπενθυμίσουμε εδώ ότι ο cpp δεν έχει ιδέα ότι επεξεργάζεται ένα πρόγραμμα που θα δώσει στον C compiler. Ο cpp δεν θα έχει κανένα ενδοιασμό να αναπτύξει το
DIVIDES(This is not, a very useful example)Τη θέση του
x
και του y
δηλ. μπορεί να την πάρει
γενικό κείμενο, κι όχι μόνο αριθμητική έκφραση.
Ο cpp δε γνωρίζει τι είναι αριθμητική έκφραση - αυτό το ξέρει
ο C compiler.
Τα macros του cpp είναι μια πολύ ισχυρή προσθήκη στη C κι αυτό θα φαίνεται συνέχεια από δω και πέρα. Δίνουν στον προγραμματιστή τη δυνατότητα να δημιουργεί ουσιαστικά καινούργιες γλώσσες ``χτίζοντας'' πάνω στη C με τη βοήθεια του cpp. Η ισχύς αυτή, αλλά και η προσοχή που χρειάζεται η χρήση του, θα γίνεται φανερή σιγά-σιγά.
Ένα χρήσιμο παράδειγμα είναι ένα macro για τον υπολογισμό του maximum δύο αριθμών:
#define MAX(x, y) (((x)<(y))?(y):(x))Ο τελεστής
expr1 ? expr2 : expr3είναι πολύ χρήσιμος και η σημασιολογία του είναι η εξής: υπολογίζεται πρώτα η ακέραια έκφραση expr1, και αν είναι true (δηλ. μη μηδενικός ακέραιος) τότε υπολογίζεται και επιστρέφεται η τιμή της expr2 αλλιώς υπολογίζεται και επιστρέφεται η τιμή της expr3. Οι expr2 και expr3 είναι οποιουδήποτε τύπου εκφράσεις.
Στον ορισμό λοιπόν του macro παραπάνω, αν η x
έφραση
είναι μικρότερη από τη y
τότε επιστρέφεται η y
αλλιώς η x
.
Ποιο είναι το πλεονέκτημα του άνω macro ως προς το να γράφαμε μια συνάρτηση
int max(int x, int y) { return (x<y)?y:x; }Το κυριότερο πλεονέκτημα είναι ότι το macro δουλεύει και για double ποσότητες ενώ η συνάρτηση όχι. Δευτερευόντως, το macro είναι συνήθως λίγο πιο γρήγορο από την αντίστοιχη συνάρτηση, λόγω του overhead που πάντα υπάρχει σε κλήσεις συνάρτησης.
Υπάρχει κάποιο μειονέκτημα του macro MAX
ως προς τη συνάρτηση
max
;
Παρατηρείστε την εξής χρήση του macro:
i = MAX(j++, k)Είναι πιθανότατο ότι όποιος γράφει κάτι τέτοιο εννοεί το εξής: να υπολογιστεί η έκφραση
j++
, να υπολογιστεί η έκφραση k
και η μεγαλύτερη από τις δύο τιμές να εκχωρηθεί στη μεταβλητή i
.
Δεν είναι όμως αυτό που γίνεται, μια και η άνω χρήση του macro θα αναπτυχθεί από τον cpp ως
i = (((j++)<(k))?(k):(j++))Ας πούμε ότι
j=5
και k=2
αμέσως πριν από αυτή την εντολή.
Τότε το i
θα πάρει τελικά την τιμή 6 ενώ αν είχαμε
καλέσει τη συνάρτηση max
i = max(j++, k)τότε το
i
θα έπαιρνε την τιμή 5.
Το πρόβλημα με τη χρήση του macro είναι φυσικά ότι
η έκφραση j++
υπολογίζεται δύο φορές, και μετά
την πρώτη κλήση η τιμή του j
έχει αυξηθεί
κατά 1, ενώ στην κλήση συνάρτησης η έκφραση j++
υπολογίζεται μόνο μια φορά, και η τιμή της περνιέται στη
συνάρτηση.
Ας δούμε τώρα το παράδειγμα ενός macro το οποίο χρησιμοποιείται
για να ανταλάσσει τις τιμές δύο μεταβλητών.
Γενικά, αν int x, y
είναι δύο μεταβλητές, για να ανταλλάξουμε
τις τιμές τους χρειάζεται να χρησιμοποιήσουμε μια τρίτη μεταβλητή
int t
ως ενδιάμεση θέση αποθήκευσης, και η ανταλλαγή των
τιμών επιτυγχάνεται από τον κώδικα
t = x; x = y; y = t;Αυτή η διαδικασία κωδικοποιείται στο macro
SWAP
του οποίου η χρήση και ο ορισμός φαίνονται στο παρακάτω απλό πρόγραμμα.
#include <stdio.h> #define SWAP(x, y) {int t; t = (x); (x) = (y); (y) = t;} main() { int x=2, y=3; SWAP(x, y) printf("x = %d, y = %d\n", x, y); }του οποίου το output είναι το
% a.out x = 3, y = 2Το
SWAP(x, y)
αντικαθίσταται από ένα ολόκληρο block
εντολών, μέσα στο οποίο υπάρχει η δήλωση μιας προσωρινής μεταβλητής
t
, η οποία δήλωση δε ``φαίνεται'' απ' έξω από το block αυτό.
Προσέξτε επίσης τις παρενθέσεις γύρω από τα x
και y
,
και ότι αυτές μπαίνου και όταν οι μεταβλητές αυτές είναι στο
αριστερό μέρος, χωρίς να δημιουργούν κανένα πρόβλημα.
Πρέπει φυσικά κανείς να είναι προσεκτικός μια και το όνομα t
δεν πρέπει να εμφανίζεται στα ορίσματα του macro.
Για παράδειγμα, η κλήση SWAP(x, t)
, είναι καταστροφική, μια και
ο cpp θα την αντικαταστήσει με
{int t; t = (x); (x) = (t); (t) = t;}το οποίο το μόνο που θα κάνει είναι να αναθέσει την τιμή της
x
στην t
.
Γι' αυτό, σε τέτοιου είδους macros οι τοπικές μεταβλητές που
δηλώνονται σε blocks καλό είναι να έχουν τόσο περίεργα ονόματα
ώστε να ελαχιστοποιηθούν οι πιθανότητες να συμπέσουν με κάποιο όρισμα
ή με μέρος κάποιου ορίσματος:
#define SWAP(x, y) {int _SWAP_TEMP; _SWAP_TEMP = (x); (x) = (y);\ (y) = _SWAP_TEMP;}Παραπάνω, αντικαταστάθηκε το
t
με κάποιο άλλο όνομα,
_SWAP_TEMP
που είναι μάλλον απίθανο να ξαναχρησιμοποιηθεί απ' έξω.
Επίσης παρατηρείστε ότι ο ορισμός του macro καταλαμβάνει τώρα δύο γραμμές
κι όχι μία, όπως έχουμε πει ότι πρέπει.
Ο τρόπος που το επιτυγχάνουμε αυτό, να χρησιμοποιήσουμε δηλ. παραπάνω χώρο
από μια γραμμή για ένα macro είναι να προτάξουμε ένα backslash (\
)
αμέσως πριν την αλλαγή γραμμής2.12Έτσι το newline αποφεύγεται, γίνεται escaped όπως λέμε, κι ο χαρακτήρας
backslash παίζει το ρόλο escape character (αποφεύγεται δηλ. η συνήθης
έννοια του newline).
Τι γίνεται τώρα αν θέλουμε να ανταλλάξουμε τις τιμές δύο doubles;
Το macro SWAP
δε δουλεύει φυσικά ως έχει, μια και η προσωρινή
μεταβλητή είναι τύπου int
.
Πώς θα τροποποιήσουμε το macro ώστε να μπορεί να ανταλλάξει τις τιμές
δυο μεταβλητών οποιουδήποτε τύπου;
Ιδού μια λύση, η οποία χρησιμοποιεί μια επιπλέον παράμετρο,
που δηλώνει τον τύπο των υπό ανταλλαγή μεταβλητών.
#define SWAP(data_type, x, y) {data_type _SWAP_TEMP;\ _SWAP_TEMP = (x); (x) = (y); (y) = _SWAP_TEMP;}το οποίο καλείται κάπως έτσι:
int a=1, b=2; double x=0.1, y=0.2; SWAP(double, x, y) SWAP(int, a, b)Εδώ φαίνεται πλέον καθαρά η υπέρβαση της γλώσσας με τη βοήθεια του cpp η οποία μας επιτρέπει να χειριστούμε τους τύπους δεδομένων σχεδόν σαν τιμές που μπορούν να περαστούν ως παράμετροι.
Τα macros θέλουν λοιπόν εξαιρετική προσοχή στη χρήση τους, αλλά αν είμαστε προσεκτικοί μπορούν πολλές φορές να μας λύσουν τα χέρια.
goto
is_prime
, η οποία θέλουμε να επιστρέφει 1 αν
ο αριθμός n
που της περνάμε είναι πρώτος, και 0 αλλιώς, υπάρχει
η εντολή
for(i=2; i<=s; i++) if(DIVIDES(i, n)) return 0; // n is not a primeΑυτή είναι ίσως η βασικότερη δομή ανακύκλωσης στη C, δηλ. επαναλαμβανόμενης εκτέλεσης μιας ομάδας εντολών. Το for loop συντάσσεται ως
for(expr1;expr2;expr3) statementΗ σημασιολογία είναι η εξής: υπολογίζεται κατ' αρχήν μια φορά η έκφραση expr1 (στην περίπτωση του άνω for loop, είναιη έκφραση
i=2
της οποίας ο υπολογισμός έχει ως αποτέλεσμα
να ανατεθεί η τιμή 2 στη μεταβλητή i
).
Έπειτα υπολογίζεται η έκφραση expr2 και αν αυτή είναι true
τότε εκτελείται το statement (το οποίο φυσικά μπορεί να
είναι είτε μια εντολή, όπως στην περίπτωσή μας που είναι ένα if statement,
είτε ένα block εντολών), και αμέσως μετά το statement
υπολογίζεται το expr3.
Μετά από αυτό, ο έλεγχος μεταφέρεται και πάλι στο σημείο όπου υπολογίζεται
το expr2, και αν αυτό είναι true
τότε εκτελείται
το statement, κ.ο.κ.
Αν κάποια στιγμή το expr2 δεν είναι true
τότε
τελειώνει το for loop.
Στη ρουτίνα is_prime
το for loop χρησιμοποιείται για να
δοκιμάσουμε όλους τους φυσικούς αριθμούς απο το 2 μέχρι και το μέγιστο
φυσικό αριθμό που δεν ξεπερνά την τετραγωνική ρίζα του n
(αυτή έχει προϋπολογιστεί στη double μεταβλητή s
),
για το αν διαιρούν το n
. Μόλις βρεθεί ένας από αυτούς που να διαιρεί
το n
τότε η συνάρτηση επιστρέφει αμέσως 0 (ο n
δεν είναι
πρώτος) ενώ αν έχει τελειώσει το for loop και η συνάρτηση δεν έχει επιστρέψει,
αυτό σημαίνει πως δε βρέθηκε κανένας διαιρέτης του n
, άρα
αυτός είναι πρώτος (αφού, όπως είπαμε και προηγούμενα, αν ένας αριθμός
έχει διαιρέτη τότε σίγουρα έχει και διαιρέτη μικρότερο ή ίσο από την
τετραγωνική ρίζα του αριθμού) και η συνάρτηση επιστρέφει τότε 1.
Στο for loop αυτό, η συνθήκη που καθορίζει το αν ο έλεγχος
θα μπεί μέσα στο loop, i<=s
, είναι φτιαγμένη ώστε να σταματά
το loop μόλις το i
περάσει το όριο που θέλουμε.
Η έκφραση i++
που εκτελείται αμέσως μετά τις εντολές που
ελέγχονται από το for loop (δηλ. το if statement) απλώς αυξάνει τη
μεταβλητή i
κατά 1 κάθε φορά.
Η συνάρτηση is_prime
θα μπορούσε να έχει γραφεί και ως εξής:
int is_prime(int n) { int i; double s=sqrt(n); i=2; test: if(DIVIDES(i, n)) return 0; i++; if(i<=s) goto test; return 1; }Εδώ η ανακύκλωση υλοποιείται σε κάπως χαμηλότερο, και ίσως όχι τόσο δομημένο επίπεδο, από το for loop που είχαμε προηγουμένως. Το
test:
πριν από το πρώτο if
είναι μια ετικέτα (label),
και έχει απλά το ρόλο ενός ενδείκτη θέσης μέσα στο πρόγραμμα, χωρίς να
εκτελεί το ίδιο κάποια εντολή.
Όταν εκτελείται η εντολή goto label; ο έλεγχος του προγράμματος
μεταφέρεται στην ετικέτα που ακολουθεί το goto
, στην περίπτωσή μας
στην ετικέτα test
.
Το όνομα μιας ετικέτας στη C μπορεί να είναι οποιοδήποτε όνομα είναι επιτρεπτό για όνομα μεταβλητής.
Υπάρχουν ακόμη άλλες δυο μορφές ανακύκλωσης στη C, η
while(condition) statementκαι η
do statement while(condition)οι οποίες λειτουργούν ως εξής:
while
ελέγχει το condition,
αν είναι αληθές εκτελεί το statement, κοκ έως ότου
το condition βρεθεί ψευδές, οπότε και σταματάει το loop.
do
εκτελεί το statement έως ότου το
condition βρεθεί αληθές, οπότε και σταματάει το loop.
Όλες οι μέθοδοι ανακύκλωσης εκτός από το goto
θεωρούνται μέθοδοι δομημένου προγραμματισμού, το δε goto
θα δείτε πάρα πολλά βιβλία προγραμματισμού να σας συνιστούν να το αποφεύγετε,
επειδή η χρήση του καθιστά ένα πρόγραμμα δυσκολοδιάβαστο.
Αυτό είναι εν μέρει μόνο σωστό. Υπάρχουν περιπτώσεις όπου η χρήση
ενός goto
για την υλοποίηση ενός loop, απλοποιεί πολύ τα πράγματα
σε σχέση με το αν είχαμε χρησιμοποιήσει μια από τις άλλες τρεις
μεθόδους ανακύκλωσης, που θεωρούνται αποδεκτές στη φιλοσοφία του δομημένου
προγραμματισμού.
Είναι ΟΚ να χρησιμοποιεί κανείς σε ένα μέρος ενός κώδικα
ένα ή και δύο goto
. Αν όμως βλέπει κανείς να χρησιμοποιεί πολλά
labels, τότε μαλλον το loop του χρειάζεται ανασχεδιασμό και υλοποίηση
με κάποια από τις άλλες μεθόδους ανακύκλωσης.
Ένα καλό παράδειγμα κώδικα για τη χρήση του goto
και κάποιου
label είναι ο τρόπος με τον οποίο χρησιμοποιούνται στη συνάρτηση
main
του primes.c
.
Εκεί υπάρχει ένα label read_int
τοποθετημένο στην αρχή του
κώδικα που διαβάζει μια γραμμή από το πληκτρολόγιο, η οποία θα πρέπει
να περιέχει ένα θετικό ακέραιο.
Η γραμμή διαβάζεται με τη ρουτίνα fgets
, και αν δεν περιέχει
ένα θετικό ακέραιο ο έλεγχος επιστρέφει στο label read_int
κι αυτό γίνεται έως ότου ο χρήστης υπακούσει και δώσει ένα input
όπως αυτό που του ζητείται από το πρόγραμμα.
Εδώ η χρήση του label και του goto
είναι πολύ φυσιολογική
και ευανάγνωστη, μια και το label read_int
συμβολίζει
εδώ μια εύκολα ορισμένη κατάσταση του προγράμματος, στην οποία
το πρόγραμμα επιστρέφει μετά από λάθος input.
Ας δούμε όμως τώρα μερικά ακόμη παραδείγματα χρήσης του for loop.
int i=0; for(; i<10; ) printf("%d\n", i++);Προσέξτε εδώ ότι μέσα στο for η πρώτη και η τρίτη έκφραση είναι κενές. Το initialization του
i
γίνεται στη δήλωσή του και η αύξηση
του i
κατά 1 γίνεται μέσα στο printf
(το i++
επιστρέφει την τιμή του i
και το αυξάνει μετά κατά 1).
for(;1;) { ... }
int i, s; for(s=1, i=1; i<=10; s+=(++i)) printf("%d\n", s);Το loop αυτό δεν είναι γραμμένο με τον πιο ξεκάθαρο τρόπο, αλλά είναι χρήσιμο για να δείξουμε ορισμένα πράγματα. Κατ' αρχήν, η πρώτη έκφραση μέσα στο for loop είναι η
s=1, i=1
.
Θυμηθείτε ότι το κόμμα είναι ένας δυαδικός τελεστής και ότι το
expr1 , expr2 υπολογίζει και τις δυο εκφράσεις κι
επιστρέφει ως αποτέλεσμα την τιμή της δεύτερης.
Έτσι, η πρώτη έκφραση μέσα στο for loop αυτό έχει ως αποτέλεσμα να
εκχωρείται η τιμή 1 πρώτα στο s
και μετά στο i
.
Το ίδιο θα μπορούσαμε να είχαμε να είχαμε πετύχει αν γράφαμε
i = s = 1
αφού αυτή η έκφραση ερμηνεύεται από τον compiler ως
i = (s = 1)
και επίσης έχει σαν αποτέλεσμα την εκχώρηση του 1
στο s
και μετά στο i
, αφού η τιμή της έκφρασης (s = 1)
είναι 1.
Στη μεταβλητή s
είναι που κρατάμε το άθροισμα που τυπώνουμε, και
κάθε φορά το αυξάνουμε κατά i
αφού πρώτα όμως αυξήσουμε την
τιμή του i
κατά 1. Αυτό είναι το νόημα της τελευταίας έκφρασης
s+=(++i)
.
int f(int x)
που
επιστρέφει true
αν και μόνο αν ο x
έχει κάποια συγκεκριμένη
ιδιότητα.
Το παρακάτω πρόγραμμα τυπώνει το μικρότερο x
από 0 έως 99
που έχει τη συγκεκριμένη ιδιότητα.
int i; for(i=0; i<100; i++) { if(!f(i)) continue; printf("%d is the smallest with the property\n", i); break; }Ο κώδικας αυτός δείχνει δυο νέες εντολές την
continue
και την
break
.
Οποτεδήποτε εκτελεστεί η continue
μέσα στο for loop σταματάει
η εκτέλεση των εντολών που ελέγχονται από το loop και το πρόγραμμα
συνεχίζει με τον υπολογισμό της τελευταίας έκφρασης μέσα στο for( .. )
και τον έλεγχο της συνθήκης του loop.
Έτσι, στο παραπάνω πρόγραμμα, όταν το f(i)
είναι false
δεν εκτελούνται οι υπόλοιπες εντολές που είναι μέσα στο block ,
υπολογίζεται η έκφραση i++
, γίνεται ο έλεγχος i<100
, κλπ.
Τέλος, οποτεδήποτε εκτελεστεί η εντολή break
μέσα στο for loop,
σταματά η εκτέλεση του loop εκεί και ο έλεγχος του προγράμματος μεταβιβάζεται
στην πρώτη εντολή μετά το for.
Έτσι, στο παραπάνω πρόγραμμα, μόλις ευρεθεί και τυπωθεί ο πρώτος ακέραιος
με την ιδιότητα που ψάχναμε, το loop σταματά και το πρόγραμμα συνεχίζει από
κει και πέρα.
Οι εντολές break
και continue
έχουν πολύ παρόμοια σημασιολογία
και μέσα σε while
ή do
loop.
main
του primes.c
υπάρχει η δήλωση μεταβλητής
char str[300];Αυτή είναι μια ειδική περίπτωση δήλωσης μονοδιάστατου πίνακα στη C, δήλωση που έχει τη γενική μορφή:
type variable[integer_constant]όπου type είναι ένας τυχόν τύπος της C, variable είναι ένα όνομα μεταβλητής και integer_constant είναι μια ακέραια σταθερά. Μια τέτοια δήλωση λέει στον compiler να κρατήσει ένα συνεχές κομμάτι μνήμης για αντικείμενα τύπου type των οποίων το πλήθος είναι ίσο με integer_constant. Στην περίπτωση μας, ο compiler κρατά 300 θέσεις για αντικείμενα τύπου char στα οποία το πρόγραμμα μπορεί να αναφέρεται με μια έκφραση της μορφής str[i], όπου i είναι ένας ακέραιος από 0 έως 299 (παρατηρείστε ότι το πλήθος των δυνατών τιμών για το δείκτη (index) i είναι ακριβώς 300).
Για παράδειγμα στο παρακάτω κομμάτι κώδικα διαβάζονται ακέραιοι αριθμοί από το πληκτρολόγιο, μέχρι 100 το πλήθος και μέχρι κάποιος από αυτούς να είναι 0, οπότε σταματάει το loop κι εκτυπώνονται οι ακέραιοι που δόθηκαν:
int num[100], N=0, i; for(i=0; i<100; i++) { int value; scanf("%d", &value); if(!value) break; // 0 was read num[i] = value; N++; } printf("The %d numbers given are:\n", N); for(i=0; i<N; i++) printf("%d\n", num[i]);Στη μεταβλητή
sum[i]
αποθηκεύεται ο i-οστός
ακέραιος που διαβάζεται, ενώ στη μεταβλητή N κρατάμε
το πλήθος των ακεραίων (εκτός από το τελευταίο μηδενικό)
που έχουν διαβαστεί, και τους οποίους εκτυπώνουμε με το δεύτερο for loop.
Παρατηρείστε επίσης τη δήλωση της μεταβλητής value μέσα στο
πρώτο block. Η μεταβλητή αυτή έχει ζωή μόνο εκεί μέσα και μόνο εκεί
μέσα χρησιμοποιείται. Είναι μια μεταβλητή τοπική στο block αυτό.
Ο χώρος που καταλαμβάνει στη μνήμη η μεταβλητή num είναι
ένα συνεχές κομμάτι από 100 επί 4 = 400 bytes (1 int
πιάνει 4 bytes, υποθέτουμε εδώ).
Η μεταβλητή str που έχει δηλωθεί μέσα στο primes.c
χρησιμοποιείται για να κρατήσει μια συμβολοσειρά (string)
το πολύ 300 χαρακτήρων.
Λαμβάνοντας δε υπ' όψην ότι κάθε συμβολσειρά στη C πρέπει να δηλώνει
το τέλος της με ένα NULL χαρακτήρα (ASCII code 0), βλέπουμε
ότι η μεταβλητή str
μπορεί να αποθηκεύσει ένα string
με μέχρι και 299 ορατούς χαρακτήρες.
Η μεταβλητή str
χρησιμοποιείται στο primes.c
για να αποθηκεύσει τα περιεχόμενα μιας ολόκληρης γραμμής που
διαβάστηκε από το πληκτρολόγιο.
Τα εξής δύο κομμάτια κώδικα είναι ισοδύναμα:
char s[10]="abc";και
char s[10]; s[0] = 'a'; s[1] = 'b'; s[2] = 'c'; s[3] = 0;Και τα δύο δηλώνουν ένα πίνακα 10 χαρακτήρων, και θέτουν τις τρεις πρώτες θέσεις σε 'a', 'b', και 'c', και την τέταρτη σε 0 (χαρακτήρα με ASCII code 0, όχι το χαρακτήρα '0') ο οποίος χαρακτήρας χρηιμοποιείται κατά σύμβαση στη C για να σημαδέψει το τέλος του string.
Πώς μπορεί κανείς να υπολογίσει το μήκος ενός string;
Για παράδειγμα το μήκος του "abc" είναι 3.
Ιδού μια ρουτίνα που το κάνει αυτό
(υπάρχει μια σχεδόν πανομοιότυπη ρουτίνα ήδη γραμμένη
στη standard C library, η ρουτίνα strlen
):
int string_length(char s[]) { int i; for(i=0; s[i]; i++); return i; }Παρατηρείστε ότι ο τύπος του ορίσματος είναι απλώς ``πίνακας από χαρακτήρες'', χωρίς να αναφέρεται ποιο είναι το μήκος που αντιστοιχεί στον πίνακα αυτό. Μέσα στη ρουτίνα, το μήκος υπολογίζεται ελέγχοντας όλους τους χαρακτήρες
s[i]
, και μόλις ο χαρακτήρας αυτός βρεθεί 0 ξέρουμε ότι έχουμε
φτάσει στο τέλος του string και τότε επιστρέφουμε την τιμή αυτού
του i
ως μήκος.
Αν π.χ. καλέσουμε string_length("abc")
τότε το loop
θα σταματήσει μόλις το i
γίνει 3 (θυμηθείτε ότι
το s[3]
είναι το 0, και η ρουτίνα γυρνάει 3, που είναι σωστό).
void make_null(int x) {x = 0;} main() { int i=1; make_null(i); printf("i=%d\n", i); }αυτό που τυπώνεται είναι 1 και όχι 0.
Αυτό συμβαίνει γιατί η ρουτίνα make_null
δεν πρόκειται να ξέρει ότι της περάστηκε μια μεταβλητή και μάλιστα
η i
, γιατί αυτό που συμβαίνει είναι ότι αυτό που περνιέται
στη ρουτίνα δεν είναι η μεταβλητή i
ως χώρος μνήμης, αλλά
η τιμή της i
και μόνο.
Η μόνη περίπτωση να μπορέσει η ρουτίνα make_null
ή οποιαδήποτε
άλλη να αλλάξει μια μεταβλητή είναι να ξέρει σε ποια ή ποιες θέσεις
μνήμης έχει αποθηκευτεί αυτή.
Αυτή η πληροφορία δε δίνεται όμως στη ρουτίνα σε μια κλήση όπως η
make_null(i)
. Αυτό που δίνεται στη make_null
είναι
η τιμή της i
η οποία ανατίθεται στην παράμετρο x
της make_null
, η οποία συμπεριφέρεται ακριβώς σα μια τοπική
μεταβλητή για τη ρουτίνα (αυτές είναι οι μεταβλητές που είναι
δηλωμένες μέσα σε ένα block και, κατά συνέπεια,
δε φαίνoνται έξω από το block).
Και είναι απολύτως λογικό ότι στη ρουτίνα αυτή περνιέται μόνο
η τιμή της μεταβλητής i
, γιατί, αν περνιόταν η
διεύθυνση, τι θα περνάγαμε σε μια κλήση της μορφής
make_null(i+1)
η οποία είναι απολύτως επιτρεπτή στη C;
Πώς μπορούμε λοιπόν να γράψουμε μια ρουτίνα η οποία
μπορεί και να αλλάζει τα ορίσματά της, π.χ. μια ρουτίνα
όπως η make_null
η οποία έχει ένα και μοναδικό ακέραιο
όρισμα το οποίο και θέλουμε να μηδενίζουμε, και ο μηδενισμός
να ισχύει και μετά από την κλήση σε αυτή;
Η απάντηση είναι φυσικά ότι, αν θέλουμε να μηδενίσουμε τη μεταβλητή
i
, τότε πρέπει στη ρουτίνα να δώσουμε τη διεύθυνση
της μεταβλητής αυτής - η τιμή της δεν αρκεί.
Θυμηθείτε ότι η μοναδική ρουτίνα που έχουμε δει ως τώρα που ``αλλάζει''
τις τιμές κάποιων εξωτερικών μεταβλητών είναι η scanf
που ακριβώς με αυτό τον τρόπο λειτουργεί (της περνάμε δηλ. τις
διευθύνσεις των μεταβλητών που θέλουμε να αλλάξει).
Η κλήση λοιπόν στη make_null
πρέπει να είναι
make_null(&i);Αλλά τότε πώς θα είναι ο ορισμός της;
Στη C για κάθε τύπο type υπάρχει ένας παραγόμενος τύπος type *, ο οποίος πρέπει να ερμηνεύεται ως ``διεύθυνση σε αντικείμενο τύπου type''. Αυτό συνήθως το λέμε και ``δείκτη σε type'' ή ``pointer σε type''. Για παράδειγμα η μεταβλητή
int* p;είναι μια διεύθυνση σε
int
, και συνήθως τον αστερίσκο
τον γράφουμε δίπλα στο όνομα της μεταβλητής και όχι του τύπου.
Παρακάτω
int *p, i;η μεταβλητή
p
είναι pointer σε int
ενώ η μεταβλητή
i
είναι int
.
Εδώ θα πρέπει να διευκρινίσουμε μερικά πράγματα που αφορούν το πώς είναι οργανωμένη η μνήμη του υπολογιστή. Η αρχή είναι απλή, τουλάχιστον όσον αφορά το πώς φαίνεται ότι είναι οργανωμένη η μνήμη του υπολογιστή. Αν είναι N bytes διαθέσιμα στη μνήμη του υπολογιστή, τότε αυτά είναι τα bytes με αριθμό από 0 έως N - 1. Πρόκειται δηλ. για μια γραμμική διάταξη από bytes.
Όταν ο compiler δει μια δήλωση int a;
τότε αυτός δεσμεύει
4 bytes μνήμης (τόσα χρειάζεται ένας ακέραιος για να αποθηκευτεί),
π.χ. τα bytes από 10160 έως 10163.
Οποιαδήποτε τιμή φέρει η μεταβλητή a
κατά τη διάρκεια της
εκτέλεσης του προγράμματος είναι ανά πάσα στιγμή αποθηκευμένη
σε αυτές τις θέσεις μνήμης.
Οποιοδήποτε κομμάτι κώδικα πρόκειται να τροποποιήσει τη μεταβλητή
a
πρέπει να γνωρίζει τον αριθμό 10160, δηλ. την
αρχική διεύθυνση μνήμης για τη μεταβλητή αυτή (αφού ξέρει
και τον τύπο της μεταβλητής και το πόσα bytes μνήμης καταλαμβάνει
αυτός, μπορεί ο κώδικας να συνάγει το δεύτερο νούμερο 10163).
Πώς γίνεται τώρα στη C όταν ξέρουμε τη διεύθυνση ενός αντικειμένου να πάρουμε την τιμή του (ξέρουμε πού είναι το ``κουτί'' και θέλουμε να μάθουμε ποια είναι τα περιεχόμενά του) ή να τροποποιήσουμε την τιμή του (να βάλουμε αυτό που εμείς θέλουμε μέσα στο κουτί); Αυτό γίνεται προτάσσοντας ένα αστερίσκο μπροστά από τον pointer. (Αυτό λέγεται pointer dereferencing.)
Έτσι, η make_null
γράφεται ως εξής:
void make_null(int *p) { *p = 0; }Ο τύπος του ορίσματος της συνάρτησης δεν είναι πλέον
int
αλλά int *
, δηλ. pointer σε int.
Το p
παριστάνει λοιπόν μέσα στη ρουτίνα make_null
τη διεύθυνση μνήμης ενός ακεραίου.
Για να μηδενιστεί ο ακέραιος αυτός γράφουμε *p = 0;
.
(Θα μπορούσαμε να είχαμε γράψει και (*p) = 0;
.)
Τώρα μπορούμε πλέον να γράψουμε μια συνάρτηση, και όχι macro, η
οποία να ανταλλάσει τις τιμές δύο int
μεταβλητών:
(Για να γίνει εφικτή αυτή η ανταλλαγή στη συνάρτηση swap
παρακάτω περνούμε όχι τις τιμές των μεταβλητών i
και j
αλλά τις διευθύνσεις τους, ώστε να μπορεί η swap
να τροποποιήσει τις τιμές των μεταβλητών αυτών.)
void swap(int *p, int *q) { int t = *p; *p = *q; *q = t; } main() {int i=1, j=2; swap(&i, &j); printf("%d %d\n", i, j);}Το πρόγραμμα αυτό τυπώνει
2 1
.
double
μήκους n
, και ανταλλάσει τα
δύο διανύσματα (τους μονοδιάστατους πίνακες τους λέμε συχνότατα και
διανύσματα ή vectors):
void swap_vectors(int n, double x[], double y[]) { int i; for(i=0; i<n; i++) { double t=x[i]; x[i] = y[i]; y[i] = t; } } double x[3]={1., 2., 3.}, y[3]={-1., -2., -3.}; main() { int i; swap_vectors(2, x, y); for(i=0; i<3; i++) printf("%g ", x[i]); printf("\n"); for(i=0; i<3; i++) printf("%g ", y[i]); printf("\n"); }Προσέξτε ότι πάλι δεν έχουμε προσδιορίσει στους τύπους των δύο vector ορισμάτων τα μήκη τους. Επίσης προσέξτε τον τρόπο που γίνονται initialized τα δύο vectors
x
και y
καθώς και το initialization της μεταβλητής t
που είναι τοπική
στο block του for loop μέσα στη swap_vectors
.
Η ρουτίνα swap_vectors
δουλεύει κανονικά, και όντως ανταλλάσει
τις τιμές των δύο vectors.
Το πρόγραμμα τυπώνει
-1 -2 3 1 2 -3αφού καλούμε τη
swap_vectors
με n
ίσο με 2 κι έτσι ανταλλάσονται
μόνο οι 2 πρώτες θέσεις των x
και y
.
Πώς γίνεται αυτό; Η απάντηση είναι ότι οποτεδήποτε η C έχει να περάσει ως όρισμα μια μεταβλητή που είναι διάνυσμα (μονοδιάστατος πίνακας) δεν περνάει τα περιεχόμενα του διανύσματος στη συνάρτηση που καλείται, αλλά μόνο μια διεύθυνση, αυτή του πρώτου στοιχείου του διανύσματος. Αυτό γίνεται για τον απλούστατο λόγο ότι το διάνυσμα θα μπορούσε να είναι τεράστιο σε μέγεθος, κι έτσι η γλώσσα αφήνει την ελευθερία στην καλούμενη συνάρτηση να αντιγράψει τις τιμές του διανύσματος σε δικό της χώρο, αν θέλει, και δεν γίνεται αυτό by default, γιατί αυτό θα ήταν μια τεράστια σπατάλη χρόνου και χώρου στη μηχανή.
Πώς γίνεται λοιπόν να γνωρίζουμε που είναι αποθηκευμένο το πέμπτο, π.χ.,
στοιχείο του πίνακα double y[100];
όταν γνωρίζουμε τη διεύθυνση του πρώτου στοιχείου του πίνακα δηλ. τη
διεύθυνση του y[0]
;
Ας πούμε ότι η διεύθυνση του πρώτου byte της μεταβλητής y[0]
είναι στη θέση 23120. Ποιο είναι το πρώτο byte του y[5]
;
Τα στοιχεία του πίνακα y
είναι double
και άρα
πιάνει το καθένα 8 = sizeof(double)
bytes.
Αποθηκεύονται δε αυτά σε διαδοχικές θέσεις μνήμης.
Έτσι το πρώτο byte του y[1]
είναι το υπ' αριθμόν 23128,
του y[2]
το 23136, κλπ, και το πρώτο byte του y[4]
,
που είναι το πέμπτο στοιχείο του πίνακα y
είναι το
a[i]
ενός πίνακα a
με στοιχεία τύπου type είναι λοιπόν το byte υπ' αριθμόν
p + i*sizeof(type), όπου p
είναι η διεύθυνση
του πρώτου στοιχείου του πίνακα a
.
Με άλλα λόγια το p
και το &(a[0])
είναι το ίδιο
πράγμα.
Στη C μπορούμε να προσθέσουμε σε μια διεύθυνση ένα, θετικό ή αρνητικό, ακέραιο αριθμό. Αν έχουμε δηλώσει
type*aτότε η διεύθυνση
a+i
, όπουν n
είναι ακέραιος,
είναι η διεύθυνση
στη μνήμη του i-οστού στοιχείου του πίνακα a
,
διαφέρει δηλαδή από το &(a[0])
κατά i . sizeof(type)
bytes.
Επίσης υπάρχει η σύμβαση ότι για να πάρουμε τη διεύθυνση του πρώτου
στοιχείου του πίνακα type a[...] γράφουμε απλά το όνομα
του πίνακα, δηλ. a
στην προκειμένη περίπτωση.
Ο τύπος δηλ. της έκφρασης a
είναι type*.
Έτσι, αν έχουμε ένα πίνακα int a[100];
με
a
και a+1
συμβολίζονται οι διευθύνσεις του πρώτου
και του δεύτερου στοιχείου αντίστοιχα και οι δυο αυτές εκφράσεις είναι
τύπου int *
.
Επίσης το δεύτερο στοιχείο του πίνακα, του οποίου η διεύθυνση
είναι a+1
έχει τιμή *(a+1)
, το οποίο άρα είναι
συνώνυμο του a[1]
.
Αυτό είναι γενικό:
είναι το ίδιο πράγμα, όποιος και να είναι ο τύπος του pointer ή πίνακαa[i]
και*(a+i)
a
και για κάθε ακέραιο i
.
char s[300]="abc";η μεταβλητή s είναι ένας πίνακας από 300 chars (δηλ. πιάνει 300 bytes στη μνήμη). Η ανάθεση της αρχικής τιμής "abc" στην s ισοδυναμεί με την τοποθέτηση των χαρακτήρων του "abc" στις αντίστοιχες θέσεις του πίνακα s. Γίνεται δηλ. το tt s[0] ίσο με το χαρακτήρα 'a', το s[1] ίσο με τον 'b' και το s[2] ίσο με το χαρακτήρα 'c'. Τέλος, το s[3] γίνεται ίσο με το χαρακτήρα 0 ή NUL. (Αυτός δεν είναι ο ίδιος με το χαρακτήρα '0'. O '0' έχει ASCII κώδικα ίσο με 48, ενώ το NUL είναι ο χαρακτήρας με κωδικό 0.) Ο λόγος που συμβαίνει αυτό είναι απλός: αυτός είναι ο μηχανισμός της C για να υποδηλώνει που τελειώνουν τα χρήσιμα περιεχόμενα του string s.
Αυτό φυσικά χρειάζεται μια και το 300 στη δήλωση της μεταβλητής s είναι απλώς η μνήμη που δεσμεύουμε για τη μεταβλητή, και άρα 300 bytes μνήμης είναι το μέγιστο που μπορεί να μας παράσχει η μεταβλητ'η s. Αλλά σίγουρα θέλουμε μια τέτοια μεταβλητή να μπορούμε να τη χρησιμοποιήσουμε και για να αποθηκεύσουμε λιγότερους χαρακτήρες (όπως στην προκειμένη περίπτωση θέλουμε να αποθηκεύσουμε μόνο τρεις χαρακτήρες). Άρα πρέπει να υπάρχει ένας τρόπος να κρατιέται η πληροφορία για το που τελειώνει το string, και το 0 μετά τον τελευταίο χαρακτήρα αυτόν ακριβώς το σκοπό εξυπηρετεί. Αυτό φυσικά δεν είναι ο μόνος τρόπος που μπορεί να παρασταθεί αυτή η πληροφορία (δηλ. που τελειώνει το string) μια και θα μπορούσε κανείς να κρατάει ένα επιπλέον ακέραιο ο οποίος να μας λέει το μήκος το string. Κι αυτή η μέθοδος μια χαρά θα δούλευε, αλλά δεν είναι αυτή που έχει επιλεγεί στη C.
Έτσι, στο char s[300]; μπορούν να παρασταθούν strings μέχρι και 299 χαρακτήρων, αφού πάντα, για κάθε string, χρειάζεται να δεσμεύσουμε ένα χαρακτήρα για να σηματοδοτήσουμε το τέλος του string.
Πώς λοιπόν μπορούμε να υπολογίσουμε το μήκος ενός string; Απλόύστατα ξεκινώντας από την αρχή του και προχωρώντας μέχρι να βρούμε το μηδενικό χαρακτήρα που σηματοδοτεί το τέλος του. Η παρακάτω ρουτίνα length υπολογίζει έτσι το μήκος του ορίσματός της:
int length(char s[]) { int i; for(i=0; s[i]; i++); return i; }Το for loop παραπάνω εκτελεί απλώς μια κενή εντολή, αλλά σταματάει μόλις το s[i] γίνει το 0, οπότε επιστρέφει το i.
Η παρακάτω ρουτίνα string_copy παίρνει ως ορίσματα δύο strings και αντιγράφει το 2ο στο τέλος του πρώτου, ώστε μετά από τον κώδικα
char s[100]="abc", t[100]="123"; string_copy(s, t);το string s έχει γίνει ίσο με "abc123".
void string_copy(char s[], char t[]) { int i, j; for(i=0; s[i]; i++); for(j=0; t[j]; j++) s[i+j] = t[j]; s[i+j] = 0; }Το πρώτο for loop μέσα στη string_copy διατρέχει απλώς το πρώτο string μέχρι να βρει το τέλος του. Όταν σταματήσει το πρώτο loop το i είναι ίσο με το μήκος του s. Το δεύτερο loop διατρέχει το δεύτερο string και αντιγράφει καθένα χαρακτήρα του στην αντίστοιχη θέση του πρώτου string. Η τελευταία εντολή της ρουτίνας (που δεν επιστρέφει καμιά τιμή) σηματοδοτεί το νέο τέλος του πρώτου string.
Καμία σχέση με ... γάτες. Η ονοματολογία προγραμμάτων στο Unix είναι λίγο περίεργη μερικές φορές, πέρα από το ότι είναι και κρυπτική. Σαν παράδειγμα αναφέρω το πρόγραμμα biff, το οποίο ειδοποιεί τον χρήστη κάνοντας beep και τυπώνοντας ένα μικρό μήνυμα στην οθόνη οποτεδήποτε φτάσει ένα νέο μήνυμα e-mail. Η ιστορία είναι ότι ο προγραμματιστής που έφτιαξε αυτό το πρόγραμμα, ο οποίος ζούσε στο Berkeley, CA, είχε ένα σκύλο με το ίδιο όνομα ο οποίος έιχε τη συνήθεια να γαυγίζει οποτεδήποτε ερχόταν ο ταχυδρόμος. Εξ ού το όνομα.
Σε πρώτη προσέγγιση δε βλάπτει ιδιαίτερα όμως το να αγνοεί κανείς το διαχωρισμό αυτό ανάμεσα σε compiler και linker.
Έτσι εξοικονομείται ο χώρος που θα έπιανε, π.χ. η printf, σε όλα τα εκτελέσιμα προγράμματα που την καλούν (πολύ πιθανόν και όλα), η οποία ρουτίνα υπάρχει τώρα στο shared library μόνο. Επίσης εξοικονομείται μνήμη μια και μπορεί ανά πάσα στιγμή να τρέχουν π.χ. 10 προγράμματα που όλα καλούν την printf, και δεν υπάρχει κανένας εγγενής λόγος να υπάρχουν 10 αντίγραφα του κώδικα μηχανής της printf φορτωμένα στη μηχανή, μια και ο κώδικας αυτός είναι ένα στατικό αντικείμενο και δεν το αλλάζει το πρόγραμμα.
Ένα άλλο μεγάλο πλεονέκτημα χρήσης shared libraries vs. archive libraries (δηλ. στατικές βιβλιοθήκες) είναι το ότι δε χρειάζεται να προγράμματα εφαρμογής να αλλάξουν στο παραμικρό αν αντικατασταθεί μια ρουτίνα από ένα shared library. Φανταστείτε για παράδειγμα ότι ο προμηθευτής σας σας δίνει μια νέα standard library της C, που περιέχει μια σαφώς βελτιωμένη έκδοση της printf, χωρίς φυσικά να έχει αλλάξει το interface (δηλ. ο τρόπος με τον οποίο καλείται, το πρωτόκολλο επικοινωνίας ρουτίνας και προγράμματος που την καλεί). Τότε απλά αντικαθιστάτε την παλιά σας βιβλιοθήκη με την καινούργια και οποιαδήποτε εφαρμογή χρησιμοποιούσε την printf μέσα από το shared library την επόμενη φορά που θα τρέξει θα χρησιμοποιήσει αυτόματα την νέα printf. Κάτι τέτοιο θα ήταν αδύνατο αν κάθε πρόγραμμα που χρησιμοποιούσε την printf την είχε συμπεριλάβει μέσα στο εκτελέσιμο κώδικα. Όλα αυτά τα προγράμματα θα έπρεπε να γίνουν compiled ξανά για να ``δουν'' την καινούργια printf.
Ο όρος χρησιμοποιείται και γενικότερα φυσικά, για οποιαδήποτε συλλογή, σε οποιαδήποτε μορφή (precompiled ή σε source code, σε ένα αρχείο ή σε εκατοντάδες αρχεία) υποπρογραμμάτων.
Αυτό είναι μάλλον απίθανο να μπορέσει να το καταφέρει ο compiler αν στη θέση της sqrt βρισκόταν μια άλλη συνάρτηση του ίδιου τύπου, που δεν ήταν εκ των προτέρων γνωστή στον compiler, για παράδειγμα θα μπορούσε να είναι μια ρουτίνα που είχαμε γράψει εμείς σε κάποιο άλλο .c αρχείο ή ρουτίνα κάποιας βιβλιοθήκης που είχαμε αγοράσει ή κατεβάσει από το δίκτυο, κλπ.