next_inactive up previous


Εισαγωγή στον πρoγραμματισμό υπολογιστών
με τη γλώσσα C

Μιχάλης Κολουντζάκης

Τμήμα Μαθηματικών
Πανεπιστήμιο Κρήτης
Λεωφόρος Κνωσού
714 09 Ηράκλειο

E-mail: mk@fourier.math.uoc.gr

Εαρινό εξάμηνο 2000-01


Περιεχόμενα

Πρόλογος

Παρακάτω προσπαθώ να δώσω τις βασικές αρχές του προγραμματισμού υπολογιστών σε μια από τις πλέον διαδεδομένες και χρήσιμες γλώσσες προγραμματισμού, τη γλώσσα C. Η διαμόρφωση του κειμένου, το οποίο θα είναι συνεχώς διαθέσιμο στο Internet στη διεύθυνση http://fourier.math.uoc.gr/ mk/prog0001/prog, θα είναι λίγο πολύ παράλληλη χρονικά με την εξέλιξη του μαθήματος Εισαγωγή στους υπολογιστές που διδάσκω το εαρινό εξάμηνο 2000-01, και πού αποτέλεσε την αφορμή για τη συγγραφή του κειμένου αυτού.

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

Κάτι άλλο που δε πρόκειται να κάνω είναι να ψάχνω να βρώ μεταφράσεις στα Ελληνικά για όλους τους όρους που χρησιμοποιούνται σήμερα σε σχέση με υπολογιστές. Η γλώσσα που θα χρησιμοποιώ θα είναι λίγο πολύ αυτή που χρησιμοποιείται σε μια τυπική συνομιλία ανάμεσα σε δυο συνεργαζόμενους προγραμματιστές (μείον κάποιες αθυροστομίες που φαίνεται να είναι απαραίτητες για την επιτυχή εκτέλεση ενός προγράμματος). Έτσι θα αναμειγνύω ελεύθερα Ελληνικά, Αγγλικά και δε θα διστάσω ακόμη και να κλίνω με τον Ελληνικό τρόπο Αγγλικές λέξεις. Αμαρτία εξομολογημένη ...

Το κείμενο αυτό είναι φτιαγμένο για να διαβάζεται online με ένα τυπικό web browser (π.χ. netscape, internet explorer, κλπ) αλλά η μορφή του θα προσπαθήσω να είναι τέτοια ώστε να μπορεί κανείς πάντα να εκτυπώσει όλο ή μέρος του κειμένου. Επίσης το κείμενο αυτό θα είναι ένα ``δυναμικό'' (δηλ. όχι στατικό στο χρόνο) κείμενο και μπορεί ανά πάσα στιγμή να αλλάξει μορφή και περιεχόμενο.

Σχόλια κάθε είδους είναι ευπρόσδεκτα και επιθυμητά από όλους, και όχι μόνο απ' όσους παίρνουν το μάθημά μου το επόμενο εξάμηνο, και μπορούν να στέλνονται με e-mail στη διεύθυνση mk@fourier.math.uoc.gr.

1. Τι είναι προγραμματισμός υπολογιστών

1.1 Ο υπολογιστής

Ο υπολογιστής είναι μια μηχανή που έχει τη δυνατότητα να εκτελεί απλές εντολές. Τι είναι μια απλή εντολή δε χρειάζεται αυτή τη στιγμή να το ορίσουμε ακριβώς. Για παράδειγμα η πρόσθεση δυο αριθμών είναι μια απλή εντολή. Ο υπολογιστής βρίσκεται συνεχώς στο loop (κύκλο που επαναλαμβάνεται) εκτέλεσης εντολών, και εκτελεί εκατοντάδες εκατομμυρίων εντολών ανά δευτερόλεπτο. Υπολογιστής με ταχύτητα 800 MHz εκτελεί περί τα 800 εκατομ. εντολές το δευτερόλεπτο (δεν παίρνουν όλες οι απλές εντολές τον ίδιο χρόνο εκτέλεσης, αλλά δε βλάπτει ιδιαίτερα στην κατανόηση της λειτουργίας της μηχανής το να θεωρήσουμε προς το παρόν ότι αυτό συμβαίνει.)

Σε κάθε κύκλο του ο υπολογιστής (για την ακρίβεια ο μικροεπεξεργαστής που είναι η καρδιά κάθε υπολογιστικού συστήματος) λαμβάνει μια εντολή και την εκτελεί. Μια τέτοια εντολή δεν είναι κατ' ανάγκη αριθμητικής φύσης, και εκεί ακριβώς έγκειται η ισχύς του υπολογιστή. Υπάρχουν, π.χ., εντολές που λένε στο μικροεπεξεργαστή να πάρει ως επόμενη εντολή που θα εκτελέσει την εντολή Α ή την εντολή Β, ανάλογα με το αν η τιμή που υπολόγισε στο αμέσως προηγούμενο βήμα είναι θετική ή αρνητική. Είναι ακριβώς τέτοιες εντολές που δίνουν τη δυνατότητα στον υπολογιστή να εκτελεί περίπλοκα ``προγράμματα'', όπως το επόμενο.

	Διάβασε τις τιμές των φυσικών αριθμών a και b
	x <- 0
      test:
	Αν a >= b τότε {
	 x <- x+1
	 a <- a - b
	 Πήγαινε στο test
	}
	Τύπωσε "Το πηλίκο της διαίρεσης ", a, "δια ", b, "είναι ", x

1.1.1 Γλώσσα μηχανής και γλώσσες υψηλού επιπέδου

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

Σε αυτή ακριβώς τη δυσκολία οφείλεται, κατά κύριο λόγο, η ύπαρξη των γλωσσών προγραμματισμού υψηλού επιπέδου όπως οι Fortran (χρονικά η πρώτη τέτοια γλώσσα), BASIC, C, Pascal, Lisp και δεκάδες άλλες.

