next_inactive up previous contents
Up: 2. Προγραμματισμός στη γλώσσα Previous: 2.4 Το πρόγραμμα cond.c   Contents

Subsections

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.



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

... βιβλιοθήκη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 contents
Up: 2. Προγραμματισμός στη γλώσσα Previous: 2.4 Το πρόγραμμα cond.c   Contents
Mihalis Kolountzakis 2001-10-21