Professional Documents
Culture Documents
Γεωργιάδης Κ. Χρήστος
Φθινόπωρο 1998
Εισαγωγή
Γιατί αρέ σει η C
Η γλώσσα C είναι μια ταχύτατη γλώσσα. Δηλαδή τα
προγράμματα που φτιάχνουμε μ’ αυτήν εκτελούνται από το μηχάνημά
μας αρκετές φορές γρηγορότερα από οποιαδήποτε άλλη καθιερωμένη
γλώσσα (όπως Pascal, Cobol κλπ). Η μόνη που την ξεπερνάει λίγο σε
ταχύτητα είναι η γλώσσα Assembly ή Συμβολική γλώσσα. Ακόμα όμως
και οι προγραμματιστές Συστημάτων (δηλαδή αυτοί που
κατασκευάζουν τα Λειτουργικά Συστήματα, τους συντάκτες (editors)
και γενικά τις πολύπλοκες κατασκευές λογισμικού), προτιμούν πια να
προγραμματίζουν σε C. Και αυτό γιατί η C υπερτερεί σε δυο σημαντικά
σημεία :
α) είναι δομημένη
β) είναι φορητή
Το ότι είναι δομημένη σημαίνει ότι μπορεί να αποκόπτει και να
κρύβει από το υπόλοιπο πρόγραμμα όλες τις πληροφορίες και τις
εντολές που είναι απαραίτητες για την εκτέλεση μιας συγκεκριμένης
εργασίας. Έτσι μπορούμε και γράφουμε κομμάτια προγράμματος με
τέτοιο τρόπο που τα γεγονότα που συμβαίνουν στο εσωτερικό τους να
μην προκαλούν παρενέργειες σε άλλα τμήματα του προγράμματος. Τα
τμήματα αυτά του προγράμματος τα λέμε υποπρογράμματα ή ρουτίνες
και στη C συγκεκριμένα αποκαλούνται συναρτήσεις ( functions ).
Το ότι είναι φορητή σημαίνει ότι τα προγράμματα σε C που
έχουν γραφτεί για ένα σύστημα, μπορούν να τρέξουν σε άλλο σύστημα
με σχεδόν καμιά τροποποίηση. Αντίθετα, ρουτίνες σε γλώσσα
1
Αssembly δεν μπορούν να μεταφερθούν εύκολα σε μηχανήματα με
διαφορετικούς επεξεργαστές.
Θεωρείται λοιπόν η C γλώσσα μέσου επιπέδου γιατί συνδυάζει
χαρακτηριστικά των γλωσσών υψηλού επιπέδου όπως η ευκολία στην
εκμάθηση (φιλικότητα), η δόμηση και η φορητότητα, με
χαρακτηριστικά της γλώσσας χαμηλού επιπέδου Assembly. Και δεν
είναι μόνο η ταχύτητα της. Είναι επιπλέον η δυνατότητα να
διαχειριζόμαστε bits, bytes και διευθύνσεις μνήμης που αποτελούν τα
βασικά στοιχεία με τα οποία λειτουργεί το υλικό (hardware) του
υπολογιστή μας.
Αξίζει ακόμη να σημειωθεί μια ευελιξία που διακρίνει την
γλώσσα C. Τι εννοούμε με αυτό; Ότι για λόγους καλής σχεδίασης
προγραμμάτων και εύκολης ανίχνευσης λαθών προσφέρει όπως και
άλλες γλώσσες (π.χ. Pascal) διάφορους τύπους δεδομένων (data
types). Δεν υπάρχει όμως αυστηρότητα. Δηλαδή ο προγραμματιστής
της C, ανάλογα με τις ανάγκες του μπορεί σχεδόν ελεύθερα να αναμίξει
τους διάφορους τύπους. Γενικότερα μπορεί να απενεργοποιήσει
διάφορους ελέγχους που κανονικά θα έκανε η γλώσσα C, για να
κερδίσει σε ταχύτητα. Φυσικά τέτοια βήματα πρέπει να γίνονται
προσεκτικά για να μην δημιουργηθούν σοβαρά προβλήματα
λειτουργίας στο μηχάνημα.
2
Unix στην αρχή με Συμβολική γλώσσα. Η έλλειψη φορητότητας όμως,
που αναφέραμε προηγουμένως, της γλώσσας Assembly τους οδήγησε
στην ανάπτυξη της γλώσσας C το 1973. Έτσι το Λειτουργικό Σύστημα
Unix ξαναγράφηκε σε γλώσσα C πλέον. Αυτό το γεγονός έδωσε νέα
πνοή και στο Unix αλλά και στη C.
Εύκολα καταλαβαίνουμε ότι αρχικά η C συνδέθηκε αποκλειστικά
με το Unix και επικράτησε στους χώρους των multi-user συστημάτων.
Έτσι από το 1978 και για πολλά χρόνια ο πρότυπος ορισμός της ήταν
αυτός που δόθηκε στο βιβλίο “The C Programming Language” των
Kernighan και Richie. Και φυσικά ήταν η C που έτρεχε στην έκδοση V
του Unix. Στη δεκαετία όμως του ‘80 έχουμε τρία σημαντικά γεγονότα
για την C :
i) Ξεκίνησε από το Αmerican National Standards Institute η
ανάπτυξη ενός προτύπου C για όλα τα περιβάλλοντα (Unix, Apple, Dos
κλπ). Η ΑΝSI-C περιλαμβάνει όλα τα χαρακτηριστικά της Unix-C μαζί
με κάποια νέα. Έτσι η C περνάει πλέον και στους χώρους των
προσωπικών υπολογιστών, οι οποίοι με την εξέλιξη της τεχνολογίας
έχουν πια την απαραίτητη ισχύ για να υποστηρίξουν την γλώσσα C.
ii) Στα εργαστήρια Bell πάλι, ο Stroustrup δημιουργεί μια
επέκταση της γλώσσας C, που ονομάζεται C++. H γλώσσα αυτή είναι
σήμερα η βασικότερη γλώσσα προγραμματισμού με αντικείμενα. Ο
αντικειμενοστραφής προγραμματισμός (object - oriented programming)
θεωρείται στις μέρες μας η τελευταία λέξη στην ανάπτυξη
προγραμμάτων. Σε πάρα πολλές περιπτώσεις, κυρίως σε γραφικά
περιβάλλοντα με εικονίδια ενεργειών τύπου Windows και όχι μόνο, η
C++ προσφέρει άμεσους τρόπους για την κατανόηση και επίλυση
προβλημάτων. Η καλή γνώση λοιπόν της C θα βοηθήσει αργότερα
όλους όσους θελήσουν να περάσουν σε μεθόδους αντικειμένων.
iii) H Εταιρεία Borland ξεκίνησε την ανάπτυξη της Turbo-C που
ακολουθεί πλήρως την ANSI-C και προσφέρει φοβερή ταχύτητα στη
3
μεταγλώττιση, δηλαδή στο πέρασμα που ο Η/Υ κάνει από τις εντολές
κώδικα C σε εντολές που εκτελεί το μηχάνημα. Στη συνέχεια
κυκλοφορεί το 1990 την Turbo C++, υποστηρίζοντας τους πρότυπους
ορισμούς ΑΝSI-C και ΑΝSI-C++. Οι εκδόσεις ΤURBO-C και ΤURBO-
C++ είναι οι πιο δημοφιλείς σήμερα στους χρήστες DOS. Προσφέρουν
ολοκληρωμένο περιβάλλον για να γράφουμε εντολές κώδικα, να
ελέγχουμε λάθη και να τρέχουμε τα προγράμματά μας. Είναι οι
καθιερωμένες εκδόσεις C/C++ για τους προσωπικούς υπολογιστές σε
επίπεδο DOS και μ’ αυτές θα ασχοληθούμε. Ας τονιστεί εδώ ότι η
εταιρεία Βorland έχει ήδη κυκλοφορήσει εκδόσεις Borland-C++ για το
περιβάλλον Microsoft Windows. Είναι επίσης πασίγνωστες και οι
εκδόσεις Turbo/Borland Pascal για τη γλώσσα αυτή
4
περιβάλλον (Ιntegrated Development Environment) της Turbo-C. Είναι
σχεδόν ίδιο. Δηλαδή έχουμε να κάνουμε με ένα πλήρες πακέτο που
περιλαμβάνει :
i) διορθωτή κειμένου (text editor)
ii) μεταγλωττιστή (compiler)
iii) συνδέτη (linker)
iv) βιβλιοθήκη συναρτήσεων (function library)
v) βοηθητικά προγράμματα (utilities) πχ. ανιχνευτής
λαθών (debugger)
Ξεκινάμε την διαδικασία καλώντας το αρχείο TC ή ΒC.
Παρουσιάζεται τότε η κεντρική οθόνη του περιβάλλοντος. Αργότερα θα
εξερευνήσουμε εντελώς τις διάφορες επιλογές που προσφέρονται. Για
αρχή όμως χρειάζονται πολύ λίγες από αυτές. Από το μενού FILE όταν
θελήσουμε να γράψουμε ένα νέο πρόγραμμα C από την αρχή,
επιλέγουμε ΝΕW. Αν όμως θέλουμε να δούμε ή να τροποποιήσουμε ένα
πρόγραμμα που ήδη έχουμε αποθηκεύσει σε περιφερειακή μνήμη
(σκληρό δίσκο ή δισκέτα), τότε επιλέγουμε OPEN (ή LOAD σε
ορισμένες εκδόσεις).
H επιλογή NEW ανοίγει ένα παράθυρο με όνομα
“ΝΟΝΑΜΕ00.C“. Έχει δηλαδή ενεργοποιήσει τον συντάκτη κειμένου
και έτσι μπορούμε να αρχίσουμε να γράφουμε τις εντολές του
προγράμματός μας. Όταν θελήσουμε να σώσουμε αυτά που έχουμε
γράψει, πάλι από το μενού FILE επιλέγουμε SAVE ή “SAVE AS...”.
Με το SAVE το πρόγραμμά μας αποθηκεύεται με τον τίτλο του
παραθύρου “NONAME00.C”, ενώ με το “SAVE AS...” μπορούμε να
διαλέξουμε εμείς το όνομά του αρχείου στην περιφερειακή μνήμη.
Βάζουμε όμως πάντα ως προέκταση το “.C” για τα προγράμματά μας C,
ενώ για τα προγράμματα C++ βάζουμε “.CPP”.
H επιλογή OPEN μας οδηγεί σε οποιοδήποτε δίσκο ή
υποκατάλογο δίσκου, για να επιλέξουμε το όνομα του αποθηκευμένου
5
αρχείου που θέλουμε να φορτώσουμε στη κεντρική μνήμη (RAM). O
editor και πάλι αναλαμβάνει ρόλο και εμφανίζεται παράθυρο που έχει
τίτλο το πλήρες όνομα του αρχείου που επιλέξαμε και περιεχόμενα τις
εντολές του προγράμματός μας. Αξίζει να τονιστεί ότι μπορούμε
ταυτόχρονα να έχουμε ανοιχτά αρκετά παράθυρα με τα ίδια ή
διαφορετικά αρχεία. Έτσι μπορούμε πολύ εύκολα να μετακινούμε και
να αντιγράφουμε κομμάτια προγραμμάτων από αρχείο σε αρχείο.
Ακολουθεί η διαδικασία μετατροπής του προγράμματος της
οθόνης μας σε εκτελέσιμο αρχείο, δηλαδή σε μορφή που το υλικό
καταλαβαίνει. Η διαδικασία αυτή είναι καλύτερα να γίνεται σε δύο
βήματα όταν ακόμη είμαστε στη φάση δοκιμών ενός προγράμματος.
Ζητάμε πρώτα από το μενού COMPILE την επιλογή “COMPILE TO
OBJ”. O μεταγλωττιστής τότε ελέγχει το συντακτικό των εντολών του
προγράμματος. Αν βρει λάθος τότε φωτίζεται η γραμμή του
προγράμματος με το λάθος και ανοίγει στο κάτω μέρος της οθόνης ένα
παράθυρο με τίτλο “MESSAGE” (μήνυμα), που εξηγεί με λίγες λέξεις
ενδεχομένως το πρόβλημα. Αυτές είναι και οι ευκολίες των
ολοκληρωμένων περιβαλλόντων ανάπτυξης (IDE). Γιατί έχει ήδη
ενεργοποιηθεί ο editor και μπορούμε να διορθώσουμε το λάθος μας και
να ξαναζητήσουμε μετά COMPILE. Αυτή η διαδικασία ελέγχου του
προγράμματός μας από τον compiler και οι διορθώσεις με τη βοήθεια
του editor συνεχίζονται μέχρι να εμφανιστεί παράθυρο με την ένδειξη
SUCCESS : Press any key to continue
Η ΕΠΙΤΥΧΙΑ είναι ότι στο δίσκο δημιουργήθηκε ένα αρχείο με
το όνομα του προγράμματός μας και προέκταση “.OBJ”. Μπορούμε πια
να ζητήσουμε από το μενού RUN την πρώτη επιλογή RUN και έτσι να
εκτελεστεί το πρόγραμμά μας. Τα αποτελέσματα ορισμένες φορές δεν
προλαβαίνουμε να τα δούμε γιατί γρήγορα το περιβάλλον μας
επιστρέφει στον editor με τον κώδικα για ενδεχόμενες διορθώσεις. Από
το μενού WINDOW όμως μπορούμε να επιλέξουμε την επιλογή USER
6
SCREEN (οθόνη χρήστη) για να δούμε σε πλήρη οθόνη τα
αποτελέσματα. Και η επιλογή WINDOW/ΟUTPUT εμφανίζει τα
αποτελέσματα της εκτέλεσης του προγράμματος αλλά σε μικρότερες
διαστάσεις παραθύρου.
Μπορούμε και απευθείας να ζητήσουμε την εκτέλεση ενός
προγράμματος C που έχουμε στην οθόνη μας, ζητώντας από το μενού
RUN την επιλογή RUN. Θα αναλάβει αρχικά ο compiler και αν δεν
βρει λάθη όπως αναφέραμε θα εκτελέσει το πρόγραμμά μας. Αυτό που
πραγματικά συμβαίνει, χωρίς να είναι εμφανές, είναι η ενεργοποίηση
του συνδέτη (linker) που συνδέει το αρχείο (*.obj) που δημιούργησε ο
compiler με έτοιμα μεταγλωττισμένα κομμάτια προγραμμάτων που
περιέχει η βιβλιοθήκη συναρτήσεων (function library). Ο linker
δημιουργεί στο δίσκο αρχείο με όνομα το όνομα του προγράμματός μας
και προέκταση “.ΕΧΕ”. Είναι το λεγόμενο εκτελέσιμο αρχείο που
μπορεί να σταθεί μόνο του. Δηλαδή και χωρίς τη βοήθεια πια του
περιβάλλοντος, έχοντας σε κάποια δισκέτα αυτό μόνο το αρχείο ΕΧΕ,
μπορούμε από τη γραμμή του DOS να εκτελέσουμε το πρόγραμμά μας.
Το αρχείο αυτό είναι σε μέγεθος πολύ μεγαλύτερο από τους
“προγόνους” του ΟBJ και C, γιατί έχει ενσωματώσει και ρουτίνες από
τη βιβλιοθήκη συναρτήσεων.
7
περιέχονται σε μια βιβλιοθήκη που συνοδεύει τον compiler και αρκεί
απλά να τις καλέσει κανείς σωστά. Η βιβλιοθήκη των functions στη
πραγματικότητα δεν αποτελεί μέρος της γλώσσας C. O
προγραμματιστής χρησιμοποιεί και τις συναρτήσεις αυτές αλλά και
δημιουργεί δικές του όπως αργότερα θα δούμε. Καθώς κάποιος
προγραμματιστής φτιάχνει τα δικά του προγράμματα δημιουργεί και
functions κατάλληλες για γενική χρήση οι οποίες μπορούν να
προστεθούν στην προσωπική του βιβλιοθήκη. Έτσι μπορούν και
δημιουργούνται βιβλιοθήκες εξειδικευμένης χρήσης.
8
το ένα από το άλλο. Οι τύποι και τα ονόματα των μεταβλητών πρέπει
να δηλωθούν πριν χρησιμοποιηθούν, πράγμα που δεν συμβαίνει στην
BASIC η οποία μπορεί να δεχτεί σαν μεταβλητές ακόμη και τα λάθη
μας στη πληκτρολόγηση.
Τα άγκιστρα { }, περικλείουν ομάδες εντολών όπως ακριβώς στη
PASCAL κάνουν τα ΒΕGIN και END. Κάθε εντολή τελειώνει με το
σύμβολο ; (ελληνικό ερωτηματικό). Προσοχή στα κεφαλαία και στα
μικρά γράμματα. Η C κάνει διάκριση ανάμεσά τους και έτσι yr, YR,
Yr, και yr είναι 4 εντελώς διαφορετικά πράγματα. Συνηθισμένοι στο
κόσμο του DOS όπου κεφαλαία - μικρά είναι το ίδιο πράγμα, πρέπει να
συνηθίσουμε την διαφορά αυτή που κουβαλάει η γλώσσα C από τα
χρόνια του εναγκαλισμού της με το UNIX.
Ας δούμε με λίγα σχόλια την κάθε εντολή του προγράμματός μας
:
#include <stdio.h> : ζητάμε από τον compiler να συμπεριλάβει το
αρχείο stdio.h στη μεταγλώττιση.
/* Ηello ... */ : επιβάλλουμε στον compiler να αγνοήσει όλα όσα
βρίσκονται μετά το /* και πριν το */. Έτσι δηλαδή περνάμε
κάποια χρήσιμα σχόλια στο πρόγραμμά μας.
void main() : καθορίζουμε τον τύπο (void) και το όνομα (main)
μιας συνάρτησης. Ότι είναι όνομα συνάρτησης φαίνεται από τις
παρενθέσεις δίπλα στο όνομα. Void δηλώνονται οι συναρτήσεις που
δεν έχουν καμιά τιμή επιστροφής και απλά κάνουν κάτι. Κάθε
πρόγραμμα μπορεί να αποτελείται από αρκετές functions. Για να είναι
όμως εκτελέσιμο πρέπει να έχει μία με το όνομα main, γιατί ο compiler
ξεκινά την εκτέλεση από την main όπου και αν είναι τοποθετημένη
μέσα στο πρόγραμμα.
{ , } : δηλώνουν την αρχή και το τέλος της συνάρτησης
main().
9
int yr; : δηλώνει μια μεταβλητή yr και ο compiler δεσμεύει
μνήμη ικανή να αποθηκεύσει έναν ακέραιο.
printf(“ χαρακτήρες”, μεταβλητή) : εμφανίζει όσους χαρακτήρες
βρίσκονται μέσα στα διπλά εισαγωγικά εκτός από ορισμένα σύμβολα
όπως ‘\n’ που πληροφορεί για αλλαγή γραμμής ή ‘%d’ που μας δείχνει
που θα εμφανιστεί η τιμή της μεταβλητής που ακολουθεί μετά το
κόμμα.
scanf(“%χαρακτήρας”,&μεταβλητή) : διαβάζει από το πληκτρολόγιο,
δηλαδή από το χρήστη την τιμή της μεταβλητής.
10
Βασικά στοιχεία : Μεταβλητές - Τύποι - Σταθερές
Λεπτομέρειες ...
Σε κάθε πρόγραμμα συναντάμε οδηγίες (directives), σχόλια,
δηλώσεις σφαιρικών δεδομένων (Global Data Declarations),
συναρτήσεις που περιέχουν δηλώσεις τοπικών δεδομένων (Local Data
Declarations) και εκτελέσιμες εντολές. Για την κατασκευή όλων αυτών
των παραπάνω, χρησιμοποιούνται τα παρακάτω δομικά στοιχεία :
11
1) identifiers πχ. pinakas_mou, onoma
2) keywords πχ. while, for
3) constants πχ. 12000, 0
4) character strings πχ. “ καλημέρα”, “want it”
5) operators πχ. +, *
6) separators πχ. ; , ‘ ‘
12
Προκαθορισμένοι Τύποι - Δηλώσεις Δεδομένων
Έχουμε ήδη αναφέρει ότι τα δεδομένα μας περιγράφονται με
τύπους, μεταβλητές και σταθερές. Προσοχή!!! Οι θέσεις μνήμης
καταλαμβάνονται μόνον από μεταβλητές και σταθερές. Οι τύποι (types)
δεν είναι πραγματικές οντότητες. Είναι απλά πληροφορίες που λένε
στον compiler πώς να χειριστεί την κάθε μεταβλητή. Οι τύποι ορίζουν
κατηγορίες μεταβλητών με διαφορετικές διαστάσεις - εννοούμε πόσος
χώρος δεσμεύεται στη μνήμη - και ιδιότητες. Έτσι το int απλά λέει
στον compiler ότι οι μεταβλητές που ακολουθούν θα έχουν μέγεθος 2
bytes και θα αντιμετωπιστούν σύμφωνα με τους κανόνες που ισχύουν
για τους ακέραιους. Μ’ αυτόν τον τρόπο κάνουμε οικονομική
διαχείριση στη μνήμη και δεσμεύουμε όσο πρέπει ανάλογα με την
περίπτωση.
Τα δεδομένα στην Τurbo-C μπορούν να ανήκουν σε έναν από
τους παρακάτω προκαθορισμένους τύπους (Predefined Types), ή σε
τύπους που προκύπτουν από συνδυασμούς αυτών των τύπων.
Τύπος /Λέξη-Κλειδί Πλάτος σε bytes Εύρος Τιμών
char 1 -128 έως 127
int 2 -32768 έως 32767
float 4 3.4E-38 έως 3.4 Ε+38
double 8 1.7E-308 έως 1.7Ε+308
void 0 χωρίς τιμές
Οι μεταβλητές τύπου char (χαρακτήρας) χρησιμοποιούνται για
να κρατάνε χαρακτήρες ASCII σαν τους Α, b ή C, ή οποιαδήποτε άλλη
ποσότητα ενός byte. Οι μεταβλητές τύπου int (ακέραιος) μπορούν να
περιέχουν ακέραιες ποσότητες που δεν έχουν ανάγκη από κλασματικό
μέρος. Οι μεταβλητές τύπου float (κινητής υποδιαστολής ή
πραγματικός) χρειάζονται όταν διαχειριζόμαστε πολύ μεγάλους
αριθμούς ή όταν υπάρχουν κλασματικά μέρη. Οι μεταβλητές double
13
(πραγματικός διπλής ακρίβειας) στο μόνο που διαφέρουν από τις
float είναι ότι χειρίζονται μικρότερα και μεγαλύτερα νούμερα. Τέλος ο
τύπος void (χωρίς τιμή) χρησιμοποιείται στη βελτίωση του ελέγχου
των τύπων.
Τροποποιητές Τύπων
Όλοι οι τύποι εκτός από τον void, μπορούν να έχουν μπροστά
τους διάφορους τροποποιητές (modifiers), οι οποίοι αλλάζουν τη
σημασία του βασικού τύπου ώστε να ικανοποιεί καλύτερα τις ανάγκες
διάφορων καταστάσεων. Οι τροποποιητές είναι :
ι) short (μικρός) ιιι) signed (προσημασμένος)
ιι) long (μεγάλος) ιv) unsigned (απρόσημος)
Ο τροποποιητής signed δεν μας ενδιαφέρει και πολύ στα
μηχανήματα που τώρα δουλεύουμε γιατί οι εξ’ ορισμού (default)
ρυθμίσεις έχουν τους int και char προσημασμένους, όπως είδαμε και
προηγουμένως από το εύρος των τιμών τους. Όσο για το short για
μεγαλύτερους υπολογιστές προσφέρει μία μικρότερη σε μέγεθος
μνήμης εναλλακτική λύση για ακέραιους.
Οι νέοι τύποι που προκύπτουν λοιπόν είναι :
Unsigned
Η διαφορά ανάμεσα στους προσημασμένους και στους
απρόσημους, δεν βρίσκεται στη δέσμευση της μνήμης. Όπως βλέπετε οι
14
ανάγκες σε μνήμη παραμένουν σε ένα και δύο bytes αντίστοιχα για
τους χαρακτήρες και τους ακέραιους. Βρίσκεται στον τρόπο που ο Η/Υ
ερμηνεύει το υψηλής τάξης bit του ακέραιου. Στους προσημασμένους
χαρακτήρες ή ακέραιους - η εξ’ ορισμού περίπτωση - το bit αυτό
δηλώνει με 0 ότι η μεταβλητή είναι θετική και με 1 ότι είναι αρνητική.
Και μάλιστα η αναπαράσταση των αρνητικών γίνεται με τη μέθοδο του
συμπληρώματος ως προς 2 . Δηλαδή ο αρνητικός προκύπτει από την
αντιστροφή όλων των bits (εκτός από αυτό που λειτουργεί σαν
πρόσημο) με αύξηση μιας μονάδας.
Παράδειγμα : ο αριθμός k
01111111 11111111
έχει το πιο σπουδαίο bit ίσο με 0. ‘Αρα δηλώνει τον αριθμό 32767. Αν
ο k είχε δηλωθεί ως unsigned, τότε πάλι θα δήλωνε τον ίδιο αριθμό.
Στις αρνητικές όμως τιμές είναι που φαίνεται η διαφορά. Δηλαδή αν
π.χ. o αριθμός m ήταν :
11111111 11111111
Τότε κανονικά (εξ’ ορισμού) ο αριθμός θα ήταν ο αρνητικός
0000000 00000000 = 0 + 1 = 1
δηλαδή m = -1. Σε περίπτωση όμως που είχε δηλωθεί ο m ως unsigned
τότε παύει να έχει νόημα προσήμου το σπουδαίο bit και ο m είναι ο
65535.
Οι απρόσημοι ακέραιοι έχουν όπως είναι φυσικό διπλάσιο εύρος
θετικών τιμών. Όμως ο πραγματικός σκοπός τους δεν είναι να
αυξήσουμε το μέγεθος αυτό, αλλά έχει σχέση με την παροχή άμεσων
διευθύνσεων (direct addressing) στη μνήμη μέσω των δεικτών θέσης
(pointers).
long
O τροποποιητής long επιβάλλει μεγαλύτερη δέσμευση σε μνήμη
και προσφέρει μεγαλύτερο εύρος τιμών. Ειδικά στους ακέραιους είναι
15
χρησιμότατος αφού για μη κλασματικά δεδομένα που ξεφεύγουν πάνω
από 33000 προσφέρει ταχύτατες πράξεις σε σχέση με τους αριθμούς
float.
Προσέξτε :
1) Ο πιο βασικός τύπος είναι ο int. Σε πολλές περιπτώσεις, κατά
τις οποίες ένας τύπος δεν έχει καθοριστεί, η C υποθέτει ότι είναι int.
16
μπορούμε ακόμη και να την χειριστούμε ως ASCII χαρακτήρα, οπότε η
c1 έχει τότε την τιμή ‘D’ αφού στον ASCII κώδικα το ‘D’ είναι το
νούμερο 69. Είναι λοιπόν το ίδιο πράγμα είτε πούμε c1 = 65 είτε c1 =
‘A’.
17
οποιουσδήποτε χαρακτήρες. Μπορούν φυσικά να αποτελέσουν μέρος
και ενός string (π.χ. “καλά\nείναι”). Οι πιο κοινοί χαρακτήρες
ανάποδης κάθετου είναι :
Κωδικός Σημασία
\n αλλαγή γραμμής
\f αλλαγή σελίδας
\b πίσω ένας χαρακτήρας (backspace)
\t μετακίνηση σαν το tab
\’ και \” μονά και διπλά εισαγωγικά
\\ ανάποδη κάθετος (backslash)
\a κουδούνι (bell)
\x δεκεξαδική σταθερά
\Ν οκταδική σταθερά,
όπου Ν 8-δικός αριθμός.
\0 μηδενικό (end of string)
iii) Προσέξτε ότι ‘b’ για παράδειγμα δεν είναι ίδιο με “b”. To
πρώτο είναι χαρακτήρας (δεσμεύει 1 byte), ενώ το δεύτερο είναι string
(δεσμεύει 2 bytes - 1 για το ‘b’ και 1 για τον χαρακτήρα ‘\0’ του
τέλους ενός string.
18
iv) Αν θέλουμε να χρησιμοποιήσουμε κάποιο όνομα για μια
σταθερά, αυτό γίνεται με την οδηγία #define. Όπως και η include που
ήδη συναντήσαμε, η define ανήκει στις εντολές αυτές που ξεκινάνε με
# και δεν τελειώνουν με ; (ερωτηματικό). Είναι οδηγίες (directives)
προς τον προεπεξεργαστή και βρίσκονται στην αρχή μιας συνάρτησης.
Συνήθως διαλέγουμε ονόματα με κεφαλαία γράμματα για να
ξεχωρίζουν οι σταθερές μας μέσα στο πρόγραμμα.
Παράδειγμα :
#define LONGZERO 0L
#define MESSAGE “Καληνύχτα παιδάκια”
Στην ΑNSI-C μια σταθερά δηλώνεται και με την χρήση της const, μαζί
με τον τύπο και την τιμή.
Π.χ. const long k = 4000;
Δηλώσεις Μεταβλητών
Οι μεταβλητές είναι θέσεις μνήμης που κατά την διάρκεια
εκτέλεσης ενός προγράμματος επιτρέπεται να αλλάζουν τιμές.
Ορίζονται κατά τύπους, δηλαδή :
int i, j, k ;
char onoma[30] ;
Επιτρέπεται και είναι καλό, να δίνουμε αρχικές τιμές στις μεταβλητές
μαζί με την δήλωσή τους :
int j=2, k=5 ;
char c= ‘’ ;
19
Κωδικός η printf εμφανίζει η scanf διαβάζει
%c ένα χαρακτήρα ένα χαρακτήρα
%d ή %i δεκαδικό δεκαδικό
%f πραγματικό πραγματικό
%e επιστημονική γραφή πραγματικό
%ld long ακέραιο long ακέραιο
%u απρόσημο -
%s αλφαριθμητικό αλφαριθμητικό
%o oκταδικό οκταδικό
%x δεκαεξαδικό δεκαεξαδικό
20
Σύμβολα ή Τελεστές (Οperators)
21
ξεκαθαριστεί εντελώς ότι τα δύο μέλη της ισότητας δεν έχουν την ίδια
συμπεριφορά. Ότι βρίσκεται δεξιά μένει ανέπαφο, ενώ ότι βρίσκεται
αριστερά χάνει το προηγούμενο περιεχόμενο του και αποκτά ίδιο
περιεχόμενο με εκείνο της δεξιάς μεριάς.
22
Αριθμητικοί Τελεστές
Τελεστής Πράξη
- αφαίρεση, αρνητικό πρόσημο
+ πρόσθεση
* πολλαπλασιασμός
/ διαίρεση
% υπόλοιπο διαίρεσης (modulo)
-- μείωση κατά ένα (αδιαίρετη)
++ αύξηση κατά ένα (αδιαίρετη)
Παραδείγματα :
a = - b ; /* το a παίρνει την τιμή -b */
/* αν b ήταν 12, τότε το a έχει τιμή -12 */
Ο τελεστής μείον ή αρνητικού προσήμου ‘-’ είναι μοναδιαίος, δηλαδή
δεν συνδέει δύο μεταβλητές αλλά αναφέρεται σε μία πάντοτε. Ο ίδιος
χαρακτήρας ‘-’ αποτελεί και σύμβολο αφαίρεσης. Τότε όμως συνδέει
δύο μεταβλητές. Έτσι
a = b - 31 ;
/* αν το b έχει τιμή 51, τότε το a έχει τιμή 20 */
Με ίδιο τρόπο ερμηνεύουμε τους τελεστές πρόσθεσης,
πολλαπλασιασμού, διαίρεσης και υπόλοιπου διαίρεσης. Δηλαδή
23
Παρατηρήσεις :
α) Αν οι μεταβλητές είναι ακέραιες, τότε γίνεται ακέραια
διαίρεση. Δηλαδή
int a=50, b=4, k ; float m ;
k = a / b ; m = a / b;
To k παίρνει την ακέραια τιμή 12 (πηλίκο διαίρεσης), ενώ το m την
πραγματική τιμή 12.0. Αν μας ενδιέφερε η πραγματική διαίρεση,
δηλαδή αν θέλαμε πηλίκο με κλασματικό μέρος (το 12.5), τότε μία
τουλάχιστον από τις a και b έπρεπε να είναι πραγματική μεταβλητή.
Βλέπε αργότερα το casting. Πάντως για τον ίδιο λόγο και όταν
διαιρούμε με σταθερές, προσθέτουμε στη σταθερά το “.0”, για
παράδειγμα
ακέραια διαίρεση a / 4
πραγματική διαίρεση a / 4.0
24
Εντολές Ελέγχου Προγράμματος
ΛΗΨΗ ΑΠΟΦΑΣΕΩΝ
Η εντολή απόφασης if
Μορφή : if (συνθήκη) εντολή1 ;
else εντολή2 ;
Ο όρος else είναι προαιρετικός. Αν η συνθήκη είναι Αλήθεια, δηλαδή
διαφορετική από το μηδέν, τότε θα εκτελεστεί η εντολή1. Αλλιώς, αν
υπάρχει το else, θα εκτελεστεί η εντολή2.
25
Η πρώτη if θα εμφανίσει το SOS γιατί το 5 είναι μεγαλύτερο από το 4.
Η δεύτερη if δεν θα εμφανίσει το ΚΑΛΗΜΕΡΑ γιατί το m ως μηδέν
είναι Ψέμα στη C. Αντίθετα, η δεύτερη if γεμίζει το b με την τιμή 10.
Παράδειγμα 3ο : scanf(“%d”,&y);
if (y <0) k = -y ;
26
else k = y;
Με τον τελεστή ?, αντί για την if γράφουμε
k = (y < 0) ? -y : y ;
Δηλαδή εδώ το k γεμίζει πάντα με την απόλυτη τιμή του y.
if ...else if ...
H “σκάλα” if-else-if προσφέρει σε αρκετές περιπτώσεις
δραστικές μειώσεις στους αριθμούς λογικών πράξεων που πρέπει να
υπολογιστούν για να ληφθούν αποφάσεις. Μελετήστε το παράδειγμα :
Παράδειγμα 4ο :
if (k<100) printf(“KAΛΑ”) ;
if (k >= 100) && (k < 500) printf(“IΣΩΣ”);
if (k >= 500)
{ b=k ; printf(“ΣΙΓΟΥΡΑ”); }
Για να εκτελεστούν οι 3 αυτές if, πρέπει να γίνουν κάθε φορά πέντε
λογικές πράξεις. Με την παρακάτω διάταξη, το πολύ δύο λογικές
πράξεις θα χρειαστούν.
if (k<100) printf(“KAΛΑ”) ;
else if (k<500) printf(“IΣΩΣ”);
else
{ b=k ; printf(“ΣΙΓΟΥΡΑ”); }
Εκμεταλλευτήκαμε δηλαδή την άρνηση που προσφέρει η else και έτσι
γράψαμε μία εντολή ταχύτερη που έχει ισοδύναμα αποτελέσματα με τις
3 if.
27
Μορφή : switch (μεταβλητή)
{ case σταθερά1 :
σειρά εντολών
break;
case σταθερά2 :
σειρά εντολών
break;
... ... ...
default :
σειρά εντολών }
Ο όρος default είναι προαιρετικός. Όταν υπάρχει και όταν δεν έχει
ταιριάξει η μεταβλητή με καμιά σταθερά, τότε εκτελείται η ομάδα
εντολών της. Και τα break είναι προαιρετικά. Τα βάζουμε όταν
θέλουμε να τερματίσουμε τη σειρά των εντολών που σχετίζονται με
κάθε σταθερά. Γιατί αν παραληφθεί μια break η εκτέλεση θα
συνεχιστεί με τις εντολές της επόμενης case μέχρι να φτάσει είτε σε
κάποιο break είτε στο τέλος της switch.
Παράδειγμα 5ο: switch ( ch )
{ case ‘Κ’ : printf(“SOS”);
b-=10;
case ‘Λ’ : printf(“GO”);
break ;
case ‘M’ :
case ‘N’ : printf(“ΦΕΥΓΩ”);
b *= 3;
default : a= 1000;
}
Όταν ο χαρακτήρας ch έχει τιμή Κ τότε θα εμφανιστεί SOS, η b θα
ελαττωθεί κατά 10 και θα εμφανιστεί GO. Όταν ο ch είναι Λ τότε απλά
θα εμφανιστεί GO. Όταν ο ch είναι Μ ή Ν τότε θα εμφανιστεί ΦΕΥΓΩ,
η μεταβλητή b θα τριπλασιαστεί και η a θα πάρει τιμή 1000. Όταν η ch
28
δεν είναι ούτε Κ ούτε Λ ούτε Μ ούτε Ν τότε η a θα πάρει την τιμή
1000.
ΑΝΑΚΥΚΛΩΣΗ - ΒΡΟΓΧΟΣ
Η εντολή while
Όταν θέλουμε ένα μέρος του προγράμματος να επαναλαμβάνεται
μόνο αν, και για όσο ισχύει μια συνθήκη, χωρίς να ξέρουμε πόσες
φορές θα γίνει αυτό, χρησιμοποιούμε συνήθως την while.
29
}
Οι συναρτήσεις διαβάσματος χαρακτήρα getchar() και εμφάνισης
χαρακτήρα putchar(), χρησιμοποιούν το αρχείο-επικεφαλίδα conio.h .
H συνθήκη είναι σύνθετη παράσταση. Για να μπορέσει να καταλήξει ο
υπολογιστής αν είναι αλήθεια ή ψέμα πρέπει να εκτελέσει πρώτα την
getchar(), να καταχωρήσει έπειτα τον χαρακτήρα που έδωσε ο χρήστης
στη μεταβλητή ch και μετά να τον συγκρίνει με την σταθερά STOP.
Όσο δεν δίνει ο χρήστης το *, τόσο εμφανίζεται ο χαρακτήρας και
αυξάνει ο μετρητής count. Μόλις δώσει το αστεράκι, εμφανίζεται το
μήνυμα για παράδειγμα
ήταν 10 χαρακτήρες
αν ο χρήστης έδωσε 10 χαρακτήρες, έναν-έναν, και μετά έδωσε το *.
30
while ( ( ch != ‘N’) && (ch != ‘O’));
return 0 ;
}
Έτσι εξασφαλίζουμε ότι ο χρήστης δίνει απάντηση μέσα σε επιτρεπτά
όρια. Άν δώσει οτιδήποτε άλλο εκτός από Ν και Ο τότε θα εμφανιστεί
ξανά το μήνυμα και θα περιμένει ο υπολογιστής απάντηση. Και αυτό
θα συνεχιστεί έως ότου ο χρήστης δώσει Ν ή Ο.
Η εντολή for
Στη C η εντολή for είναι πάρα πολύ ισχυρή. Συνήθως η
ανακύκλωση ελέγχεται από ένα μετρητή που είναι μια μεταβλητή
ακέραια ή χαρακτήρας.
Ισοδυναμεί με παράσταση1;
while (παράσταση2)
{ εντολές ;
παράσταση3;
}
Δηλαδή η παράσταση1 δίνει αρχικές τιμές σε μεταβλητές, η
παράσταση2 είναι μια συνθήκη που καθορίζει αν θα συνεχιστεί η
επανάληψη και η παράσταση3 είναι το βήμα, δηλαδή ο τρόπος αύξησης
ή μείωσης μεταβλητών (ενημέρωση). Προσοχή !!! Πρώτα από όλα και
για μία φορά, εκτελείται η παράσταση1. Μετά ελέγχεται η αλήθεια της
παράστασης2. Άν είναι αληθής, τότε εκτελούνται οι εντολές του
κορμού της for. Άν όχι, τότε σταματάει η for. Όπως βλέπετε υπάρχει
πιθανότητα, σαν την while, ο κορμός της for να μην εκτελεστεί ούτε
μια φορά.
31
Παρατηρήσεις: α)Μπορεί να λείπουν κάποιες από τις παραστάσεις
της for, αλλά είναι υποχρεωτικό να υπάρχουν τα ερωτηματικά.
β) Άν λείπει η παράσταση2 ή αν δεν μεταβάλλονται μεταβλητές
που επιδρούν στην παράσταση2 είναι σίγουρο ότι θα έχουμε infinite
loop (ατέρμονος βρόγχος).
iv) ans=2;
for (n=3; ans<=25;)
{ans*=n; printf(“%d->”,ans); }
Εμφανίζει 6->18->54->.
32
γ) Το σύμβολο % χρησιμοποιείται μόνο με ακέραιες μεταβλητές
ή σταθερές. Και το υπόλοιπο διαίρεσης έχει πάντοτε ίδιο πρόσημο με
αυτό του διαιρετέου.
δ) Η διαίρεση διά μηδέν (0 ή 0.0) και το υπόλοιπο διαίρεσης διά
του μηδενός προκαλεί πάντοτε σοβαρό σφάλμα που σταματάει την
εκτέλεση του προγράμματος.
Παράδειγμα : x = 100 ;
y = ++x;
Πρώτα η x αποκτά την τιμή 101 και μετά και η y φυσικά αποκτά
την τιμή αυτή του 101. Ενώ όταν
x = 100 ;
y = x++;
33
τότε πρώτα η y αποκτά την τιμή 100 και μετά η x γίνεται 101.
Προσοχή!!! Και στις δύο περιπτώσεις η x παίρνει την τιμή 101, η
διαφορά είναι πότε την παίρνει.
Προτεραιότητες ...
Από υψηλότερη προς χαμηλότερη προτεραιότητα, η σειρά είναι :
++ --
-
* / %
+ -
Όταν έχουμε ισοδύναμους τελεστές, ο υπολογιστής δίνει
προτεραιότητα στον τελεστή που βρίσκεται στην έκφραση αριστερά.
Μπορούμε όμως να χρησιμοποιούμε παρενθέσεις για να αλλάξουμε τις
προτεραιότητες. Έτσι
k = m + 7 * 3 ;
δίνει στο k την τιμή 31 όταν το m είναι 10, ενώ
k = ( m + 7 ) * 3;
δίνει στο k την τιμή 51 όταν το m είναι 10.
34
Συσχετιστικοί Τελεστές
Τελεστής Ενέργεια
> μεγαλύτερος από
< μικρότερος από
>= μεγαλύτερος ή ίσος
<= μικρότερος ή ίσος
== ίσος με
!= όχι ίσος
Η βασική ιδέα εδώ είναι η έννοια της Αλήθειας και του Ψέματος.
Είπαμε ήδη ότι στη C, αληθής είναι οποιαδήποτε τιμή διάφορη του
μηδενός, ενώ ψευδής τιμή είναι το μηδέν. Έτσι οι παραστάσεις που
χρησιμοποιούν συσχετιστικούς τελεστές θα επιστρέψουν το 0 σαν
ψευδή τιμή και το 1 σαν αληθή τιμή.
Παράδειγμα : int x=12;
printf(“%d”, x>10);
Θα εμφανιστεί στην οθόνη το 1.
Προσοχή : α) Όπως στη Pascal τα σύμβολα εκχώρησης ‘:=‘ και ελέγχου
ισότητας ‘=‘ είναι διαφορετικά, έτσι και στη C άλλο σύμβολο (‘=‘)
χρησιμοποιούμε για απόδοση τιμής και άλλο (‘==‘) χρησιμοποιούμε
για σύγκριση.
β) Το θαυμαστικό ‘!’ έχει όπως θα δούμε και αργότερα την
έννοια της άρνησης, οπότε ‘!=‘ είναι το διάφορο ‘<>‘ της Pascal.
Λογικοί Τελεστές
Τελεστής Ενέργεια
&& ΑΝD
|| OR
! NOT
35
Όπως και οι συσχετιστικοί έτσι και οι λογικοί επιστρέφουν το 0
σαν ψευδή τιμή και το 1 σαν αληθή. Οι λογικοί τελεστές υποστηρίζουν
τις βασικές λογικές πράξεις ΚΑΙ, Ή, ΟΧΙ που ορίζονται με τους
ακόλουθους πίνακες αλήθειας, όπου 1 αλήθεια και 0 ψέμα.
p q p && q p || q !p
0 0 0 0 1
0 1 0 1 1
1 0 0 1 0
1 1 1 1 0
36
Μπορούμε να υποχρεώσουμε μια παράσταση να είναι κάποιου
συγκεκριμένου τύπου, δίνοντας πριν την πράξη μέσα σε παρένθεση τον
ζητούμενο τύπο. Δηλαδή
int i = 61;
printf(“%7.2f”, (float)i / 5) ;
εμφανίζει 12.20, γιατί ο i προσαρμόστηκε σε πραγματικό και στη C
ισχύει ότι όταν σε μια παράσταση ανακατεύονται σταθερές και
μεταβλητές διάφορων τύπων τότε θα γίνει πράξη προς πράξη
μετατροπή στον τύπο της μεγαλύτερης μεταβλητής ή σταθερής. Έτσι το
(float)i είναι float, το 5 είναι integer και άρα το (float)i / 5 είναι float.
Αν γράφαμε όμως
int i = 61;
printf(“%7.2f”, (float)(i / 5)) ;
τότε i/5 δίνει 12 και απλά θα εμφανιζόταν 12.00 αφού το (float)
αναφέρεται πλέον στον ακέραιο i/5.
Παρατήρηση : Συνηθισμένο πρόβλημα στον πολλαπλασιασμό είναι το
γινόμενο δύο integer μεταβλητών να ξεφεύγει πάνω από το 32767.
Δηλαδή
long k ; int i=2000, m=30 ;
k = i * m ;
δεν θα αποθηκεύσει το αναμενόμενο 60000 στην μεταβλητή k αλλά το -
5536. Ο λόγος είναι ότι i και m είναι integer και έτσι η πράξη i*m
είναι ακέραια. Άρα όταν ξεφεύγει πάνω από τα όρια του integer πέφτει
πάνω στις αρνητικές τιμές. Η λύση είναι βέβαια η προσαρμογή του
ενός από τους τελεσταίους (μεταβλητές εδώ), δηλαδή
long k ; int i=2000, m=30 ;
k = (long)i * m ;
και το k θα αποθηκεύσει το αναμενόμενο 60000.
37
Εισαγωγή στις Συναρτήσεις
38
Τι κερδίζουμε όμως γράφοντας κώδικα με συναρτήσεις;
α)Γράφουμε μια φορά ένα μικρό κομμάτι προγράμματος που
επαναλαμβάνεται. Μπορούμε ακόμη να χρησιμοποιήσουμε την ίδια
συνάρτηση και σε διαφορετικά προγράμματα. Έτσι γλιτώνουμε
γράψιμο κώδικα και επομένως αποφεύγουμε ενδεχόμενα λάθη.
β)Έχουμε σε μικρά κομμάτια το πρόγραμμα, σπονδυλωτό, και
άρα πιο εύκολο σε έλεγχο και τροποποιήσεις.
γ)Κερδίζουμε χώρο εκτελέσιμου κώδικα στη μνήμη γιατί δεν
τον επαναλαμβάνουμε.
δ)Κερδίζουμε χώρο μεταβλητών στη μνήμη κατά την εκτέλεση
γιατί ο χώρος των τοπικών μεταβλητών ξαναχρησιμοποιείται.
Για να κατασκευάσουμε μια συνάρτηση πρέπει πρώτα να την
δηλώσουμε και μετά όπου θέλουμε να την καλέσουμε.
39
ΠΡΟΣΟΧΗ : Στη σύγχρονη μέθοδο δήλωσης οι δηλώσεις τύπων
παραμέτρων γίνονται όχι από κάτω, αλλά μέσα στη λίστα-παραμέτρων.
Δηλαδή γράφουμε
float jim(int x, int y, float z)
{ κορμός συνάρτησης }
και όχι όπως στη κλασσική μέθοδο δήλωσης
float jim(x,y,z)
int x, y;
float z;
{ κορμός συνάρτησης }
Προσέξτε ότι σε αντίθεση με την δήλωση μεταβλητών, όπου
μεταβλητές κοινού τύπου δηλώνονται όλες μαζί με κόμματα, κάθε
παράμετρος στη λίστα πρέπει να περιλαμβάνει και τον τύπο της.
Επίσης παρατηρήστε ότι η επικεφαλίδα της συνάρτησης ΔΕΝ τελειώνει
με “;”.
Κλήση συνάρτησης
Στη C η κλήση συνάρτησης είναι σαν πράξη και μπορεί να γίνει
οπουδήποτε, στην κύρια συνάρτηση main(), ή μέσα σε κάποια άλλη
συνάρτηση. Και βέβαια οι συναρτήσεις εκτελούνται όταν κληθούν από
κάποια άλλη συνάρτηση που ήδη εκτελείται. Όταν όμως η εκτέλεση
του προγράμματος φθάσει στη κλήση μιας συνάρτησης τότε
δημιουργούνται οι μεταβλητές της συνάρτησης, εκτελούνται οι εντολές
της και όταν τερματίσει, εξαφανίζονται οι τοπικές μεταβλητές της και
η εκτέλεση του προγράμματος συνεχίζεται με την εντολή που
ακολουθεί την κλήση, στο μέρος από όπου κλήθηκε.
Παράδειγμα 1ο : Εδώ η συνάρτηση1 καλεί τη συνάρτηση2 που με τη
σειρά της καλεί την συνάρτηση3.
Συνάρτηση 1
(καλεί) Συνάρτηση 2
40
(καλείται και καλεί) Συνάρτηση3
(καλείται)
x *= 3 ; int fun1() void fun2()
y = fun1() ; ==> { fun2(); ==> { k = x+ m ;
x += y ; <== return 0; } <== }
Η εντολή return
Έχει δύο χρήσεις:
α)Προκαλεί άμεση έξοδο από τη συνάρτηση που την περιέχει.
Δηλαδή υποχρεώνει το πρόγραμμα να αγνοήσει τις υπόλοιπες εντολές
της συνάρτησης και συνεχίζει από τον κώδικα που πραγματοποίησε τη
κλήση. Εδώ η return δεν έχει δίπλα της κάποια τιμή.
β)Είναι ένας από τους τρόπους που μια συνάρτηση επιστρέφει
αποτέλεσμα εκεί από όπου κλήθηκε. Με την return συγκεκριμένα, μια
συνάρτηση επιστρέφει ΜΙΑ τιμή στο όνομά της. Και τότε, όπως
είδαμε, πρέπει η τιμή να είναι ίδιου τύπου με το καθοριστικό-τύπου
που δώσαμε κατά την δήλωση-ορισμό της.
Παράδειγμα 2ο: ... ...
float f ; με float f1()
f = f1(); { return 25.678 ; }
... ...
Μια συνάρτηση μπορεί να μην περιέχει εντολή return. Τότε
τερματίζει την εκτέλεσή της μόλις εκτελέσει την τελευταία εντολή της
και συναντήσει το “}”. Επειδή όμως ΟΛΕΣ οι συναρτήσεις, εκτός από
αυτές που δηλώθηκαν void, επιστρέφουν μια τιμή θεωρείται το μηδέν
ως τιμή επιστροφής.
Μία συνάρτηση επίσης μπορεί να περιέχει πολλές εντολές return,
αν αυτό απλοποιεί τον αλγόριθμο.
41
Για να καταλάβουμε καλύτερα τους μηχανισμούς επικοινωνίας
των συναρτήσεων, δηλαδή αυτό που ονομάζουμε “πέρασμα
παραμέτρων”, θα πρέπει να έχουμε στο μυαλό μας ότι στη C υπάρχουν
τρεις τύποι μεταβλητών:
α) Τοπικές Μεταβλητές (local variables)
β) Καθολικές Μεταβλητές (global variables)
γ) Τυπικές Παράμετροι (formal parameters)
Οι τοπικές μεταβλητές στη C δεν περιορίζονται απλά σε επίπεδο
υποπρογράμματος, όπως συμβαίνει σε άλλες γλώσσες. Κάθε τμήμα
κώδικα, δηλαδή εντολές που ξεκινούν με αριστερό άγκιστρο και
τελειώνουν με δεξί άγκιστρο, μπορεί να ορίζει δικές του ιδιωτικές
μεταβλητές που καταστρέφονται έξω από αυτό. Και μάλιστα δεν είναι
υποχρεωτικό να τις δηλώνουμε στην αρχή του κώδικα. Πολλές φορές
τις δηλώνουμε μέσα σε ένα τμήμα με συνθήκη, εξασφαλίζοντας ότι
δέσμευση μνήμης θα γίνει μόνον όταν θα χρειαστεί.
Παράδειγμα 3ο: ...
if (ch==‘N’)
{ float s ;
scanf(“%f”, &f) ; ... }
Βλέπουμε ότι θα κρατηθεί χώρος για τη μεταβλητή s μόνον όταν
χρειαστεί, δηλαδή όταν η ch είναι ίση με ‘Ν’.
Η λέξη-κλειδί auto της γλώσσας C υπάρχει για να δηλώνουμε ότι
μια μεταβλητή είναι τοπική. Όμως ΔΕΝ χρησιμοποιείται καθόλου γιατί
όλες οι μη καθολικές μεταβλητές είναι τύπου auto εξ’ ορισμού.
Οι καθολικές μεταβλητές αντίθετα είναι γνωστές σε ολόκληρο
το πρόγραμμα και μπορούν να χρησιμοποιούνται από οποιοδήποτε
τμήμα κώδικα. Η δήλωσή τους γίνεται έξω από όλες τις συναρτήσεις.
Παράδειγμα 4ο: #include <stdio.h>
char ch ; /* η ch είναι καθολική */
main()
42
{ ... }
Παρατηρήσεις :
α)Όταν μια καθολική και μια τοπική μεταβλητή έχουν το ίδιο
όνομα, πχ. count, όλες οι αναφορές στο εσωτερικό της συνάρτησης
όπου δηλώνεται η count, για την μεταβλητή count, αφορούν την τοπική
μεταβλητή και δεν έχουν καμιά επίδραση στην καθολική.
Παράδειγμα 5ο: #include <stdio.h>
int count ;
main()
{ void f1();
count =50;
f1();
printf(“%d”, count);
return 0; }
void f1()
{ int count ;
for (count=1;count<6;count++)
printf(“*”); }
Αυτό που προκύπτει είναι “ *****50 “. Δηλαδή άλλος χώρος στη
μνήμη είναι η count της f1(), που ξεκινάει από το 1 και φτάνει ως το 6,
και άλλος χώρος είναι η count η ολική που περιέχει την τιμή 50.
β)Οι καθολικές μεταβλητές είναι χρήσιμες όταν χρησιμοποιούμε
τα ίδια δεδομένα σε πολλές συναρτήσεις του προγράμματός μας.
Ωστόσο, αποφεύγουμε τις μη απαραίτητες καθολικές μεταβλητές για
τους ακόλουθους λόγους:
i) Καταλαμβάνουν μνήμη σε όλη τη διάρκεια εκτέλεσης ενός
προγράμματος και όχι μόνον όταν είναι χρήσιμες.
ii) Οδηγούν σε προγραμματιστικά λάθη λόγω άγνωστων και
ανεπιθύμητων παρενεργειών (πχ. κατά λάθος αλλαγή τιμής μιας
μεταβλητής).
43
iii) Κάνουν την συνάρτηση λιγότερο γενική γιατί την
αναγκάζουν να στηρίζεται σε έννοιες ορισμένες έξω από αυτήν.
Χαλάνε δηλαδή την ζητούμενη αυτοδυναμία και ανεξαρτησία των
υποπρογραμμάτων.
Παράδειγμα 6ο: Προσέξτε τους δύο τρόπους γραφής μιας
συνάρτησης add(), που υπολογίζει το άθροισμα δύο πραγματικών.
Γενική μορφή Ειδική μορφή
float add (float x, float y) float x, y ;
{ return ( x+y ) } float add()
{ return ( x+y ) }
Το πλεονέκτημα της γενικής ή παραμετροποιημένης συνάρτησης είναι
ότι επιστρέφει το άθροισμα δύο οποιωνδήποτε πραγματικών, ενώ η
ειδική συνάρτηση βρίσκει το άθροισμα ΜΟΝΟΝ των δυο καθολικών
μεταβλητών x και y.
Οι τυπικές παράμετροι είναι αυτές που αποτελούν την λίστα-
παραμέτρων που είδαμε στη γενική μορφή δήλωσης μιας συνάρτησης.
Η δουλειά τους είναι να δέχονται τις τιμές των ορισμάτων που
μεταβιβάζονται σε μια συνάρτηση. Συμπεριφέρονται κατά τα άλλα σαν
να είναι απλές τοπικές μεταβλητές.
44
{ int z=5;
printf(“%d*%d”, tetr(z), z) ; }
tetr( int a)
{ a *= a ;
return a ; }
Το πρώτο τμήμα κώδικα έχει μια μεταβλητή z με τιμή 5 που αποτελεί
κατά την κλήση της συνάρτησης tetr(), το όρισμά της. Έτσι
δημιουργείται μια τοπική μεταβλητή a που γεμίζει με την τιμή 5. Αυτή
η a γίνεται 25 και η τιμή αυτή επιστρέφει μέσω της return έξω από την
συνάρτηση και έχοντας όνομα tetr(z). H z που χρησιμοποιήθηκε για το
κάλεσμα της tetr(), εξακολουθεί να περιέχει την τιμή 5. Έτσι το
αποτέλεσμα του παραδείγματος θα είναι “25*5”.
extern - register
Αυτές οι λέξεις-κλειδιά λένε στον μεταγλωττιστή πως να
αποθηκεύσει τη μεταβλητή που ακολουθεί. Λέγονται μετατροπείς
αποθήκευσης και στην ίδια κατηγορία ανήκει η auto που αναφέραμε
προηγουμένως και η static που θα δούμε αργότερα.
Το καθοριστικό extern βοηθάει στη διαχείριση προγραμμάτων
που αναφέρονται σε περισσότερα από ένα αρχεία. Συγκεκριμένα λέει
στον μεταγλωττιστή ότι οι μεταβλητές που ακολουθούν έχουν ήδη
δηλωθεί κάπου αλλού και έτσι δεν δεσμεύεται επιπλέον χώρος. Έτσι το
extern είναι ο τρόπος για να λέμε σε όλα τα αρχεία ενός προγράμματος
ποιες είναι οι καθολικές του μεταβλητές. Μπορούμε λοιπόν να
συνδέουμε ξεχωριστά μεταγλωττισμένα αρχεία, δίνοντάς τα και
επιπλέον κοινές καθολικές μεταβλητές.
45
int f1(); void f2()
void main() {
{ ... } a = 100 + b; }
f1() f3()
{ a=4000; { k = 2.65 ;
.... } ... }
Για να μην ξαναδεσμευτεί χώρος στη μνήμη για τις καθολικές
μεταβλητές a, b, k του προγράμματος, βάλαμε το extern. Αυτό γίνεται
επειδή οι συναρτήσεις μας (main, f1, f2 και f3) εκτείνονται σε δύο
αρχεία.
Το καθοριστικό register εφαρμόζεται μόνο σε μεταβλητές int
και char. Επίσης ΔΕΝ μπορούμε να το χρησιμοποιήσουμε στις
καθολικές μεταβλητές. Υποχρεώνει το μεταγλωττιστή να αποθηκεύει
τις τιμές των μεταβλητών που ζητάμε όχι στη μνήμη, όπως είναι το
κανονικό, αλλά σε καταχωρητή του επεξεργαστή. Έτσι φτιάχνουμε
“γρήγορες” μεταβλητές, ιδανικές για εντολές επανάληψης, έλεγχο
ανακυκλώσεων κλπ. Μπορούμε να δηλώνουμε όσες μεταβλητές register
θέλουμε αλλά η ΤURBO-C ανάλογα με τον επεξεργαστή, τις μετατρέπει
σε κανονικές μόλις ο αριθμός τους φθάσει στο επιτρεπόμενο όριο.
46
Προχωρημένα θέματα συναρτήσεων
Μεταβλητέ ς static
H λέξη-κλειδί static, όπως auto, register και extern, είναι ένας
μετατροπέας αποθήκευσης μεταβλητών. Σημαίνει γενικά ότι η
μεταβλητή παραμένει όπως είναι. Διαφορετικό αποτέλεσμα έχει πάνω
σε τοπικές μεταβλητές από ότι σε καθολικές. Θα δούμε λοιπόν τις δύο
αυτές περιπτώσεις.
i) Τοπικές static : Οι μεταβλητές αυτές είναι τοπικές, που όμως
διατηρούν την τιμή τους μεταξύ των κλήσεων της συνάρτησης που
ανήκουν. Δηλαδή ο μεταγλωττιστής της C, όταν σε μια συνάρτηση f1()
βλέπει μεταβλητή static δεν την σώζει στον προσωρινό χώρο μνήμης
(stack) της f1(), όπου σώζονται κανονικά οι τοπικές μεταβλητές. Σε
άλλο χώρο, όπου φυλάσσονται μόνιμα τα δεδομένα, αποθηκεύονται οι
static μεταβλητές. Εκεί που βρίσκονται οι καθολικές
μεταβλητές.Υπάρχει όμως διαφορά ανάμεσα σε τοπικές static και
καθολικές μεταβλητές. Μια τοπική static είναι γνωστή μόνο στο τμήμα
του κώδικα που δηλώνεται. Δηλαδή ο compiler εξασφαλίζει ότι μια
τοπική static είναι προσπελάσιμη ΜΟΝΟΝ από εκεί που πρέπει.
Παράδειγμα 1ο: #include <stdio.h>
void main()
{ void teststat();
int metr ;
for (metr=1; metr <=3; metr++)
{ printf(“Επανάληψη %d : “, metr);
teststat(); } }
void teststat()
{int apli =1;
static int st =1 ;
printf(“apli = %d, st = %d\n”,apli++, st++);
47
}
H έξοδος αυτού του παραδείγματος θα είναι:
Επανάληψη 1 : apli = 1, st = 1
Επανάληψη 2 : apli = 1, st = 2
Επανάληψη 3 : apli = 1, st = 3
Παρατηρήστε ότι η στατική τοπική μεταβλητή st, θυμάται ότι η τιμή
της είχε αυξηθεί κατά 1 στη προηγούμενη κλήση της συνάρτησης
teststat(), ενώ η μεταβλητή apli όχι. Δηλαδή με το static η st έγινε
σαν καθολική. Όμως μπορούμε ΜΟΝΟΝ μέσα στη teststat να την
χρησιμοποιήσουμε. Προσέξτε επίσης και τη διαφορά στην απόδοση
αρχικών τιμών. Το static υποχρεώνει την μεταβλητή st να πάρει μόνο
ΜΙΑ ΦΟΡΑ αρχική τιμή. Αντίθετα η κανονική τοπική μεταβλητή apli
παίρνει αρχική τιμή ΚΑΘΕ ΦΟΡΑ που καλείται η συνάρτηση teststat().
Ένα ακόμη σημείο που πρέπει να προσέξετε στο προηγούμενο
παράδειγμα είναι η δήλωση της συνάρτησης teststat() μέσα στην
main(). Γενικά όταν θέλουμε μέσα σε μια συνάρτηση να
χρησιμοποιήσουμε μια συνάρτηση που ο ορισμός της ακολουθεί, πρέπει
να την δηλώσουμε. Και η δήλωση γίνεται με την πρώτη γραμμή-
επικεφαλίδα της συνάρτησης. Άν όμως έχουμε ορίσει μια συνάρτηση
ΠΡΙΝ την χρησιμοποίησή της, τότε ΔΕΝ χρειάζεται και να την
δηλώσουμε. Δηλαδή στο παράδειγμά μας θα μπορούσαμε να μην
γράφαμε την 3η γραμμή void teststat(); αν οι τελευταίες 5 γραμμές
(ορισμός της teststat) είχαν τοποθετηθεί κάτω από την γραμμή
#include ...
48
αποφεύγουμε παρενέργειες. Στη πραγματικότητα, αυτή είναι η
εξ’ορισμού ρύθμιση για τις καθολικές μεταβλητές. Μόνο αν βάλουμε
στα άλλα αρχεία το extern, οι καθολικές μεταβλητές είναι γνωστές σ’
αυτά. Γι΄ αυτό και δεν είναι απαραίτητο το static στις καθολικές
μεταβλητές. Το χρησιμοποιούμε όταν θέλουμε να τονίσουμε στο
πρόγραμμα ότι κάποια μεταβλητή ΔΕΝ θα χρησιμοποιηθεί σε άλλο
αρχείο.
Ξεκίνημα με Δείκτες
Οι δείκτες (pointers) είναι ίσως το ισχυρότερο χαρακτηριστικό
της C. Είναι όμως και πολύ επικίνδυνοι γιατί η λανθασμένη χρήση τους
49
(πχ. χρήση χωρίς αρχικές τιμές ή χωρίς έλεγχο) μπορεί να προκαλέσει
“κρέμασμα” του συστήματος. Μια ακόμα δυσκολία τους είναι ότι δεν
εντοπίζονται εύκολα τα λάθη μας κατά την χρησιμοποίησή τους. Όμως
αξίζει το κόπο να μάθουμε να δουλεύουμε με αυτούς για τρεις λόγους:
α)Προσφέρουν τρόπους ώστε μια συνάρτηση να μπορεί να
τροποποιεί τα ορίσματα με τα οποία καλείται. Είναι η λεγόμενη κλήση
κατά αναφορά (call by reference) που θα δούμε αναλυτικά στη
συνέχεια.
β)Χρησιμοποιούνται στη θέση των πινάκων και των strings,
προσφέροντας μεγαλύτερες δυνατότητες.
γ)Μπορούμε μέσω αυτών, όπως και στη Pascal, να
υποστηρίξουμε την δυναμική κατανομή της μνήμης. Δηλαδή να
δεσμεύουμε μνήμη κάθε φορά μόνο για το απολύτως απαραίτητο
χρονικό διάστημα.
δ)Επιτρέπουν την πρόσβαση σε συγκεκριμένες θέσεις μνήμης,
πράγμα που χρειάζεται όταν γράφουμε προγράμματα που κάνουν άμεσο
έλεγχο του hardware.
ε)Χρησιμοποιούνται για κατασκευή πολύπλοκων δομών
δεδομένων, όπως οι λίστες, οι σωροί και τα δέντρα.
Τι είναι όμως οι δείκτες; H Κεντρική Μνήμη (RAM) είναι
οργανωμένη σαν μια σειρά από bytes (8 bits το καθένα). Τα bytes αυτά
είναι αριθμημένα από 0 ως τον μέγιστο αριθμό που επιτρέπει το
σύστημα. Το κάθε byte της μνήμης αναγνωρίζεται μοναδικά από τον
αριθμό του. Ο αριθμός αυτός είναι η διεύθυνση (address) του byte. Οι
δείκτες στη C είναι μεταβλητές που περιέχουν διευθύνσεις. Λέγονται
έτσι γιατί “δείχνουν” σε κάποια θέση στη μνήμη. Οι δείκτες έχουν όλοι
το ίδιο μέγεθος στο ίδιο μηχάνημα. Έτσι σε μηχανήματα με
επεξεργαστές από 80386 και πάνω οι δείκτες έχουν μέγεθος 4 bytes.
Και αυτό γιατί αυτοί οι 32-bit επεξεργαστές υποστηρίζουν διευθύνσεις
50
4-bytes (4x8bits = 32bits). Ονομάζονται και επεξεργαστές με λέξεις
(words) τάξης 4-bytes.
Στις περισσότερες περιπτώσεις, όταν ένας δείκτης δείχνει μια
διεύθυνση, αυτή η διεύθυνση είναι η θέση μιας άλλης μεταβλητής. Και
τότε λέμε ότι η πρώτη μεταβλητή δείχνει την δεύτερη.
Δήλωση Δεικτών
Οι δείκτες δηλώνονται κυρίως, βάζοντας ένα αστεράκι “*” πριν
από το όνομά τους. Φυσικά το καθοριστικό τύπου πρέπει να υπάρχει.
Πχ. int *k, *m;
/* οι k,m είναι δείκτες σε ακέραιες μεταβλητές */
char *s ;
/* η s είναι δείκτης σε μεταβλητή χαρακτήρα */
Σχετικά με τους δείκτες η C ορίζει δύο ειδικούς τελεστές-
σύμβολα:
1) Το “&” που επιστρέφει την διεύθυνση της μεταβλητής που
ακολουθεί. Το έχουμε ήδη συναντήσει στην συνάρτηση scanf().
Λέγεται τελεστής διεύθυνσης (address). Όπως καταλαβαίνετε, οι
δείκτες μπορούν να πάρουν τιμές μέσω αυτών των τελεστών. Η γενική
μορφή είναι &μεταβλητή.
2) Το “*” που επιστρέφει την τιμή της μεταβλητής ΠΟΥ
ΔΕΙΧΝΕΙ ο δείκτης που ακολουθεί. Λέγεται τελεστής έμμεσης
προσπέλασης (dereferencing). H γενική μορφή είναι *δείκτης.
Παράδειγμα 3ο : ...
float i=25.4, *j, *k ;
k = &i ;
*k = 7.75 ;
i = *k + 5;
...
51
Στη πρώτη γραμμή δηλώνουμε μια πραγματική μεταβλητή i με αρχική
τιμή 25.4 και δύο δείκτες j και k σε πραγματικούς. Στη δεύτερη
γραμμή η διεύθυνση της μεταβλητής i αποθηκεύεται στον δείκτη k.
Δηλαδή ο k δείχνει την μεταβλητή i. Έτσι, αν η μεταβλητή i, καθώς
τρέχει το πρόγραμμα, είναι αποθηκευμένη στη διεύθυνση μνήμης 2000,
τότε η k γεμίζει με την τιμή 2000.
Στη τρίτη γραμμή η τιμή 7.75 αποθηκεύεται ΕΚΕΙ ΠΟΥ
ΔΕΙΧΝΕΙ η k. Και επειδή η k δείχνει την μεταβλητή i, ουσιαστικά το
7.75 αποθηκεύεται στη μεταβλητή ι.
Στη τέταρτη γραμμή η τιμή της μεταβλητής που δείχνει η k,
δηλαδή η τιμή της μεταβλητής i, αυξημένη κατά 5 θα αποθηκευτεί
στην μεταβλητή i. Έτσι η i θα γεμίσει με την τιμή 12.75
Παρατήρηση: Οι δυο ειδικοί αυτοί τελεστές, “*” και “&”, έχουν
προτεραιότητα μεγαλύτερη από όλους τους αριθμητικούς τελεστές.
Είναι στο ίδιο επίπεδο με το “-” του αρνητικού προσήμου.
52
Παράδειγμα 4ο: #include <stdio.h>
void swap ( int *x, int *y)
{ int temp ;
temp = *x ;
*x = *y ;
*y = temp ; }
void main()
{ int k=50, m=100 ;
swap(&k, &m) ;
printf(“k=%d και m=%d”, k, m) ;
}
Οι μεταβλητές k και m ξεκινάνε με αντίστοιχες τιμές 50 και 100. Η
συνάρτηση όμως swap(), εναλλάσσει τις τιμές αυτές, έτσι ώστε στο
τέλος η printf να εμφανίζει “k=100 και m=50”. Καλέσαμε την swap με
πραγματικές παραμέτρους &k και &m (διευθύνσεις), για να επιδράσει
η συνάρτηση swap στις k και m της συνάρτησης main. Επιπλέον όμως
προσέξαμε στον ορισμό της swap οι τυπικές παράμετροι να είναι *x
και *y (δείκτες). Προσέξτε τέλος ότι χρησιμοποιήσαμε σαν βοηθητική-
τοπική μεταβλητή την temp που είναι μια απλή ακέραια μεταβλητή. Ο
ρόλος της ήταν να φυλάξει την τιμή που έδειχνε η x. Κατά την κλήση η
παράμετρος &k έκανε την x να δείχνει στην μεταβλητή k. Και έτσι η
temp τελικά φύλαξε την τιμή της μεταβλητής k.
53
κατά 1 (σύμβολα ++ και --) καθώς και η πρόσθεση και αφαίρεση
(σύμβολα += και -= ).
Προσέξτε όμως. Η πρόσθεση ενός ακέραιου n σε δείκτη κάνει
τον δείκτη να δείχνει n μεταβλητές πιο πέρα και η αφαίρεση τον κάνει
να δείχνει n μεταβλητές πιο μπροστά.
Παράδειγμα 5ο: int *p ;
char *c1, *c2 ;
printf(“&p=%d &c1=%d &c2=%d\n”, p, c1, c2);
c2-- ; /* η c2 δείχνει 1 byte πιο πίσω */
p++ ; /* η p δείχνει 2 bytes πιο πέρα */
c1 += 7 ; /* η c1 δείχνει 7 bytes πιο πέρα */
p -= 3 ; /* η p δείχνει 6 bytes πιο πίσω */
printf(“&p=%d &c1=%d &c2=%d\n”, p, c1, c2);
...
Επειδή η c2 είναι δείκτης σε μεταβλητή χαρακτήρα, μεγέθους
ενός byte, στη 4η γραμμή η c2 δείχνει 1 μόνο byte πιο πίσω, δηλαδή
δείχνει την προηγούμενη μεταβλητή. Όμοια και η c1, στην 6η γραμμή,
δείχνει την 7η κατά σειρά μεταβλητή χαρακτήρα και άρα δείχνει 7
bytes μετά.
Η p όμως είναι δείκτης σε μεταβλητή integer, μεγέθους 2 bytes.
Έτσι στην 5η γραμμή, η p για να δείξει τον επόμενο ακέραιο, δείχνει 2
bytes μετά. Και στην 7η γραμμή, επειδή θέλει να δείξει 3 ακέραιους
πιο πίσω, δείχνει στην μνήμη 6 bytes πιο πίσω.
H έξοδος λοιπόν του παραδείγματος θα είναι :
&p=65500 %c1=36790 &c2=870
&p=65496 &c1=36797 &c2=869
54
ΟΛΟΙ οι πίνακες είναι δείκτες ΑΠΟ ΜΟΝΟΙ ΤΟΥΣ. Δηλαδή η
δήλωση int k[10]; σημαίνει ότι ο k είναι δείκτης που δείχνει το πρώτο
στοιχείο (k[0]) του πίνακα k των 10 ακέραιων. Έτσι
το k ισοδυναμεί με &k[0] (διεύθυνση του 1ου στοιχείου)
το *k ισοδυναμεί με k[0] (τιμή του 1ου στοιχείου)
το k[i] ισοδυναμεί με *(k+i) (τιμή του i+1-στοιχείου)
το k++ ισοδυναμεί με &k[1] (διεύθ. του 2ου στοιχείου)
55
Πίνακες και Δείκτες
56
Μ’ αυτόν τον τρόπο, για να αναφερθούμε στο (i+1)-στοιχείο
ενός πίνακα k, γράφουμε k[i]. Και αυτό γιατί οι πίνακες
χρησιμοποιούν την θέση 0 για το πρώτο στοιχείο τους. Έτσι k[3] είναι
το 4ο στοιχείο, k[6] το 7ο και k[0] τo 1o στοιχείο. Επειδή οι πίνακες
έχουν σταθερό μέγεθος, συνήθως χρησιμοποιούμε εντολές for για τις
εργασίες μαζί τους. Αρκετές φορές όμως και οι εντολές while και
do...while μπορούν να χρησιμοποιηθούν για το “γέμισμα” ή το
διάβασμα τιμών ενός πίνακα βάσει κάποιας συνθήκης.
Παράδειγμα 1ο: Υπολογισμός μέσης τιμής 20 πραγματικών αριθμών.
#include <stdio.h>
void main()
{ float pin[20], mesi =0 ;
char i;
for (i=0; i<20; )
{ printf(“δώσε %dο αριθμό : “, i+1);
scanf(“%f”, &pin[i]);
mesi += pin[i++]; }
printf(“Μέσος όρος : %7.2f\n”, mesi/20);
}
Προσέξτε ότι στη for, η μεταβλητή i που αποτελεί τον δείκτη-
θέσης του πίνακά μας pin, ξεκινάει από την τιμή 0 και φτάνει ως την
τιμή 19 για να αναφερθούμε στα 20 στοιχεία του πίνακα. Έτσι, μέσα
στην printf κάθε φορά για την i θέση του πίνακα εμφανίζουμε στο
σχόλιο “δώσε ...” το περιεχόμενο της μεταβλητής i αυξημένο κατά 1.
Επίσης, σημειώστε ότι η i δηλώθηκε char γιατί οι τιμές της δεν
ξεφεύγουν πάνω από το 20, αφού 20 είναι οι θέσεις του πίνακα. Τέλος
προσέξτε ότι στην εντολή for δεν υπάρχει τίποτα μετά το δεύτερο
ερωτηματικό γιατί η ενημέρωση, δηλαδή η αύξηση κατά 1 της
μεταβλητής i γίνεται στην 8η γραμμή με την πράξη ++.
57
Χρησιμοποιώντας δείκτες-διευθύνσεων
Μ’ αυτόν τον τρόπο, για να αναφερθούμε στο (i+1)-στοιχείο
ενός πίνακα k, γράφουμε *(k+i). Δηλαδή το k[i] είναι ισοδύναμο με το
*(k+i). Αυτό συμβαίνει γιατί κάθε όνομα πίνακα χωρίς δείκτη-
θέσης(index) είναι δείκτης(pointer) στο πρώτο του στοιχείο.
Παράδειγμα 2ο: Υπολογισμός μέσης τιμής 20 πραγματικών αριθμών
με χρήση pointers.
#include <stdio.h>
void main()
{ float pin[20], mesi =0, *k ;
char i;
k = pin;
for (i=1; i<21; i++)
{ printf(“δώσε %dο αριθμό : “, I);
scanf(“%f”, k);
/* ή scanf(“%f”, &*k); */
mesi += *k++; }
printf(“Μέσος όρος : %7.2f\n”, mesi/20); }
Το στοιχείο *k, την πρώτη φορά που εκτελείται η for, είναι το
pin[0], δηλαδή το 1ο στοιχείο του πίνακά μας. Και στην 8η γραμμή του
προγράμματός μας, αφού ο αθροιστής mesi πάρει την τιμή του, με τη
πράξη ++ το στοιχείο *k πλέον είναι το 2ο στοιχείο του πίνακα. Με
αυτήν την τιμή θα εκτελεστεί η 2η επανάληψη της for και αυτό θα
συνεχιστεί για 20 επαναλήψεις. Τις επαναλήψεις τις μετρά η μεταβλητή
ελέγχου i, αλλά τα διάφορα στοιχεία του πίνακα τα διατρέχει ο δείκτης
k.
ΠΡΟΣΟΧΗ !!!
Είναι απαραίτητο να οριστεί ένας δείκτης μεταβλητών float,
όπως ο k, που θα διατρέχει τις θέσεις του πίνακά μας pin. Και αυτό
γιατί, όπως αναλυτικά θα δούμε και αργότερα, o pin, όπως κάθε όνομα
58
πίνακα, είναι ΣΤΑΘΕΡΑ δείκτη που δείχνει το πρώτο του στοιχείο και
που δεν επιτρέπεται λοιπόν να μεταβάλλουμε την τιμή του.
59
χρησιμοποιούμε pin+1 όταν θέλουμε να περάσουμε στο επόμενο
στοιχείο του, γιατί ΔΕΝ ΜΠΟΡΟΥΜΕ να αλλάζουμε την τιμή της
σταθεράς-δείκτη pin.
60
α) τις διευθύνσεις των ίδιων των μεταβλητών k (pointer) και
h(πίνακας),
β) τα περιεχόμενα των k και h, δηλαδή τις διευθύνσεις των
μεταβλητών/σταθερών που δείχνουν
γ) τις τιμές των μεταβλητών/σταθερών που δείχνονται από τους k
και h.
Παράδειγμα 4ο: #include <stdio.h>
#include <conio.h>
int h[4],i,*k;
void emfanisi()
{
printf("\n%u %u \n",&k,&h);
printf("%u %u \n",k,h);
printf("%u %u \n",*k,*h);
printf("******\n");
}
void main()
{ clrscr();
emfanisi();
k=h; /* h=k; δεν είναι σωστό */
emfanisi();
for (i=100;i<104;i++)
/* h[i-100]=i; ισοδύναμο με την επόμενη γραμμή*/
*k++=i;
for (i=0;i<4;i++)
printf("%d ",h[i]);
k--;
emfanisi();
k=h+1; /* h++ ; δεν είναι σωστό */
emfanisi();
61
printf("%d\n",*k+200);
for (i=0,k=h;i<4;i++)
printf("%d ",*k++);
/* printf("%d ",*h++); δεν είναι σωστό*/
}
Στη μνήμη ο πίνακας h και ο δείκτης k είναι :
h[0] h[1] h[2] h[3]
62
Με την απόδοση k=h; κάνουμε τον δείκτη k να δείχνει όπου και
ο h. Έτσι η δεύτερη κλήση της emfanisi δίνει :
892 896
896 896
0 0
******
Το μόνο που άλλαξε είναι ότι η τιμή του k, στη 2η γραμμή, έγινε 896,
δηλαδή η διεύθυνση που δείχνει ΣΤΑΘΕΡΑ ο δείκτης h.
Στη συνέχεια η εντολή for γεμίζει τον πίνακα h με τις τιμές 100
ως 103. Παρατηρήστε τους δύο εναλλακτικούς τρόπους, με index ή
pointer που μπορεί αυτό να γίνει. Η επόμενη for εμφανίζει τα
περιεχόμενα του h, χρησιμοποιώντας δείκτες-θέσης, και δίνει λοιπόν :
100 101 102 103
63
******
Η εντολή printf που ακολουθεί εμφανίζει 301 γιατί το *k είναι 101 και
του προσθέτουμε 200 μονάδες.
Τέλος η εντολή for εμφανίζει τα περιεχόμενα του πίνακα h
χρησιμοποιώντας τον δείκτη k. Προσέξτε ότι στη παρένθεση της for
έχουμε δύο αρχικές τιμές που χωρίζονται με κόμμα. Πρέπει ο δείκτης k
να δείξει το πρώτο στοιχείο του πίνακα για να μπορέσει να τον
διατρέξει όλον. Εμφανίζεται λοιπόν :
100 101 102 103
Σημείωση : Παρατηρήστε ότι h έχει σταθερά την τιμή 896.
64
Προχωρημένα Θέματα Πινάκων - Strings
65
Μήνας 1ος:31 μέρες
Μήνας 2ος:28 μέρες
...
Μήνας 12ος:31 μέρες
Παρατηρήστε ότι μετά το “}” των τιμών υπάρχει “;”.
66
for (k=0 ; k < sizeof (days)/sizeof(int) ; k++)
Προσέξτε ότι διαιρέσαμε το μέγεθος σε bytes του πίνακα days, με το
μέγεθος σε bytes του τύπου integer που είναι ο τύπος των στοιχείων
του days. Βρίσκουμε έτσι ΠΟΣΑ στοιχεία έχει ο πίνακας days και άρα
η C μόνη της ρυθμίζει τον αριθμό επαναλήψεων. Αντί για sizeof(int)
θα μπορούσαμε να βάλουμε 2, αλλά τότε θα χρειαζόταν μετατροπές το
πρόγραμμά μας αν μεταφερόταν σε σύστημα με μέγεθος μεταβλητών
integer διαφορετικό από 2 bytes.
67
tel[i]=i ;
emfanise(tel);
}
Βλέπουμε ότι στη main() γίνεται κλήση της συνάρτησης emfanise() με
παράμετρο τον πίνακα tel. Δείτε τώρα τις τρεις εκδοχές για την
συνάρτηση emfanise().
α) void emfanise( int num[5])
{ char k;
for (k=0; k<5; k++)
printf(“%d “, num[k]);
}
β) Όμως είπαμε ότι η C δεν έχει έλεγχο ορίων. Έτσι το πραγματικό
μέγεθος του num δεν έχει σχέση με την παράμετρο. Μπορούμε λοιπόν
αντί για την 1η γραμμή να δώσουμε :
void emfanise( int num[])
γ) Ο πιο επαγγελματικός τρόπος είναι με δείκτες. Αντί λοιπόν για την
1η γραμμή δίνουμε :
void emfanise( int *num)
Αυτό επιτρέπεται γιατί μπορούμε να αποδίδουμε index σε έναν
pointer χρησιμοποιώντας τα “[]”, σαν να είναι πίνακας ο δείκτης.
Δείτε στο σώμα της emfanise ότι αυτό συμβαίνει.
Επίσης, όποια και από τις 3 εκδοχές και αν χρησιμοποιήσουμε,
μπορούμε αντί για την 4η γραμμή να δώσουμε
printf(“%d ”, *num++);
ΠΡΟΣΟΧΗ:
1)Ο πίνακας σαν παράμετρος κάνει call by reference, δηλαδή ο
κώδικας που βρίσκεται μέσα στη συνάρτηση θα δουλεύει και θα
τροποποιεί τα πραγματικά περιεχόμενα του πίνακα που θα
χρησιμοποιήσουμε για να καλέσουμε την συνάρτηση. Και μην ξεχνάτε
ότι η εντολή return ΔΕΝ μπορεί να επιστρέφει πίνακες. Οπότε ο μόνος
68
δρόμος τροποποίησης τιμών ενός πίνακα μέσα από συνάρτηση είναι το
πέρασμά του σαν παράμετρο της συνάρτησης.
2)Αν pin πίνακας, έστω 5 ακέραιων, τότε μπορούμε να γράψουμε
είτε
scanf(“%d”, &pin[i]); /* με i = 0,1,..4 */
είτε scanf(“%d”, pin);
Ειδικά η δεύτερη scanf είναι ισοδύναμη με την πρώτη για i=0.
69
μπαίνει αυτόματα στο τέλος των σταθερών και μεταβλητών μας από
τον μεταγλωττιστή. Δηλαδή με τις εντολές :
#define TOP “τώρα”
char onoma[11]=“χρήστος” ;
void main()
...
δημιουργήσαμε μια αλφαριθμητική σταθερά TOP με περιεχόμενο τη
λέξη τώρα και μια καθολική αλφαριθμητική μεταβλητή onoma με
δυνατότητες αποθήκευσης strings 10 χαρακτήρων που η αρχική τιμή
του είναι χρήστος. Μπορούμε άνετα να μεταχειριζόμαστε τα στοιχεία
των αλφαριθμητικών σαν χαρακτήρες σύμφωνα με όσα γνωρίζουμε.
Έτσι
TOP[0] είναι ‘τ’, ΤOP[4] είναι ‘\0’,
onoma[1] είναι ‘ρ’ και onoma[7] είναι ‘\0’.
Θυμηθείτε ότι μπορούμε να παραλείψουμε την διάσταση κατά
την απόδοση αρχικής τιμής σε πίνακες. Έτσι η μεταβλητή μπορούσε να
δηλωθεί και
char onoma[]=“χρήστος” ;
Με δείκτες ισοδύναμα θα γράφαμε
char *onoma=“χρήστος” ;
Για να αλλάξουμε τιμές στις μεταβλητές string, καθώς
εκτελείται το πρόγραμμα, χρησιμοποιούμε για το πληκτρολόγιο την
συνάρτηση scanf(), με προσδιοριστή φόρμας “%s”. Και ΔΕΝ βάζουμε
τον χαρακτήρα ‘&’ πριν το όνομα του αλφαριθμητικού που
διαβάζουμε. Γιατί το όνομα της μεταβλητής string, όπως και τα
ονόματα όλων των πινάκων, είναι από μόνα τους ΔΙΕΥΘΥΝΣΕΙΣ του
πρώτου τους στοιχείου.
Ένας καλύτερος τρόπος για διάβασμα είναι η συνάρτηση gets()
(get-string) που και αυτή βρίσκεται στο “stdio.h”. Έτσι οι δύο
παρακάτω εντολές είναι ισοδύναμες:
70
scanf(“%s”, onoma);
gets(onoma);
Σημειώστε ότι και οι δύο συναρτήσεις ΔΕΝ κάνουν έλεγχο ορίων
(αναμενόμενο) και θέλουν προσοχή.
Για εμφάνιση ενός αλφαριθμητικού η printf() έχει τον ίδιο
προσδιοριστή φόρμας “%s”. Αν θέλουμε να εμφανίσουμε ΜΟΝΟ το
αλφαριθμητικό, τότε μπορούμε να παραλείψουμε τον προσδιοριστή
φόρμας. Υπάρχει επίσης και η puts() (put-string), η οποία μετά την
εμφάνιση του string αλλάζει και γραμμή. Δηλαδή είναι ισοδύναμες οι :
printf(“%s\n”, onoma);
{ printf(onoma); printf(“\n”); }
puts(onoma);
71
st2 είχε τιμή “ Πέτρο”, τότε μετά την κλήση το st1 έχει πιά τιμή “με
λένε Πέτρο” ενώ το st2 δεν άλλαξε καθόλου. Εννοείται ότι το
strlen(st1) είναι πλέον 13.
72
Αναδρομή (Recursion)
73
Παρατηρήστε ότι η do ... while στη main() εξασφαλίζει ότι ο χρήστης
θα δώσει αριθμό 0 ή θετικό. Ακόμη δείτε ότι όταν η συνάρτηση
parag() καλείται με όρισμα 0, τότε επιστρέφει 1. Όταν καλείται όμως
με οποιοδήποτε άλλο όρισμα n τότε επιστρέφει το γινόμενο parag(n-1)
* n. Για να υπολογιστεί αυτή η παράσταση καλείται αναδρομικά η
parag() με n-1. Αυτή η διαδικασία συνεχίζεται μέχρι το n να γίνει 0
και να αρχίσουν τότε να επιστρέφουν οι κλήσεις προς τη συνάρτηση.
Για να γίνει αυτό το τελευταίο κατανοητό δείτε το ακόλουθο
παράδειγμα.
Παράδειγμα 2ο: #include <stdio.h>
recur(int a)
{ int apot;
if (a<3)
{ /* το a αυξάνει τώρα ... */
printf("*** a=%d\n", a);
apot = recur(a+1)+10;
}
else apot = 0;
/* Από εδώ το a μειώνεται ... */
printf("a=%d apot=%d\n", a, apot);
return apot;
}
#include <conio.h>
void main()
{ int b;
clrscr();
b = recur(1);
printf("b=%d ", b);
}
Το αποτέλεσμα του παραδείγματος είναι :
74
*** a=1
*** a=2
a=3 apot=0
a=2 apot=10
a=1 apot=20
b=20
Σημειώστε αρχικά ότι κάθε συνάρτηση μπορεί να έχει τις δικές της
προτάσεις “#include...” και γενικά να έχει δικές της κάθε είδους
εντολές προεπεξεργαστή. Έτσι η main(), για να μπορεί να καθαρίσει
την οθόνη με την συνάρτηση συστήματος clrscr(), περιέχει include για
την conio.h.
Ας περάσουμε όμως στα θέματα αναδρομής. Στην main()
καλείται για πρώτη φορά η συνάρτηση recur(). Tην καλούμε με όρισμα
την μονάδα και το αποτέλεσμά της θέλουμε να το αποθηκεύσει η
μεταβλητή b, η οποία στη συνέχεια και θα το εμφανίσει. Όπως όμως
βλέπετε πριν την γραμμή “b=20”, εμφανίζονται 5 ακόμη γραμμές.
Φυσικά είναι έργο της συνάρτησης recur() που διαδοχικά καλεί τον
εαυτό της. Συγκεκριμένα: Η α’ κλήση της recur() είδαμε ότι έγινε από
την main() και θα εκτελεστεί μέχρι την 7η γραμμή. Και αυτό γιατί το a
είναι 1, οπότε μέσα στον κορμό της if θα εκτελεστεί η printf που θα
εμφανίσει την γραμμή
*** a=1
και μετά θα γίνει η β’ κλήση της recur() με όρισμα 2. Τι συμβαίνει
τώρα; Όταν μια συνάρτηση καλεί τον εαυτό της, ο υπολογιστής
κατανέμει μνήμη στη στοίβα για νέες τοπικές μεταβλητές και
παραμέτρους και εκτελεί από την αρχή τον κώδικα της συνάρτησης με
αυτές τις νέες μεταβλητές. Άλλη λοιπόν a δημιουργείται, με τιμή 2, και
άλλο apot. Προσοχή όμως. Δεν δημιουργείται ένα νέο αντίγραφο της
συνάρτησης. Μόνο τα ορίσματα, δηλαδή τα τοπικά της μεγέθη είναι
καινούργια. Στο παράδειγμά μας, επειδή 2<3, και η β’ κλήση της
recur() θα εκτελεστεί μέχρι την 7η γραμμή. Δηλαδή θα προλάβει να
εμφανίσει
75
*** a=2
και στη συνέχεια θα κάνει την γ’ κλήση της. Τρίτη λοιπόν μεταβλητή
apot στη στοίβα και τρίτη παράμετρος a με τιμή 3. Τώρα όμως η
εκτέλεση περνάει στο else και η μεταβλητή apot γίνεται 0. Είναι
βασικό σημείο αυτό στη κατασκευή αναδρομικών συναρτήσεων. Πρέπει
κάπου να υπάρχει μια εντολή if για να υποχρεώνουμε την συνάρτηση
να επιστρέφει από κάποιο σημείο χωρίς να εκτελεί την αναδρομική
κλήση. Θα εμφανιστεί λοιπόν η γραμμή
a=3 apot=0
και με την εντολή return ολοκληρώνεται η γ’ κλήση της recur(). Και
τώρα; Με την επιστροφή κάθε αναδρομικής κλήσης, ο υπολογιστής
αφαιρεί από τη στοίβα τα τρέχουσα τοπικά μεγέθη και συνεχίζει την
εκτέλεση στο σημείο που κλήθηκε η συνάρτηση μέσα στη συνάρτηση.
Δηλαδή με την return ουσιαστικά επιστρέφουμε στην 7η γραμμή της
β’ κλήσης της συνάρτησης. Εκεί λοιπόν είναι recur(3) ίσο με το 0 και
το apot γεμίζει με την τιμή 10. Στη συνέχεια η printf εμφανίζει την
γραμμή
a=2 apot=10
αφού κατά την β’ κλήση η a είναι 2. Η return ολοκληρώνει και την β’
κλήση και έτσι επιστρέφουμε πια στην 7η γραμμή της α’ κλήσης. Έτσι
εκεί που το a είναι 1, οπότε a+1 είναι 2, έχουμε ότι recur(2) είναι ίσο
με 10 και άρα το apot είναι 20. Εμφανίζεται λοιπόν
a=1 apot=20
και με την return τελειώνει και η α’ κλήση, οπότε recur(1) γίνεται ίσο
με 20. Είναι η τιμή που παίρνει και η b, γι’ αυτό και από την main()
εμφανίζεται
b=20
Παρατηρήσεις
1)Πολλές φορές υπάρχουν ισοδύναμες λύσεις προβλημάτων
χωρίς αναδρομικότητα. Είναι οι επονομαζόμενες επαναληπτικές
76
εκδοχές των αλγορίθμων. Για παράδειγμα το παραγοντικό υπολογίζεται
και από τον παρακάτω, μη-αναδρομικό αλγόριθμο:
par(int n)
{ int apot=1;
char m;
for (m=2;m<=n;m++)
apot=apot*m;
return apot; }
Το μεγαλύτερο πλεονέκτημα των αναδρομικών λύσεων είναι ότι
δημιουργούν καθαρότερους και απλούστερους αλγορίθμους σε
ορισμένες περιπτώσεις όπως ταξινόμηση (quicksort) και τεχνητή
νοημοσύνη. Όσον αφορά την ταχύτητα εκτέλεσης οι μη-αναδρομικοί
αλγόριθμοι ορισμένες φορές έχουν μικρό πλεονέκτημα. Τέλος προσοχή
χρειάζεται στη χρήση των αναδρομικών συναρτήσεων γιατί υπάρχει
κίνδυνος υπερχείλισης της στοίβας (stack overflow).
2)Και η main() είναι μια συνάρτηση που μπορεί να κληθεί από
οποιαδήποτε άλλη συνάρτηση. Και μάλιστα μπορεί και η ίδια να
καλέσει τον εαυτό της, δηλαδή μπορεί να είναι και η ίδια η main()
αναδρομική συνάρτηση.
77
Συνάρτηση Σαν Παράμετρος Συνάρτησης
78
{ char st1[N], st2[N] ;
printf(" Δώσε 2 αλφαριθμητικά\n ");
gets(st1) ; gets(st2) ;
if ( (tolower(*st1) <= 'z' && tolower(*st1) >= 'a')
|| (tolower(*st2) <= 'z' && tolower(*st2) >= 'a'))
{ printf(“χρήση strcmp()\n”)
check(st1, st2, strcmp);
}
else { printf(“χρήση numcmp()\n”);
check(st1, st2, numcmp);
} }
Χρησιμοποιούμε για αυτό το παράδειγμα, εκτός της γνωστής strcmp(),
δύο συναρτήσεις συστήματος πολύ χρήσιμες. Η tolower() δέχεται
παράμετρο τύπου char και επιστρέφει τον αντίστοιχο πεζό χαρακτήρα.
Αν η παράμετρος δεν είναι κάποιος κεφαλαίος χαρακτήρας τότε δεν τον
αλλάζει καθόλου. Π.χ.
tolower(‘M’) =>‘m’, tolower(‘2’) =>‘2’, tolower(‘a’) =>‘a’.
Σημειώστε ότι στην ίδια βιβλιοθήκη για μεταβλητές τύπου char,
δηλαδή στην ctype.h, υπάρχει και η συνάρτηση toupper() που κάνει το
αντίστροφο: δίνει τον αντίστοιχο κεφαλαίο χαρακτήρα.
Η συνάρτηση atoi() μετατρέπει το string που δέχεται σαν
παράμετρο σε integer. Στην ίδια βιβλιοθήκη stdlib.h υπάρχουν και οι
atol(), atof() που κάνουν μετατροπές σε long και float αντίστοιχα.
Πως γίνεται η μετατροπή; Αν ο πρώτος χαρακτήρας δεν είναι ψηφίο ή
πρόσημο, τότε η μετατροπή σταματάει και το αποτέλεσμα είναι 0.
Αλλιώς ένα-ένα τα ψηφία περνούν και η μετατροπή σταματάει στο
πρώτο χαρακτήρα που δεν είναι ψηφίο. Π.χ.
atoi(“ed234”)=>0, atoi(“15x4d”)=>15, atoi(“-3.4qw”)=>-3
Παρατηρήσεις: Στη main() θα μπορούσαμε να δηλώσουμε τα
αλφαριθμητικά χρησιμοποιώντας δείκτες ως εξής :
79
char *st1, *st2;
Έτσι δεν θα υπήρχε και ο περιορισμός της διάστασης Ν=80. Θυμηθείτε
ότι τα strings είναι πίνακες και οι πίνακες χειρίζονται αμεσότερα με
δείκτες. Θυμηθείτε ακόμη ότι *st1 είναι το st1[0]. Oυσιαστικά λοιπόν
η περίπλοκη αυτή συνθήκη της if γίνεται ΑΛΗΘΕΙΑ όταν ο πρώτος
χαρακτήρας είτε του πρώτου string st1 είτε του δεύτερου string st2
είναι ένα από τα γράμματα του αγγλικού αλφάβητου, κεφαλαίο ή
μικρό. Την περίπτωση αυτή την ξεχωρίζουμε γιατί είδαμε ότι η atoi()
δίνει 0 και άρα χρειαζόμαστε σύγκριση αλφαβητική μέσω της γνωστής
strcmp(). Αν όμως και τα δύο strings έχουν έστω και ένα στην αρχή
ψηφίο, τότε η atoi() κάνει τις μετατροπές και γίνεται αριθμητική
σύγκριση μέσω της numcmp() που εμείς φτιάχνουμε.
Η numcmp(), όπως και η strcmp(), επιστρέφει 0 όταν οι
παράμετροι είναι ίσοι και 1 όταν είναι άνισοι. Παρατηρήστε τον τρόπο
που περνάμε τα αλφαριθμητικά σαν παραμέτρους συναρτήσεων. Δεν
διαφέρει από όσα αναφέραμε για τους πίνακες άλλων τύπων.
Εξακολουθούμε δηλαδή να προτιμούμε δείκτες (pointers) για τις
δουλειές αυτές. Η επικεφαλίδα της εναλλακτικά θα μπορούσε να
γραφεί με αδιάστατους πίνακες :
numcmp( char a[], char b[] )
Η check() θέλει προσοχή. Δηλώνει τις δύο πρώτες παραμέτρους
ως δείκτες χαρακτήρων, που είναι ισοδύναμοι με string. Δείτε όμως
πως δηλώνει την τρίτη παράμετρο που είναι δείκτης συνάρτησης.
Υπάρχει ο τύπος int και υποχρεωτικά σε παρένθεση το αστεράκι
ακολουθούμενο από το όνομα που θέλουμε να δώσουμε εμείς στη
παράμετρο συνάρτησης. Είναι λοιπόν (*cmp) και ακολουθείται πάλι
υποχρεωτικά από () γιατί έτσι αναγνωρίζει ο μεταγλωττιστής τα
ονόματα των συναρτήσεων. Πλήρες λοιπόν είναι int (*cmp) ().
80
Μέσα στη check() αναφερόμαστε πάλι με (*cmp) αλλά τώρα
ακολουθούν μέσα στις παρενθέσεις και τα ορίσματα a,b που θέλουμε.
Δείτε πράγματι ότι λέμε
if ( ! (*cmp) (a,b) ) ...
H cmp γίνεται είτε strcmp είτε numcmp από τη κλήση της check στη
main. Ότι όμως και να είναι το αποτέλεσμα 0 σημαίνει ίσα και το
διάφορο του μηδενός άνισα.
ΠΡΟΣΟΧΗ: Αν θέλουμε να δηλώσουμε μια μεταβλητή p σαν δείκτη σε
συνάρτηση πχ. ακέραια, τότε γράφουμε
int (*p) ();
Επίσης η διαδικασία εύρεσης της διεύθυνσης μιας συνάρτησης είναι
παρόμοια με τους πίνακες, που η διεύθυνσή τους βρίσκεται από το
όνομα τους ΧΩΡΙΣ τετραγωνισμένες αγκύλες και δείκτες-θέσεις.
Δηλαδή χρησιμοποιούμε το όνομα της συνάρτησης ΧΩΡΙΣ παρενθέσεις
και ορίσματα. Έτσι αν θελήσουμε η p να δείχνει στη συνάρτηση
strcmp, δίνουμε
p=strcmp;
Στο παράδειγμά μας τότε, μια κλήση
check(st1,st2,p) ;
θα γέμιζε την παράμετρο cmp με την συνάρτηση strcmp.
81
Πίνακες Περισσοτέρων Διαστάσεων
82
Πίνακες Αλφαριθμητικών
Χρησιμοποιούμε έναν δισδιάστατο πίνακα χαρακτήρων, όπου το
μέγεθος του αριστερού δείκτη καθορίζει τον αριθμό των strings και το
μέγεθος του δεξιού δείκτη καθορίζει το μέγιστο μήκος, μείον1, για το
κάθε string. Πχ. ένας πίνακας 10 strings με μήκος το πολύ 80
χαρακτήρων δηλώνεται :
char pin_str[10] [81];
Για να διαβάσουμε πχ. το 5ο string του πίνακα, αρκεί να καθορίσουμε
τον αριστερό δείκτη, δηλαδή δίνουμε
gets(pin_str[4]);
Δομέ ς
Είναι σύνθετοι τύποι που περιέχουν μεταβλητές διάφορων τύπων.
Αποτελούν ισοδύναμη έννοια με τα records στη γλώσσα Pascal. Είναι
η ιδανική λύση για την αποθήκευση δεδομένων, ΟΧΙ ίδιου τύπου, που
συνδέονται λογικά. Χρησιμοποιούμε τη λέξη-κλειδί struct για τον
ορισμό μιας δομής. Έτσι ο παρακάτω κώδικας
struct atomo {
char onoma[35] ;
unsigned etos_gen ;
char filo ;
float varos, ipsos ;
} petros, kostas ;
83
ορίζει μια δομή μέ το όνομα atomo που αποτελείται από 5 επιμέρους
στοιχεία-πεδία. Το πεδίο onoma είναι string 34 χαρακτήρων για το
ονοματεπώνυμο, το etos_gen είναι απρόσημος ακέραιος για το έτος
γέννησης, το filo είναι χαρακτήρας (‘Α’ για αρσενικό και ‘Θ’ για
θυληκό), το varos και το ipsos είναι πραγματικοί αριθμοί για το βάρος
και το ύψος αντίστοιχα. Τα petros και kostas είναι μεταβλητές τύπου
atomo. Mέσα στο πρόγραμμά μας λοιπόν μπορούμε να δουλέυουμε με
τα ονόματα petros και kostas και όχι με το όνομα atomo που είναι
απλά ένα όνομα τύπου δεομένων όπως int, float κλπ.
Θα μπορούσαμε να παραλείψουμε από τον ορισμό της δομής τις
δηλώσεις των μεταβλητών petros, kostas. Σ’ αυτή τη περίπτωση η
δήλωσή τους θα έπρεπε να γίνει τοποθετώντας μέσα στο πρόγραμμα
την παρακάτω πρόταση
struct atomo petros, kostas ;
οπουδήποτε στις δηλώσεις, αλλά μετά φυσικά από τον ορισμό της
δομής atomo.
Eπίσης δεν είναι υποχρεωτικό να δώσουμε όνομα κατά τον
ορισμό μιας δομής. Δηλαδή θα μπορούσε να παραληφθεί η λέξη atomo
και η δομή να οριζόταν με τον παρακάτω τρόπο :
struct {
char onoma[35] ;
.....
} petros, kostas ;
Τότε όμως οι δηλώσεις των μεταβλητών petros και kostas,
YΠΟΧΡΕΩΤΙΚΑ θα έπρεπε να υπάρχουν, γιατί μια ανώνυμη δομή δεν
μπορούμε αργότερα να την χρησιμοποιήσουμε για δηλώσεις
μεταβλητών της.
84
Ο τελεστής-τελεία χρησιμοποιείται για να αναφερόμαστε στο
πεδίο της δομής που κάθε φορά θέλουμε. Έτσι η παρακάτω εντολή
gets(petros.onoma);
ζητάει από τον χρήστη-πληκτρολόγιο ένα string το οποίο πλέον θα
αποτελεί το περιεχόμενο του πεδίου onoma της μεταβλητής petros.
Παρόμοια, η εντολή
printf(“%3.2f”, kostas.ipsos) ;
εμφανίζει με ακρίβεια 2 δεκαδικών ψηφίων το πεδίο ipsos της
μεταβλητής kostas.
Μπορούμε να δώσουμε σε μια μεταβλητή δομής αρχική τιμή,
μαζί με την δήλωσή της. Για παράδειγμα η παρακάτω δήλωση
struct atomo nikos =
{“Νίκος Αποστόλου”,1966,’Α’,85.5,1.77};
που καταχωρεί στα πεδία της μεταβλητής nikos τις αντίστοιχες τιμές.
Π.χ. βάρος 85,5 κιλά, έτος γέννησης 1966 κλπ.
Πίνακες Δομών
Oι Πίνακες Δομών είναι ο πιο συνηθισμένος τρόπος για να
οργανώνουμε τα δεδομένα μας. Για να δηλώσουμε ένα πίνακα δομών
πρέπει πρώτα να ορίσουμε την δομή και μετά να ορίσουμε μια
μεταβλητή-πίνακα αυτού του τύπου. Έτσι γράφουμε
struct atomo pelates[200] ;
και δημιουργούμε 200 μεταβλητές τύπου atomo. Δηλαδή η καθεμία
από αυτές έχει 5 πεδία για να κρατάει τα στοιχεία όνομα, έτος, φύλο,
βάρος και ύψος. Το γενικό όνομα αυτών των 200 μεταβλητών είναι
pelates.
Όλα όσα έχουμε πεί για τους πίνακες ισχύουν και στους Πίνακες
Δομών. Εξακολουθεί λοιπόν η θέση 0 να είναι η πρώτη θέση ενός
πίνακα. Και έχουμε στη διάθεσή μας είτε τους δείκτες-θέσης είτε τους
85
pointers για να μετακινούμαστε στις διάφορες θέσεις των πινάκων των
δομών μας. Για παράδειγμα η παρακάτω εντολή
printf(“%u”, pelates[4].etos_gen);
εμφανίζει το έτος γέννησης του 5ου πελάτη μας, κατά σειρά
αποθήκευσης μέσα στον πίνακα.
Ενώσεις (Unions)
Η ένωση επιτρέπει να αποθηκεύουμε στον ίδιο χώρο μνήμης
πολλές διαφορετικές μεταβλητές, οι οποίες μπορεί να είναι ακόμη και
διαφορετικού τύπου. Ορίζουμε μια ένωση με τον ίδιο τρόπο που το
κάνουμε και στις δομές. Χρησιμοποιούμε όμως τη λέξη-κλειδί union
αντί της struct. Π.χ. union sos {
int k ;
char ch ;
float pr;
} metr ;
Όσα είπαμε για τις δηλώσεις μεταβλητών στις δομές, ισχύουν και για
τις ενώσεις. Δηλαδή το όνομα της ένωσης sos και το όνομα της
μεταβλητής metr δεν είναι απαραίτητα, αλλά ΔΕΝ μπορούν να λείπουν
ΚΑΙ ΤΑ ΔΥΟ ταυτόχρονα. Γιατί αν δεν βάλουμε το sos, τότε ορίσαμε
έναν ανώνυμο τύπο ένωσης και όσες μεταβλητές αυτού του τύπου
χρειαστούμε θα πρέπει να γραφούν εκεί που βρίσκεται η metr. Αν πάλι
δεν βάλουμε το metr, τότε πρέπει να υπάρχει το όνομα sos για να
μπορούμε να δηλώσουμε μεταβλητές με μια πρόταση όπως η παρακάτω
:
union sos times1, times2 ;
Όταν δηλώνουμε μια ένωση, η C δημιουργεί αυτόματα μια
μεταβλητή αρκετά μεγάλη, έτσι ώστε να μπορεί να αποθηκεύει τον
μεγαλύτερο τύπο από τα πεδία της ένωσης. Δηλαδή στο παράδειγμά
μας δεσμεύονται 4 bytes γιατί ο μεγαλύτερος τύπος της ένωσης είναι ο
86
float που απαιτεί τόσα ακριβώς. Προσοχή όμως!!! Ένα μόνο από τα
πεδία μπορούμε κάθε φορά να χρησιμοποιήσουμε. Δηλαδή την ίδια
στιγμή μπορούμε στο παράδειγμα της ένωσης sos, να αποθηκεύσουμε
ΕΙΤΕ έναν ακέραιο, ΕΙΤΕ έναν χαρακτήρα, ΕΙΤΕ τέλος έναν
πραγματικό. Και αυτό συμβαίνει γιατί το πρώτο από τα 4 δεσμευμένα
bytes το χρησιμοποιούν και τα 3 πεδία της ένωσης. Το δεύτερο byte
στη συνέχεια το χρειάζονται μόνο τα πεδία k και pr, ενώ τα δύο
τελευταία bytes τα χρειάζεται μόνο το pr. H πρόσβαση στα διάφορα
πεδία μιας ένωσης γίνεται με τον ίδιο τελεστή που χρησιμοποιούν και
οι δομές. Δηλαδή με τον τελεστή-τελεία. Όμως ο τρόπος δέσμευσης-
οικονομίας της μνήμης που ακολουθούν οι unions και που αναφέραμε
παραπάνω μας θέτουν κάποιους περιορισμούς.
Π.χ. times1.ch = ‘G’ ;
printf(“%d”,times1.k * 1.18);
Oι γραμμές αυτές κώδικα ΕΙΝΑΙ ΛΑΘΟΣ γιατί αποθηκεύεται σε μια
ένωση times1 ένας χαρακτήρας και στην επόμενη γραμμή
χρησιμοποιείται η ένωση αυτή σαν να περιέχει ακέραιο. Για να γίνει
ακόμη πιο κατανοητό το ζήτημα δείτε και τις επόμενες γραμμές κώδικα
με τα σχόλια τους :
metr.k=1966; /* το 1966 αποθηκεύεται στη metr και
χρησιμοποιήθηκαν 2 bytes */
metr.ch=‘C’; /* το 1966 χάθηκε, αποθηκεύτηκε το ‘C’
και χρησιμοποιήθηκε 1 byte */
metr.pr=85.5; /* το ‘C’ χάθηκε, αποθηκεύτηκε το 85,5
και χρησιμοποιήθηκαν 4 bytes */
H Δήλωση typedef
87
Mε την λέξη-κλειδί typedef μπορούμε να ορίσουμε νέα ονόματα
για υπάρχοντες τύπους. Η γενική μορφή είναι
typedef τύπος ΟΝΟΜΑ;
Π.χ. typedef float REAL;
To REAL είναι πια ένα επιπρόσθετο όνομα για τον τύπο float.
Μπορούμε δηλαδή να δηλώσουμε μια πραγματική μεταβλητή
apotelesma με οποιαδήποτε από τις ακόλουθες γραμμές :
float apotelesma; /* είναι ισοδύναμες οι 2 γραμμές */ REAL
apotelesma;
Συνήθως το όνομα το βάζουμε με κεφαλαία γράμματα, όπως και τις
σταθερές, για να υπενθυμίζουν σ’ αυτόν που βλέπει τον κώδικα ότι
πρόκειται για μια συντομογραφία συμβολική.
Η εμβέλεια αυτού του επιπλέον ονόματος εξαρτάται από την
θέση της πρότασης typedef. Aν η θέση της είναι μέσα σε μια
συνάρτηση τότε φυσικά το επιπλέον όνομα ισχύει μόνο εκεί, τοπικά.
Μπορούμε όμως να έχουμε και καθολικής εμβέλειας νέα ονόματα
τύπων.
Οι δηλώσεις typedef χρησιμοποιούνται για την τεκμηρίωση
κυρίως των προγραμμάτων μας: Επιτρέπουν περιγραφικά ονόματα για
τους καθιερωμένους τύπους δεδομένων και έτσι ο κώδικας που
γράφουμε αποκτά κάποιο προσωπικό ύφος.
Επίσης ένας άλλος λόγος για την χρήση της typedef είναι ότι
διευκολύνουν την φορητότητα του κώδικα: Ανάλογα με το μηχάνημα, η
C δεσμεύει διαφορετικό πλήθος bytes για την αποθήκευση των βασικών
της τύπων όπως int, float κλπ. Βάζοντας λοιπόν στα προγράμματά μας
τις μεταβλητές να δηλώνονται με ονόματα τύπων από typedef,
μπορούμε κάθε φορά που αλλάζουμε μηχάνημα να αλλάζουμε απλά τις
δηλώσεις typedef.
Π.χ. η δήλωση typedef short diobytes;
88
προγράμματος σε μηχάνημα με τύπο short των 2 bytes, θα γίνει
typedef int diobytes;
όταν περάσουμε σε μηχάνημα με int των 2 bytes.
89
Aν χρησιμοποιήσουμε το σύμβολο της ισότητας στον ορισμό
μιας απαρίθμησης, μπορούμε να αλλάξουμε τις αντιστοιχίες.
Π.χ. enum xromata {kokino,prasino=6,mavro,ble=21};
printf(“%d %d %d %d”, kokino,prasino,mavro,ble):
θα εμφανίσει το 0 6 7 21 στην οθόνη.
ΠΡΟΣΟΧΗ
Μην ξεχνάτε ότι τα στοιχεία-σύμβολα όπως kokino, prasino
κλπ. ΔΕΝ ΕΙΝΑΙ αλφαριθμητικά αλλά απλώς είναι ονόματα κάποιων
ακεραίων. Έτσι η είσοδος και η έξοδος δεν γίνεται απευθείας, π.χ.
printf(“%s”, xro1); /* ΛΑΘΟΣ */
αλλά χρησιμοποιούμε συνήθως εντολή switch . Δηλαδή :
switch (xro1) {
case kokino : printf(“κόκινο”);
break ;
case prasino : printf(“πράσινο”);
break ;
...
}
90
Προχωρημένα Θέματα Δομών
Πίνακες και Δομέ ς μέ σα σε Δομέ ς
Ένα στοιχείο δομής μπορεί να είναι απλό ή σύνθετο. Δείτε το
παρακάτω :
struct a {
int x[5][5]; /* πίνακας 5x5 */
float y;
} help;
Για να αναφερθούμε ας πούμε στον ακέραιο της 1ης γραμμής και 2ης
στήλης, γράφουμε help.x[0][1].
Όταν μάλιστα μια δομή έχει σαν στοιχείο άλλη δομή, όπως
παρακάτω, τότε μιλάμε για ένθετες δομές :
struct addr { char name[30];
char street[40];
char city[15]; } ;
struct emp { struct addr dieft;
float misthos;
} ergatis ;
Η δομή emp έχει στοιχείο μια δομή τύπου addr. Ισχύουν όλα όσα
έχουν αναφερθεί. Για παράδειγμα, δίνουμε την τιμή Κιλκίς στο πεδίο
city του ergatis με την εντολή:
strcpy(ergatis.dieft.city,”Κιλκίς”);
91
Θυμηθείτε ότι μια συνάρτηση επηρεάζει τα ορίσματά της, μόνο όταν
κάνουμε κλήση κατά αναφορά. Βέβαια αν b ήταν πεδίο πίνακα κάποιου
τύπου, τότε θα γράφαμε
func(met1.b) ; /* κλήση κατά αναφορά */
γιατί έχουμε πει ότι τα ονόματα πινάκων είναι διευθύνσεις από μόνα
τους.
Τι κάνουμε όμως όταν θέλουμε μεταβίβαση ολόκληρων δομών; Η
δομή σαν όρισμα διαφέρει στη συμπεριφορά της με τον πίνακα. Δηλαδή
το όνομα μιας δομής ΔΕΝ είναι δείκτης στη διεύθυνσή της. Έτσι στο
παρακάτω παράδειγμα έχουμε κλήση κατά αξία για την δομή par.
Παράδειγμα 1ο:
#include <stdio.h>
struct s_type {
int a,b;
char ch;
char m[11];
};
void f1(struct s_type param)
{ printf("f1:%d\n",param.a);
}
void f2(int z)
{ printf("f2:%d\n",z);
}
void f3(int *z)
{ *z=500;
}
void f4 (char *k)
{ printf("δώσε string: ");
gets(k);
}
92
void main()
{
struct s_type arg;
arg.a=200;
f1(arg);
f2(arg.a);
f3(&arg.a);
f1(arg);
f4(arg.m);
puts(arg.m);
}
Το αποτέλεσμα θα είναι
f1:200
f2:200
f1:500
δώσε string:
Δίνοντας εκεί οποιαδήποτε σειρά χαρακτήρων, αυτή θα επαναληφθεί
από κάτω.
Ας δούμε από την αρχή τις συναρτήσεις. Η συνάρτηση f1 με
όρισμα δομή τύπου s_type, θα εμφανίσει f1:200, αφού αυτή είναι η
τιμή του πεδίου a της δομής arg. Οι υπόλοιπες συναρτήσεις έχουν
ορίσματα που είναι όχι ολόκληρες δομές, αλλά πεδία δομών. Έτσι η f2
κάνει κλήση κατά αξία στο ακέραιο πεδίο της δομής, ενώ η f3 κάνει
κλήση κατά αναφορά. H f2 είναι ισοδύναμη με την f1. Γι’ αυτό
εμφανίζει ίδιο αποτέλεσμα f2:200. Όμως η πρώτη δέχεται παράμετρο
δομή, ενώ η δεύτερη δέχεται ακέραιο. Η f3 είναι παράδειγμα
συνάρτησης που τροποποιεί πεδίο δομής. Δίνει τιμή 500 στο πεδίο a,
οπότε στη συνέχεια η f1 εμφανίζει f1:500.
Τέλος, παρατηρήστε ότι η f4 κάνει κλήση κατά αναφορά, χωρίς
να χρειαστεί να βάλει τον χαρακτήρα &. Βέβαια στους ορισμούς των
93
συναρτήσεων f3 και f4, οι παράμετροι είναι δείκτες (int * και char *
αντίστοιχα), για να επιδράσουν στην δομή arg της main.
Προσοχή: Για κλήση δομής κατά αναφορά πρέπει να χρησιμοποιήσουμε
Δείκτες σε Δομές.
Οδηγία #include
Δίνει εντολή στον μεταγλωττιστή να συμπεριλάβει ένα άλλο
αρχείο κώδικα στο τρέχον αρχείο. Υπάρχουν δύο παραλλαγές :
#include “stdio.h”
#include <stdio.h>
Και οι δύο λένε ότι το αρχείο επικεφαλίδας stdio.h πρέπει να
διαβαστεί και να μεταγλωττιστεί από τα υποπρογράμματα βιβλιοθήκης.
Αν καθορίσουμε και διαδρομή μαζί με το όνομα του αρχείου, πχ.
c:\test\mylib.h, τότε η αναζήτηση θα γίνει μόνο σε αυτή τη διαδρομή.
Αν δεν καθορίσουμε διαδρομή και κλείσουμε το όνομα του αρχείου σε
εισαγωγικά, η αναζήτηση ξεκινάει από τον τρέχοντα κατάλογο. Αν δεν
καθορίσουμε διαδρομή και κλείσουμε το όνομα αρχείου σε γωνιώδεις
παρενθέσεις, τότε η αναζήτηση ξεκινάει από τους καταλόγους που
94
έχουμε ορίσει κατά την προσαρμογή του ολοκληρωμένου
περιβάλλοντος στις ανάγκες μας.
Είναι σημαντικό ότι ο μεταγλωττιστής ενσωματώνει στο
πρόγραμμά μας, μόνο όσες από τις πληροφορίες του αρχείου
συμπερίληψης είναι απαραίτητες. ΄Ετσι όταν συμπεριλάβουμε ένα
αρχείο άχρηστο, το πρόγραμμά μας δεν γίνεται μεγαλύτερο.
Τα αρχεία “συμπερίληψης” μπορούν να περιέχουν οδηγίες
#include. Είναι η περίπτωση των ένθετων συμπεριλήψεων. Και μάλιστα
συνήθως η κατάληξη .h χρησιμοποιείται για τα αρχεία επικεφαλίδας,
δηλαδή για αυτά που έχουν πληροφορίες που πηγαίνουν στην αρχή ενός
προγράμματος όπως είναι οι εντολές προεπεξεργαστή. Φυσικά εκτός
από αυτά που έρχονται μαζί με την γλώσσα, όπως stdio.h, string.h
κλπ., μπορούμε να δημιουργούμε και δικά μας αρχεία συμπερίληψης.
Σημειώστε τέλος ότι και ο προεπεξεργαστής αναγνωρίζει τα σύμβολα
των σχολίων /* και */, οπότε μπορούμε να βάζουμε σχόλια σε αρχεία
μας που θα ενσωματωθούν σε άλλα προγράμματα.
Οδηγία #define
Μέχρι τώρα είδαμε ότι μπορεί να ορίσει συμβολικές σταθερές.
Π.χ.
#define SOS “Καλημέρα”
#define MAX. 1000
#define TELOS ‘@’
Αυτό λέει στον μεταγλωττιστή κάθε φορά που συναντά στον κώδικα το
SOS να το αντικαθιστά με το string Καλημέρα. Παρόμοια το ΜΑΧ και
το TELOS να αντικατασταθούν αντίστοιχα με τον ακέραιο 1000 και
τον χαρακτήρα @. Σημειώστε ότι η οδηγία #define μπορεί να μπεί
οπουδήποτε μέσα στο πρόγραμμα.
Η γενική μορφή της οδηγίας είναι
#define όνομα συμβολοσειρά
95
όπου η συμβολοσειρά είναι μέσα σε διπλά εισαγωγικά μόνον όταν
ορίζουμε συμβολική σταθερά τύπου string. Συνήθως το όνομα, γνωστό
και ως μακρο-όνομα (macro-name), το δίνουμε με ΚΕΦΑΛΑΙΟΥΣ
χαρακτήρες για να αναγνωρίζεται γρήγορα μέσα σε ένα πρόγραμμα ότι
αποτελεί συμβολική σταθερά. Και βέβαια όταν ορίσουμε ένα μακρο-
όνομα, μπορούμε να το χρησιμοποιήσουμε στους ορισμούς άλλων
μακρο-ονομάτων.
Η διαδικασία αντικατάστασης από τον προεπεξεργαστή των
συμβολικών σταθερών με τις αντίστοιχες τιμές τους, ονομάζεται
μακρο-αντικατάσταση. Δεν είναι παρά μια αντικατάσταση ενός
αναγνωριστικού ονόματος με μια συγκεκριμένη τιμή ακέραια,
χαρακτήρα κλπ. Όταν η τιμή αυτή, δηλαδή η συμβολοσειρά είναι
μεγαλύτερη από μία γραμμή, μπορούμε να την συνεχίσουμε στην
επόμενη τοποθετώντας ένα \ στο τέλος της γραμμής :
#define BIG “αυτό είναι ένα πολύ μεγάλο \
αλφαριθμητικό “
Το μακρο-όνομα λέγεται και μακροεντολή όταν η συμβολοσειρά
είναι έκφραση της C, όπως στο παρακάτω παράδειγμα :
#define EMFX printf(“Το Χ είναι %d\n”, x)
Μπορούμε μέσα σε μια συνάρτηση να γράψουμε EMFX; και τότε θα
εμφανιστεί το μήνυμα
Το Χ είναι 12
αν είχε την τιμή 12 το x.
Μακροεντολέ ς με Ορίσματα
Η οδηγία #define έχει ένα ισχυρό χαρακτηριστικό: Το μακρο-
όνομα μπορεί να έχει και ορίσματα. Μια τέτοια μακροεντολή με
ορίσματα μοιάζει πάρα πολύ με μια συνάρτηση, γιατί τα ορίσματα
96
περικλείονται μέσα σε παρενθέσεις. Έτσι οι μακροεντολές αυτές είναι
γνωστές και ως συναρτήσεις μακροεντολών.
Παράδειγμα 2ο:
#define TETR(x) x*x
#define EM(x) printf(“είναι %d\n”, x)
#define MIN(a,b) (a<b) ? a : b
#include <stdio.h>
void main()
{ int x=4, k;
k= TETR(x);
EM(k);
k= TETR(3);
EM(k);
printf(“το ελάχιστο είναι %d\n”, MIN(x,k)); }
Θα εμφανιστεί ως αποτέλεσμα:
είναι 16
είναι 9
το ελάχιστο είναι 4
Η χρήση μακρο-αντικαταστάσεων στη θέση πραγματικών
συναρτήσεων έχει σαν πλεονέκτημα την αύξηση της ταχύτητας του
κώδικα γιατί αποφεύγουμε την κλήση συνάρτησης. Επίσης άλλο ένα
πλεονέκτημα είναι ότι στις μακροεντολές δεν μας απασχολεί ο τύπος
των μεταβλητών, γιατί διαχειρίζονται συμβολοσειρές και όχι
πραγματικές τιμές. Πράγματι, στο παράδειγμά μας, η μακροεντολή
TETR(x) μπορεί άνετα να χρησιμοποιηθεί και με μεταβλητές int και
με float. Ωστόσο η χρήση πραγματικών συναρτήσεων κάνει τα
προγράμματά μας να καταλαμβάνουν λιγότερο χώρο, γιατί οι
μακροεντολές αυξάνουν το μέγεθος των προγραμμάτων μας. Έτσι
ανάλογα με τις ανάγκες μας σε μνήμη και ταχύτητα, κάνουμε τις
επιλογές μας.
97
Μεταγλώττιση υπό συνθήκη
Ορισμένες οδηγίες προς τον επεξεργαστή μας βοηθούν να
παράγουμε αρχεία που μπορούν να μεταγλωττιστούν με πολλούς
τρόπους. Συγκεκριμένα, μπορούμε να ζητήσουμε από το σύστημα να
“βλέπει” ορισμένα μόνο τμήματα του κώδικά μας, ανάλογα με τη
περίπτωση.
98
Η #else αποτελεί την εναλλακτική περίπτωση όταν αποτύχει η
#if. Δηλαδή παίζει τον ρόλο της else στην εντολή if. Kαι μάλιστα
υπάρχει και η δυνατότητα κλιμάκωσης των if/else/if αφού η οδηγία
#elif ισοδυναμεί με την else if.
Παράδειγμα 4ο:
#define SYS “IBMPC”
#if SYS == “IBMPC”
#include <ibmpc.h>
#elif SYS == “MAC”
#include <mac.h>
#else
#include “general.h”
#endif
Το αποτέλεσμα είναι να συμπεριληφθεί μόνο το αρχείο “ibmpc.h”.
Αλλά αν στην οδηγία #define αλλάζουμε την τιμή, μπορούμε κάθε
φορά να επιλέγουμε ποιό αρχείο θα συμπεριλάβουμε. Γίνεται λοιπόν
κατανοητό ότι οι οδηγίες αυτές χρησιμεύουν πολύ στην παραγωγή και
συντήρηση εξειδικευμεύνων εκδόσεων του ίδιου προγράμματος.
99
void main() {
#ifdef US
printf(“δολάριο\n”);
#else
printf(“δεν ορίστηκε χώρα\n”);
#endif
#ifndef GREECE
printf(“δεν ορίστηκε η Ελλάδα\n”);
#else
printf(“δραχμή \n“);
#endif
}
Θα εμφανιστεί
δολάριο
δεν ορίστηκε η Ελλάδα
γιατί το US είναι ορισμένο, ενώ το GREECE όχι.
Προσοχή
Πριν τον χαρακτήρα # επιτρέπονται μόνο κενοί χαρακτήρες. Και επειδή
τις γραμμές αυτές τις βλέπει μόνο ο προεπεξεργαστής, τις εντολές για
το κάθε τμήμα #if, #else, και #elif τις βάζουμε να ξεκινάνε από νέα
γραμμή.
Λίστες
Γραμμική Λίστα
100
Γραμμική λίστα είναι κάθε σύνολο από n στοιχεία - λέγονται
κόμβοι - που έχουν την ιδιότητα ότι κάθε ενδιάμεσο στοιχείο k
ακολουθεί το k-1 και προηγείται του στοιχείου k+1. Έτσι και οι
πίνακες που είδαμε, είναι μια μορφή γραμμικής λίστας.
Ανάλογα με τον τρόπο υλοποίησής τους, τις διακρίνουμε σε
σειριακές(sequential) και συνδεδεμένες(linked). Οι πρώτες
χρησιμοποιούν συνεχόμενες θέσεις μνήμης για την αποθήκευση των
κόμβων, ενώ οι δεύτερες αποθηκεύουν τους κόμβους σε
απομακρυσμένες θέσεις μνήμης οι οποίες όμως μεταξύ τους είναι
συνδεμένες.
Οι σειριακές λέγονται και στατικές γιατί καθώς
προγραμματίζουμε τις λειτουργίες των λιστών, πρέπει να καθορίσουμε
ακριβώς και την μνήμη που απαιτούν οι λίστες, ανεξάρτητα από τα
τρέχοντα δεδομένα. Έτσι θυμηθείτε δουλεύαμε με τους πίνακες.
Αντίθετα οι συνδεδεμένες, λέγονται επιπλέον στη γλώσσα C
(αλλά και στη Pascal), δυναμικές, γιατί μπορούμε κατά την εκτέλεση
του προγράμματος να δεσμεύσουμε ή να ελευθερώσουμε μνήμη,
ανάλογα με τις ανάγκες μας.
Όλες οι λίστες μπορούν να κατασκευαστούν τόσο με σειριακό,
όσο και με συνδεδεμένο τρόπο. Οι σειριακές παρουσιάζουν
προβλήματα ταχύτητας στην εισαγωγή και διαγραφή ενδιάμεσων
κόμβων. Επίσης δεν μπορούν να εκμεταλλευτούν τις δυνατότητες
δυναμικής κατανομής της μνήμης μιας γλώσσας όπως η C. Έτσι, λόγω
οικονομίας μνήμης προτιμούμε τις δυναμικές συνδεδεμένες λίστες, αν
και απαιτούν σημαντικές ικανότητες προγραμματισμού.
101
θα μπορέσουμε να υλοποιήσουμε τις συνδεδεμένες λίστες που μας
ενδιαφέρουν.
Χρησιμοποιούμε κυρίως τις δύο συναρτήσεις malloc() και free()
της βιβλιοθήκης “stdlib.h”. Οι συναρτήσεις αυτές συνεργάζονται για
να χρησιμοποιήσουν την ελεύθερη μνήμη που υπάρχει στο σύστημά
μας και λέγεται σωρός(heap). Είναι ο χώρος ανάμεσα στη μόνιμη
περιοχή αποθήκευσης(με το πρόγραμμα, τις καθολικές και τις στατικές
μεταβλητές) και στη στοίβα(με τις τοπικές μεταβλητές).
Κάθε φορά που ζητάμε μνήμη με τη malloc(), κατανέμεται ένα
τμήμα της απομένουσας ελεύθερης μνήμης, ενώ κάθε φορά που
καλούμε την free() επιστρέφεται μνήμη στο σύστημα.
Η malloc() παίρνει σαν όρισμα τον αριθμό bytes που θέλουμε να
δεσμεύσουμε. Γι’ αυτό και συνήθως χρησιμοποιούμε την sizeof για τον
προσδιορισμό των απαιτήσεων του κάθε τύπου δεδομένων. Η malloc()
επιστρέφει ένα δείκτη void. Τι σημαίνει αυτό; Σημαίνει ότι πρέπει με
άμεση προσαρμογή τύπου(casting) να αλλάξουμε τον τύπο του δείκτη
που επιστρέφεται από την malloc() σε ένα δείκτη του τύπου που
θέλουμε.
H free() παίρνει σαν όρισμα έναν δείκτη. Η C θυμάται πόση
μνήμη δεσμεύτηκε με την malloc(), και στη συνέχεια αποδεσμεύει
όλον αυτό τον χώρο.
Παράδειγμα 1ο: Να κατανεμηθεί χώρος αποθήκευσης για 10
ακέραιους, να εμφανιστούν οι τιμές τους και στη συνέχεια να
αποδεσμευτεί η μνήμη και θα επιστραφεί στο σύστημα.
#include <stdlib.h>
#include <stdio.h>
#define M 10
void main()
{int *p;
char ch;
102
p = (int *) malloc(M*sizeof(int)); /* casting */
if (!p) /* έλεγχος αποτυχίας της malloc */
printf("δεν υπάρχει μνήμη ");
else
{ for (ch=0;ch<M;ch++)
*(p+ch) = ch+100;
/* ισοδύναμο με p[ch]=ch +100 */
for (ch=0;ch<M;ch++)
printf("%d\n",*(p+ch));
free(p);
} }
Προσέξτε στην 7η γραμμή την προσαρμογή του void τύπου που
επιστρέφει η malloc(), σε int. Επίσης από κάτω υπάρχει έλεγχος για το
αν η κλήση δέσμευσης μνήμης ήταν επιτυχής. Πρέπει πάντα πριν τη
χρήση ενός δείκτη που επιστράφηκε από τη malloc(), να ελέγχουμε
την επιστρεφόμενη τιμή ως προς το μηδέν. Διότι αν η μνήμη δεν είναι
αρκετή η malloc επιστρέφει ένα μηδενικό(null). Μόνο αν δεν υπάρξει
πρόβλημα, επιστρέφει από τον σωρό έναν δείκτη void, που υποδεικνύει
το πρώτο byte της μνήμης που κατανεμήθηκε.
Δείκτες σε Δομέ ς
Έχουμε ήδη αναφέρει ότι η κλήση κατά αναφορά συναρτήσεων
με ορίσματα δομές, μπορεί να γίνει μόνο με χρήση δεικτών. Αλλά και
οι δυναμικές λίστες κατασκευάζονται μόνο με δείκτες δομών. Έτσι το
θέμα αξίζει προσοχής. Ιδιαίτερα προσέξτε τον τελεστή εμμέσου μέλους
“->“ για τη προσπέλαση πεδίων δομών, όταν δείκτης δείχνει τη δομή.
Ακόμη σημειώστε τη χρήση typedef που αν και δεν είναι υποχρεωτική,
είναι πολύ διαδεδομένη.
Παράδειγμα 2ο:
#include <stdio.h>
103
#include <stdlib.h>
void main()
{ struct vivlio
{ char *titlos ;
unsigned timi;
} *vptr;
typedef struct vivlio VIVLIO ;
vptr = (VIVLIO *) malloc(sizeof(VIVLIO));
vptr->timi=12000; /* (*vptr).timi =12000; */
vptr->titlos="Mathematica";
printf("%s %u",vptr->titlos,vptr->timi);
}
Θα εμφανίσει φυσικά “Mathematica 12000”, δηλαδή τα περιεχόμενα
μιας δομής βιβλίου που εμείς γεμίσαμε. Το σημαντικό είναι ότι ΔΕΝ
δηλώσαμε καμιά μεταβλητή βιβλίου. Κατασκευάσαμε δείκτη vptr στη
δομή και μετά δεσμεύσαμε μνήμη ικανή για αποθήκευση των
δεδομένων ενός βιβλίου. Μέσα σε σχόλια φαίνεται η ισοδύναμη -
γνωστή εντολή για προσπέλαση πεδίου χωρίς τον τελεστή ‘->‘.
ΠΡΟΣΟΧΗ:Επειδή δηλώσαμε το string titlos ως δείκτη και όχι ως
πίνακα, μπορούμε να του αποδώσουμε τιμή με μια σταθερά εντός
διπλών εισαγωγικών. Δηλαδή αν αντί για *char δίναμε πχ. char[20],
τότε η απόδοση θα γραφόταν μόνο ως
strcpy(vptr->titlos,”Mathematica”);
104
κόμβος, ονομάζονται αντίστοιχα κεφαλή και ουρά της λίστας. Το πεδίο
δείκτη της ουράς περιέχει την τιμή NULL.
Παράδειγμα 3ο: Πρόκειται για κατασκευή μιας διατεταγμένης
λίστας με ακέραιους που δίνονται σε τυχαία σειρά από το
πληκτρολόγιο. Περιλαμβάνει ΕΙΣΑΓΩΓΗ και ΔΙΑΓΡΑΦΗ κόμβου σε
οποιοδήποτε σημείο της λίστας χρειαστεί(κεφαλή, ουρά ή ενδιάμεσα),
έτσι ώστε να συνεχίσει να μένει ταξινομημένη. Για επιτάχυνση των
αναζητήσεων, χρησιμοποιούμε έναν επιπλέον πλασματικό κόμβο -
”σκοπό”, όπου βάζουμε την τιμή που αναζητούμε. Ο δείκτης end
υπάρχει για να διακρίνουμε τον κόμβο αυτό από όλους τους άλλους. Ο
σκοπός βρίσκεται στο τέλος της λίστας. Ο δείκτης start δείχνει την
αρχή της λίστας μας.
#include <stdlib.h>
#include <stdio.h>
#define NULL 0
struct node
{ int timi;
struct node *next;
} ;
typedef struct node TIMH;
TIMH *start, *end, *p, *q;
int x;
TIMH *getnode()
{ TIMH *p;
p = (TIMH *) malloc(sizeof(TIMH));
if (p==NULL) {
printf("μνήμη ανεπαρκής");
exit(1); }
return p;
}
105
void emfanisi()
{ printf("Εμφάνιση\n");
for (p=start; p != end; p=p->next)
printf("%d\n",p->timi);
}
char anazito()
{ end->timi=x;
p=start;
while ( p->timi < x)
p=p->next;
if (p->timi == x && p != end)
return 1 ; else return 0 ;
}
void main()
{ /* σχηματισμός άδειας λίστας */
start=end=getnode();
printf("Δώσε ακέραιους ή # για τέλος\n");
while ( scanf("%d",&x) > 0)
{ if (anazito())
printf("υπάρχει ήδη\n");
else
{q=getnode();
if ( p == end) end=q;
else *q = *p ;
p->next = q;
p->timi=x;
} }
emfanisi();
while(getchar() == '\n');
printf(“Διαγραφές\n”);
106
printf("Δώσε ακέραιους ή # για τέλος\n");
while ( scanf("%d",&x) > 0)
{ if (anazito())
{ q=p->next;
if ( q == end) end=p;
else *p = *q ;
free(q);
emfanisi();
} else printf("Δεν υπάρχει\n");
} }
Η getnode() επιστρέφει δείκτη για κόμβο, δηλαδή δεσμεύει
μνήμη όταν όλα πάνε καλά. Αλλιώς με την exit, σταματάει την
εκτέλεση του προγράμματος δίνοντας κωδικό λάθους 1. Η emfanisi()
χρησιμοποιεί δείκτη p για να διατρέξει τη λίστα από την αρχή μέχρι το
τέλος, δηλαδή τον σκοπό.
Η anazito() επιστρέφει 1 όταν η τιμή που ψάχνουμε υπάρχει στη
λίστα και 0 όταν δεν υπάρχει. Χρησιμοποιείται και στις εισαγωγές
αλλά και στις διαγραφές. Στην main(), προσέξτε ότι ελέγχουμε στην
εντολή scanf αν έγινε ή όχι απόδοση τιμής στην μεταβλητή x, για να
καταλάβουμε το σημάδι τέλους των δεδομένων του χρήστη. Τέλος
προσέξτε ότι και στην εισαγωγή και στην διαγραφή υπάρχει εντολή if
που προβλέπει ειδικές περιπτώσεις: Όταν ο νέος κόμβος πρέπει να
είναι ο νέος σκοπός(end) και όταν ο κόμβος που διαγράφεται βρίσκεται
πριν τον σκοπό.
107
σχηματίζουν σωρό με κεφαλή το δείκτη start. Η εισαγωγή κόμβου
γίνεται με τη συνάρτηση push(), η οποία έχει όρισμα την τιμή του νέου
κόμβου. Η emfanisi() εδώ έχει στη for συνθήκη p != NULL, αφού ΔΕΝ
υπάρχει ο κόμβος - σκοπός end. Το πρόγραμμα μετά από την εμφάνιση
του σωρού, διαγράφει την κεφαλή με την συνάρτηση pop και εμφανίζει
τον τροποποιημένο σωρό. H pop δεν έχει όρισμα, αλλά επιστρέφει την
τιμή της κεφαλής.
#include <stdlib.h>
#include <stdio.h>
#define NULL 0
struct node
{ int timi;
struct node *next;
} ;
typedef struct node SOROS;
int x;
SOROS *start=NULL,*p;
SOROS *getnode()
{ SOROS *p;
p = (SOROS *) malloc(sizeof(SOROS));
if (p==NULL) {
printf("ανεπαρκής μνήμη\n");
exit(1); }
return p;
}
void emfanisi()
{ printf("εμφάνιση\n");
for (p=start; p != NULL; p=p->next)
printf("%d\n",p->timi);
}
108
void push(int x)
{
p=getnode();
p->next = start;
p->timi = x;
start = p ;
}
int pop()
{ int k;
if (start == NULL) {
printf("εξαγωγή από άδειο σωρό\n");
exit(2); }
k= start->timi;
p=start;
start =start->next;
free(p) ;
return k;
}
void main()
{ printf("δώσε ακέραιους, # για τέλος\n");
while ( scanf("%d",&x) > 0)
push(x);
emfanisi();
x=pop();
printf("Η κεφαλή ήταν:%d",x);
emfanisi();
}
Ουρέ ς(Queues)
109
Είναι οι λίστες που η διαχείρισή τους γίνεται ως εξής: Νέοι
κόμβοι εισάγονται από την ουρά της λίστας, η οποία σημαδεύεται από
τον δείκτη new, ενώ παλιοί κόμβοι διαγράφονται από την κεφαλή, η
οποία σημαδεύεται από τον δείκτη old.
Παράδειγμα 5ο: Είναι ολοκληρωμένο παράδειγμα διαχείρισης ουράς.
Εκτελεί επανειλημμένα τις εξής πράξεις:
α) διαβάζει ακέραιο από τον χρήστη που ακολουθεί ένα
θαυμαστικό(‘!’) και τον καταχωρεί σε νέο κόμβο ουράς.
β) αν αντί για θαυμαστικό, βάλουμε ερωτηματικό(‘?’), τότε
διαγράφεται κόμβος από τη λίστα και εκτυπώνεται η τιμή του.
Με ‘$’ διακόπτεται η λειτουργία του προγράμματος
#include <stdlib.h>
#include <stdio.h>
#define NULL 0
struct node
{ int timi;
struct node *next;
};
typedef struct node OYRA;
OYRA *new=NULL,*old, *p;
int x; char ch;
OYRA *getnode()
{ p = (OYRA *) malloc(sizeof(OYRA));
return p; }
void emfanisi()
{ printf("εμφάνιση\n");
for (p=old; p != NULL; p=p->next)
printf("%d\n",p->timi); }
void push(int x)
{ p=getnode();
110
if (new == NULL)
old = p;
else new->next = p;
p->next = NULL;
p->timi = x;
new = p ;
}
int pop()
{ int k;
k= old->timi;
p=old;
if (old == new) new =NULL ;
old=old->next;
free(p) ;
return k;
}
void main()
{ printf("Δώστε κάθε φορά \n\n");
printf(“ ! με ακέραιο για καταχώρηση \n“);
printf(“ ? για την εμφάνιση του πρώτου ακέραιου\n”);
printf(“ της ουράς, και διαγραφή του \n”);
printf(“ $ για τέλος \n”);
while (1)
{ do ch=getchar();
while (ch != '!' && ch != '?' && ch != '$');
if (ch == '!')
{ if ( scanf("%d",&x) > 0)
{ push(x);
if (p==NULL) {
printf("ανεπαρκής μνήμη\n");
111
printf("δώσε ? ή $ \n");
continue;
}
emfanisi();
}
else printf("περιμένω ακέραιο\n");
} else
if (ch =='?')
{
if (new == NULL) {
printf("άδεια ουρά\n");
printf("δώσε πρώτα ! ή $ \n");
continue; }
x=pop();
printf("H κεφαλή ήταν :%d\n",x);
emfanisi();
} else break; /* ch == '$' */
} }
Η εντολή continue, μας επαναφέρει στην αρχή του ατέρμονου
βρόγχου while (1), για νέα πληκτρολόγηση. Αντίθετα η break, μας
βγάζει έξω εντελώς από αυτόν. H do... while, μας υποχρεώνει να
ξεκινήσουμε τα δεδομένα με !, ? ή $.
Όπως παρατηρείτε, η δυσκολία γενικά στις δυναμικές λίστες
είναι η παρακολούθηση των δεσμών τους κατά τις εισαγωγές και
διαγραφές τους. Δεν πρέπει μάλιστα να ξεχνάμε τους ελέγχους για
ειδικές περιπτώσεις, όπως άδεια λίστα, ανεπάρκεια μνήμης κλπ.
112
Αρχεία Εισόδου/Εξόδου σε Βάθος
Επικοινωνία με Αρχεία
Τα προγράμματα που κατασκευάζουμε πρέπει να έχουν την
δυνατότητα, όποτε το χρειαζόμαστε, να διαβάζουν δεδομένα από ένα
αρχείο ή να τοποθετούν τα αποτελέσματα σε ένα αρχείο. Και βέβαια
σαν αρχείο, θεωρούμε ένα τμήμα μιας περιφερειακής μνήμης όπως ο
σκληρός δίσκος, η δισκέτα κλπ.
Το λειτουργικό σύστημα UNIX έχει μόνο μια μορφή δομής
αρχείων, τα αρχεία κειμένου(text). Ετσι, η C που αναπτύχθηκε αρχικά
σε αυτό το περιβάλλον, έχει σαν εξ' ορισμού(default) ρύθμιση, την
δημιουργία και οργάνωση τέτοιων αρχείων. Σε αυτά οι πληροφορίες
αποθηκεύονται σαν χαρακτήρες με την χρήση του κώδικα ASCII. Θα
εξετάσουμε λοιπόν στην αρχή συναρτήσεις Εισόδου/Εξόδου αρχείων
κειμένου. Αργότερα θα δούμε και ορισμένα πράγματα πάνω στα
δυαδικά αρχεία, δηλαδή στα αρχεία που αποθηκεύουν κώδικα γλώσσας
μηχανής και χρησιμοποιούνται σαν ένας επιπλέον τρόπος αποθήκευσης
πληροφοριών σε λειτουργικά συστήματα όπως το MS-DOS.
113
if ( (in = fopen(s,"r")) != NULL)
{ while ( (ch=fgetc(in)) != EOF)
fputc(ch, stdout);
fclose(in);
} else printf("δεν μπόρεσα να ανοίξω το \"%s\"\n",s);
}
Δηλώνουμε λοιπόν την μεταβλητή αρχείου in, στην 3η γραμμή,
σαν δείκτη τύπου FILE. Είναι ένας τύπος δομής για αρχεία που
ορίζεται στο stdio.h και χρησιμοποιείται ευρύτατα.
Στη συνέχεια, καλούμε την fopen(). Η συνάρτηση αυτή
επιστρέφει έναν δείκτη τύπου FILE, δηλαδή μια μεταβλητή που δείχνει
σε αρχείο. Γι' αυτό και στην 8η γραμμή, αποδίδουμε αυτόν τον δείκτη
στη μεταβλητή μας in. Από εδώ και πέρα, in θα είναι το λογικό όνομα
του αρχείου. Αν όμως, δεν μπορέσει για οποιοδήποτε λόγο να ανοίξει
το αρχείο μας, τότε η fopen() επιστρέφει μηδενική τιμή(NULL). Αυτός
ο έλεγχος, πρέπει πάντα να γίνεται και εδώ βλέπουμε στην 12η γραμμή
το else να εμφανίζει κατάλληλο μήνυμα.
Η fopen() δέχεται δύο παραμέτρους. Η πρώτη είναι τύπου string
και αποτελεί το συμβολικό όνομα του αρχείου. Είναι δηλαδή το
πραγματικό όνομά του, έτσι όπως το αναγνωρίζει το λειτουργικό
σύστημα. Σημειώστε ότι όταν το γεμίζει ο χρήστης, όπως εδώ, μπορεί
να δίνει και πλήρη διαδρομή για προσπέλαση αρχείων ακόμη και
διαφορετικών καταλόγων. Η fopen() λοιπόν έχει παρόμοια δράση με
την assign της Pascal. Επιπλέον όμως διαθέτει και δεύτερη παράμετρο
που περιγράφει την χρήση του αρχείου. Οι τρεις βασικές χρήσεις είναι
:
114
δεν υπάρχει, θα δημιουργηθεί τώρα.
115
χρησιμοποιώντας και τις συναρτήσεις που εργάζονται με αρχεία text
περιφερειακής μνήμης.
Σημειώστε τέλος ότι υπάρχουν και οι μακροεντολές getc() και
putc(), που εργάζονται όμοια με τις fgetc() και fputc() αντίστοιχα.
116
fclose(fin);
fclose(fout);
} else printf("δεν μπόρεσα να ανοίξω το \"%s\"\n",s);
}
Οπως και οι fgetc(), fputc(), οι συναρτήσεις fscanf(), fprintf()
χρησιμοποιούνται αφού η fopen() έχει ανοίξει ένα αρχείο και πριν το
κλείσει η fclose().
117
while ( (fgets(str,MAXLIN,fin)) != NULL)
fputs(str,fout);
fclose(fin);
fclose(fout);
} else printf("δεν μπόρεσα να ανοίξω το \"%s\"\n",s);
}
Η fgets() δέχεται τρία ορίσματα. Το πρώτο, κατά τα γνωστά,
είναι η μεταβλητή τύπου string που γεμίζει. Το δεύτερο όρισμα βάζει
ένα όριο στο μήκος της γραμμής που πρόκειται να διαβαστεί. Το τρίτο
όρισμα είναι το αρχείο που διαβάζουμε. Έτσι στην 13η γραμμή, η
fgets() σταματάει όταν διαβάσει τον χαρακτήρα νέας γραμμής ή όταν
διαβάσει 80 χαρακτήρες, ανάλογα με το ποια περίπτωση έρθει πρώτη.
Και στις δύο περιπτώσεις προστίθεται ο χαρακτήρας '\0' στο τέλος της
str.
ΠΡΟΣΟΧΗ: α) Ενώ η gets() αντικαθιστά τον χαρακτήρα νέας γραμμής
με '\0', η fgets() τον διατηρεί.
β) Η fgets(), όπως και η gets(), επιστρέφουν την τιμή
NULL όταν βρουν το τέλος του αρχείου(EOF). Γι' αυτό και στην 13η
γραμμή ελέγχουμε για το τέλος αρχείου, όχι όπως στα προηγούμενα
παραδείγματα με το EOF, αλλά με το NULL.
118
long apost =0L;
int ch;
if (number < 2)
puts("δεν έδωσες όνομα αρχείου\n");
else
{ if ( (fp = fopen(names[1] ,"r")) != NULL)
{ while ( fseek(fp,apost++,0) == 0
&& (ch=getc(fp)) != EOF)
{ putchar(ch);
if ( fseek(fp,-(apost+2),2) == 0)
putchar(getc(fp));
}
fclose(fp);
}
else printf("δεν μπόρεσα να ανοίξω το \"%s\"\n",names[1] );
} }
Η συνάρτηση fseek() μας επιτρέπει να μεταχειριστούμε ένα
αρχείο που ανοίχτηκε με την fopen() σαν ένα πίνακα και να
μετακινηθούμε κατ' ευθείαν σε μια συγκεκριμένη θέση του. Επιστρέφει
τιμή 0 αν όλα είναι εντάξει, ενώ επιστρέφει τιμή -1 όταν προσπαθούμε
να μετακινηθούμε έξω από τα όρια του αρχείου. Γι' αυτό και στις
γραμμές 10 και 13, συγκρίνουμε την fseek() με το μηδέν.
Τρία είναι τα ορίσματά της. Το πρώτο είναι η μεταβλητή αρχείου
που διατρέχουμε. Το δεύτερο καλείται απόσταση (offset) και πρέπει να
είναι τύπου long. Μας λέει πόσο μακριά να μετακινηθούμε από το
σημείο εκκίνησης. Θετική απόσταση σημαίνει κίνηση προς τα εμπρός,
ενώ αρνητική είναι κίνηση προς τα πίσω.
Το σημείο εκκίνησης προσδιορίζεται από το τρίτο όρισμα.
Καλείται κατάσταση (mode), και ισχύει :
κατάσταση σημείο εκκίνησης
119
0 αρχή του αρχείου
1 τρέχουσα θέση
2 τέλος του αρχείου
Στην 10η λοιπόν γραμμή, μεταφερόμαστε στη θέση που απέχει
αρχικά 0 από την αρχή. Δηλαδή διαβάζουμε και εμφανίζουμε τον
πρώτο χαρακτήρα. Μετά, στην 13η γραμμή μεταφερόμαστε στην θέση
που απέχει 2 από το τέλος του αρχείου. Αυτό το +2 υπάρχει για να
αρχίζουμε από τον τελευταίο κανονικό χαρακτήρα. Παραλείπουμε
δηλαδή τον χαρακτήρα ΕΟF(control-Z) και τον χαρακτήρα νέας
γραμμής('\n'). Ετσι εμφανίζεται ο τελευταίος κανονικός χαρακτήρας.
Οταν ξαναγυρίσει στην 10η γραμμή το πρόγραμμα θα εμφανίσει τον
χαρακτήρα που απέχει 1 από την αρχή του αρχείου κ.ο.κ.
Παράδειγμα 5ο : Εχω ένα πρόγραμμα στο αρχείο test.c και μετά την
μεταγλώττιση παίρνω το εκτελέσιμο αρχείο "test.exe". Αν δώσω στην
προτροπή C: του λειτουργικού συστήματος
test new.dat τώρα
τότε το argc έχει τιμή 2 και για τον πίνακα argv ισχύει
argv[0] δείχνει το "test"
120
argv[1] δείχνει το "new.dat"
argv[2] δείχνει το "τώρα"
121
Αν συναντήσετε δίπλα στους κωδικούς που αναφέρθηκαν αντί για
το γράμμα b το γράμμα t (text), τότε και πάλι η fopen() υποστηρίζει
αρχεία κειμένου. Οπως όμως αναφέραμε δεν είναι απαραίτητο το t,
γιατί τα αρχεία κειμένου είναι η εξ' ορισμού επιλογή.
122