Όπως θα δούμε παρακάτω οι γλώσσες αυτές παρέχουν σημαντικές ευκολίες στον προγραμματιστή, οι οποίες δεν υφίστανται στη γλώσσα μηχανής, ή έστω στη γλώσσα assembly που αποτελεί ένα ενδιάμεσο σημείο ανάμεσα στη γλώσσα μηχανής και στις γλώσσες υψηλού επιπέδου (πολύ πλησιέστερα όμως στη γλώσσα μηχανής απ' ότι στις τελευταίες).

Επειδή όμως κάθε υπολογιστής μια γλώσσα μόνο ``καταλαβαίνει'' κάθε πρόγραμμα που έχει γραφεί σε μια γλώσσα προγραμματισμού (C, BASIC, Fortran κλπ, ή ακόμη και σε γλώσσα assembly) πρέπει πρώτα να μεταγλωττιστεί σε γλώσσα μηχανής προτού μπορέσει να εκτελεστεί. Η διαδικασία αυτή της μεταγλώττισης συναντάται σε τρεις, λίγο-πολύ, διαφορετικές μορφές:

  1. Προγράμματα που έχουν γραφεί σε γλώσσα assembly ``περνάνε'' από το λεγόμενο assembler, που δεν είναι τίποτε άλλο από ένα εκτελέσιμο (δηλ. ήδη μεταφρασμένο σε γλώσσα μηχανής) πρόγραμμα που διαβάζει το πρόγραμμα assembly και παράγει ένα λειτουργικά ισοδύναμο εκτελέσιμο πρόγραμμα.
  2. Προγράμματα που έχουν γραφεί σε μια γλώσσα υψηλού επιπέδου περνάνε από το λεγόμενο compiler (γίνονται compiled, όπως λέμε).
  3. Υπάρχει και μια τρίτη μορφή μεταγλώττισης ενός προγράμματος, αυτή στην οποία δεν δημιουργείται κάποιο εκτελέσιμο πρόγραμμα το οποίο στη συνέχεια εκτελείται, αλλά ο κύκλος μεταγλώττιση-εκτέλεση είναι πολύ πιο βραχύς, και συνήθως εκτελείται μια ``εντολή'' της γλώσσας υψηλού επιπέδου αμέσως μόλις μεταγλωττιστεί και προτού μεταγλωττιστεί η ``επόμενη'' εντολή. Προγράμματα που κάνουν αυτού του είδους τη μεταγλώττιση-εκτέλεση λέγονται interpreters και χαρακτηριστικό τους είναι ότι συνήθως δε δημιουργούν κάποια μορφή εκτελέσιμου προγράμματος που μπορεί να εκτελεστεί χωρίς τη βοήθεια του interpreter και μόνο από τη μηχανή.
Το φάσμα βέβαια ανάμεσα στο compilation και στο interpretation είναι γεμάτο από ενδιάμεσες καταστάσεις, με πιο συχνή, τελευταία, τη μετατροπή του προγράμματος από τη γλώσσα υψηλού επιπέδου σε μια γλώσσα χαμηλού επιπέδου (που έχει επικρατήσει να λέγεται bytecode) η οποία όμως είναι ανεξάρτητη μηχανής. Γι' αυτό το πρόγραμμα σε μορφή bytecode μπορεί να εκτελεστεί σε οποιαδήποτε μηχανή, αρκεί βέβαια να υπάρχει σε αυτή τη μηχανή ένα πρόγραμμα που διαβάζει bytecode και το εκτελεί. Τέτοια προγράμματα είναι συνήθως πολύ απλά να γραφούν, γιατί ακριβώς το bytecode είναι μια πολύ απλή γλώσσα που γράφεται και διαβάζεται εύκολα από μηχανή.

Το bytecode interpretation προσφέρει μια ενδιάμεση αποδοτικότητα (προγράμματα που έχουν γίνει fully compiled και έχουν μετατραπεί σε εκτελέσιμο κώδικα σε γλώσσα μηχανής είναι πολύ ταχύτερα από προγράμματα που διαβάζονται και εκτελούνται ταυτόχρονα από interpreters, είναι όμως καταδικασμένα να τρέχουν μόνο στον τύπο μηχανής (η έννοια ``τύπος της μηχανής'' εδώ περιλαμβάνει το είδος του hardware αλλά και το είδος και τις διάφορες παραμέτρους του λειτουργικού συστήματος) για τον οποίο έχουν μεταγλωττιστεί. Αντίθετα, ένα πρόγραμμα που τρέχει μέσα από interpreter τρέχει οπουδήποτε υπάρχει εγκατεστημένος ένας interpreter για τη γλώσσα στην οποία έχει το πρόγραμμα γραφεί. Είναι όμως πολύ πιο αργό. Το bytecode προσφέρει μια μέση οδό ανάμεσα στο portability (μεταφερσιμότητα) και στο efficiency, που είναι μάλιστα ιδιαίτερα σημαντική στην εκτέλεση προγραμμάτων διά μέσου του Internet για τον απλούστατο λόγο ότι θέλει κανείς να κάνει κλικ σε ένα πρόγραμμα που βρίσκεται στο τάδε site και αυτό να τρέχει τοπικά, στη μηχανή του. Εκεί έχει σημασία τα προγράμματα να είναι αποθηκευμένα σε γλώσσα ανεξάρτητη μηχανής, μια και δεν μπορεί κανείς να υποθέσει κάτι για ένα remote site, αλλά είναι επίσης σημαντικό να είναι αποθηκευμένα σε μια μορφή που να γίνεται interpreted αποτελεσματικά. Το bytecode προσφέρει μια καλή μέση οδό.

1.2 Πρόγραμμα

Ένα πρόγραμμα λοιπόν είναι ένα σύνολο από οδηγίες που μπορούν να εκτελεστούν μηχανικά, χωρίς την απαίτηση για την ύπαρξη κάποιας ``κρίσης'' εκ μέρους του μηχανήματος που εκτελεί.

Το πρόγραμμα που περιγράφτηκε αμέσως προηγούμενα δεν είναι γραμμένο σε κάποια γλώσσα προγραμματισμού. Οι γλώσσες αυτές έχουν πολύ αυστηρό συντακτικό το οποίο είναι αναγκαστικά περιοριστικό στο προγραμματιστή ακριβώς για να είναι ερμηνεύσιμο από μια μηχανή.

Για παράδειγμα, στη γλώσσα προγραμματισμού 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 . b$ \le$a. Ο τρόπος που έχουμε επιλέξει εδώ για να υπολογίσουμε το k είναι το να αρχίσουμε να προσθέτουμε το b στον εαυτό του μέχρι αυτό το άθροισμα να ξεπεράσει το a. Το πλήθος των φορών που προσθέσαμε πριν ξεπεραστεί το a είναι το πηλίκο k.

1.2.1 Μεταβλητές

Οι μεταβλητές (variables) είναι το πιο βασικό κομμάτι ενός προγράμματος. Τις φανταζόμαστε σαν ένα κουτί, που έχει πάντα ένα, συνήθως περιγραφικό, όνομα, και κάποια περιεχόμενα που μπορεί να αλλάζουν από βήμα σε βήμα του προγράμματος. Ακόμη, σε πολλές γλώσσες προγραμματιμού (η C και η παλιά αυτή BASIC που δείχνουμε εδώ συμπεριλαμβάνονται σε αυτές) κάθε μεταβλητή έχει το δικό της σταθερό τύπο δεδομένων που μπορεί να κρατήσει. Για παράδειγμα μια μεταβλητή μπορεί να έχει τύπο ακέραιο αριθμό, πραγματικό αριθμό (συνήθως λέγονται αριθμοί κινητής υποδιαστολής ή floating point numbers), λογική τιμή (true ή false, δηλ. να είναι αληθής ή ψευδής), λέξη (string), κ.ά. Το προηγούμενο πρόγραμμα σε BASIC έχει τρεις μεταβλητές: τα a, b, x, που και οι τρεις είναι τύπου ακέραιου.

Οι δύο μεταβλητές a και b χρησιμοποιούνται κατ' αρχήν ως μεταβλητές εισόδου: εκεί κρατάει το πρόγραμμα, στην αρχή τουλάχιστον, τους δύο ακεραίους των οποίων το πηλίκο θέλουμε να υπολογίσουμε. Αντιθέτως η μεταβλητή x είναι μεταβλητή που χρησιμοποιείται και για να κρατήσει μερικά ενδιάμεσα αποτελέσματα, αλλά είναι και η μεταβλητή που στο τέλος των βασικών υπολογισμών κρατάει το επιθυμητό αποτέλεσμα.

1.2.2 I/O (input/output)

Οι εντολές στις γραμμές 10, 20, 30 και 100 είναι εντολές που είτε διαβάζουν (εντολή input της BASIC) κάποια στοιχεία που ο χρήστης γράφει στο πληκτρολόγιο είτε τυπώνουν (εντολή print της BASIC) στην οθόνη του υπολογιστή ή του τερματικού κάποιο κείμενο. Είναι προφανές ότι δεν μπορεί να υπάρξει χρήσιμο πρόγραμμα χωρίς κάποια τέτοια αλληλεπίδραση με το χρήστη. Οι εντολές αυτές λέγονται εισόδου/εξόδου (I/O).

1.2.3 Εντολές ανάθεσης τιμής σε μεταβλητές

Αυτές είναι οι εντολές 50, 70 και 80. Η 50 θέτει τη μεταβλητή x ίση με 0, η 70 αυξάνει την τιμή της ίδιας μεταβλητής κατά 1 και η 80 αφαιρεί από τη μεταβλητή a την τιμή της μεταβλητής b. Η χρήση του συμβόλου = που μοιάζει κατ' αρχήν αντιφατική σε σχέση με όσα έχει κανείς συνηθίσει από την αλγεβρική του χρήση, πρέπει στη γλώσσα BASIC (όπως και στη γλώσσα C) να ερμηνεύεται ως εξής: αριστερά του = υπάρχει πάντα μια μεταβλητή και δεξιά του υπάρχει πάντα μια παράσταση. Το πρόγραμμα πρώτα κάνει όσους υπολογισμούς χρειάζεται για να υπολογίσει την τιμή της παράστασης (ενδεχομένως χρησιμοποιώντας και την μεταβλητή που υπάρχει αριστερά του =, και το αποτέλεσμα αυτό εκχωρεί στη μεταβλητή που βρίσκεται αριστερά.

1.2.4 Εντολές εκτελούμενες υπό συνθήκη

Η εντολή στη γραμμή 60 αποτελείται από μια συνθήκη (a<b) και μια εντολή (goto 100) που εκτελείται μόνο αν ισχύει τη συγκεκριμένη εκείνη χρονική στιγμή εκτέλεσης του προγράμματος η συνθήκη.

1.2.5 Εντολές μεταφοράς ροής προγράμματος

Τέλος η εντολή goto <line number> που εμφανίζεται στις γραμμές 60 και 90 είναι μια εντολή που δεν κάνει τίποτε άλλο από το να μεταφέρει τον έλεγχο ροής του προγράμματος σε μια συγκεκριμένη γραμμή. Αμέσως μετά τη γραμμή 90, για παράδειγμα, εκτελείται η γραμμή 60 και ποτέ η 100.

Ας δώσουμε τώρα ένα παράδειγμα της ροής του προγράμματος όταν το πρόγραμμα τρέξει με a ίσο με 13, και b ίσο με 10 (εδώ εννοούμε ότι ο χρήστης πληκτρολογεί τους αριθμούς 13 και 10 στο πληκτρολόγιο και με αυτή τη σειρά). Η ροή λοιπόν του προγράμματος σε αριθμούς γραμμών είναι η εξής:

10, 20 (ο χρήστης πληκτρολογεί 13), 30, 40 (ο χρήστης πληκτρολογεί 10), 50, 60, 70, 80, 90, 60, 100 (εκτυπώνεται The quotient a/b is 1).

2. Προγραμματισμός στη γλώσσα C - Εισαγωγικά

2.1 Γενικά για τη γλώσσα

Η γλώσσα C είναι άρρηκτα συνδεδεμένη με το λειτουργικό σύστημα Unix Δημιουργήθηκε στις αρχές της δεκαετίας του 1970 με σκοπό να είναι μια γλώσσα προγραμματισμού που θα διευκόλυνε ακριβώς τη δουλειά της δημιουργίας ενός νέου λειτουργικού συστήματος το οποίο θα ήταν γραμμένο σε μια portable (εύκολα μεταφερόμενη) γλώσσα υψηλού επιπέδου, εκτός από ένα πολύ μικρό κομμάτι του που αναγκαστικά θα γραφόταν σε γλώσσα assembly (τη γλώσσα που βρίσκεται στο χαμηλότερο σχεδόν επίπεδο, δηλ. πλησιέστερα στη μηχανή, και όπου κάθε παραμικρό κομμάτι της μηχανής ελέγχεται πλήρως).

Αυτό το χαρακτηριστικό του λειτουργικού συστήματος 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. Αφορά την ιστορία της ανάπτυξης της γλώσσας κι ίσως είναι λίγο τεχνικό σε ορισμένα σημεία του.

2.1.1 Η γλώσσα C++

Από τα μέσα της 10ετίας του 1980 έχει κάνει την εμφάνισή της η C++ (αργότερα, όταν θα έχετε μάθει λίγη C θα δείτε γιατί αυτή η περίεργη ονομασία) που είναι μια επέκταση της C προς την κατεύθυνση του object-oriented προγραμματισμού. Το μερίδιο της C++ στους προγραμματιστές αυξάνει διαρκώς, μια και προσφέρεται ιδιαίτερα για δουλειές όπως την κατασκευή φιλικών user interfaces (δηλ. περιβαλλόντων αλληλεπίδρασης με το χρήστη), και το object oriented programming προσφέρει ένα πολύ καλό πρότυπο οργάνωσης μεγάλων προγραμμάτων που έχουν να κάνουν όχι τόσο με αριθμητικούς υπολογισμούς αλλά με διαχείριση πολλών διαφορετικών τύπων δεδομένων πάνω στα οποία θέλουμε κάπως να κάνουμε ίδιες εργασίες.

Για παράδειγμα, αν καθήσετε μπροστά σε ένα υπολογιστή με γραφικό περιβάλλον και παράθυρα, όπως τα Miscrosoft Windows ή τα X Windows (το standard γραφικό περιβάλλον για Unix συστήματα) θα δείτε ότι όλα τα αντικείμενα που βρίσκονται πάνω στην οθόνη, είτε παράθυρα είτε διαφόρων τύπων εικονίδια, υποστηρίζουν την έννοια του ``ανοίγματος'' (open). Όταν κάνετε open σε ένα παράθυρο που είναι ελαχιστοποιημένο αυτό μεγαλώνει, όταν κάνετε open σε ένα εικονίδιο που αντιπροσωπεύει ένα modem2.1τότε το modem ενεργοποιείται και παίρνει τηλέφωνο, κλπ. Τα διάφορα εικονίδια δηλ. ανταποκρίνονται στο ίδιο ``σήμα'' (open) με διαφορετικό τρόπο, που εν γένει δεν είναι γνωστός στο περιβάλλον σύστημα αλλά υλοποιείται από το κάθε αντικείμενο (παράθυρο, modem, πίνακα ελέγχου, κλπ) με δικό του ξεχωριστό τρόπο. Εναλλακτικά θα έπρεπε το περιβάλλον σύστημα να γνωρίζει τα ``εσωτερικά'' του κάθε αντικειμένου που ``φιλοξενεί'', πράγμα ανέφικτο. Αυτού του είδους η προσέγγιση είναι χαρακτηριστική των object oriented συστημάτων.

2.2 Το πρόγραμμα hello.c

Τηρώντας την παράδοση, αρχίζουμε περιγράφοντας ένα απλό πρόγραμμα σε C το οποίο δεν κάνει τίποτε άλλο από το να τυπώνει
Hello, world!
στην οθόνη του υπολογιστή.



   1 #include        <stdio.h>
   2 
   3 main()
   4 {
   5         printf("Hello, world!\n");
   6 }

Το αρχείο hello.c

Οι αριθμοί που εμφανίζονται μπροστά από κάθε γραμμή δεν υπάρχουν στην πραγματικότητα αλλά έχουν προστεθεί για να μας διευκολύνουν να αναφερόμαστε στις διάφορες γραμμές του κώδικα, κατά την ανάλυση που κάνουμε εδώ.

2.2.1 Η διαδικασία συγγραφής και εκτέλεσης ενός προγράμματος

Προτού αναφερθούμε στο πρόγραμμα αυτό και στο πώς λειτουργεί, είναι πλέον κατάλληλη η στιγμή για να εξηγήσουμε το ποια είναι η διαδικασία την οποία ακολουθούμε για να γράψουμε ένα πρόγραμμα.

Ότι πούμε παρακάτω ισχύει στο λειτουργικό σύστημα 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 του προγράμματος που μόλις εκτελέσαμε.)

2.2.2 Ανάλυση του hello.c

Ας αναλύσουμε τώρα το 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 εκτός ίσως από αυτά που ο ίδιος έχει γράψει) που είναι πιο συχνά απαραίτητες για να δουλέψει το πρόγραμμα σωστά παρά όχι.

