next up previous contents
Next: 2.5 Το πρόγραμμα primes.c Up: 2. Προγραμματισμός στη γλώσσα Previous: 2.3 Το πρόγραμμα mean.c   Contents

Subsections

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.


next up previous contents
Next: 2.5 Το πρόγραμμα primes.c Up: 2. Προγραμματισμός στη γλώσσα Previous: 2.3 Το πρόγραμμα mean.c   Contents
Mihalis Kolountzakis 2001-10-21