2.2.3 Παρένθεση: το manual στο Unix

Πώς γνωρίζαμε ποιό αρχείο να κάνουμε #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.

2.2.4 Ανάλυση του hello.c - Συνέχεια

Πριν συνεχίσουμε την ανάλυση του προγράμματος hello.c να πούμε πως το ίδιο πρόγραμμα θα μπορούσε να είχε γραφεί και έτσι:
#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 θα τυπωνόταν αμέσως μετά το θαυμαστικό.

2.2.5 Παρένθεση: ο κώδικας ASCII

Υπάρχει ένας κώδικας αντιστοίχισης, ο ASCII (American Standard Code for Information Interchange), ανάμεσα στους αριθμούς από 0 έως 255 - αυτοί είναι οι αριθμοί που μπορούν να γραφούν με 8 δυαδικά ψηφία - και σε διάφορα σύμβολα, που μπορεί να υπάρχουν στο πληκτρολόγιό σας, μπορεί και όχι. Τα σύμβολα αυτά συμπεριλαμβάνουν όλα τα γράμματα του λατινικού αλφαβήτου, μικρά και κεφαλαία, σημεία στίξης, διαφόρων ειδών παρενθέσεις, και διάφορα άλλα σύμβολα που δεν έχουν κάποιο σχήμα αλλά χρησιμοποιούνται κατά σύμβαση για κάποιο συγκεκριμένο σκοπό. Υπάρχει για παράδειγμα στο Unix ένας χαρακτήρας, ο line-feed (LF) με αριθμό 10, που χρσιμοποιείται για να δηλώσει ότι έχει φτάσει το τέλος της τρέχουσας γραμμής. Έτσι όταν ένα πρόγραμμα διαβάζει ένα αρχείο και συναντήσει το χαρακτήρα LF συμπεραίνει συνήθως (αυτό πραγματικά είναι θέμα του συγκεκριμένου προγράμματος) ότι έχει επέλθει αλλαγή γραμμής και ενεργεί ανάλογα. Αν, για παράδειγμα, το πρόγραμμα αυτό είναι πρόγραμμα εκτύπωσης στον εκτυπωτή τότε, κατά πάσα πιθανότητα, θα στείλει στον εκτυπωτή εντολή να αλλάξει γραμμή εκτύπωσης προτού συνεχίσει να του στέλνει άλλους χαρακτήρες για εκτύπωση.

Για να είμαστε λίγο πιο ακριβείς, ο κώδικας 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

2.3 Το πρόγραμμα mean.c

Το παρακάτω πρόγραμμα mean.c διαβάζει από το πληκτρολόγιο δύο ακεραίους a και b, υπολογίζει το μέσο όρο τους και τον τυπώνει στην οθόνη.



   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

Τα νέα στοιχεία που συναντά κανείς στο mean.c είναι τα ακόλουθα:

  1. Δηλώσεις μεταβλητών στις γραμμές 5 και 6.
  2. Ανάγνωση τιμών από το πληκτρολόγιο στις γραμμές 9 και 11.
  3. Αριθμητικές πράξεις και εκχώρηση του αποτελέσματος σε μεταβλητή.
  4. Πιο περίπλοκη χρήση της printf στη γραμμή 13.
  5. Τα σύμβολα &a και &b στις γραμμές 9 και 11.

2.3.1 Τύποι μεταβλητών

Οι βασικοί (και όχι παραγόμενοι) τύποι μεταβλητών στη C είναι οι εξής
Όνομα τύπου Περιγραφή Μέγεθος σε bytes
char Χαρακτήρας 1
int Προσημασμένος Ακέραιος 4
float Αριθμός Κινητής Υποδιαστολής (πραγματικός αριθμός) 4
double Αριθμός Κινητής Υποδιαστολής (πραγματικός αριθμός) 8
void Κενός τύπος -
Πρέπει να τονίσουμε εδώ ότι αν και τα παραπάνω μεγέθη σε bytes2.7είναι σχεδόν standard σήμερα, δεν είναι σε καμιά περίπτωση σωστά σε όλα τα υπολογιστικά συστήματα. Το ``16-bito'' MS-DOS είναι ίσως η πιο κοινή περίπτωση όπου, π.χ., ένας int έχει μέγεθος 2 και όχι 4 bytes. Γι' αυτό και δεν πρέπει κανείς να παίρνει τα μεγέθη αυτά ως απολύτως δεδομένα, αν δεν ξέρει που τρέχει το πρόγραμμα που γράφει.

Για το ξεκαθάρισμα τυχόν τέτοιων αμφιβολιών υπάρχει ο τελεστής 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 * σε μια τελείως γενική διεύθυνση μνήμης, που δεν ξέρουμε δηλ. σε τι τύπο αντικειμένου ``δείχνει'' (αυτό θα γίνει πιο καθαρό παρακάτω).

2.3.2 Απουσία λογικού τύπου

Μια σημαντική παρατήρηση είναι ότι η C δεν έχει λογικό τύπο, κάποιο τύπο δηλ. του οποίου οι τιμές να είναι είτε true (αληθής) είτε false (ψευδής). Στη C η σύμβαση είναι ότι οποιαδήποτε μη μηδενική ακέραια τιμή (και συνηθέστατα το 1) θεωρείται αληθής και το 0 μόνο ψευδής. Όταν αργότερα θα δούμε την εντολή if, για παράδειγμα, θα δούμε ότι αυτή συντάσσεται με μια λογική συνθήκη και κάποιες εντολές που εκτελούνται αν και μόνο αν η λογική αυτή συνθήκη είναι αληθής. Αυτό σημαίνει ότι η συνθήκη αυτή δεν είναι τίποτε άλλο από μια έκφραση ακέραιου τύπου (δηλ. int ή short ή char κλπ) και για να εκτελεστούν οι εντολές που ακολουθούν πρέπει η έκφραση αυτή να μην είναι ίση με 0.

Για παράδειγμα, θα δούμε αργότερα, ο δυαδικός τελεστής && είναι το λογικό και. Αυτό σημαίνει ότι η τιμή του expr1&& expr2 είναι 1 αν και μόνο αν δεν είναι και οι δυο εκφράσεις expr1 και expr2 ίσες με 0, και είναι ίση με 0 αλλιώς. Έτσι, η έκφραση 5 && 3 είναι απολύτως θεμιτή στη C και είναι ίση με 1.

2.3.3 Δηλώσεις μεταβλητών

Πώς δηλώνεται τώρα μια μεταβλητή; (Όλες οι μεταβλητές στη C πρέπει να δηλωθούν προτού χρησιμοποιηθούν, ώστε ο compiler να γνωρίζει τον τύπο τους όταν τις πρωτοσυναντήσει.) Ο κανόνας είναι πως μια δήλωση μεταβλητών συνίσταται από ένα όνομα τύπου ακολουθούμενο από ένα ή περισσότερα (χωριζόμενα με κόμμα) ονόματα μεταβλητών. Η λίστα των μεταβλητών που δηλώνονται τερματίζεται με το σύμβολο ; (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), φτάνει η λέξη να μην αρχίζει με ψηφίο.

2.3.4 Εισαγωγή δεδομένων με τη scanf

Οι γραμμές 9 και 11 διαβάζουν τους ακεραίους a και b από το πληκτρολόγιο. Η ρουτίνα scanf (κάντε man 3 scanf) παίρνει πάντα ως πρώτη παράμετρο ένα string (το format string) το οποίο είναι έτσι φτιαγμένο που να υποδηλώνει στη scanf τί είδους τύπους δεδομένων θέλουμε να διαβάσουμε. Οι επιπλέον παράμετροι (λίγο-πολύ αυτές οι επιπλέον παράμετροι θα είναι τόσες τον αριθμό όσα και τα % που βρίσκονται μέσα στο 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, αλλά θα δούμε μερικές από τις δυνατότητές της παρακάτω, και μερικές δυσκολίες που παρουσιάζει η χρήση της.

2.3.5 Αριθμητικές πράξεις και εκχώρηση αποτελέσματος σε μεταβλητή

Ας ασχοληθούμε τώρα με τη γραμμή
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 }

Το αρχείο expr.c

Ιδού το 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.

2.3.6 Χρήση της printf - το πρόγραμμα format.c

Στη γραμμή 12
        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 }

Το αρχείο format.c

δοκιμάζουμε διάφορους τρόπους να μορφοποιηθεί η εκτύπωση για τον ακέραιο 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 μπορεί κάθε φορά να βρίσκει λύση στο πρόβλημά του. Αυτή η λύση βέβαια σπανίως είναι η μόνη δυνατή.

2.4 Το πρόγραμμα cond.c

Είμαστε έτοιμοι για ένα πιο δύσκολο πρόγραμμα, το οποίο εισάγει διάφορα καινούργια στοιχεία της C. Ας υποθέσουμε ότι για ένα μάθημα το βαθμολογικό σύστημα έχει ως εξής. Ο φοιτητής δίνει μια πρόοδο (midterm) και παραδίδει και κάποια, ας πούμε αβδομαδιαίά, φυλλάδια ασκήσεων, για τα οποία παίρνει ένα συνολικό βαθμό (homework). Επίσης υπάρχει κι ένα τελικό διαγώνισμα (final exam). Τα σχετικά βάρη είναι 40% για την πρόοδο και τον τελικό και 20% για τα φυλλάδια ασκήσεων. Αν όμως ο τελικός βαθμός είναι μεγαλύτερος από το συνδυασμένο βαθμό τότε είναι αυτός που μετράει κι όχι ο συνδυασμένος. Τέλος, η γραμματεία δε δέχεται νούμερα για βαθμούς, αλλά θέλει γράμματα. Ο καθηγητής έχει αποφασίσει να δώσει A για βαθμούς πάνω από 0.9 (στην κλίμακα από 0 ως 1), να δώσει B για πάνω από 0.7 και C για πάνω από 0.5. Αν ο βαθμός είναι κάτω από 0.5 τότε δίνει F (fail).



   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 }

Το αρχείο cond.c

Παρακάτω δείχνουμε δυο διαφορετικά τρεξίματα του προγράμματος:

% 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 είναι μεγαλύτερο. Έπειτα υπολογίζεται το γράμμα που αντιστοιχεί στον αριθμητικό βαθμό που έχουμε το οποίο και τυπώνεται μαζί με ένα μήνυμα που εξαρτάται από το ποιος είναι ο βαθμός.

2.4.1 Σχόλια (comments)

Στις πρώτες γραμμές του προγράμματος εμφανίζεται ελεύθερο κείμενο εγκλεισμένο ανάμεσα σε /* και */. Ανάμεσα στα δύο αυτά σύμβολα μπορούμε να βάλουμε οτιδήποτε κείμενο θέλουμε. Επίσης, μέσα στη συνάρτηση main παρατηρείστε ότι υπάρχει ελεύθερο κείμενο μετά το σύμβολο //.

Αυτά τα σχόλια σκοπό έχουν να δώσουν τη δυνατότητα στον προγραμματιστή να συμπεριλαμβάνει κάποιες επεξηγήσεις δίπλα στον, συνήθως, αρκετά κρυπτικό κώδικα σε C. Αυτές οι επεξηγήσεις βοηθούν πάρα πολύ στο να μπορεί αργότερα ένας άλλος (ή και ο ίδιος) προγραμματιστής να καταλάβει πώς λειτουργεί ένα πρόγραμμα και να κάνει αλλαγές ή διορθώσεις. Συνιστάται ιδιαίτερα να συμπεριλαμβάνει κανείς αρκετά σχόλια στα προγράμματά του, αν μη τι άλλο, για να τα καταλαβαίνει αργότερα ο ίδιος.

2.4.2 #define για σταθερές

Εδώ εξηγούμε τι συμβαίνει στις γραμμές που αρχίζουν με τη λέξη #define. Αυτές αποτελούν εντολές για τον cpp και όχι για τον compiler. Το νόημα μιας εντολής
#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, τα οποία θα δούμε αργότερα.

2.4.3 Συναρτήσεις

Στις γραμμές αμέσως μετά τα #define υπάρχει η δήλωση μιας συνάρτησης, της check_number. Ο τύπος της συνάρτησης αυτής είναι αυτό που βλέπει κανείς στη γραμμή
int     check_number(double grade)
Είναι μια συνάρτηση που πάιρνει ως όρισμα ένα double και επιστρέφει ένα int. Η λειτουργία που πραγματοποιεί η συνάρτηση αυτή είναι ότι ελέγχει το όρισμά της (grade κι αν αυτό είναι εκτός του διαστήματος [0, 10] τότε τυπώνει ένα μήνυμα και σταματά την εκτέλεση του προγράμματος.

Αντίθετα με γλώσσες όπως η Pascal και η Fortran, όπου τα υποπρογράμματα είναι δύο ειδών, αυτά που επιστρέφουν κάτι (functions στην Pascal) και αυτά που δεν επιστρέφουν τίποτα (procedures στην Pascal), στη C όλα τα υποπρογράμματα επιστρέφουν μια τιμή (σχεδόν - εξαιρούνται αυτά που είναι δηλωμένα ότι επιστρέφουν void - κενό). Δεν είναι υποχρεωμένος όμως ο χρήστης να χρησιμοποιήσει αυτή την τιμή, ούτε καν ο προγραμματιστής της συνάρτησης είναι υποχρεωμένος να ορίσει κώδικα για αυτό που επιστρέφεται (αν και μερικοί compilers θα φροντίσουν να του τονίσουν πως δεν κάνει καλά να αφήσει κάτι τέτοιο στην τύχη του, κανείς compiler δε θα επιμείνει σε αυτή του τη θέση).

Ο ορισμός λοιπόν μιας συνάρτησης στη C αποτελείται από μια δήλωση τύπου (τι ορίσματα παίρνει και τι τύπου είναι αυτό που επιστρέφει) ο οποίος ακολουθείται από ένα block, δηλ. μια σειρά από εντολές που περικλείονται μέσα σε δύο άγκιστρα ({ ... }). Μέσα σε κάθε block μπορούν να περιέχονται κι άλλα blocks.

2.4.4 Δήλωση μεταβλητής string

Όπως έχουμε ήδη πει, στην αρχή ενός block μπορούν να υπάρχουν δηλώσεις μεταβλητών. Στο block ορισμού της συνάρτησης check_number υπάρχει ορισμός μιας μεταβλητής, του string message:
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.

2.4.5 Η εντολή if

H πρώτη εντολή μετά τη δήλωση μεταβλητής στη συνάρτηση check_number είναι του τύπου
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;
 }
}
Πρέπει να είναι ήδη προφανές πως μια σωστή στοίχιση καθιστά ένα πρόγραμμα σημαντικά πιο ευανάγνωστο από μια στοίχιση που δεν υπακούει σε κάποιους κανόνες, κι αυτό βοηθά τα μέγιστα στην κατανόηση ενός προγράμματος από τρίτους (ή και από τον ίδιο το συγγραφέα του μετά από λίγο καιρό) και στο να αποφεύγονται επίσης λάθη κατά το γράψιμο του προγράμματος.

2.4.6 Επιστροφή τιμής από συνάρτηση

Η τελευταία εντολή μέσα στη συνάρτηση check_number είναι η return 0;. Οποτεδήποτε μέσα στη συνάρτηση εκτελεσεί η εντολή
return;
ή
return value;
σταματάει η εκτέλεση του κώδικα της συνάρτησης και επιστρέφει ο έλεγχος στο κομμάτι του προγράμματος που είχε ενεργοποιήσει (καλέσει) το υποπρόγραμμα. Στην πρώτη περίπτωση η τιμή επιστροφής είναι αόριστη (με άλλα λόγια δεν μπορούμε να πούμε οτιδήποτε γι' αυτήν - μας είναι άχρηστη) ενώ στη δεύτερη περίπτωση επιστρέφεται η τιμή value το οποίο μπορεί να είναι οποιαδήποτε έφραση του τύπου που επιστρέφει η συνάρτηση.

Στη συνάρτηση check_number επιστρέφουμε (όταν επιστρέφουμε, αφού η ίδια συνάρτηση μπορεί να τερματίσει το όλο πρόγραμμα, αν ο έλεγχος μπει μέσα στο if) τον ακέραιο 0. Ακολουθούμε έτσι τη σύμβαση που θέλει τις συναρτήσεις που δεν έχουν τίποτε χρήσιμο να επιστρέψουν, να γυρνούν το 0, αν όλα έχουν πάει καλά (δηλ. δε διαπιστώθηκε κάποιο σφάλμα) ενώ επιστρέφουν κάποια μη μηδενική τιμή όταν έχει ευρεθεί κάποιο σφάλμα, και μάλιστα φροντίζουμε ώστε το ποια μη μηδενική τιμή θα επιστρέψουν να είναι επεξηγηματικό του τι είδους σφάλμα υπήρξε.

Στο αντίστοιχο manual page για μια συνάρτηση δηλώνεται επίσης και το πώς πρέπει να ερμηνευτεί η επιστρεφόμενη τιμή. Για παράδειγμα, αν κάνετε man 3 printf θα δείτε ότι η συνάρτηση αυτή επιστρέφει τον συνολικό αριθμό χαρακτήρων που γράφτηκαν στο output κατά την εκτέλεσή της.

2.4.7 Συνέχεια ανάλυσης του cond.c

Στη συνάρτηση main του cond.c υπάρχουν κάποιες δηλώσεις μεταβλητών, με ονόματα που είναι επεξηγηματικά (αν και όχι ιδιαίτερα μεγάλα ώστε να καθιστούν το πρόγραμμα δυσανάγνωστο). Στις double μεταβλητές mid, hm και fin, τοποθετούνται αρχικά οι βαθμοί, που δίνει ο χρήστης από το πληκτρολόγιο, και το περιεχόμενο των μεταβλητών φαίνεται κι από το κείμενο που τυπώνεται (με τα αντίστοιχα printf) πριν αυτές διαβαστούν με scanf.

Μόλις διαβαστεί, κάθε μια από αυτές τις τρεις μεταβλητές δίνεται ως όρισμα στη ρουτίνα check_number η οποία ελέγχει αν είναι στο σωστό εύρος 0 έως 10, κι αν δεν είναι διακόπτει το πρόγραμμα.

Στη συνέχεια η μεταβλητή numeric_grade υπολογίζεται ως ο ζυγισμένος μέσος όρος των mid, hm και fin. Βλέπουμε εδώ πως μπορεί κάλλιστα μια εντολή να καταλαμβάνει κάμποσες γραμμές στο κείμενο και δεν είναι ανάγκη να χωράει σε μια και μόνη γραμμή, και ο τρόπος που επιλέγουμε να τη γράψουμε είναι τέτοιος που να την κάνει ευανάγνωστη.

Η επόμενη εντολή if ελέγχει αν το fin είναι μεγαλύτερο του numeric_grade κι αν είναι τότε αντικαθιστά την τιμή του numeric_grade με αυτή του fin.

2.4.8 Τελεστές

Στην επόμενη εντολή
numeric_grade /= 10.;
βλέπουμε για πρώτη φορά ένα τέτοιου είδους τελεστή. Το νόημα του
a /= expression
όπου το a είναι μεταβλητή αριθμητικού τύπου (δηλ. ακέραια ή δεκαδική) και το expression είναι τυχαία έκφραση, είναι ότι το expression υπολογίζεται, η τιμή που έχει το a διαιρείται με την τιμή που υπολογίστηκε για το expression, και το αποτέλεσμα της διαίρεσης εκχωρείται στο a.

Υπάρχουν κι άλλοι τελεστές με παρόμοιο νόημα. Το τι κάνουν οι

a += expression
a -= expression
a *= expression
πρέπει ήδη να είναι ξεκάθαρο.

Πρέπει επίσης να αναφέρουμε τους τελεστές που φαίνονται παρακάτω

a++
++a
a--
--a
Ο τελεστής a++ αυξάνει την τιμή της μεταβλητής a κατά 1 κι ο ++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++) ), η οποία όμως δε χρησιμοποιείται περαιτέρω.

2.4.9 Ποιοι τελεστές υπάρχουν;

Παραθέτουμε παρακάτω ένα πίνακα με όλους τους τελεστές που χρησιμοποιεί η C σε φθίνουσα σειρά ισχύος (precedence). Για παράδειγμα ο τελεστής * (πολλαπλασιασμός) εμφανίζεται σε πιο πάνω γραμμή από τον τελεστή + (πρόσθεση).

Επίσης για κάθε τελεστή περιγράφουμε και ποια είναι η προσεταριστικότητά του. Για τον υποθετικό τελεστή με σύμβολο 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 με το δεξιό μέλος
ΑπΔ , επιστρέφεται το δεξιό μέλος

Πολλούς από αυτούς τους τελεστές δεν τους έχουμε ακόμη συναντήσει και πρέπει να περιμένουμε άρα μέχρι αργότερα για να γίνει αυτός ο πίνακας πλήρως κατανοητός.

2.4.10 Συνέχεια ανάλυσης του cond.c

Αφού λοιπόν με τα διαδοχικά if .. else υπολογίσουμε ποιος θα είναι ο βαθμός σε γράμμα, και το αναθέσουμε στη μεταβλητή τύπου char με όνομα letter_grade (προσέξτε εδώ πως συμβολίζονται οι σταθερές τύπου char μέσα σε single quotes ') τυπώνουμε το πρώτο μήνυμα-αποτέλεσμα του προγράμματος με την εντολή printf (θυμηθείτε εδώ ότι η μεταβλητή numeric_grade βρίσκεται σε κλίμακα 0 έως 1, γι' αυτό και μέσα στην printf την πολλαπλασιάζουμε με 10, ώστε να τη φέρουμε στο εύρος 0 έως 10 στο οποίο θέλουμε να τυπωθεί ο βαθμός.

Μετά αναθέτουμε στη μεταβλητή ret την τιμή 0. Η ret είναι η μεταβλητή που κρατά τον ακέραιο που θα επιστρέψει η main (στο λειτουργικό σύστημα που την κάλεσε). Κατά παράδοση, επιστρέφει 0 αν όλα πάνε καλά και δεν έχει τίποτε άλλο πιο χρήσιμο να επιστρέψει. Αναθέτουμε λοιπόν στην ret την τιμή 0 αρχικά με την πρόθεση να αλλάξουμε από δω και πέρα την τιμή της μόνο αν εμφανιστεί κάποιο πρόβλημα. Αυτό ακριβώς γίνεται στο default κομμάτι του switch statement αμέσως παρακάτω.

Η main, τέλος, επιστρέφει το ret.

2.4.11 Η εντολή switch

Η τελευταία νέα εντολή της C που εισάγει το πρόγραμμα cond.c είναι η εντολή switch στο τέλος της main, της οποίας το συντακτικό είναι
switch ( expression ) {
case value1:
statement1; ... statementN; break;
...
case valueK:
statement1; ... statementM; break;
default:
statement1; ... statementL;
}
Με τη switch υπολογίζεται η έκφραση expression και αν η τιμή της είναι ίση με value1 τότε εκτελούνται οι εντολές που βρίσκονται μετά το case value1: και μέχρι το επόμενο break;, αν η τιμή είναι value2 εκτελούνται οι εντολές που βρίσκονται μετά το case value2:, κλπ. Αν η τιμή δεν ισούται με καμιά από τις τιμές που έχουν απαριθμηθεί στα διάφορα case τότε εκτελούνται οι εντολές που βρίσκονται μετά το default, και μέχρι το τέλος του switch statement.

Η έκφραση 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.

2.5 Το πρόγραμμα primes.c

Το ακόλουθο πρόγραμμα primes.c διαβάζει ένα θετικό ακέραιο N και τυπώνει όλους τους πρώτους αριθμούς από το 2 έως το N. Ένας θετικός ακέραιος αριθμός k λέγεται πρώτος αν δεν υπάρχει κανένας φυσικός αριθμός που να τον διαιρεί ακριβώς εκτός από τον 1 και τον ίδιο τον k. Για παράδειγμα, οι αριθμοί 2, 3, 5, 7, 11, 13, 17, 19, 23 είναι όλοι πρώτοι αριθμοί.

Να πούμε εδώ ότι ο αλγόριθμος που χρησιμοποιούμε για να βρούμε όλους τους πρώτους μέχρι το N είναι πολύ απλός: απλώς παίρνουμε κάθε i = 2, 3,...N και εξετάζουμε αν αυτό είναι πρώτος. Ελέγχουμε δηλ. αν υπάρχει φυσικός αριθμός μεγαλύτερος ή ίσος του 2 και μικρότερος από i ο οποίος να διαιρεί το i.

Το μόνο έξυπνο που έχει το πρόγραμμα που ακολουθεί είναι ότι δε χρειάζεται να δοκιμάσει κανείς όλους τους φυσικούς τους μικρότερους από i για να δει αν το i είναι πρώτος αριθμός, αλλά μόνο αυτούς τους φυσικούς που είναι $ \le$$ \sqrt{i}$. Ο λόγος είναι απλός. Αν ο i είναι σύνθετος, δηλ. γράφεται ως i = a . b, για δυό φυσικούς a και b, τότε δεν μπορεί και ο a και ο b να είναι μεγαλύτεροι του $ \sqrt{i}$, γιατί τότε το γινόμενό τους θα ήταν αυστηρά μεγαλύτερο του 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 }

Το αρχείο primes.c

Δείχνουμε παρακάτω ένα τυπικό τρέξιμο του προγράμματος:

% 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

2.5.1 Τύποι συναρτήσεων και μετατροπές δεδομένων (casts)

Το παραπάνω πρόγραμμα το περνάμε από compiler δίνοντας την εντολή
% 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.

2.5.2 Macros με το #define του cpp

Αμέσως μετά τα δύο #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 θέλουν λοιπόν εξαιρετική προσοχή στη χρήση τους, αλλά αν είμαστε προσεκτικοί μπορούν πολλές φορές να μας λύσουν τα χέρια.

2.5.3 for loop και 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)
οι οποίες λειτουργούν ως εξής: Η σημαντικότερη ίσως διαφορά των δύο αυτών τρόπων για να υλοποιήσει κανείς μια ανακύκλωση είναι ότι στο do .. while ( .. ) loop ο ελεγχόμενος κώδικας από το το do θα εκτελεστεί τουλάχιστον μια φορά, ενώ στο while και στο for loop κάτι τέτοιο δεν ισχύει.

Άσκηση 1   Γράψτε τη ρουτίνα is_prime του προγράμματος primes.c χρησιμοποιώντας (α) ένα while loop, και (β) ένα do 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.

  1. Απαρίθμηση των ακεραίων από 0 έως 9
    int	i=0;
    for(; i<10; ) printf("%d\n", i++);
    
    Προσέξτε εδώ ότι μέσα στο for η πρώτη και η τρίτη έκφραση είναι κενές. Το initialization του i γίνεται στη δήλωσή του και η αύξηση του i κατά 1 γίνεται μέσα στο printf (το i++ επιστρέφει την τιμή του i και το αυξάνει μετά κατά 1).
  2. Loop που δεν επιστρέφει ποτέ (infinite loop):
    for(;1;) {
     ...
    }
    
  3. Υπολογισμός των ακεραίων 1, 1+2, 1+2+3, ..., 1+2+3+..+10:
    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).

  4. Ας υποθέσουμε ότι έχουμε μια συνάρτηση 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.

2.5.4 Πίνακες (μονοδιάστατοι)

Στη 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, που είναι σωστό).

2.5.5 Πως περνιούνται τα ορίσματα σε συναρτήσεις. Διευθύνσεις μνήμης. Pointers.

Στη C όλα τα ορίσματα περνιούνται, όπως λέμε, by value, δηλ. σε μια περίπτωση όπως η παρακάτω
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.

2.5.6 Τοποθέτηση πινάκων στη μνήμη. Αριθμητική σε pointers.

Η μόνη περίπτωση που ο κανόνας της C, ότι όλα περνιούνται by value, παραβιάζεται, είναι αυτή των ορισμάτων που είναι πίνακες. Η παρακάτω ρουτίνα παίρνει δυο ορίσματα που το καθένα είναι πίνακας από 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 είναι το

23120 + 8 . 4 = 23152

Το πρώτο byte του στοιχείου 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]. Αυτό είναι γενικό:

a[i] και *(a+i)
είναι το ίδιο πράγμα, όποιος και να είναι ο τύπος του pointer ή πίνακα a και για κάθε ακέραιο i.

2.5.7 Strings (συμβολοσειρές)

Στη C όταν θέλουμε να κρατήσουμε σε μια μεταβλητή ένα τυχόν κείμενο, π.χ. το κείμενο "Life is good." πρέπει να έχουμε ορίσει μια μεταβλητή τύπου πίνακα από chars. Οι χαρακτήρες του κειμένου αποθηκεύονται με την αντίστοιχη σειρά τους στον πίνακα. Στον κώδικα
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.



Υποσημειώσεις

... BASIC1.1
Μπορεί κανείς να πειραματιστεί με ένα BASIC interpreter, χωρίς να εγκαταστήσει κάποιον στον υπολογιστή του. Υπάρχουν BASIC interpreters (δηλ. προγράμματα που διαβάζουν ένα πρόγραμμα BASIC και το εκτελούν) στο Internet, για παράδειγμα στο http://professionals.com/ cmcmanis//java/javaworld/examples/BASIC.html
... modem2.1
modem είναι η συσκευή που χρησιμοποιείται για να συνδεθούν δύο υπολογιστές μεταξύ τους χρησιμοποιώντας ως μέσο επικοινωνίας μια τηλεφωνική γραμμή
... κατάληξη2.2
Είναι ο κανόνας, σχεδόν ανεξάρτητα από λειτουργικό σύστημα, να χρησιμοποιείται η κατάληξη του ονόματος ενός αρχείου, δηλ. το κομμάτι του ονόματος μετά την τελευταία τελεία (αν υπάρχει), για να υποδηλώσει το είδους των περιεχομένων του αρχείου. Έτσι, π.χ., στα MS Windows, κατάληξη .doc υποδηλώνει ένα αρχείο που έχει φτιαχτεί από το MS Word πρόγραμμα, κατάληξη .exe υποδηλώνει ένα εκτελέσιμο πρόγραμμα, στο Unix κατάληξη .f δηλώνει ένα πρόγραμμα Fortran, κοκ. Είναι σημαντικό εδώ να πούμε και να τονίσουμε ότι το ράσο δεν κάνει τον παπά. Δεν μπορεί κανείς, για παράδειγμα, μετονομάζοντας ένα άσχετο αρχείο σε κατάληξη .exe να το καταστήσει εκτελέσιμο. Αυτό φυσικά εξαρτάται από το περιεχόμενο του αρχείου.
... εντολή2.3
Η εντολή cat του Unix τυπώνει τα περιεχόμενα ενός ή περισσοτέρων αρχείων στην οθόνη.

Καμία σχέση με ... γάτες. Η ονοματολογία προγραμμάτων στο Unix είναι λίγο περίεργη μερικές φορές, πέρα από το ότι είναι και κρυπτική. Σαν παράδειγμα αναφέρω το πρόγραμμα biff, το οποίο ειδοποιεί τον χρήστη κάνοντας beep και τυπώνοντας ένα μικρό μήνυμα στην οθόνη οποτεδήποτε φτάσει ένα νέο μήνυμα e-mail. Η ιστορία είναι ότι ο προγραμματιστής που έφτιαξε αυτό το πρόγραμμα, ο οποίος ζούσε στο Berkeley, CA, είχε ένα σκύλο με το ίδιο όνομα ο οποίος έιχε τη συνήθεια να γαυγίζει οποτεδήποτε ερχόταν ο ταχυδρόμος. Εξ ού το όνομα.

... εντολή2.4
Ανάλογα σε τι σύστημα δουλεύετε μπορεί να χρειαστεί η εντολή αυτή να είναι λίγο διαφορετική. Τώρα πλέον όλα σχεδόν τα Unix συστήματα είναι εφοδιασμένα με τον GNU C Compiler (gcc) (ο οποίος είναι ένας πάρα πολύ καλός, δωρεάν C compiler), αλλά αν δεν υπάρχει η εντολή gcc (αν δηλ. πάρετε μήνυμα command not found ή κάτι παρόμοιο) δοκιμάστε να γράψετε cc αντί για gcc. Σε συστήματα MS Windows αντικαταστείστε τον gcc με το όνομα του compiler που χρησιμοποιείτε (υπάρχει και ο gcc για MS Windows).
...linker2.5
Ο linker είναι ένα πρόγραμμα που δε μεταφράζει από C ή κάποια άλλη γλώσσα υψηλού επιπέδου σε γλώσσα μηχανής, αλλά παίρνει ένα σύνολο από ήδη μεταφρασμένες υπορουτίνες και τις διασυνδέει: αν υπάρχει μια ρουτίνα που καλέι π.χ. την printf θα φροντίσει ο linker να υπάρχει στο αντίστοιχο σημείο της ρουτίνας αυτής μια μεταπήδηση στη θέση μνήμης όπου αρχίζει ο κώδικας της printf. Αυτή είναι μια δουλειά που δε μπορεί να γίνει από τον compiler ο οποίος δεν έχει ιδέα του ποιές είναι όλες οι ρουτίνες που θα χρησιμοποιηθούν σε ένα πρόγραμμα.

Σε πρώτη προσέγγιση δε βλάπτει ιδιαίτερα όμως το να αγνοεί κανείς το διαχωρισμό αυτό ανάμεσα σε compiler και linker.

... πρόγραμμα2.6
Και αυτό δεν είναι τελείως ακριβές, μια και με τα λεγόμενα shared libraries, που χρησιμοποιουνται στα σύγχρονα λειτουργικά συστήματα, πολλές κοινόχρηστες υπορουτίνες δεν είναι πλέον μέρος του εκτελέσιμου προγράμματος, αλλά αναλαμβάνει το λειτουργικό σύστημα να τις παρέχει στα εκτελέσιμα προγράμματα που τις ζητούν, at run-time.

Έτσι εξοικονομείται ο χώρος που θα έπιανε, π.χ. η 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.

... bytes2.7
Το byte είναι μια ομάδα από 8 δυαδικά ψηφία, τα bits. Είναι μάλλον η μικρότερη ομαδοποίηση δυαδικών ψηφίων σε υπολογιστές σήμερα, δηλ. όλα τα μεγέθη εκφράζονται σε ακέραιο αριθμό bytes. Αυτό φυσικά δεν υπαγορεύεται από κάποιο φυσικό νόμο αλλά είναι απλώς μια σύμβαση που έχει επικρατήσει. Τι μπορεί να παραστήσει ένα byte; Κατ' αρχήν τίποτα. Εννοώ με αυτό ότι το πώς ερμηνεύει κανείς μια ομάδα από bytes, είτε λίγα είτε πολλά, είναι αποκλειστικά και μόνο θέμα δικό του, δηλ. θέμα της συγκεκριμένης εφαρμογής. Αυτό που είναι ξεκάθαρο είναι πως μια ομάδα από 8 bits (ένα byte δηλαδή) μπορεί να πάρει 28 = 256 διαφορετικές τιμές. Αυτές μπορούν για παράδειγμα να ερμηνευτούν ως οι ακέραιοι από 0 έως 255 (αυτός είναι ο τύπος unsigned char της C), ως οι ακέραιοι από -128 έως 127 (ο τύπος char), ή ως οτιδήποτε άλλο η εφαρμογή θελήσει.
... μνήμης2.8
Η μνήμη του υπολογιστή πρέπει να θεωρείται μια γραμμική διάταξη από bytes. Αν, για παράδειγμα, ο υπολογιστής σας έχει μνήμη (RAM) 8 megabytes (δηλ. 8.220 = 8388608 bytes) τότε πρέπει να τα θεωρούμε αυτά ως τοποθετημένα στις θέσεις μνήμης από 0 έως 8388607. Μερικά από αυτά τα bytes χρησιμοποιούνται από διάφορα προγράμματα για κώδικα (δηλ. αποθήκευση εντολών) και για δεδομένα και μερικά από το δικό μας πρόγραμμα, ομοίως για την αποθήκευσή του (δηλ. την αποθήκευση των εντολών που απαρτίζουν το πρόγραμμά μας) και την αποθήκευση των δεδομένων του, δηλ. των μεταβλητών του. Κάθε μεταβλητή καταλαμβάνει ένα συνεχές κομμάτι στη μνήμη του υπολογιστή. Για παράδειγμα η μεταβλητή a στο πρόγραμμα mean.c μπορεί να καταλαμβάνει τις θέσεις μνήμης 8152320 έως και την 8152323 ενώ η μεταβλητή mean μπορεί να καταλαμβάνει τα επόμενα 8 bytes, δηλ. τα 8152324 έως και 8152331.
... 0.2.9
Την πράξη a%b να την εμπιστεύεστε μόνο όταν και οι δύο operands a και b είναι θετικοί. Για παράδειγμα, η C υπολογίζει το υπόλοιπο της διαίρεσης του -5 δια του 3 σε -2, πράγμα που διαφωνεί με τον παντού αποδεκτό μαθηματικό ορισμό που θέλει το υπόλοιπο της διαίρεσης του a δια του b να είναι ένας φυσικός αριθμός από 0 έως b-1. (Αυτή η παρατήρηση ισχύει λίγο πολύ σε όλες τις γλώσσες προγραμματισμού.) Αν κάποιος θέλει να έχει στη διάθεσή του τη σωστή πράξη υπολοίπου, ανεξάρτητα από τα πρόσημα, τότε πρέπει να την υλοποιήσει μόνος του, χρησιμοποιώντας φυσικά την υπάρχουσα πράξη %. Αργότερα θα δούμε πώς να το κάνουμε αυτό.
... βιβλιοθήκη2.10
Βιβλιοθήκη είναι απλώς μια συλλογή από υποπρογράμματα, με κάποιο κοινό θέμα, π.χ. η standard library της C, η math library της C, κλπ, που έχουν συνήθως ήδη μεταφραστεί από τον compiler, και βρίσκονται μαζεμένες, κατά κανόνα σε λίγα αρχεία, συνήθως απλώς ένα αρχείο.

Ο όρος χρησιμοποιείται και γενικότερα φυσικά, για οποιαδήποτε συλλογή, σε οποιαδήποτε μορφή (precompiled ή σε source code, σε ένα αρχείο ή σε εκατοντάδες αρχεία) υποπρογραμμάτων.

... απρόβλεπτη2.11
Αυτό σημαίνει ότι το αν θα τρέξει σωστά αυτό το πρόγραμμα εξαρτάται πάρα πολύ από τον compiler που χρησιμοποιείται. Στη συγκεκριμένη έκδοση του gcc που χρησιμοποιώ τώρα ο compiler έδωσε τα εξής μηνύματα κατά τη μετάφραση του προγράμματος αυτού:
tmp.c: In function `main':
tmp.c:4: warning: type mismatch in implicit declaration for built-in function `sqrt'
Δηλαδή ο compiler, με κάποιο τρόπο είχε πρότερη γνώση του τι τύπου είναι η sqrt, και φρόντισε να κάνει μόνος του τις απαραίτητες μετατροπές τύπων ώστε να τρέξει σωστά το πρόγραμμα. Το πρόγραμμα αυτό εκτελέστηκε έτσι χωρίς λάθη.

Αυτό είναι μάλλον απίθανο να μπορέσει να το καταφέρει ο compiler αν στη θέση της sqrt βρισκόταν μια άλλη συνάρτηση του ίδιου τύπου, που δεν ήταν εκ των προτέρων γνωστή στον compiler, για παράδειγμα θα μπορούσε να είναι μια ρουτίνα που είχαμε γράψει εμείς σε κάποιο άλλο .c αρχείο ή ρουτίνα κάποιας βιβλιοθήκης που είχαμε αγοράσει ή κατεβάσει από το δίκτυο, κλπ.

... γραμμής2.12
Αυτό ίσως να μην είναι εφικτό με ορισμένους editors οι οποίοι προσπαθούν να είναι εξυπνότεροι απ' ότι τους χρειαζόμαστε και παίρνουν την ελευθερία να διαμορφώνουν το κείμενο χωρίς να μας ρωτούν.

next_inactive up previous
Mihalis Kolountzakis 2001-03-21