Η C, πέραν των ενσωματωμένων τύπων
δεδομένων (char, int, float, double), παρέχει τη δυνατότητα στο χρήστη να δημιουργήσει τους
δικούς του τύπους, οι οποίοι είναι ενεργοί αποκλειστικά μέσα στο πρόγραμμα που
δημιουργούνται. Στην απλούστερη περίπτωση ονομάζονται απαριθμητικοί τύποι δεδομένων (enumerated types) ενώ οι πιο σύνθετες μορφές ονομάζονται δομές (structures). Οι δομές θα μελετηθούν στη συνέχεια
του κεφαλαίου.
Ο απαριθμητικός τύπος ορίζεται
στην αρχή του προγράμματος, με χρήση της λέξης κλειδί enum, ως εξής:
enum <όνομα_τύπου>
{ πεδίο τιμών };
Το πεδίο τιμών του είναι ένα σύνολο από σταθερές, στις
οποίες έχουν δοθεί συμβολικά ονόματα. Η σημασία των απαριθμητικών τύπων είναι
διττή: αφενός μεν διευκολύνουν την κατανόηση του προγράμματος, αφετέρου δε
επιτρέπουν στο μεταγλωττιστή να ελέγχει τους τύπους ώστε να μην προκαλούνται
λογικά λάθη. Για παράδειγμα, εάν χρησιμοποιηθεί μία ακέραια μεταβλητή days για να επεξεργασθεί πληροφορία σχετική με τις ημέρες της
εβδομάδας, θα μπορούσε να αντιστοιχηθεί το 1
στην Κυριακή, το 2 στη Δευτέρα
κ.ο.κ. Ωστόσο, στην περίπτωση που ανατεθεί στην days το 9, ο μεταγλωττιστής δε θα εντοπίσει το
λάθος, καθώς υπάρχει λογικό και όχι προγραμματιστικό σφάλμα. Για το λόγο αυτό
μπορεί να ορισθεί ένας νέος τύπος δεδομένων με το όνομα week_days, με πεδίο τιμών επτά συμβολικά ονόματα, καθένα από τα
οποία αντιστοιχεί σε μία ημέρα της εβδομάδας:
enum week_days { Sun, Mon, Tue, Wed, Thu, Fri, Sat };
Το πεδίο τιμών του τύπου week_days είναι αυστηρά καθορισμένο, γεγονός που σημαίνει ότι μία
μεταβλητή τύπου week_days μπορεί να λάβει μόνο τις ανωτέρω επτά
τιμές. Η δήλωση μεταβλητής τύπου week_days έχει την ακόλουθη μορφή:
enum week_days days;
Παρατηρήσεις:
1. Εσωτερικά, ο χειρισμός των τύπων δεδομένων enum γίνεται σαν να
ήταν ακέραιοι (έτσι μπορούν να εκετελεσθούν αριθμητικές και συγκριτικές πράξεις
με αυτούς). Στο πρώτο όνομα δίνεται η τιμή 0
(Sun στην προηγούμενη περίπτωση), στο
επόμενο η τιμή 1 (Mon) κ.ο.κ. Μπορεί η αρίθμηση να ξεκινά από άλλον ακέραιο,
όπως φαίνεται ακολούθως:
enum
week_days { Sun=30, Mon, Tue, Wed, Thu, Fri, Sat };
οπότε η αρίθμηση ξεκινά από το 30. Ωστόσο, εάν αντί για days=Sun ή Mon τεθεί
π.χ. days=5, o μεταγλωττιστής
θα βγάλει μήνυμα σφάλματος.
Τέλος,
μπορούν να τεθούν ακέραιες τιμές σε οποιαδήποτε από τα συμβολικά ονόματα, όπως
ακολούθως:
enum
week_days { Sun, Mon=30, Tue, Wed=500, Thu=1000, Fri, Sat };
οπότε Sun=0, Mon=30, Tue=31, Wed=500, Thu=1000, Fri=1001, Sat=1002.
2. Με τον τύπο enum μπορούν να
ορισθούν οι λογικές τιμές της άλγεβρας Boole true και false ως
εξής:
enum
boolean {False, True};
οπότε η False=0 και η True=1. Έτσι μπορούν στη
συνέχεια να ορισθούν boolean μεταβλητές.
Εναλλακτικά τα παραπάνω
επιτελούνται με χρήση της πρότασης #define του
προεπεξεργαστή:
#define False 0
#define True 1
3. Οι μεταβλητές απαριθμητικού τύπου
παρουσιάζουν το μειονέκτημα ότι δεν μπορούν να χρησιμοποιηθούν άμεσα
στις συναρτήσεις εισόδου–εξόδου (scanf(), printf()). Οι
συναρτήσεις αυτές τυπώνουν τον ακέραιο στον οποίο αντιστοιχεί το συμβολικό
όνομα, όπως φαίνεται στο Παράδειγμα 1.1.
4. Στη
C απαιτείται να συμπεριληφθεί
στη δήλωση μεταβλητής απαριθμητικού τύπου η λέξη κλειδί enum, π.χ. enum week_days day1,day2;. Στη C++ η λέξη enum δεν είναι
απαραίτητη και η δήλωση γίνεται week_days day1,day2;, όπως ακριβώς ορίζεται ένας
βασικός τύπος δεδομένου, π.χ. int var1.
________________________________________________________________________
Παράδειγμα 1.1
#include <stdio.h> //
Προσδιορισμός τύπου enum enum week_days { Sun, Mon, Tue, Wed, Thu, Fri, Sat }; void main() { week_days day1,day2; // Ορισμός μεταβλητών day1=Mon; // Απόδοση τιμής. Δεν
περικλείεται σε εισαγωγικά. day2=Thu; int diff=day2-day1; // Εκτελείται αριθμητική ακεραίων printf( "day1 is %d\n",day1 ); printf( "day2 is %d\n",day2 ); printf( "Days between =%d\n",diff ); if (day1<day2) // Δύναται να
κάνει συγκρίσεις printf( "day1 is followed by day2\n" ); } |
________________________________________________________________________
Η C παρέχει τη δυνατότητα απόδοσης νέων
ονομάτων σε τύπους δεδομένων. Ο μηχανισμός απόδοσης ονομάτων βασίζεται στη λέξη
κλειδί typedef,
βρίσκει ιδιαίτερη χρήση στις δομές και έχει την ακόλουθη σύνταξη:
typedef <τύπος>
<όνομα>;
Για παράδειγμα, η δήλωση
typedef float real_number;
καθιστά το όνομα real_number συνώνυμο του float. Ο τύπος real_number μπορεί πλέον να χρησιμοποιηθεί σε
δηλώσεις, μετατροπές τύπων κ.λ.π., όπως ακριβώς χρησιμοποιείται ο τύπος float, με τη διαφορά ότι o real_number θα είναι ενεργός αποκλειστικά μέσα στο
πρόγραμμα που δημιουργείται [1]. Θα πρέπει να σημειωθεί ότι
με την typedef δε
δημιουργούνται νέοι τύποι, απλά αλλάζουν οι ετικέτες. Έτσι, η παρακάτω δήλωση
real_number num1, num2;
δηλώνει τις μεταβλητές κινητής υποδιαστολής num1 και num2.
Η δομή αποτελεί έναν
συναθροιστικό τύπο δεδομένων, οριζόμενο από το χρήστη, και χρησιμοποιείται είτε
για να αναπαρασταθεί μία έννοια που μπορεί να διαθέτει διαφορετικού τύπου
επιμέρους ιδιότητες είτε για να ομαδοποιηθούν διαφορετικού τύπου μεταβλητές.
Μπορεί να ορισθεί ως μία συλλογή μεταβλητών, η οποία αποθηκεύεται και
παρουσιάζεται ως μία λογική οντότητα. Διαφέρει από τους πίνακες καθόσον οι
τελευταίοι αποτελούνται από μεταβλητές ίδιου τύπου.
Οι επιμέρους μεταβλητές
ονομάζεται μέλη ή πεδία και μπορούν να είναι:
· Οι
βασικοί τύποι δεδομένων (int, char, float, double).
· Απαριθμητικοί
τύποι, οι οποίοι θα πρέπει να έχουν ορισθεί πριν τον ορισμό της δομής.
· Πίνακες.
· Άλλες
δομές, οι οποίες θα πρέπει να έχουν ορισθεί πριν τον ορισμό της δομής.
Ο ορισμός της δομής έχει την
ακόλουθη μορφή:
struct
<όνομα_δομής> { <τύπος_δεδομένων> <όνομα_1ου_μέλους>; <τύπος_δεδομένων> <όνομα_2ου_μέλους>; . . . . . . <τύπος_δεδομένων> <όνομα_τελευταίου_μέλους>; }; |
Εάν δύο ή περισσότερα μέλη έχουν τον ίδιο τύπο, η αναφορά
τους γίνεται με την πιο απλοποιημένη μορφή
<τύπος_δεδομένων> <όνομα_1ου_μέλους> <όνομα_2ου_μέλους>;
Για
παράδειγμα, μία ταχυδρομική διεύθυνση αποτελεί σύνθετη πληροφορία, καθώς περιλαμβάνει
το ονοματεπώνυμο του κατόχου, την οδό και τον αριθμό, την πόλη, τον ταχυδρομικό
κώδικα. Ομαδοποιώντας σε έναν τύπο δεδομένου τις τέσσερις παραπάνω συνιστώσες,
δημιουργείται η δομή addrΤ [1],
η οποία περιέχει τέσσερα μέλη:
struct addrΤ
{
char
name[40]; // πρώτο μέλος
char
street[40];
char
city[30];
unsigned
int zip_code; // τέταρτο μέλος
};
ή εναλλακτικά:
struct addrΤ
{
char
name[40], street[40], city[30];
unsigned int zip_code;
};
Οι
μεταβλητές τύπου δομής δηλώνονται όπως και οι απλές μεταβλητές. Η δήλωση
struct addrT
address1, address2;
ορίζει οι address1, address2 να είναι
μεταβλητές τύπου addrT. Η δήλωση των
μεταβλητών μπορεί να συνδυασθεί με τον ορισμό της δομής. Η παραπάνω δήλωση θα
μπορούσε να γίνει ως εξής:
struct addrΤ
{
char
name[40];
char
street[40];
char
city[30];
unsigned
int zip_code;
}
address1, address2;
Με παρόμοιο τρόπο δηλώνεται και
ένας πίνακας με στοιχεία δομές. Η δήλωση
struct addrT addresses[100];
δημιουργεί τον πίνακα εκατό στοιχείων addresses, κάθε στοιχείο του οποίου είναι μία μεταβλητή τύπου
δομής addrT και περιέχει τέσσερα
μέλη.
Παρατηρήσεις:
1. H λέξη κλειδί struct προσδιορίζει
την αρχή τού ορισμού μίας δομής και δημιουργεί έναν νέο τύπο. Το όνομα του
τύπου είναι struct addrT. Το όνομα addrT είναι η ετικέτα της δομής (structure tag) και
χρησιμοποιείται μαζί με τη struct για να δηλώνονται μεταβλητές τύπου struct addrT. Στη C++ η λέξη struct δεν είναι απαραίτητη στη δήλωση μεταβλητών
αλλά μόνο στον ορισμό του τύπου.
2. Κάθε ορισμός δομής πρέπει να τελειώνει με
ερωτηματικό (;).
3. Ο ορισμός μίας δομής δε δεσμεύει χώρο στη
μνήμη για μεταβλητές. Απλά δημιουργεί ένα νέο τύπο.
4. Η ετικέτα μίας δομής είναι μεν προαιρετική
αλλά στην πράξη θα πρέπει να χρησιμοποιείται πάντοτε, για να αποφεύγονται
δυσχέρειες στον κώδικα. Εάν δεν υπάρχει η ετικέτα, τότε μεταβλητές του
συγκεκριμένου τύπου δομής μπορούν να δηλωθούν μόνο κατά τον ορισμό του
τύπου και όχι αργότερα. Εάν οριζόταν μία δομή χωρίς τον προσδιοριστή addrT:
struct
{
char name[40], street[40], city[30];
unsigned int zip_code;
}
address1;
τότε θα προέκυπτε η μεταβλητή address1 του
συγκεκριμένου τύπου αλλά ο τύπος δε θα μπορούσε να ξαναχρησιμοποιηθεί γιατί δε
θα είχε όνομα. Σε περίπτωση που απαιτείτο να δηλωθεί νέα μεταβλητή τέτοιου
τύπου, π.χ. address2, θα έπρεπε να
ορισθεί εκ νέου ο τύπος, δηλαδή
struct
{
char
name[40], street[40], city[30];
unsigned
int zip_code;
}
address2;
5. Ο ορισμός μίας δομής πρέπει να γίνεται στην
αρχή του κώδικα, πριν από τη main(), ώστε να μπορεί
να χρησιμοποιηθεί τόσο μέσα στη main() όσο και σε
οποιοδήποτε σημείο του προγράμματος (σε συναρτήσεις κ.λ.π.).
6. Οι μόνες λειτουργίες που μπορούν να
εκτελεσθούν σε μία δομή είναι:
Ø
Ανάθεση μεταβλητών του ίδιου τύπου. Εάν address1 και address2 είναι
δύο μεταβλητές τύπου δομής addrT, με την ανάθεση address1=address2 τα
μέλη τής address1 αποκτούν τις τιμές των αντίστοιχων μελών τής address2.
Ø
Η διεύθυνση μίας μεταβλητής τύπου δομής
μπορεί να ανατεθεί σε ένα δείκτη (pointer). Περισσότερες λεπτομέρειες θα δοθούν
στο κεφάλαιο των δεικτών.
Ø
Χρήση του τελεστή sizeof. Η εντολή sizeof(struct address1)
επιστρέφει τον αριθμό των bytes που απαιτούνται για την αποθήκευση στη
μνήμη μίας μεταβλητής τύπου δομής addrT.
________________________________________________________________________
Μία
επιχείρηση πώλησης αυτοκινήτων διαθέτει τα ακόλουθα αυτοκίνητα:
Κατασκευαστής |
Τύπος |
Τιμή |
Διαθέσιμα τεμάχια |
Mercedes |
SL500 |
87000 |
4 |
Mercedes |
SLK320 |
44500 |
2 |
BMW |
M3 |
54000 |
4 |
Audi |
A4 |
34000 |
1 |
Για την καταχώρηση των προϊόντων της επιχείρησης μπορεί να
χρησιμοποιηθεί μία δομή με τέσσερα μέλη:
make: απαριθμητικός τύπος
δεδομένων, που αφορά στον κατασκευαστή
model: πίνακας χαρακτήρων για την
περιγραφή του μοντέλου
price: αριθμός κινητής
υποδιαστολής (float) για την τιμή του κάθε μοντέλου
avail: ακέραιος (integer) για τον αριθμό των διαθέσιμων τεμαχίων κάθε
μοντέλου
Το πρώτο μέλος απαιτεί τη δημιουργία του απαριθμητικού τύπου make, η οποία θα προηγείται του ορισμού της δομής:
enum carmakeT { Mercedes, BMW,
Audi };
Κατά συνέπεια ο κώδικας ορισμού της δομής και δήλωσης ενός πίνακα που
θα περιλαμβάνει τα προϊόντα της επιχείρησης είναι ο ακόλουθος:
enum carmakeT { Mercedes, BMW, Audi }; struct stockT { carmakeT make; char model[15]; float price; int avail; }; void main() { stockT inventory[40]; . . . . . . } |
________________________________________________________________________
Οι αρχικές τιμές μπορούν να
αποδοθούν στις μεταβλητές δομής και τη στιγμή της δήλωσής τους, όπως στην
περίπτωση των πινάκων, με λίστες από αρχικές τιμές μέσα σε άγκιστρα. H απόδοση των
τιμών μπορεί να γίνει:
·
είτε μαζί με ορισμό και δήλωση:
struct addrΤ
{
char
name[40];
char
street[40];
char
city[30];
unsigned
int zip_code;
} address1={ "Demis Pappas",
"Rodou 23", "Serres", 61124 },
address2={ "John Doe", "Limnou 32",
"Serres", 61124 };
·
είτε με τη δήλωση:
struct addrT address1={ "Demis Pappas", “Rodou 23",
"Serres", 61124 },
address2={ "John Doe", "Limnou 32",
"Serres", 61124 };
Εάν σε μία δομή υπάρχουν
λιγότερες αρχικές τιμές από τον αριθμό των μελών, τα υπόλοιπα μέλη
αρχικοποιούνται με το μηδέν (ή με το NULL εάν το
μέλος είναι δείκτης).
Στην περίπτωση του πίνακα δομής,
η απόδοση αρχικής τιμής στη δομή ακολουθεί το γενικό κανόνα αρχικοποίησης.
Έτσι, η αρχικοποίηση των τριών πρώτων στοιχείων τoυ πίνακα addr γίνεται ως
ακολούθως:
struct addrΤ addr[10]={
{ "Demis Pappas", "Rodou 23", "Serres",
61124 },
{ "John Doe", "Limnou 32", "Serres", 61124
},
{ "Eleni", "Skirou 12", "Serres", 61124 }
};
Η προσπέλαση των μελών μίας δομής
γίνεται με χρήση δύο τελεστών: του τελεστή
μέλους δομής (.) ή τελεστή τελείας και του τελεστή δείκτη δομής (->) ή τελεστή βέλους. Ο τελεστής βέλους θα μελετηθεί στο κεφάλαιο των
δεικτών.
Η αναφορά στα μέλη δομής με χρήση
του τελεστή τελεία γίνεται ως εξής:
<όνομα_μεταβλητής>.<όνομα_μέλους>
Έτσι η έκφραση address1.street αναφέρεται στο μέλος street της μεταβλητής address1, η
οποία είναι τύπου δομής addrT.
Σε έναν πίνακα address2 με
στοιχεία δομές τύπου addrT, η ανάθεση στο
μέλος city του δέκατου στοιχείου
έχει την ακόλουθη μορφή:
address[9].city =
"Serres"
Παρατήρηση: Η έκφραση address1.city = "Serres" είναι
σωστή και αποδίδει τιμή στο μέλος city της μεταβλητής address1. H έκφραση addrT.city =
"Serres" είναι λανθασμένη γιατί η addrT είναι τύπος
δεδομένων. Δε θα πρέπει να συγχέεται
ο τύπος δεδομένων που ορίσθηκε με τις μεταβλητές τέτοιου τύπου.
Μία δομή μπορεί να περιλαμβάνει
μέλη τα οποία με τη σειρά τους είναι δομές. Η C δε θέτει περιορισμό στο βαθμό ένθεσης
αλλά επιτάσσει κάθε ένθετη δομή να έχει ορισθεί πριν τη χρήση της ως τύπος
δεδομένου ενός μέλους ευρύτερης δομής. Για παράδειγμα, ο τύπος δεδομένου personT
struct personΤ
{
char name[16];
char address[12];
char tel[10];
struct dateΤ birthdate;
struct dateΤ hiredate;
};
περιλαμβάνει τα μέλη birthdate (ημερομηνία
γέννησης) και hiredate (ημερομηνία
πρόσληψης), τα οποία είναι μεταβλητές τύπου δομής dateT. Ο
τύπος της dateT είναι ο ακόλουθος:
struct dateΤ
{
int day;
char month_name[4];
int year;
};
και ο ορισμός του πρέπει να προηγείται του ορισμού του
τύπου δεδομένων δομής personT.
Θεωρώντας τη
μεταβλητή bemp, η οποία είναι τύπου personT, οι αναφορές
στο έτος γέννησης και στον τρίτο χαρακτήρα του αλφαριθμητικού που
αντιστοιχεί στο μήνα πρόσληψης έχουν
τη μορφή
bemp.birthdate.year
και
bemp.hiredate.month_name[2]
αντίστοιχα.
________________________________________________________________________
Στο
πρόγραμμα που ακολουθεί συνοψίζονται τα στοιχεία που αφορούν στις δομές, με
ιδιαίτερη έμφαση στην ένθεση δομών και στους πίνακες δομών. Να γίνει σχολιασμός
του κώδικα.
#include <stdio.h> struct addressT { char name[40]; char street[15]; int number; int zip_code; char city[15]; }; //
τέλος του τύπου struct addressT struct dayT { int date; int month; int year; }; //
τέλος του τύπου struct dayT struct personT { struct addressT addr; struct dayT birthday; }; //
τέλος του τύπου struct personT void main() { struct addressT
addr1={"John Doe","Telou
Agra",10,61124,"Serres"}; struct addressT addr[10]={ {"Ken
Thomson","Rodou",23,61124,"Serres"}, {"Brian
Kernighan","Dilou",26,61124,"Serres"}, }; struct personT p={ {"Brian
Kernighan","Dilou",26,61124,"Serres"}, {28,1,79}, }; printf( "\n\tstruct
address\n"); printf(
"%s\n%s\n%d\n%d\n%s\n",addr1.name, addr1.street, addr1.number, addr1.zip_code,
addr1.city ); printf( "\n\tstruct
person\n"); printf(
"%s\n%s\n%d\n%d\n%s\n",p.addr.name, p.addr.street, p.addr.number, p.addr.zip_code,
p.addr.city ); printf(
"%d-%d-%d\n",p.birthday.date, p.birthday.month, p.birthday.year ); printf(
"\n\tPinakas\n" ); printf(
"%s\n%s\n",addr[0].name,addr[1].name ); printf(
"%c\n",addr[1].name[0] ); } //
τέλος της main |
Αρχικά
ορίζεται ο τύπος δεδομένων struct addressT. Η νέα δομή περιλαμβάνει τα
μέλη name, street, number, zip και city.
Ακολουθεί ο ορισμός του
τύπου dayΤ με μέλη date, month, year και ο ορισμός του τύπου personΤ με μέλη addr και birthday, τα οποία είναι και αυτά δομές τύπων addressΤ και dayΤ, αντίστοιχα.
Στην αρχή της main() δηλώνονται:
· Η μεταβλητή τύπου addressΤ με όνομα addr1, η οποία αρχικοποιείται.
· Ένας πίνακας με όνομα addr, ο οποίος έχει 10 στοιχεία τύπου
address. Αρχικοποιούνται τα δύο πρώτα στοιχεία του πίνακα.
· Η μεταβλητή p ως τύπου person. Θα πρέπει να προσεχθεί πως οι αρχικοποιήσεις κάθε μέλους της δομής
περικλείονται σε άγκιστρα.
Στη συνέχεια της main() παρουσιάζονται εντολές
εκτύπωσης. Στην εντολή
printf( "%d-%d-%d\n",p.birthday.date,p.birthday.month,p.birthday.year );
θα πρέπει να προσεχθεί η αναφορά στα μέλη ένθετων δομών.
Χρησιμοποιείται ο τελεστής τελεία (.) χωρίς περιορισμό στο βάθος
έκθεσης.
Με την εντολή
printf(
"%s\n%s\n",addr[0].name,addr[1].name );
εκτυπώνεται το μέλος name του πρώτου και του
δεύτερου στοιχείου του πίνακα addr ενώ με την εντολή
printf(
"%c\n",addr[1].name[0] );
εκτυπώνεται ο πρώτος χαρακτήρας του name του δεύτερου στοιχείου του
πίνακα addr.
________________________________________________________________________
________________________________________________________________________
Να δημιουργηθεί ο πίνακας directory[40], ο οποίος θα αντιστοιχεί σε προσωπική ατζέντα. Κάθε στοιχείο του directory θα αποτελεί μεταβλητή τύπου δομής personT, η
οποία θα έχει μέλη:
α) Δομή idT με: i) το ονοματεπώνυμο και ii) δομή addressT με τη διεύθυνση του καταγεγραμμένου.
β) Δομή teleT με τα τηλέφωνα (σταθερό, κινητό) και fax του καταγεγραμμένου.
γ) Δομή emT με το προσωπικό email και αυτό της εργασίας του
καταγεγραμμένου.
Nα γραφεί η main, μέσα στην οποία θα γίνεται
εγγραφή και εκτύπωση δύο στοιχείων του directory.
Παρατηρήσεις:
1) Σύμφωνα με την υπόθεση ο τύπος
personΤ περιλαμβάνει ως μέλη μεταβλητές που είναι αποκλειστικά τύπου
δομής. Επιπρόσθετα ο τύπος idT περιλαμβάνει ένα μέλος που είναι τύπου
δομής (addressT). Κατά συνέπεια, η δήλωση των τύπων δεδομένων θα πρέπει
να γίνει με την ακόλουθη σειρά:
struct addressT
{
. . .
};
struct idT Ή struct teleT Ή struct
emT
{ { {
. . . . . . .
. .
}; }; };
struct personT
{
. . .
};
2) Εφόσον δεν προσδιορίζονται
επακριβώς τα περιεχόμενα του τύπου addressT, αυτά επιλέγονται κατά το
δοκούν:
struct addressT
{
char
street_name[ ];
int
street_number;
char city[ ];
int zip_code;
};
3) Είναι προτιμητέο οι τηλεφωνικοί
αριθμοί να δηλώνονται ως αλφαριθμητικά γιατί αποτελούν 10-ψήφιους ή 14-ψήφιους
ακέραιους. Κατά συνέπεια ο τύπος teleT μπορεί να έχει τη μορφή:
struct teleT
{
char wr_no[ ]; // σταθερό τηλέφωνο
char cell_no[
]; // κινητό τηλέφωνο
char fax_no[ ];
} ;
Συνολικά ο κώδικας είναι ο ακόλουθος:
#include <stdio.h> #include <conio.h> struct addressT { char street_name[ ]; int street_number; char city[ ]; int zip_code; }; struct idT { char name[ ]; char surname[ ]; addressT addr; }; struct teleT { char wr_no[ ]; char cell_no[ ]; char fax_no[ ]; } ; struct emT { char em_work[ ]; char em_home[ ]; } ; struct personT { idT ident; teleT tel; emT email; }; void main() { personT directory[40]; int i; for (i=0;i<=1;i++)
{ printf( "\nRecord %d:",i+1 ); printf( "\n\tName: "); scanf(
"%s",directory[i].ident.name ); printf( "\n\tSurname: ");
scanf( "%s",directory[i].ident.surname ); printf( "\n\tStreet name: "); scanf( "%s",directory[i].ident.addr.street_name
); printf( "\n\tStreet number: "); scanf( "%d",&directory[i].ident.addr.street_number
); printf( "\n\tCity: "); scanf(
"%s",directory[i].ident.addr.city ); printf( "\n\tZip code: "); scanf( "%d",&directory[i].ident.addr.zip_code
); printf( "\n\tTelephone: "); scanf(
"%s",directory[i].tel.wr_no ); printf( "\n\tCell telephone: "); scanf(
"%s",directory[i].tel.cell_no ); printf( "\n\tFax: "); scanf(
"%s",directory[i].tel.fax_no ); printf( "\n\tE-mail (work): "); scanf( "%s",directory[i].email.em_work ); printf( "\n\tE-mail (home): "); scanf( "%s",directory[i].email.em_home ); } // τέλος της for }
// τέλος της main |
________________________________________________________________________
Ο δομημένος προγραμματισμός στηρίζεται στην έννοια
του αρθρωτού σχεδιασμού (modular design), δηλαδή στο μερισμό σύνθετων
προβλημάτων σε επιμέρους μικρά και απλούστερα τμήματα. Κατ’ αντιστοιχία, ένα
μεγάλο πρόγραμμα μπορεί να τεμαχισθεί σε μικρότερες γλωσσικές κατασκευές. Στη C αυτές οι
κατασκευές ονομάζονται συναρτήσεις (functions) και αποτελούν
αυτόνομες, επώνυμες μονάδες κώδικα, σχεδιασμένες να επιτελούν συγκεκριμένο
έργο. Μπορούν να κληθούν επανειλημμένα σε ένα πρόγραμμα, δεχόμενες κάθε φορά
διαφορετικές τιμές στις εισόδους τους.
Έως τώρα έχει μελετηθεί και χρησιμοποιηθεί μία σειρά
συναρτήσεων όπως η main(), οι printf() και scanf(), οι
συναρτήσεις χειρισμού αλφαριθμητικών κ.λ.π. Με βάση τις συναρτήσεις αυτές
μπορούν να εξαχθούν τα ακόλουθα βασικά χαρακτηριστικά:
Ø
Μία συνάρτηση εκτελεί ένα σαφώς καθορισμένο
έργο (π.χ. η printf() παρέχει μορφοποιημένη
έξοδο).
Ø
Μπορεί να χρησιμοποιηθεί από άλλα
προγράμματα.
Ø
Είναι
ένα «μαύρο κουτί», ένα μικροπρόγραμμα το οποίο έχει:
·
ένα
όνομα, για να λειτουργήσει μία συνάρτηση πρέπει να κληθεί κατ’
όνομα
·
ένα σώμα, ένα
σύνολο προτάσεων και μεταβλητών
· (προαιρετικά) εισόδους, μία λίστα ορισμάτων
·
(προαιρετικά) μία έξοδο, η οποία με το
τέλος της συνάρτησης επιστρέφει μία τιμή στο σημείο του προγράμματος από το
οποίο εκλήθη η συνάρτηση
Κάθε πρόγραμμα αποτελείται από μία ή περισσότερες συναρτήσεις,
συμπεριλαμβανομένης πάντοτε της main(), από την οποία
αρχίζει η εκτέλεση του προγράμματος. Το σχήμα 2.1 δίνει τη μορφή ενός δομημένου
προγράμματος στη C:
εντολές προεπεξεργαστή (#include, #define,…) δηλώσεις συναρτήσεων δηλώσεις μεταβλητών (εφόσον
είναι απαραίτητες) void main() { δηλώσεις
μεταβλητών προτάσεις } func1() { .
. . . . . } func2() { .
. . . . . } |
Σχ. 2.1 Γενική
μορφή προγράμματος στη C
Μία συνάρτηση περιλαμβάνει τρεις φάσεις:
Στη δήλωση μίας συνάρτησης παρουσιάζεται το
«πρότυπο» ή «πρωτότυπο» συνάρτησης, το οποίο αποτελείται από τρία τμήματα, όπου
ορίζονται:
<τύπος δεδομένων επιστροφής> <όνομα συναρτησης>( λίστα
ορισμάτων );
Για παράδειγμα, η πρόταση
int maximum_two_integers( int
first_integer, int second_integer );
αποτελεί τη
δήλωση μίας συνάρτησης ονόματι maximum_two_integers, η οποία δέχεται δύο εισόδους,
τις ακέραιες μεταβλητές first_integer και second_integer, και επιστρέφει
ακέραια έξοδο (ο τύπος int πριν από το
όνομά της).
Η δήλωση των συναρτήσεων γίνεται πριν από τη main(), συνήθως μετά τις
εντολές προεπεξεργαστή (#include, #define).
Ο ορισμός μίας συνάρτησης περιλαμβάνει το πρότυπο συνάρτησης χωρίς το
καταληκτικό ερωτηματικό, ακολουθούμενο από το σώμα της συνάρτησης, το οποίο
αναπτύσσεται μέσα σε άγκιστρα:
<τύπος
δεδομένων επιστροφής> <όνομα
συναρτησης>( λίστα ορισμάτων ) { πρόταση; . . . . . . πρόταση επιστροφής; //
εφόσον η συνάρτηση επιστρέφει τιμή } |
Για παράδειγμα,
ο ορισμός της συνάρτησης maximum_two_integers() είναι ο ακόλουθος:
int maximum_two_integers( int first_integer, int second_integer )
{
if (first_integer >
second_integer) return(first_integer);
else return(second_integer);
}
Παρατηρήσεις:
1. Εάν η
συνάρτηση έχει έξοδο, αυτή θα πρέπει να επιστρέφεται με χρήση της εντολής return στο τέλος του σώματος της
συνάρτησης. Εάν δεν έχει έξοδο, ο <τύπος δεδομένων επιστροφής>
αντικαθίσταται από τη λέξη void, π.χ.
void print_max_two_integers( int first_integer, int second_integer )
{
if (first_integer > second_integer)
printf( "max=%d\n",first_integer );
else printf( "max=%d\n",first_integer
);
}
2. Οι
συναρτήσεις μπορούν να έχουν τις δικές τους εσωτερικές μεταβλητές, όπως ακριβώς
έχει η main().
Μία συνάρτηση ενεργοποιείται όταν κληθεί. Εάν η συνάρτηση δεν επιστρέφει
τιμή, η κλήση της γίνεται από ένα σημείο του προγράμματος ως εξής:
<όνομα συνάρτησης> ( πρώτο όρισμα, ..., τελευταίο όρισμα );
Οι τιμές πρώτο
όρισμα, ..., τελευταίο όρισμα
καλούνται πραγματικά ορίσματα ή πραγματικές παράμετροι (actual parameters). Τα πραγματικά ορίσματα χωρίζονται με κόμμα
και περικλείονται σε παρενθέσεις, αντιστοιχούν δε ένα προς ένα στη λίστα
τυπικών ορισμάτων. Ακόμη κι όταν δεν υπάρχουν ορίσματα οι παρενθέσεις είναι
υποχρεωτικές, καθώς δηλώνουν στο μεταγλωττιστή ότι το όνομα αντιπροσωπεύει
συνάρτηση και όχι μεταβλητή. Τα πραγματικά ορίσματα μπορούν να είναι σταθερές,
τιμές μεταβλητών ή ακόμη και τιμές εκφράσεων αλλά σε κάθε περίπτωση πρέπει να
είναι ίδιου τύπου με τα τυπικά ορίσματα. Για παράδειγμα, η κλήση της συνάρτησης
print_max_two_integers() μπορεί να λάβει την ακόλουθη μορφή:
x=10;
print_max_two_integers(
x, 32 );
<επόμενη πρόταση>;
Όταν κληθεί η συνάρτηση, το τυπικό όρισμα first_integer
αντιστοιχεί στο πραγματικό όρισμα x, οπότε first_integer=x=10, και το
τυπικό όρισμα second_integer λαμβάνει την τιμή 32. O έλεγχος
του προγράμματος περνάει στις επόμενες προτάσεις της συνάρτησης. Μετά την
εκτέλεση και της τελευταίας πρότασης η συνάρτηση τερματίζει και ο έλεγχος
μεταφέρεται στο κυρίως πρόγραμμα, στην <επόμενη πρόταση>;.
Όταν η συνάρτηση έχει έξοδο υπάρχει μία μικρή διαφοροποίηση · για
παράδειγμα η κλήση της maximum_two_integers() μπορεί να λάβει την ακόλουθη μορφή:
x=10;
y=maximum_two_integers(
x, 32 );
<επόμενη πρόταση>;
H διαφορά έγκειται στο ότι στο τέλος της συνάρτησης η
επιστρεφόμενη τιμή θα πρέπει να αποδοθεί σε μία μεταβλητή τού κυρίως
προγράμματος, στην παρούσα περίπτωση στην y. Κατά
συνέπεια, αφενός μεν η y θα πρέπει να
είναι ίδιου τύπου με την επιστρεφόμενη τιμή (integer στο συγκεκριμένο παράδειγμα), αφετέρου
δε μετά το τέλος της συνάρτησης ο έλεγχος του προγράμματος περνά στην πρόταση
ανάθεσης y=maximum_two_integers( x, 32 ); και ακολούθως στην <επόμενη
πρόταση>;.
Προσοχή: Θα πρέπει να σημειωθεί ότι μπορεί να παραληφθεί η
δήλωση της συνάρτησης εάν η συνάρτηση παρουσιάζεται μέσα στο πρόγραμμα πριν
από την πρώτη κλήσης της, όπως φαίνεται στο παράδειγμα 2.1. Ωστόσο αυτή είναι
μία ριψοκίνδυνη τακτική και θα πρέπει να αποφεύγεται. |
________________________________________________________________________
Στο
πρόγραμμα που ακολουθεί παραλήφθηκε η δήλωση της square() γιατί αυτή ορίζεται προτού κληθεί. Εάν όμως
η square() οριζόταν κάτω από τη main() έπρεπε να
δηλωθεί.
#include <stdio.h> float square(float y) { return(y*y); } void
main() { float x=15.2; printf( "x^2=%f\n",square(x) ); } |
________________________________________________________________________
________________________________________________________________________
Παράδειγμα 2.2
Να γραφεί
πρόγραμμα μετατροπής θερμοκρασιών από την κλίμακα Farenheit στην
κλίμακα Celcius, με βάση
την εξίσωση μετατροπής Celcius = (Farenheit-32)*5/9.
Το πρόγραμμα
θα δέχεται μία θερμοκρασία στην κλίμακα Farenheit, θα τη μετατρέπει στην κλίμακα Celcius και θα την
εκτυπώνει:
#include<stdio.h> void main () { float degF,degC, ratio; printf( "Enter degrees F: " ); scanf( "%f",°F ); // Κώδικας
μετατροπής: ratio = (float) 5/9;
degC = (degF-32)*ratio; printf( "%f degrees F are %f degrees C\n",degF,degC ); } |
Σε περίπωση που η μετατροπή χρειαζόταν σε πολλά σημεία ενός
προγράμματος, δε θα έπρεπε να επαναλαμβάνεται ο κώδικας της μετατροπής των
θερμοκρασιακών κλιμάκων. Για το λόγο αυτό πρέπει ο κώδικας της μετατροπής να
ενταχθεί σε μία ανεξάρτητη οντότητα του προγράμματος, η οποία θα δέχεται ως
είσοδο τη θερμοκρασία στη μία κλίμακα και θα αποδίδει στην έξοδό της τη
θερμοκρασία στην άλλη κλίμακα. Ακολούθως παρατίθεται το πρόγραμμα με χρήση
συναρτήσεων:
#include<stdio.h> float F_to_C (float far); void main() { float degF,degC; printf( "Enter degrees F: " ); scanf( "%f",°F ); degC = F_to_C (degF); printf( "%f degrees F are %f degrees C\n",degF,degC ); } //
τέλος της main float F_to_C (float far) { float ratio = 5.0 / 9.0; return((far-32)*ratio); } // τέλος της συνάρτησης |
________________________________________________________________________
________________________________________________________________________
Παράδειγμα 2.3
Να γραφεί πρόγραμμα, στο οποίο να καλούνται οι συναρτήσεις, τα
πρωτότυπα των οποίων δίνονται ως ακολούθως:
int max(int a, int
b);
double power(double
x, int n);
Πριν από
κάθε κλήση συνάρτησης πρέπει να υπάρχει στον πηγαίο κώδικα το πρωτότυπό της,
έτσι ώστε ο μεταγλωττιστής να ελέγχει εάν κάθε πρόταση κλήσης είναι σύμφωνη ως
προς τον αριθμό και τον τύπο των ορισμάτων, καθώς και τον τύπο της
επιστρεφόμενης τιμής. To κώδικα που
υλοποιεί την υπόθεση είναι το ακόλουθο:
#include <…h> #define ...... int max(int a, int b); double power(double x, int n); int num=5; void main() { printf( "%d\n",max(12/2,num+3,2*num ); printf( "%f\n",power(num,3) ); } |
________________________________________________________________________
Στο παράδειγμα 2.2 η συνάρτηση F_to_C() δημιουργεί στον
κορμό της τη μεταβλητή ratio. Η μεταβλητή
αυτή είναι ενεργή μόνο μέσα στο τμήμα κώδικα στο οποίο ορίζεται (σώμα της
συνάρτησης) και παύει να υφίσταται μετά το πέρας της συνάρτησης. Μεταβλητές
τέτοιου είδους καλούνται τοπικές
μεταβλητές (local variables). Δύο
συναρτήσεις μπορούν να έχουν τοπικές μεταβλητές με το ίδιο όνομα χωρίς να παρουσιάζεται
πρόβλημα, καθώς καθεμιά ισχύει μέσα στη συνάρτηση που δηλώνεται. Κατ’
αντιστοιχία,οι μεταβλητές που δηλώνονται μέσα στη main() δεν επηρεάζουν
τις μεταβλητές των υπόλοιπων συναρτήσεων του προγράμματος.
________________________________________________________________________
Στο πρόγραμμα που ακολουθεί αναδεικνύεται η συμπεριφορά των τοπικών
μεταβλητών. Δημιουργούνται δύο μεταβλητές με το ίδιο όνομα out, η πρώτη μέσα στη main() και η δεύτερη μέσα στη
συνάρτηση square(). Όπως προκύπτει από τα αποτελέσματα, οι δύο μεταβλητές ενεργούν
ανεξάρτητα η μία από την άλλη.
#include<stdio.h> float square (float x); void main() { float in,out; in=-4.0; out=square(in); printf( "out within main()
is=%f\n",out ); printf( "%f squared is
%f\n",in,out ); } float
square (float x) { float out; out=24.5; printf( "out within
square() is=%f\n",out ); return(x*x); } |
________________________________________________________________________
Σε αντιδιαστολή με τις τοπικές μεταβλητές, οι οποίες
είναι ενεργές μόνο μέσα στο σώμα της συνάρτησης που δηλώνονται, υπάρχει μία
κατηγορία μεταβλητών που είναι ενεργές σε όλα τα τμήματα ενός προγράμματος. Οι
μεταβλητές αυτές καλούνται καθολικές
μεταβλητές (global variables) και δηλώνονται
πριν από τη main(). Όταν
μεταβάλλεται η τιμή μίας καθολικής μεταβλητής σε οποιοδήποτε σημείο του
προγράμματος, η νέα τιμή μεταφέρεται σε όλο το υπόλοιπο πρόγραμμα.
Εν γένει οι καθολικές μεταβλητές είναι ένα ριψοκίνδυνο προγραμματιστικό
εργαλείο, καθώς αποτρέπουν τον ξεκάθαρο μερισμό του προβλήματος σε
ανεξάρτητα τμήματα. Επιπρόσθετα, μία καθολική μεταβλητή δεσμεύει μνήμη καθόλη
τη διάρκεια του προγράμματος, ενώ για μία τοπική μεταβλητή ο χώρος στη μνήμη
δεσμεύεται μόλις ο έλεγχος περάσει στη συνάρτηση, αποδεσμεύεται δε με το τέλος
αυτής, οπότε και η μεταβλητή δεν έχει πλέον νόημα.
________________________________________________________________________
Στο πρόγραμμα που ακολουθεί αναδεικνύεται η συμπεριφορά των καθολικών
μεταβλητών και τα προβλήματα που πιθανόν να ανακύψουν από εσφαλμένη χρήση τους.
#include <stdio.h> float glob; //
καθολική μεταβλητή float square(float x); void main() { float in; glob=2.0; printf( "glob within main
is = %f\n",glob ); in=square(glob); printf( "%f squared is
%f\n",glob,in ); in=square(glob); printf( "%f squared is
%f\n",glob,in ); } float square(float x) { glob=glob+1.0; printf( "glob within
square is = %f\n",glob ); return (x*x); } |
Δηλώνεται
μία καθολική μεταβλητή glob, η οποία λαμβάνει μέσα στη main() την τιμή 2.
Ακολούθως καλείται η square() με όρισμα την τιμή της glob, έτσι ώστε να προκύψει
το τετράγωνο της glob. Η τιμή της glob περνά στην τυπική παράμετρο x, η οποία και υψώνεται
στο τετράγωνο. Ωστόσο, μέσα στη συνάρτηση square() μεταβάλλεται η τιμή της glob κατά μία μονάδα. Κατά
συνέπεια η έξοδος της square() είναι το τετράγωνο του 2 αλλά
επιστρέφοντας στη main() η glob έχει λάβει την τιμή 3,
οδηγώντας την printf() σε εσφαλμένη έξοδο όπως φαίνεται στα
αποτελέσματα. Στην επόμενη κλήση της, η square() δέχεται ως όρισμα το 3
και επιστρέφει το 9, έχοντας όμως μεταβάλλει τη glob.
________________________________________________________________________
Στις §2.3.1 και 2.3.2
παρουσιάσθηκαν δύο είδη μεταβλητών με διαφορετικό εύρος λειτουργίας. Το τμήμα
του πηγαίου κώδικα στο οποίο μία μεταβλητή είναι ενεργή προσδιορίζεται με τους κανόνες εμβέλειας (scope rules). Διακρίνονται τέσσερις τύποι
εμβέλειας:
·
Εμβέλεια
προγράμματος: μεταβλητές αυτής
της εμβέλειας είναι οι καθολικές. Είναι ορατές από όλες τις συναρτήσεις που
απαρτίζουν το πρόγραμμα, έστω κι αν βρίσκονται σε διαφορετικά αρχεία πηγαίου
κώδικα.
·
Εμβέλεια
αρχείου: μεταβλητές
αυτής της εμβέλειας είναι ορατές μόνο στο αρχείο που δηλώνονται και μάλιστα από
το σημείο της δήλωσής τους και κάτω. Μεταβλητή που δηλώνεται έξω από το μπλοκ
με τη λέξη κλειδί static πριν από τον τύπο, έχει εμβέλεια
αρχείου, π.χ. static int velocity.
·
Εμβέλεια
συνάρτησης: Προσδιορίζει
την ορατότητα του ονόματος από την αρχή της συνάρτησης έως το τέλος της.
Εμβέλεια συνάρτησης έχουν μόνο οι goto ετικέτες.
·
Εμβέλεια
μπλοκ: Προσδιορίζει την
ορατότητα από το σημείο δήλωσης έως το τέλος του μπλοκ στο οποίο δηλώνεται.
Μπλοκ είναι ένα σύνολο από προτάσεις, οι οποίες περικλείονται σε άγκιστρα.
Μπλοκ είναι η σύνθετη πρόταση αλλά και το σώμα συνάρτησης. Εμβέλεια μπλοκ έχουν και τα τυπικά
ορίσματα των συναρτήσεων.
Η C επιτρέπει τη χρήση ενός ονόματος για την αναφορά σε
διαφορετικά αντικείμενα, με την προϋπόθεση ότι αυτά έχουν διαφορετική εμβέλεια
ώστε να αποφεύγεται η σύγκρουση
ονομάτων (name conflict). Εάν οι περιοχές εμβέλειας έχουν επικάλυψη, τότε το
όνομα με τη μικρότερη εμβέλεια αποκρύπτει το όνομα με τη μεγαλύτερη.
________________________________________________________________________
Να προσδιορισθεί η εμβέλεια των μεταβλητών στον ακόλουθο πηγαίο κώδικα:
1 #include <stdio.h> 2 int max(int a, int b); 3 static void func(int x); 4 int a; static int b; 5 void main() { 6 a=12; b=a--; 7 printf(
"a:%d\tb:%d\tmax(b+5,a):%d\n",a,b,max(b+5,a) ); 8 func(a+b); 9 } // τέλος της main 10 int c=13; 11 int max(int a, int b){ 12 return(a>b?a:b); 13 } // τέλος της
max 14 static void func(int x){ 15 int b=20; 16 printf(
"a:%d\tb:%d\tc:%d\tx:%d\tmax(x,b):%d\n", 17 a,b,c,x,max(x,b) ); 18 } // τέλος της
func |
·
4 int a; static int b; Η a είναι καθολική
μεταβλητή με εμβέλεια προγράμματος. Η b έχει εμβέλεια αρχείου, όπως
προσδιορίζει η λέξη static.
·
10 int c=13; Έχει εμβέλεια προγράμματος
αλλά είναι ενεργή από το σημείο δήλωσής της και κάτω (γραμμή 10).
·
11 int max(int a, int b){ Οι a και b έχουν
εμβέλεια μπλοκ και αποκρύπτουν για το σώμα της max() τις
καθολικές μεταβλητές a και b.
·
6 a=12; b=a--; Αποδίδονται οι τιμές 11 και 12
στις a και b, αντίστοιχα.
·
7 printf( "a:%d\tb:%d\tmax(b+5,a):%d\n",a,b,max(b+5,a) );
Καλείται η συνάρτηση max() και
αυτή δίνει στα τυπικά ορίσματα a και b τις τιμές 11 και 12,
αντίστοιχα. Η max() επιστρέφει το 17, το οποίο τυπώνει η printf().
·
8 func(a+b); Καλείται η συνάρτηση func()
και αυτή δίνει στο τυπικό όρισμα x την τιμή 11+12=23. Το όρισμα x
έχει εμβέλεια μπλοκ. Η τοπική μεταβλητή b=20, που δηλώνεται στη γραμμή
15, αποκρύπτει από το σώμα της func() την καθολική μεταβλητή b.
Δε συμβαίνει όμως το ίδιο και για την καθολική μεταβλητή a, η οποία
είναι ορατή από το σώμα της func().
________________________________________________________________________
Η διάρκεια
ορίζει το χρόνο κατά τον οποίο το όνομα της μεταβλητής είναι συνδεδεμένο με τη
θέση μνήμης που περιέχει την τιμή της μεταβλητής. Ορίζονται ως χρόνοι δέσμευσης και αποδέσμευσης οι χρόνοι που το
όνομα συνδέεται με τη μνήμη και αποσυνδέεται από αυτή, αντίστοιχα.
Για τις καθολικές μεταβλητές δεσμεύεται χώρος με την
έναρξη εκτέλεσης του προγράμματος και η μεταβλητή συσχετίζεται με την ίδια θέση
μνήμης έως το τέλος του προγράμματος. Είναι πλήρους διάρκειας.
Αντίθετα, οι τοπικές μεταβλητές είναι περιορισμένης διάρκειας. Η
ανάθεση της μνήμης σε τοπική μεταβλητή γίνεται με τη είσοδο στο χώρο εμβέλειάς
της και η αποδέσμευσή της με την έξοδο από αυτόν. Δηλαδή η τοπική μεταβλητή δε
διατηρεί την τιμή της από τη μία κλήση της συνάρτησης στην επόμενη.
Εάν προστεθεί στη δήλωση μίας τοπικής μεταβλητής η
λέξη static, διατηρεί την τιμή της και καθίσταται πλήρους
διάρκειας.
Στη συνάρτηση
func(int x);
{
int temp;
static int num;
. . . . . .
}
η μεταβλητή num είναι
τοπική αλλά έχει διάρκεια προγράμματος, σε αντίθεση με την temp, η
οποία έχει διάρκεια συνάρτησης.
Παρατήρηση: Θα πρέπει να δοθεί προσοχή στην
αρχικοποίηση των τοπικών μεταβλητών. Μία τοπική μεταβλητή περιορισμένης
διάρκειας αρχικοποιείται, εφόσον βέβαια κάτι τέτοιο έχει ορισθεί να γίνεται, με
κάθε είσοδο στο μπλοκ που αυτή ορίζεται. Αντίθετα, μία τοπική μεταβλητή πλήρους
διάρκειας αρχικοποιείται μόνο με την ενεργοποίηση του προγράμματος.
________________________________________________________________________
α) Να περιγραφεί η επίδραση της λέξης κλειδί static στις δύο δηλώσεις του ακόλουθου πηγαίου κώδικα.
β) Πότε
αρχικοποιείται η count και πότε η num;
static int num; void func(void) { static int count=0; int num=100; . . . . . . . } |
α) Η static στη δήλωση της καθολικής μεταβλητής num περιορίζει την ορατότητά
της μόνο στο αρχείο που δηλώνεται. Αντίθετα η static στη
δήλωση της τοπικής μεταβλητής count ορίζει γι’ αυτήν διάρκεια
προγράμματος.
β) Η count ως τοπική μεταβλητή πλήρους διάρκειας αρχικοποιείται μία φορά με
την είσοδο στο πρόγραμμα. Αντίθετα η num ως τοπική μεταβλητή
περιορισμένης διάρκειας αρχικοποιείται σε κάθε ενεργοποίηση της
συνάρτησης func.
________________________________________________________________________
________________________________________________________________________
Ο κώδικας που ακολουθεί αποτελεί παράδειγμα χρήσης στατικών μεταβλητών.
Στη συνάρτηση get_average() οι μεταβλητές static float total και static int count εκτελούνται μόνο την πρώτη φορά.
Τις επόμενες διατηρούν το αποτέλεσμα της προηγούμενης κλήσης και σε αυτό
προστίθενται στη μεν total το newdata, στη δε count η μονάδα.
#include <stdio.h> float get_average(float newdata); void main() { float data=1.0; float average; while (data!=0) { printf( "\n Give a number
or press 0 to finish: " ); scanf( "%f",&data
); average=get_average(data); printf( "The new average
is %f\n",average ); } } //
τέλος της main float get_average(float newdata) { static float total=0.0; static int count=0; count++; total=total+newdata; return(total/count); } //
τέλος της get_average |
________________________________________________________________________
Εάν κατά την κλήση μίας συνάρτησης η πραγματική παράμετρος είναι όνομα πίνακα
(πχ. t), δεν αποστέλλει στη συνάρτηση ολόκληρο τον πίνακα
αλλά τη διεύθυνση του πρώτου byte του πίνακα. Η
παράμετρος στη δήλωση της συνάρτησης είναι ένα όνομα τοπικού πίνακα (π.χ. number). Κρατά ένα αντίγραφο της ίδιας διεύθυνσης αλλά
χρησιμοποιεί διαφορετικό όνομα. Επιπρόσθετα, στη δήλωση δεν αναφέρεται
το ακριβές μέγεθός του αλλά μόνο το γεγονός ότι είναι πίνακας καθώς και ο τύπος
στοιχείων του. Επομένως ουσιαστικά γνωστοποιείται στο μεταγλωττιστή η διεύθυνση
του πρώτου byte και
η απόσταση (αριθμός bytes) μεταξύ των στοιχείων:
#include
<stdio.h> display(int
num[ ]); void main() { int t[10],i; for (i=0; i<=9; i++) t[i]=i; display(t); } display(int
num[ ]) { int i; for (i=0; i<=9; i++) printf( “%d “,num[i] ); } |
________________________________________________________________________
Παράδειγμα 2.9
Στον κώδικα που ακολουθεί παρουσιάζονται συναρτήσεις που δέχονται και
επιστρέφουν δομές. Ο κώδικας υλοποιεί έναν αθροιστή μεγεθών που είναι
εκφρασμένα σε πόδια και ίντσες.
/* Αν ο αριθμός των ιντσών υπερβαίνει το 12 συμπληρώνεται ένα πόδι, επομένως πρέπει να προστεθεί η μονάδα στη μεταβλητή feet και να αφαιρεθούν 12 ίντσες από τη μεταβλητή inches. */ #include <stdio.h> struct Distance { int feet; int inches; }; Distance addeng1(Distance dd1, Distance dd2); // δήλωση addeng1 void eng1disp(Distance dd); // δήλωση eng1disp void main() { Distance d1,d2,d3; printf( "\n1st number of
feet:" ); scanf("%d",&d1.feet); printf( "\n1st number of
inches:" ); scanf("%d",&d1.inches
); printf( "\n2nd second
number of feet:" ); scanf("%d",&d2.feet); printf( "\n2nd second
number of inches:" ); scanf("%d",&d2.inches); d3=addeng1(d1,d2); eng1disp(d1); printf( " +" ); eng1disp(d2); printf( " =" ); eng1disp(d3); printf( "\n" ); } //
τέλος της main void eng1disp(Distance dd) { printf( "%d'%d''
",dd.feet,dd.inches ); } //
τέλος της eng1disp Distance addeng1(Distance dd1, Distance dd2) { Distance dd3; dd3.inches=dd1.inches+dd2.inches; dd3.feet=0; if (dd3.inches>=12) { dd3.inches=dd3.inches-12; dd3.feet++; } dd3.feet=dd3.feet+dd1.feet+dd2.feet; return(dd3); } //
τέλος της addeng1 |
________________________________________________________________________
Μία
συνάρτηση ονομάζεται αναδρομική όταν μία εντολή του σώματος της συνάρτησης
καλεί τον ίδιο της τον εαυτό. Η αναδρομή είναι μία διαδικασία με την οποία
ορίζεται κάτι μέσω του ίδιου του οριζόμενου.
Έστω ότι μία αναδρομική
συνάρτηση καλείται να λύσει ένα πρόβλημα. Μία τέτοια συνάρτηση δύναται να λύσει
μόνο την απλούστερη περίπτωση, τη λεγόμενη βάση της αναδρομής (base case). Εάν η περίπτωση είναι
πολύπλοκη, το πρόβλημα μερίζεται σε ένα ή περισσότερα υποπροβλήματα, τα οποία
μοιάζουν με το αρχικό πρόβλημα αλλά αποτελούν μικρότερες εκδοχές του. Η
αναδρομική συνάρτηση καλεί τον εαυτό της για την επίλυση των υποπροβλημάτων.
Αυτή είναι μία αναδρομική κλήση ή αναδρομικό βήμα (recursion step). Η διαδικασία συνεχίζεται έως
ότου ο μερισμός σε υποπροβλήματα οδηγήσει στη βάση της αναδρομής, η οποία
επιλύεται άμεσα. Κατόπιν ακολουθείται η αντίστροφη διαδικασία, επιλύοντας
αρχικά τα μικρότερα υποπροβλήματα και προχωρώντας προς τα μεγαλύτερα. Η όλη
διαδικασία θα αποσαφηνισθεί με τη βοήθεια του ακόλουθου παραδείγματος:
Παράδειγμα: Να ορισθεί συνάρτηση που υπολογίζει το άθροισμα των
αριθμών από 1 έως n.
// Χωρίς αναδρομή int sum(int n) { int i, total=0; for (i=0; i<=n; i++) total+=i; return(total); } |
// Με αναδρομή int sum(int n) { if (n<=1) return(n); else return(sum(n-1)+n); } |
Επεξηγήσεις της
αναδρομικής εκδοχής:
if (n<=1)
return(n);
else return(sum(n-1)+n);
Εάν το n είναι ίσο με 1, τότε το
άθροισμα ταυτίζεται με το n
(βάση της αναδομής). Στη γενική περίπτωση, ο υπολογισμός του αθροίσματος n μπορεί να θεωρηθεί ως
υπολογισμός του αθροίσματος των αριθμών από το 1 έως το n-1 συν το n. Αντίστοιχα, ο υπολογισμός του
αθροίσματος n-1 μπορεί
να θεωρηθεί ως υπολογισμός του αθροίσματος των αριθμών από το 1 έως το n-2 συν το n-1. Ακολουθώντας την παραπάνω
διαδικασία, μπορούμε να ορίσουμε τα εξής:
1+…+n = (1+…+(n-1)) + n
(1+…+(n-1)) = (1+…+(n-2)) +
(n-1)
(1+…+(n-2)) =
(1+…+(n-3)) + (n-2)
(1+…+(n-3)) = (1+…+(n-4)) + (n-3)
κ.ο.κ.
Από τα παραπάνω προκύπτει
ότι κάθε σχέση είναι ίδια με την προηγούμενη, με απλή αλλαγή των ορισμάτων.
Την πρώτη φορά καλείται η sum() με όρισμα n. Με την πρόταση return(sum(n-1)+n) η συνάρτηση sum() καλεί τον
ίδιο της τον εαυτό με διαφορετικό όμως όρισμα (n-1). Η ενεργοποίηση αυτή θα προκαλέσει με τη σειρά της νέα ενεργοποίηση και
αυτό θα συνεχισθεί έως ότου προκληθεί διακοπή. Η διακοπή είναι αποκλειστική
ευθύνη του προγραμματιστή. Στο συγκεκριμένο παράδειγμα η διακοπή προκαλείται με
την πρόταση if (n<=1) return(n), που σημαίνει ότι όταν το n φθάσει να γίνει 1 υπάρχει πλέον αποτέλεσμα.
Έτσι οι διαδοχικές κλήσεις για n=4 είναι:
sum(4) καλεί τη sum(3)
sum(3) καλεί τη sum(2)
sum(2) καλεί τη sum(1)
η sum(1) δίνει αποτέλεσμα 1 και το επιστρέφει στη sum(2)
η sum(2)
δίνει αποτέλεσμα 1+2=3 και το επιστρέφει στη sum(3)
η sum(3) δίνει αποτέλεσμα 3+3=6 και το επιστρέφει στη sum(4)
η sum(4) δίνει αποτέλεσμα 6+4=10, το οποίο είναι και το τελικό
Το πλήρες πρόγραμμα έχει την ακόλουθη μορφή:
#include
<stdio.h> #
include <conio.h> int
sum(int n); int
number_of_calls=0; void main() { int n=4; printf(
"\n n=%d",n ); printf(
"\n Sum = %d", sum(n) ); printf( "\n Press any key to finish" ); getch(); } // Τέλος της main int
sum(int n) { if (n<=1) { number_of_calls++; printf( "\nNumber of
calls:%d",number_of_calls ); return(n); } else { number_of_calls++; printf( "\nNumber of
calls:%d",number_of_calls ); return(sum(n-1)+n); } } // τέλος της sum |
Πλεονεκτήματα
των αναδρομικών συναρτήσεων:
·
Το βασικότερο
πλεονέκτημα των αναδρομικών συναρτήσεων είναι ότι μπορούν να χρησιμοποιηθούν
για να δημιουργηθούν καθαρότερες και απλούστερες εκδοχές πολλών αλγορίθμων.
·
Δημιουργείται
συμπαγέστερος κώδικας και είναι ιδιαίτερα χρήσιμες σε αναδρομικώς οριζόμενα δεδομένα
όπως οι λίστες και τα δένδρα.
Μειονεκτήματα
των αναδρομικών συναρτήσεων:
·
Οι
περισσότερες αναδρομικές συναρτήσεις δεν εξοικονομούν σημαντικό μέγεθος κώδικα
ή μνήμης για τις μεταβλητές.
·
Οι αναδρομικές
εκδοχές των περισσότερων συναρτήσεων μπορεί να εκτελούνται κάπως πιο αργά από
τα επαναληπτικά τους ισοδύναμα εξαιτίας των πρόσθετων κλήσεων σε συναρτήσεις. Η πιθανή
μείωση όμως δεν είναι αξιοσημείωτη.
·
Υπάρχει μικρή
πιθανότητα οι πολλές αναδρομικές κλήσεις μίας συνάρτησης να προκαλέσουν
υπερχείλιση της στοίβας (stack overflow), επειδή ο χώρος αποθήκευσης των παραμέτρων και των
τοπικών μεταβλητών της συνάρτησης είναι στη στοίβα και κάθε νέα κλήση παράγει
ένα νέο αντίγραφο αυτών των μεταβλητών. Ωστόσο, εφόσον διατηρείται ο έλεγχος
της αναδρομικής συνάρτησης και υπάρχει συνθήκη διακοπής, το ζήτημα είναι
ήσσονος σημασίας.
________________________________________________________________________
Να καταστρωθεί πρόγραμμα, με χρήση αναδρομικής συνάρτησης, το οποίο
δέχεται ως είσοδο από το πληκτρολογίο έναν ακέραιο αριθμό n και επιστρέφει στην οθόνη το παραγοντικό του (n! = 1x2x…xn).
Το πρόβλημα είναι απολύτως αντίστοιχο με εκείνο του προηγούμενου
παραδείγματος. Οι μόνες διαφορές είναι ότι πρέπει να διαβάζεται ο n
και ότι πολλαπλασιάζονται ακέραιοι και δεν αθροίζονται. Κατά
συνέπεια, μία πιθανή λύση είναι η ακόλουθη:
#include <stdio.h> #include <conio.h> int fact(int n); void main() { int n; printf(
"Give n:" ); scanf(
"%d",&n ); printf( "\n The factorial
of %d is %d",n,fact(n) ); printf( "\n Press any key to finish" ); getch(); } //
τέλος της main int fact(int n) { if (n<=0) { printf( "\nERROR! n should
be a positive integer\n"); return(0); // σε περίπτωση σφάλματος επιστροφή με το 0 } else if (n<=1) return(n); else return(fact(n-1)*n); } // τέλος
της fact |
________________________________________________________________________
Κάθε μεταβλητή σχετίζεται με μία
θέση στην κύρια μνήμη του υπολογιστή, η οποία χαρακτηρίζεται από τη διεύθυνσή
της. Στη γλώσσα μηχανής μπορεί να γίνει άμεση χρήση αυτής της διεύθυνσης για να
αποθηκευθούν ή να ανακληθούν δεδομένα. Αντίθετα, στις γλώσσες προγραμματισμού
υψηλού επιπέδου οι διευθύνσεις δεν είναι άμεσα ορατές από τον προγραμματιστή
καθώς καλύπτονται από το μανδύα των συμβολικών ονομάτων, τα οποία το σύστημα
αντιστοιχεί στις πραγματικές διευθύνσεις.
Η γλώσσα C, θέλοντας να
δώσει στον προγραμματιστή τη δυνατότητα συγγραφής αποδοτικού κώδικα,
υποστηρίζει την άμεση διαχείριση των περιεχομένων της μνήμης, εισάγοντας την έννοια
του δείκτη (pointer). Ο
δείκτης αποτελεί μία μεταβλητή που περιέχει μία διεύθυνση μνήμης. Οι
δείκτες είναι ένα ισχυρό προγραμματιστικό εργαλείο, που εφαρμόζεται:
·
στη δυναμική εκχώρηση μνήμης
·
στη διαχείριση σύνθετων δομών δεδομένων
·
στην αλλαγή τιμών που έχουν εκχωρηθεί ως
ορίσματα σε συναρτήσεις
·
για τον αποτελεσματικότερο χειρισμό
πινάκων
Ωστόσο, καθώς η χρήση των δεικτών
οδηγεί σε επεμβάσεις στη μνήμη και πολλές φορές σε προγραμματιστικές
ακροβασίες, συχνά αποτελεί αιτία δύσκολων στον εντοπισμό σφαλμάτων, γι’ αυτό
και πρέπει να γίνεται με ιδιαίτερη προσοχή.
Η δήλωση μίας μεταβλητής δείκτη ακολουθεί τον εξής
φορμαλισμό:
<τύπος_δεδομένων> * <όνομα_δείκτη>;
π.χ.
int *pnum;
·
Όταν ένας δείκτης έχει αποθηκευμένη μία
διεύθυνση έχει επικρατήσει να λέγεται ότι ο
δείκτης «δείχνει» στη διεύθυνση. Στη δήλωση δείκτη ο <τύπος_δεδομένων> αφορά στο είδος των δεδομένων που
αποθηκεύονται στη διεύθυνση που «δείχνει» ο δείκτης. Ο τύπος της κανονικής
μεταβλητής πρέπει να δηλώνεται γιατί μία μεταβλητή δεσμεύει συγκεκριμένη μνήμη
(8 bytes για float, 4 bytes για int κ.ο.κ.). Εφόσον
ο δείκτης χρησιμοποιείται για να γίνεται έμμεση αναφορά στην τιμή της
μεταβλητής, πρέπει να είναι γνωστό πόση μνήμη καταλαμβάνει αυτή η τιμή.
·
Τον τύπο δεδομένων ακολουθεί ο αστερίσκος,
ο οποίος είναι απαραίτητος γιατί προσδιορίζει ότι δηλώνεται μία μεταβλητή
δείκτη κι όχι μία «κανονική» μεταβλητή. Ο αστερίσκος συνδέεται με το όνομα και
όχι με τον τύπο δεδομένων. Θα πρέπει να σημειωθεί ότι αν και η μεταβλητή με
δείκτη περιέχει μία διεύθυνση (δηλαδή έναν ακέραιο αριθμό), δεν είναι ίδια
με μία κανονική ακέραια μεταβλητή. Μέσω του αστερίσκου ο μεταγλωττιστής
γνωρίζει ότι η τιμή της μεταβλητής με δείκτη είναι μία συγκεκριμένη διεύθυνση
μνήμης, σε αντιδιαστολή με την «κανονική» ακέραια τιμή.
·
Τη δήλωση δείκτη κλείνει το όνομα του
δείκτη. Επιλέγεται με βάση τις ίδιες συμβάσεις που ισχύουν στις κανονικές
μεταβλητές. Ωστόσο, συνήθως ο αρχικός χαρακτήρας του ονόματος δείκτη είναι το p, έτσι ώστε το πρόγραμμα να καθίσταται περισσότερο
ευανάγνωστο, καθώς με τον πρώτο χαρακτήρα φαίνεται εάν μία μεταβλητή είναι
δείκτης ή όχι. Εναλλακτικά, μπορεί να προστεθεί η κατάληξη _ptr.
Ενδεικτικές είναι οι ακόλουθες δηλώσεις:
int *pcount, *count_ptr; /* δείκτες σε ακέραιες μεταβλητές
*/
char *pword, *word_ptr; /* δείκτες σε μεταβλητές χαρακτήρα */
Καθώς το περιεχόμενο ενός δείκτη είναι μία
διεύθυνση, δηλαδή ένας ακέραιος αριθμός, ένας δείκτης θα καταλαμβάνει όσα bytes αντιστοιχούν σε
ακέραιο (π.χ. 4 bytes), ανεξάρτητα
από τον τύπο της κανονικής μεταβλητής στην οποία δείχνει. Στο σχήμα 3.1
απεικονίζεται ο τρόπος λειτουργίας των δεικτών, με τη x να είναι μία κανονική μεταβλητή και τον px να είναι δείκτης που δείχνει στη x.
Σχ. 3.1 Απεικόνιση του τρόπου λειτουργίας των δεικτών
Η ανάθεση τιμής σε δείκτη μπορεί να γίνει με έναν
από τους ακόλουθους τέσσερις τρόπους:
Ø
Με χρήση πινάκων, δεδομένου ότι το όνομα ενός πίνακα
αντιστοιχεί στη διεύθυνση του πρώτου στοιχείου του. Έτσι, με τον ακόλουθο
κώδικα αποδίδεται στο δείκτη ακεραίων pint η τιμή 60, δηλαδή η διεύθυνση
του πρώτου στοιχείου του πίνακα numArray. Στο σχήμα 3.2 απεικονίζεται η διαδικασία ανάθεσης τιμής στο δείκτη pint.
int numArray[5] = {1,2,3,4,5};
int
*pint;
pint
= numArray;
Σχ. 3.2 Ανάθεση
τιμής σε δείκτη με χρήση πίνακα
Ø
Με χρήση άλλων δεικτών ίδιου τύπου. Στον
κώδικα που ακολουθεί ο δείκτης pint δείχνει ήδη σε κάποια διεύθυνση. Με την έκφραση pnum = pint; το περιεχόμενο του pint αντιγράφεται στον pnum κι έτσι ο τελευταίος
δείχνει στην ίδια διεύθυνση, όπως φαίνεται στο σχήμα 3.3.
int numArray[5] = {1,2,3,4,5};
int
*pint, *pnum;
pint
= numArray;
pnum
= pint;
Σχ. 3.3 Ανάθεση
τιμής σε δείκτη με χρήση άλλων δεικτών ίδιου τύπου
Ø
Με χρήση αριθμητικής δεικτών. Οι δείκτες υποστηρίζουν
εκφράσεις της μορφής
pnum=pint+y; ή pnum=pint-y;
όπου οι
pnum, pint είναι δείκτες ίδιου τύπου και ο y είναι ακέραιος. Οι παραπάνω είναι οι μόνες πράξεις που μπορούν να
γίνουν με δείκτες. Η διαφοροποίηση της πρώτης έκφρασης έγγειται στο ότι στον pnum δε θα ανατεθεί το
περιεχόμενο του pint αυξημένο
κατά y μονάδες αλλά αυξημένο κατά y επί τον αριθμό των bytes που καταλαμβάνει ο τύπος
δεδομένων του δείκτη. Δηλαδή, εάν οι pnum, pint είναι δείκτες ακεραίων και το y=2, ο pnum θα δείχνει 2x4=8 bytes κάτω από τη διεύθυνση που
δείχνει ο pint, όπως φαίνεται στο σχήμα 3.4.
int numArray[5] = {1,2,3,4,5};
int
*pint, *pnum;
pint
= numArray;
pnum
= pint+2;
Σχ. 3.4 Ανάθεση
τιμής σε δείκτη με χρήση αριθμητικής δεικτών
Ø
Με χρήση του τελεστή διεύθυνσης (&)
(address-of operator). Ο τελεστής διεύθυνσης &, ο οποίος πρωτοαπαντήθηκε στη συνάρτηση εισόδου scanf(), εισάγεται μπροστά από μία μεταβλητή εκφράζοντας τη
διεύθυνσή της. Ο συμβολισμός
&count
ερμηνεύεται
«στη διεύθυνση της count». Έτσι, με τον ακόλουθο κώδικα ανατίθεται στο δείκτη pnum η διεύθυνση στην οποία βρίσκεται
αποθηκευμένη η ακέραια μεταβλητή count. Η διαδικασία απεικονίζεται παραστατικά στο σχήμα 3.5.
int *pnum;
int count;
pnum
= &count;
Σχ. 3.5 Ανάθεση
τιμής σε δείκτη με χρήση του τελεστή διεύθυνσης
Παρατήρηση: Θα
μπορούσε να γίνει άμεση ανάθεση μίας διεύθυνσης, π.χ. pnum=1000; Ωστόσο, μία τέτοια επιλογή είναι πολύ επικίνδυνη
και πρέπει να αποφεύγεται. Τέτοιου είδους αναθέσεις χρησιμοποιούνται συνήθως
μόνο για άμεση πρόσβαση στο υλικό (hardware).
Η
προσπέλαση μίας μεταβλητής με χρήση δείκτη και η μεταβολή της τιμής της γίνεται με χρήση του τελεστή
περιεχομένου (*) (dereferencing operator) ή τελεστή έμμεσης αναφοράς, η
λειτουργία του οποίου περιγράφεται με τη βοήθεια του ακόλουθου κώδικα:
int *pcount, num;
num=10;
pcount=#
*pcount = 20;
Στον
παραπάνω κώδικα αρχικά ορίζεται μία ακέραια μεταβλητή num, η οποία λαμβάνει την τιμή 10, και ένας δείκτης σε ακέραιο pint, ο οποίος αρχικά δεν έχει τιμή. Στο
σχήμα 3.6α παρουσιάζεται ο χάρτης μνήμης μετά το τέλος της δεύτερης γραμμής. Ως
junk (απορρίματα) συμβολίζεται το περιεχόμενο μίας θέσης μνήμης όταν αυτή δεν
έχει ορισθεί στο τρέχον πρόγραμμα.
Ακολούθως
ανατίθεται στο δείκτη pcount η διεύθυνση της num (σχήμα
3.6β). Στην τελευταία γραμμή χρησιμοποιείται ο τελεστής περιεχομένου μπροστά
από το δείκτη. Ο συμβολισμός *pcount ερμηνεύεται «στη διεύθυνση που
δείχνει ο pcount». Έτσι η τελευταία γραμμή κώδικα
ερμηνεύεται «να τοποθετηθεί το 20
στη διεύθυνση που δείχνει ο pcount», δηλαδή να μεταβληθεί έμμεσα η τιμή της num από 10 σε 20 (σχήμα 3.6γ).
(α) (β) (γ)
Σχ. 3.6 Προσπέλαση μεταβλητής με χρήση δείκτη
________________________________________________________________________
Να περιγραφεί αναλυτικά η λειτουργία κάθε γραμμής κώδικα και να
απεικονισθούν τα περιεχόμενα των θέσεων μνήμης που καταλαμβάνουν οι μεταβλητές.
1: int *px,
*py, x=1, y=0; 7: y = *px; |
Ø Γραμμή 1: Δήλωση
δύο δεικτών σε ακέραιο (px, py), ακολουθούμενη από δήλωση και
αρχικοποίηση δύο ακέραιων μεταβλητών (x, y) (σχήμα 3.7α).
Ø Γραμμή 2: Δήλωση
πίνακα ακεραίων πέντε στοιχείων (a) και αρχικοποίηση αυτού (σχήμα 3.7β).
|
|
Σχ. 3.7α |
Σχ. 3.7β |
Ø Γραμμή 3: Δήλωση
ακέραιας μεταβλητής (i) (σχήμα 3.7γ).
Ø Γραμμή 4: Ανάθεση
της διεύθυνσης της μεταβλητής x στο δείκτη px, δηλαδή το περιεχόμενο του px είναι η διεύθυνση - το πρώτο byte – του x (σχήμα 3.7δ).
|
|
Σχ. 3.7γ |
Σχ. 3.7δ |
Ø Γραμμή 5: Ανάθεση
της διεύθυνσης του πρώτου στοιχείου του πίνακα a στο δείκτη py, δηλαδή το περιεχόμενο του py είναι η διεύθυνση του a[0] (σχήμα 3.7ε).
|
Σχ. 3.7ε |
Ø Γραμμή 6: Επαναληπτική
έκφραση, σε κάθε επανάληψη της οποίας λαμβάνουν χώρα τα ακόλουθα: Η τιμή 2*i τοποθετείται σε θέση μνήμης με διεύθυνση 2*i bytes μετά τη διεύθυνση που δείχνει ο py (σχήματα 3.7στ–3.7ι). Για παράδειγμα, εάν i=3, η τιμή 6 αποθηκεύεται στη θέση μνήμης py+i=1245028+3*4=1245028+12=1245040, θέση που καταλαμβάνει το στοιχείο a[3]. Με
αυτόν τον τρόπο αποδίδεται στο a[3] η τιμή 6.
|
|
Σχ. 3.7στ |
|
|
|
Σχ. 3.7ζ |
Σχ. 3.7η |
|
|
Σχ. 3.7θ |
Σχ. 3.7ι |
Ø Γραμμή 7: Το
περιεχόμενο της θέσης στην οποία δείχνει ο px, δηλαδή το 1, αποδίδεται
στο y (σχήμα 3.7ια).
Ø Γραμμή 8: Ανάθεση
της διεύθυνσης της μεταβλητής y
στο δείκτη py (σχήμα 3.7ιβ).
|
|
Σχ. 3.7ια |
Σχ. 3.7ιβ |
Ø Γραμμή 9: Ο
δείκτης px δείχνει 12 bytes (3*4) μετά τη διεύθυνση του a[0], δηλαδή στη διεύθυνση του πρώτου byte του
a[3] (σχήμα
3.7ιγ).
|
Σχ. 3.7ιγ |
Ø Γραμμή 10: Το
περιεχόμενο της διεύθυνσης στην οποία δείχνει ο px γίνεται 21 (σχήμα 3.7ιδ).
|
Σχ. 3.7ιδ |
Ø Γραμμή 11: Το
περιεχόμενο της διεύθυνσης στην οποία δείχνει ο py γίνεται ίσο με το περιεχόμενο της διεύθυνσης στην οποία δείχνει ο px, αυξημένο κατά 9, δηλαδή ισούται με 30
(σχήμα 3.7ιε).
Ø Γραμμή 12: Η
μεταβλητή x γίνεται ίση με τη μεταβλητή y (σχήμα 3.7ιστ).
|
|
Σχ. 3.7ιε |
Σχ. 3.7ιστ |
________________________________________________________________________
Στο προηγούμενο κεφάλαιο αναφέρθηκε ο
τρόπος κλήσης μίας συνάρτησης και η μεταβίβαση των πραγματικών ορισμάτων στις
παραμέτρους της καλούμενης συνάρτησης, σύμφωνα με τον οποίο κατά την κλήση μίας
συνάρτησης οι παράμετροι αποτελούν αντίγραφο των πραγματικών ορισμάτων,
καταλαμβάνοντας διαφορετικές θέσεις μνήμης. Κατά συνέπεια, η όποια επεξεργασία
υφίστανται οι τυπικές παράμετροι δεν επηρεάζει τις τιμές των πραγματικών
ορισμάτων. Ο παραπάνω τρόπος κλήσης ονομάζεται κλήση κατ’ αξία (call by value).
Ωστόσο υπάρχουν περιπτώσεις, όπως θα
φανεί στο παράδειγμα 3.3, κατά τις οποίες ο συγκεκριμένος
τρόπος κλήσης μίας συνάρτησης είναι ανεπαρκής και απαιτείται να δύναται η
συνάρτηση να μεταβάλλει τις τιμές των πραγματικών ορισμάτων. Σε τέτοιες
περιπτώσεις χρησιμοποιείται η κλήση κατ’
αναφορά (call by reference), σύμφωνα με την οποία δε μεταβιβάζονται στις τυπικές παραμέτρους οι τιμές
των πραγματικών ορισμάτων αλλά οι διευθύνσεις τους. Κατά συνέπεια, η όποια
επεξεργασία υποστούν οι τυπικές παράμετροι θα επηρεάσει τις τιμές των
πραγματικών ορισμάτων. Η κλήση κατ’ αναφορά χρησιμοποιεί ως πραγματικά ορίσματα
τις διευθύνσεις των μεταβλητών και ως τυπικές παραμέτρους δείκτες.
Θα πρέπει να σημειωθεί ότι η κλήση
συναρτήσεων με ορίσματα πίνακες είναι κλήση κατ’ αναφορά, καθώς το όνομα ενός
πίνακα αντιστοιχεί στη διεύθυνση του πρώτου στοιχείου του.
Παρατήρηση: Μπορεί
να χρησιμοποιηθεί δείκτης για να αλλαχθεί το περιεχόμενο της θέσης στην οποία
δείχνει, αλλά δεν πρέπει να αλλαχθεί ο ίδιος ο δείκτης μέσα στην καλούμενη
συνάρτηση. Ο λόγος είναι ότι οι πραγματικές
παράμετροι που είναι δείκτες αντιγράφουν μία διεύθυνση στις παραμέτρους της
συνάρτησης, αλλά εάν αλλαχθεί η παράμετρος στη συνάρτηση (δηλαδή η διεύθυνση)
δε θα αλλαχθεί η πραγματική παράμετρος! Το ακόλουθο παράδειγμα αναδεικνύει το
πρόβλημα.
________________________________________________________________________
Να αναλυθεί η λειτουργία του ακόλουθου προγράμματος:
#include <stdio.h> void print(int *ptr); void
main() { int *pscore, num; num=32; pscore=# printf( "Before function,\t\t*pscore=%d\n", *pscore); print(pscore); printf( "After function,\t\t*pscore=%d\n", *pscore); } void print(int *ptr) { printf(
"During function,\t\t*ptr=%d\n", *ptr); ptr=ptr+1; } |
Αρχικά δηλώνονται η ακέραια μεταβλητή num, η οποία λαμβάνει την τιμή 32, και ο δείκτης σε ακέραιο pscore. Ακολούθως στον pscore ανατίθεται η διεύθυνση
της num και στη συνέχεια καλείται η συνάρτηση print() με πραγματικό όρισμα τον pscore. Η τυπική παράμετρος της print() είναι o
δείκτης σε ακέραιο ptr, ο οποίος λαμβάνει το περιεχόμενο του pscore, δηλαδή τη διεύθυνση της num.
H συνάρτηση εκτυπώνει το περιεχόμενο της θέσης μνήμης στην οποία δείχνει
ο ptr, δηλαδή το 32, και στη συνέχεια ο ptr αυξάνεται κατά ένα, δείχνοντας σε θέση μνήμης 4 bytes κάτω από τη θέση μνήμης που
έδειχνε προηγουμένως. Αυτή η μεταβολή στο περιεχόμενο του ptr δεν έχει νόημα, γιατί με το πέρας της
συνάρτησης παύει να ισχύει ο ptr και δεν επιδρά στα αποτελέσματα, που παρατίθενται ανωτέρω.
________________________________________________________________________
________________________________________________________________________
Να γίνει συγκριτική ανάλυση της λειτουργίας των ακόλουθων προγραμμάτων:
#include <stdio.h> void
swap(int a, int b); void main() {
int x=10, y=25;
printf( "Before:x=%d,
y=%d\n", x, y);
swap(x,y);
printf( "After:x=%d, y=%d\n", x, y); } void swap(int a, int b) { int temp=a; a=b; b=temp; } |
#include <stdio.h> void
swap(int *pa, int *pb); void main() { int
x=10, y=25;
printf( "Before:x=%d, y=%d\n", x, y);
swap(&x, &y);
printf( "After:x=%d, y=%d\n", x, y); } void swap(int *pa, int *pb) { int
temp=*pa; *pa=*pb; *pb=temp; } |
|
|
Tα
παραπάνω προγράμματα καλούν τη συνάρτηση swap() για να αντιμετατεθούν τα περιεχόμενα
των μεταβλητών x, y. Στο αριστερό πρόγραμμα
χρησιμοποιείται η κλήση κατ’ αξία και οι τιμές των x και y αντιγράφονται στις τυπικές παραμέτρους a και b, αντίστοιχα. Αυτό που επιτυγχάνει η swap() είναι να
αντιμετατεθούν οι τιμές των a και b, με αποτέλεσμα μετά το πέρας της swap() τα a και b να μην είναι πλέον ενεργά και τα x και y να μην έχουν αλλάξει. Κατά συνέπεια το αριστερό πρόγραμμα δεν εκτέλεσε
επιτυχώς το έργο της αντιμετάθεσης, όπως άλλωστε προκύπτει και από τα
αποτελέσματα.
Στο δεξί πρόγραμμα χρησιμοποιείται η
κλήση κατ’ αναφορά και οι διευθύνσεις των x και y αντιγράφονται στις τυπικές παραμέτρους pa και pb, αντίστοιχα, οι οποίες είναι δείκτες
ακεραίων. Χρησιμοποιώντας τον τελεστή περιεχομένου (*) και την τοπική μεταβλητή temp, η θέση που αντιστοιχεί στη x αποκτά το περιεχόμενο της θέσης που αντιστοιχεί στην y και τανάπαλιν. Μετά το πέρας της swap() παύουν
να υφίστανται οι δείκτες pa και pb και το έργο της αντιμετάθεσης έχει επιτευχθεί, όπως φανερώνουν και τα
αποτελέσματα.
________________________________________________________________________
________________________________________________________________________
Να γραφεί μία συνάρτηση, η οποία θα δέχεται ως ορίσματα: α) τη διεύθυνση του πρώτου στοιχείου
ενός πίνακα ακεραίων 4x3 και β) το πλήθος των στοιχείων του, και θα
επιστρέφει το άθροισμα των τετραγώνων των στοιχείων του πίνακα.
#include <stdio.h> #define rows 4 #define columns 3 int add(int *pin, int number) { int k, sum=0; for
(k=0;k<number;k++) sum=sum+pin[k]*pin[k]; // Ισοδύναμη έκφραση: // for
(k=0;k<number;k++) sum=sum+(*(pin+k))*(*(pin+k)); return(sum); }
// τέλος της συνάρτησης void main() { int i,j; int a[rows][columns]; for (i=0;i<rows;i++) for (j=0;j<columns;j++) { a[i][j]=i*j; // π.χ. printf( "a[%d][%d]=%d\n",i,j,a[i][j] ); } printf( "Sum=%d\n",add(&a[0][0],rows*columns)
); } |
________________________________________________________________________
________________________________________________________________________
Να περιγραφεί αναλυτικά η λειτουργία του ακόλουθου προγράμματος και να
δοθούν τα αποτελέσματά του.
#include <stdio.h> void f1 ( char *e ); void main() { int a = 8, *b; f1("tei"); b
= &a; a = 14; *b = 13; printf(
"\n%d\n",a ); } // Τέλος της main void f1 ( char *e ) { char
*s; s
= e; while
(*e) e++; do { e
- - ; printf(
"%c",*e ); } while(
e > s ); } // Τέλος της συνάρτησης f1 |
Ø Στον κορμό του προγράμματος, αρχικά
ορίζονται η ακέραια μεταβλητή a, στην οποία δίνεται αρχική τιμή 8,
και ο δείκτης σε ακέραιο b. Στη συνέχεια καλείται η συνάρτηση f1() με όρισμα τη συμβολοσειρά "tei", η οποία αποδίδεται στο δείκτη
χαρακτήρα e, δηλαδή ο e θα δείχνει στη διεύθυνση του χαρακτήρα t.
Ø Μέσα στη συνάρτηση f1() ορίζεται
ως τοπική μεταβλητή ο δείκτης s, στον οποίο αποδίδεται το περιεχόμενο του e, επομένως ο s θα δείχνει στο χαρακτήρα t. Ακολούθως εκτελείται ένας βρόχος while με τη συνθήκη το περιεχόμενο της θέσης μνήμης να είναι διάφορο του μηδενικού χαρακτήρα. Επομένως θα εκτελεσθεί 3 φορές,
μεταφέροντας σε κάθε επανάληψη ο e κατά ένα byte παρακάτω. Στο τέλος του βρόχου ο e θα δείχνει στο \0 του "tei".
Ø Ακολουθεί ένας βρόχος do–while, στον οποίο ο e μεταφέρεται ένα byte ψηλότερα και στη
συνέχεια τυπώνεται το περιεχόμενο της διεύθυνσης στην οποία δείχνει. Ο βρόχος
διαρκεί όσο ισχύει η συνθήκη e>s. Κατά συνέπεια ο βρόχος θα εκτελεσθεί τρεις φορές με τα αποτελέσματα
να παρουσιάζονται κατωτέρω. Στο τέλος της τρίτης επανάληψης η συνθήκη
καθίσταται ψευδής καθώς καθώς e=s=4239887, οπότε τερματίζεται η f1() και ο
έλεγχος του προγράμματος περνά στη γραμμή
b = &a; a = 14;
*b = 13;
Στη γραμμή αυτή ο δείκτης b δείχνει στη διεύθυνση του a. Ακολούθως η τιμή του a γίνεται 14. Τέλος, το
περιεχόμενο της διεύθυνσης στην οποία δείχνει ο b γίνεται 13, δηλαδή η μεταβλητή a έμμεσα αποκτά την τιμή 13.
Η τιμή αυτή αποτυπώνεται στην οθόνη μέσω της τελευταίας εντολής του
προγράμματος.
________________________________________________________________________
<τύπος δεδομένων του επιστρεφόμενου δείκτη> *<όνομα συνάρτησης>(παράμετροι)
π.χ. στη δήλωση
char *incr(int x)
ο επιστρεφόμενος δείκτης είναι
τύπου χαρακτήρα.
Για να αποφευχθούν πιθανά προβλήματα που
οφείλονται στις ιδιαιτερότητες των δεικτών, προτείνεται να αποφεύγονται οι
συναρτήσεις με επιστρεφόμενο δείκτη, εκτός εάν χρησιμοποιείται η λέξη κλειδί static, όπως
φαίνεται στο παράδειγμα 3.6.
________________________________________________________________________
Παράδειγμα 3.6
int *incr(); void main() { int *pscore; pscore=incr(); pscore++; } int *incr() { int y; y=10; return(&y); } |
Στο παραπάνω πρόγραμμα υπάρχει σφάλμα γιατί όταν τελειώνει η συνάρτηση incr() η y
εξαφανίζεται και η τιμή της χάνεται. Ωστόσο, πίσω στη συνάρτηση ο pscore θα
δείχνει στη διεύθυνση που επέστρεψε από τη συνάρτηση αλλά θα υπάρχει «σκουπίδι»
(junk) σ’
αυτή τη διεύθυνση, εφόσον το y έχει παύσει να
ισχύει. Μάλιστα κατά τη μεταγλώττιση ο μεταγλωττιστής δίνει μήνυμα
προειδοποίησης (warning) για
«ύποπτη μετατροπή δείκτη» (suspicious pointer conversion). Όταν
αντικατασταθεί η int y; από static int y; η
μεταγλώττιση εκτελείται επιτυχώς, καθώς η στατική μεταβλητή y θα διατηρηθεί και μετά την έξοδο από τη συνάρτηση incr().
________________________________________________________________________
Όπως κάθε μεταβλητή έτσι και μία μεταβλητή τύπου δομής (π.χ. δομή addressΤ) που
ορίζεται από τη δήλωση
struct addressΤ addr1;
έχει διεύθυνση. Η
διεύθυνση αυτή μπορεί να ληφθεί εφαρμόζοντας τον τελεστή διεύθυνσης στη μεταβλητή
addr1. Εάν
δηλωθεί ένας δείκτης σε δομή addressΤ, αυτός θα
δείχνει στη μεταβλητή addr1 με τη δήλωση:
stuct addressΤ *paddr;
paddr=&addr1;
Πλέον ο δείκτης paddr θα δείχνει στη δομή addr1, παρέχοντας
έναν εναλλακτικό τρόπο πρόσβασης στα μέλη της.
Η προσπέλαση ενός μέλους της δομής μέσω
ενός δείκτη γίνεται με χρήση του τελεστή
βέλους ή έμμεσης προσπέλασης ή δείκτη δομής (structure pointer operator) (αποτελείται από το «μείον» και το
«μεγαλύτερο» ->). H πρόταση
printf( "%s\n",paddr->name );
τυπώνει το μέλος
name της δομής addr1 ενώ η πρόταση
paddr->zip_code=61124;
αναθέτει το
61124 στο zip_code της δομής addr1.
Η έκφραση paddr->zip_code είναι
ισοδύναμη με την έκφραση (*paddr).zip_code. Οι παρενθέσεις είναι απαραίτητες επειδή ο τελεστής
τελείας (.) έχει μεγαλύτερη
προτεραιότητα από τον τελεστή (*).
Ένας δείκτης μπορεί να αποτελεί μέλος δομής, π.χ.
struct
struct_typeΤ
{
int *point1;
char *point2;
float
var3;
};
void
main()
{
struct
struct_typeΤ deikt;
int
x=10;
char y;
deikt.point1=&x; // Ο δείκτης deikt.point1
δείχνει στη διεύθυνση της
// ακέραιας μεταβλητής x
deikt.point2=&y; // Ο δείκτης deikt.point2
δείχνει στη διεύθυνση της
// μεταβλητής χαρακτήρα y
*deikt.point1=13; // Το περιεχόμενο της διεύθυνσης που δείχνει ο
// δείκτης deikt.point1
γίνεται 13
. . . . . .
}
Καθώς στην εντολή *deikt.point1=13; ο
τελεστής (.) έχει υψηλότερη προτεραιότητα από τον τελεστή (*),
προτείνεται να χρησιμοποιούνται παρενθέσεις έτσι ώστε να αποφευχθούν πιθανά σφάλματα:
*(deikt.point1)=13;
________________________________________________________________________
Ο κώδικας που ακολουθεί χρησιμοποιεί δείκτες σε δομές για την ανάγνωση, την
άθροιση και τον υπολογισμό του εσωτερικού γινομένου διανυσμάτων.
#include<stdio.h> typedef struct vect // Ορισμός της δομής vect { float
x,y; } vector; //Τύπου vect // Δήλωση συναρτήσεων void prvect(char d, vector v); // Εκτύπωση διανύσματος, κλήση κατ’ αξία void scanvect( vector *p); //
Ανάγνωση διανύσματος float inprodr(vector *p, vector *q); //
Εσωτερικό γινόμενο διανυσμάτων vector addvectr(vector *p, vector *q); // Άθροισμα διανυσμάτων void main() { vector
a,b,c; scanvect(&a); prvect('a',a); scanvect(&b); prvect('b',b); printf("\tThe
inner product of a and b is:%.2f\n",inprodr(&a,&b)); c=addvectr(&a,&b); prvect('c',c); } // Τέλος της main void prvect(char d,
vector v) { printf(
"Vector %c is ",d ); printf(
"(%.2f,%.2f)\n",v.x,v.y ); } // Τέλος της prvect void scanvect( vector *p) { printf(
"Give the x co-ordinate:" ); scanf("%f",&p->x); printf(
"Give the y co-ordinate:" ); scanf("%f",&p->y); } // Τέλος της scanvect float inprodr(vector
*p, vector *q) { return((*p).x*(*q).x+(p->y)*(q->y)); } // Τέλος της inprodr vector
addvectr(vector *p, vector *q) { vector
sum;
sum.x=(p->x)+(q->x); sum.y=(p->y)+(q->y);
return(sum); } // Τέλος της addvectr |
________________________________________________________________________
________________________________________________________________________
Να αναπτυχθεί πρόγραμμα που να λαμβάνει από το
πληκτρολόγιο τα στοιχεία ενός εργαζόμενου και να δημιουργεί πίνακα εργαζόμενων,
με τύπο δεδομένου κατάλληλη δομή. Η διαδικασία θα επαναλαμβάνεται για 10
διαφορετικούς εργαζόμενους, όσο είναι και το μέγεθος του πίνακα. Οι πληροφορίες
που διαβάζονται για κάθε εργαζόμενο είναι:
Ονοματεπώνυμο: Όνομα και επώνυμο ξεχωριστά (σε μεταβλητή
τύπου δομής)
Διεύθυνση: Όνομα οδού, αριθμός οδού, πόλη, ταχ. Κώδικας
(σε μεταβλητή τύπου δομής)
Τηλέφωνα: Τηλέφωνο εργασίας, κινητό (σε μεταβλητή
τύπου δομής)
Θέση: Τίτλος, κωδικός αριθμός εργαζόμενου, τομέας
της επιχείρησης στον οποίο εργάζεται, αριθμός γραφείου, ονοματεπώνυμο
προϊσταμένου, ημερομηνία πρόσληψης (ημέρα, μήνας, έτος), μισθός (σε
μεταβλητή τύπου δομής – θα απαιτηθεί ένθετη δομή για την ημερομηνία πρόσληψης)
Στη συνέχεια να δίνεται κάποιος κωδικός αριθμός
εργαζόμενου από το πληκτρολόγιο και να αναζητείται στον πίνακα. Αν υπάρχει,
τότε να εμφανίζονται στην οθόνη οι πληροφορίες του αντίστοιχου εργαζόμενου,
αλλιώς να εμφανίζεται ένα ανάλογο μήνυμα.
#include<conio.h> #include<stdio.h> #define N
10 //Αριθμός υπαλλήλων struct nmT { char
name[40],surname[40]; }; struct
addressT { char
street_name[40], city[40]; int
street_number,zip_code; }; struct teleT { char
office_number[14],home_number[14]; }; struct
hiredateT { int
year,month,date; }; struct
job_descriptionT { char
title[40], sector[100], boss_name[40]; int
code_number,office_number,salary; hiredateT
hire; }; struct
personnelT { nmT nm; addressT
addr; teleT
tele; job_descriptionT
job; }; void
get_employee(personnelT *pers_ptr); void
search_employee(int i, personnelT person[N]); main() { personnelT
pers[N]; int i; for
(i=0;i<N;i++) get_employee(&pers[i]); printf("\nGive
an employee's code_number: "); scanf("%d",&i); search_employee(i,pers); } void
get_employee(personnelT *pers_ptr) { printf("\t\tAdd
new employee:\n"); printf("\nEmployee's
name: "); scanf("%s",pers_ptr->nm.name); printf("\nSurname: "); scanf("%s",pers_ptr->nm.surname); printf("\t\nEmployee's
job details:"); printf("\nCode
number: "); scanf("%d",&pers_ptr->job.code_number); printf("\nSalary: "); scanf("%d",&pers_ptr->job.salary); printf("\nYear
of recruitment: "); scanf("%d",&pers_ptr->job.hire.year); } void
search_employee(int code, personnelT person[N]) { int
j=0; int
index=-1; while
((j<N) && (index==-1)) { if
(code==person[j].job.code_number) index=j; j++; } if
(index==-1) printf("\nThe given code does not
match to an existing one\n"); else { printf("\n\t\tSearch
results:\n"); printf("\nEmployee's
name: %s",person[index].nm.name); printf("\nSurname:
%s",person[index].nm.surname); printf("\t\nJob
details:"); printf("\nCode
numer: "); printf("%d",person[index].job.code_number); printf("\nSalary:
%d",person[index].job.salary); printf("\nYear
of recruitment: "); printf("%d",person[index].job.hire.year); } } |
________________________________________________________________________
________________________________________________________________________
Παράδειγμα 3.9
Να γραφεί πρόγραμμα
κωδικοποίησης δεδομένων, το οποίο θα επιτελεί τα παρακάτω:
Θα διαβάζει N τετραψήφιους αριθµούς από το πληκτρολόγιο.
Μετά από κάθε ανάγνωση αριθμού, ο
θα μετασχηματίζεται
στον κωδικοποιημένο αριθμό
, ο οποίος θα εμφανίζεται στην οθόνη. Ακολουθείται η εξής
µέθοδος µετασχηµατισµού:
Ø
Για κάθε αριθµό της µορφής , όπου ως
, k=3,2,1,0 συμβολίζονται τα τέσσερα ψηφία, υπολογίζεται ο αριθµός
όπου το
ψηφίο
προκύπτει
ως το υπόλοιπο της διαίρεσης του αριθµού (
+7) µε το 10.
Ø
Ακολούθως αντιµετατίθεται το
πρώτο ψηφίο του αριθμού µε το τρίτο και το
δεύτερο µε το τέταρτο. Κατά συνέπεια ο αριθµός
µετασχηµατίζεται
στον αριθμό
.
Ο μετασχηματισμός θα υλοποιηθεί με τη
συνάρτηση int transform(int x), η οποία θα δέχεται τον αριθμό και θα επιστρέφει τον
αριθμό
. Μέσα στη συνάρτηση transform() θα χρησιμοποιηθούν οι ακόλουθες συναρτήσεις:
α) Η
συνάρτηση void give_digits(int x, int *arr), η οποία δέχεται ως εισόδους έναν τετραψήφιο
θετικό ακέραιο και έναν δείκτη που
δείχνει σε πίνακα τεσσάρων θέσεων, στον οποίο αποθηκεύονται τα ψηφία του
αριθμού
.
β) Η
συνάρτηση void swap_digits(int *arr), η οποία δέχεται ως είσοδο δείκτη, ο οποίος δείχνει στον πίνακα που
βρίσκονται αποθηκευμένα τα ψηφία ,
,
,
και αντιμεταθέτει τις τιμές τους σύμφωνα με την ανωτέρω
μέθοδο μετασχηματισμού.
β) Η
συνάρτηση int get_digits(int *arr), η οποία δέχεται ως είσοδο δείκτη που
δείχνει σε πίνακα τεσσάρων θέσεων, στον οποίο βρίσκονται αποθηκευμένα τα
τέσσερα ψηφία ,
,
,
, και επιστρέφει τον αριθμό
.
#include
<stdio.h> #define N 3 void give_digits(int x, int *arr); int get_digits(int *arr); int transform(int x); void swap_digits(int *arr); void main() { int x,y,i=0; for (i=0;i<N;i++) { printf("\n\n\nGive
a number: "); scanf("%d",&x); printf("\nCoded
number=%d",transform(x)); } } void give_digits(int x, int *arr) { int y; arr[3]=x/1000; y=x%1000; arr[2]=y/100; y=y%100; arr[1]=y/10; arr[0]=y%10; } int get_digits(int *arr) { return(arr[0]+arr[1]*10+arr[2]*100+arr[3]*1000); } int transform(int x) { int
y,arr_x[4],arr_y[4],i,temp; give_digits(x,arr_x); for (i=0;i<4;i++)
arr_y[i]=(arr_x[i]+7)%10; swap_digits(arr_y); return(get_digits(arr_y)); } void swap_digits(int *arr) { int temp; temp=arr[0]; arr[0]=arr[2]; arr[2]=temp; temp=arr[3]; arr[3]=arr[1]; arr[1]=temp; } |
________________________________________________________________________
Ενδιαφέρον παρουσιάζει η χρήση της λέξης κλειδί typedef σε
συνδυασμό με δείκτες. Ειδικότερα, συχνά χρησιμοποιείται για να απλοποιήσει τις
δηλώσεις μεταβλητών αλφαριθμητικού τύπου, με την πρόταση
typedef char
*String;
Κατ’ αυτόν τον
τρόπο η ακόλουθη δήλωση είναι αφενός μεν έγκυρη αφετέρου δε ευανάγνωστη:
String pch, name[10];
O παρακάτω
ορισμός είναι περισσότερο χαρακτηριστικός της απλοποίησης των δηλώσεων:
typedef int (*func)(char *, char
*);
Η δήλωση
καθορίζει ότι το όνομα func αναπαριστά τον
τύπο που είναι δείκτης σε συνάρτηση, η οποία έχει ως τυπικές παραμέτρους δύο
δείκτες σε χαρακτήρες και επιστρέφει ακέραιο. Χρησιμοποιώντας το νέο όνομα, η
δήλωση δείκτη σε συνάρτηση αυτής της μορφής, που θα έπρεπε να είναι
int (*pfunction)(char *, char *)
παίρνει την απλή
μορφή
func pfunction;
Μετά από αυτή τη δήλωση μπορεί να γραφεί
pfunction=strcmp;
για να δείχνει ο
δείκτης pfunction στη συνάρτηση strcmp() της
βασικής βιβλιοθήκης.
Όπως κάθε συνάρτηση, έτσι κι η main() μπορεί
να δεχθεί παραμέτρους, οι οποίες επιτρέπουν να δίνεται στο καλούμενο πρόγραμμα
ένα σύνολο από εισόδους, που καλούνται ορίσματα γραμμής διαταγής (command line arguments). Ο
μηχανισμός περάσματος ορισμάτων βασίζεται στην ακόλουθη δήλωση της main():
void main(int argc, char *argv[])
{
. . . . . .
}
όπου
·
argc (argument count): είναι ο
αριθμός των ορισμάτων της γραμμής διαταγής, συμπεριλαμβανομένου και του
ονόματος του προγράμματος.
·
argv (argument vector): είναι δείκτης
σε πίνακα από δείκτες, που δείχνουν στα ορίσματα της γραμμής διαταγής, τα οποία
αποθηκεύονται με τη μορφή αλφαριθμητικών. Ο πίνακας στον οποίο δείχνει ο argv έχει
ένα επιπλέον στοιχείο, το argv[argc], το
οποίο έχει τιμή NULL.
Παρατήρηση: Σε μία έκφραση δήλωσης, ο τελεστής πίνακα έχει μεγαλύτερη
προτεραιότητα από τον τελεστή (*). Οι δύο παρακάτω δηλώσεις βασίζονται
στο γεγονός αυτό:
int *ar[2];
int (*ptr)[2];
Η
πρώτη δήλωση ορίζει έναν πίνακα δύο δεικτών σε ακεραίους και στο χρόνο
εκτέλεσης έχει ως αποτέλεσμα, όπως φαίνεται στο σχήμα 3.8, τη δέσμευση δύο
θέσεων μνήμης για μελλοντική αποθήκευση δεικτών σε ακεραίους. Αντίθετα, η
δεύτερη δήλωση ορίζει ένα δείκτη σε πίνακα δύο ακεραίων, τον οποίο όμως δε
δηλώνει και κατά συνέπεια δε δεσμεύει τον απαιτούμενο χώρο.
Σχ. 3.8
________________________________________________________________________
Να
καταστρωθεί πρόγραμμα που θα τυπώνει τα ορίσματα της γραμμής διαταγής.
Έστω echo το όνομα
του προγράμματος. Όταν το καλούμε με την εντολή
echo one two three
θα πρέπει να
εκτελείται και να τυπώνει στην οθόνη one two three.
To σώμα της main() έχει
πρόσβαση στις μεταβλητές argc και argv όπως
παριστάνονται από το ακόλουθο σχήμα, με βάση το οποίο η διαμόρφωση του σώματος
της main είναι απλή:
void main(int argc, char *argv[]) { int i; for (i=1; i<argc; i++)
printf( "%s%s",argv[i], " " ); printf( "\n" ); } |
Η printf( "%s%s",argv[i], "
" ); τυπώνει μετά από κάθε όρισμα ένα κενό. Εάν δεν
πρέπει να τυπώνεται το κενό μετά το τελευταίο όρισμα, η πρόταση πρέπει να
διαμορφωθεί όπως παρακάτω:
for (i=1; i<argc; i++) printf(
"%s%s",argv[i],(i<argc-1)? "
":" " );
Εάν χρησιμοποιηθεί η σημειολογία των δεικτών αντί αυτής του πίνακα, η
πρόταση μπορεί να γραφεί ως εξής:
while (--argc) printf(
"%s%s",*++argv,(i<argc-1)? "
":" " );
________________________________________________________________________
Όταν δηλώνεται ένας πίνακας
προσδιορίζεται το μέγεθός του, το οποίο αποτελεί το μέγιστο αριθμό στοιχείων
που μπορεί να έχει ο πίνακας, και αυτό παραμένει σταθερό καθόλη τη διάρκεια του
προγράμματος, ανεξάρτητα από τον πραγματικό αριθμό στοιχείων του πίνακα που θα
χρησιμοποιηθούν. Για παράδειγμα, η δήλωση
int array[40];
δεσμεύει 40 τετράδες bytes έως ότου τελειώσει το πρόγραμμα. Αυτός ο τρόπος
δέσμευσης μνήμης είναι στατικός και δεν μπορεί να ανταποκριθεί στην περίπτωση
που το μέγεθος του πίνακα πρέπει είτε να επιλέγεται είτε να μεταβάλλεται μετά
την έναρξη εκτέλεσης του προγράμματος.
Στη C υπάρχουν δομές
όπως η στοίβα (stack) ή η
συνδεδεμένη λίστα (linked list), οι οποίες
επεκτείνονται δυναμικά κατά τη διάρκεια εκτέλεσης του προγράμματος,
χαρακτηριστικό που τις καθιστά ιδιαίτερα χρήσιμες για τις περιπτώσεις που κατά
το χρόνο μεταγλώττισης δεν είναι γνωστό το μέγεθος της μνήμης που θα απαιτηθεί
για την αποθήκευση των δεδομένων. Ενδεικτικά μπορεί να αναφερθεί ότι για ένα
πρόγραμμα διαχείρισης ταχυδρομικών διευθύνσεων οι απαιτήσεις μνήμης δεν είναι
γνωστές εκ των προτέρων, καθώς κατά τη διάρκεια της εκτέλεσης δημιουργούνται
νέες διευθύνσεις και διαγράφοναι παλιές. Σε μία τέτοια περίπτωση θα πρέπει να
γίνεται δυναμική διαχείριση της μνήμης:
όταν καταχωρούνται νέες ταχυδρομικές διευθύνσεις θα πρέπει να εκχωρείται μνήμη
στο πρόγραμμα, ενώ κατά τη διαγραφή διευθύνσεων η μνήμη που αυτές καταλάμβαναν
θα πρέπει να απελευθερώνεται και να αποδίδεται στο σύστημα.
Η C υποστηρίζει τη δυναμική
διαχείριση μνήμης παρέχοντας ένα σύνολο από συναρτήσεις της βασικής
βιβλιοθήκης. Οι συνήθεις συναρτήσεις διαχείρισης μνήμης είναι:
·
Οι malloc(), calloc() για τον
καθορισμό του μεγέθους της εκχωρούμενης μνήμης κατά την εκτέλεση.
·
Η realloc() για την αλλαγή
του μεγέθους της εκχωρούμενης μνήμης κατά την εκτέλεση.
·
Η free() για την
απελευθέρωση μνήμης.
H συνάρτηση malloc()
χρησιμοποιείται για τη δέσμευση μνήμης. Δεσμεύει ένα μπλοκ διαδοχικών θέσεων
μνήμης. Ορίζεται στα αρχεία κεφαλίδας stdlib.h και alloc.h και δηλώνεται
ως εξής:
void * malloc(int size);
Η malloc()
επιστρέφει ένα δείκτη στην αρχή του μπλοκ, στο οποίο γίνεται η εκχώρηση. Πρέπει
πάντοτε να γίνεται μετατροπή τύπου έτσι ώστε ο τύπος του δείκτη να είναι
ίδιος με τα στοιχεία στα οποία δείχνει. Η παράμετρος size δίνει τον αριθμό των bytes που θα εκχωρηθούν. Δε θα πρέπει να εισάγεται
συγκεκριμένος αριθμός bytes αλλά να
χρησιμοποιείται η sizeof για να
βρεθεί το μέγεθος ενός τύπου. Ο λόγος είναι ότι εάν η size λάβει συγκεκριμένο αριθμό bytes δεν είναι ξεκάθαρο πόσοι αριθμοί θα
ενταχθούν σ’ αυτό το μπλοκ (για την περίπτωση των ακεραίων μπορεί κάθε αριθμός
να αντιστοιχεί σε 2 ή σε 4 bytes). Για παράδειγμα, εάν πρέπει να δεσμευθεί μνήμη για 20
ακεραίους η malloc()
συντάσσεται ως εξής:
int
*pstart;
pstart=(int
*)malloc(20*sizeof(int));
Στην παραπάνω πρόταση το size=20*sizeof(int) και
γίνεται μετατροπή τύπου (int *)
ώστε να ταιριάζει η έξοδος της malloc() με το δείκτη pstart.
Εάν δεν υπάρχει διαθέσιμη μνήμη η
malloc()
επιστρέφει NULL, δηλαδή τη διεύθυνση 0. Το NULL είναι έγκυρη
διεύθυνση, που εγγυημένα δεν περιέχει ποτέ έγκυρα δεδομένα. Είναι καλή
προγραμματιστική πρακτική να γίνεται πάντοτε έλεγχος κατά πόσον η malloc()
επιστρέφει NULL,
όπως φαίνεται στο πρόγραμμα που ακολουθεί:
char *pmessage;
pmessage=(char *)malloc(20*sizeof(char);
if (pmessage= =NULL)
{
printf(
“Insufficient memory. Exiting...” );
return(-1);
}
Εναλλακτικά, μπορεί να
χρησιμοποιηθεί η μακροεντολή assert(), η οποία
ορίζεται στο αρχείο κεφαλίδας assert.h και ελέγχει κατά πόσον ισχύει μία
συνθήκη. Σε περίπτωση που δεν ισχύει διακόπτεται το πρόγραμμα. Έτσι, θέτοντας
ως συνθήκη ο δείκτης που δείχνει στο μπλοκ μνήμης να μην είναι NULL, ο έλεγχος διαθέσιμης μνήμης λαμβάνει την ακόλουθη
μορφή:
char *pmessage;
pmessage=(char *)
malloc(20*sizeof(char);
assert(pmessage!=NULL);
Σε περίπτωση που δεν υπάρχει διαθέσιμη μνήμη το πρόγραμμα
σταματά και εμφανίζεται το μήνυμα Assertion failed, καθώς και η
γραμμή κώδικα στην οποία εμφανίσθηκε η έλλειψη μνήμης.
H συνάρτηση calloc()
χρησιμοποιείται για τη δέσμευση μνήμης, δεσμεύοντας χώρο για έναν πίνακα n στοιχείων, μεγέθους size το
καθένα. Ορίζεται στα αρχεία κεφαλίδας stdlib.h και alloc.h και δηλώνεται
ως εξής:
void
* calloc(int
n, int size);
Η calloc()
επιστρέφει ένα δείκτη στην αρχή του μπλοκ, στο οποίο γίνεται η εκχώρηση ή το NULL εάν δεν υπάρχει διαθέσιμη μνήμη. Το NULL επιστρέφεται και όταν n=0 ή size=0. Tέλος, το
δεσμευμένο μπλοκ αρχικοποιείται με την τιμή 0.
Ένα παράδειγμα χρήσης της calloc() είναι
το ακόλουθο, όπου δεσμεύεται μνήμη για αλφαριθμητικό δέκα χαρακτήρων, δηλαδή n=10 και size=sizeof(char).
char *str = NULL;
str=(char *) calloc(10,
sizeof(char));
H συνάρτηση free()
χρησιμοποιείται για τη αποδέσμευση μνήμης. Δεσμεύει ένα μπλοκ διαδοχικών θέσεων
μνήμης. Ορίζεται στα αρχεία κεφαλίδας stdlib.h και alloc.h και δηλώνεται
ως εξής:
void free (void *);
Η free() δέχεται
ως όρισμα ένα δείκτη, ο οποίος δείχνει στην αρχή του μπλοκ που απελευθερώνεται.
Για παράδειγμα, εάν έχει δεσμευθεί μνήμη για 20 ακεραίους η free()
συντάσσεται ως εξής:
int
*pstart;
pstart=(int
*)malloc(20*sizeof(int));
free(pstart);
Η free() δεν
επιστρέφει τίποτε. Απλώς αποδεσμεύει τη μνήμη που είχε εκχωρηθεί από τη malloc(). Οι
συναρτήσεις malloc()/calloc() και free()
αναγκαιούν η μία την άλλη. Εάν χρησιμοποιηθεί η malloc()/calloc() για
εκχώρηση μνήμης, πρέπει οπωσδήποτε να ακολουθήσει η free() για την
αποδέσμευσή της.
Παρατηρήσεις:
1. Η πρόταση free(ptr); είναι επικίνδυνη εάν ο ptr δεν είναι
έγκυρος. Το αποτέλεσμα είναι απρόβλεπτο: μπορεί να μη συμβεί τίποτε είτε να
υπάρξει κάποιο σφάλμα είτε ακόμη και να κολλήσει το πρόγραμμα. Ωστόσο ο δείκτης
που χρησιμοποιείται στη free() δεν είναι κατ’
ανάγκη ο ίδιος δείκτης που χρησιμοποιήθηκε στη malloc()/calloc().Το
ακόλουθο τμήμα κώδικα είναι σωστό:
char *pmessage, *pmsg, aLetter;
pmessage=(char *)malloc(20*sizeof(char);
. . . . . .
pmsg=pmessage; // Πλέον και οι δύο δείκτες δείχνουν στην ίδια
θέση
pmessage=&aLetter; // Πλέον ο pmessage δείχνει στο aLetter
free(pmsg); /* Απελευθερώνεται η δεσμευθείσα μνήμη, στην οποία
αρχικά
έδειχνε ο pmessage */
2. Όταν σε ένα πρόγραμμα γίνουν
επαναλαμβανόμενες εκχωρήσεις μνήμης χωρίς τις αντίστοιχες απελευθερώσεις,
συμβαίνουν «διαρροές μνήμης»: το πρόγραμμα αυξάνει συνεχώς καθώς εκτελείται και
τελικά είτε θα πρέπει να σταματήσει
είτε θα κολλήσει. Για την αποφυγή αυτών των δυσχερειών θα πρέπει τα
ζεύγη malloc()/calloc() - free() να
διατηρούνται στο ίδιο τμήμα κώδικα.
3. Δε θα πρέπει να επιχειρηθεί να
απελευθερωθεί η ίδια μνήμη δύο φορές. Το ακόλουθο τμήμα κώδικα είναι εσφαλμένο:
char *pmessage, *pmsg, aLetter;
pmessage=(char
*)malloc(20*sizeof(char);
. . . . .
pmsg=pmessage; // Πλέον και οι δύο
δείκτες δείχνουν στην ίδια θέση
free(pmsg);
free(pmessage); // ΛΑΘΟΣ: Το μπλοκ μνήμης έχει ήδη
απελευθερωθεί!
________________________________________________________________________
Να περιγραφεί αναλυτικά η λειτουργία του ακόλουθου προγράμματος και να
απεικονισθούν οι μεταβολές που συντελούνται στο χάρτη μνήμης:
#include<stdio.h> #include<stdlib.h> void main() { int *pstart; int size=3; pstart=(int *) malloc (size*sizeof(int)); *pstart=15; *(pstart+1)=28; *(pstart+2)=*(pstart+1)+12; free(pstart); } |
Ø
Δηλώνονται ο δείκτης σε ακέραιο pstart και η ακέραια μεταβλητή size, η οποία αρχικοποιείται λαμβάνοντας την τιμή 3 (σχήμα 4.1α).
Ø
H malloc() αναζητά ένα συνεχές μπλοκ 3x4=12 bytes και όταν το
βρει επιστρέφει ένα δείκτη στην αρχή του μπλοκ. Δηλαδή, επιστρέφει τη
διεύθυνση του πρώτου byte σ’ αυτό το
μπλοκ. Η διεύθυνση αυτή ανατίθεται στο δείκτη pstart
(σχήμα 4.1β).
Ø
Με χρήση αριθμητικής δεικτών και του τελεστή περιεχομένου αποδίδονται
οι 15, 28 και 40 στις
διευθύνσεις 908-911, 912-915 και 916-919, αντίστοιχα (σχήμα 4.1γ).
Ø
Με χρήση της free() απελευθερώνεται η δεσμευθείσα μνήμη
και ο χάρτης μνήμης επανέρχεται στην αρχική του μορφή (σχήμα 4.1α).
(α) (β) (γ)
Σχ. 4.1 Χάρτης μνήμης
________________________________________________________________________
H συνάρτηση realloc()
χρησιμοποιείται για τη διεύρυνση ή συρρίκνωση ενός ήδη δεσμευμένου μπλοκ
μνήμης. Ορίζεται στα αρχεία κεφαλίδας stdlib.h και στο alloc.h και δηλώνεται
ως εξής:
void
* realloc(void
*block, int size);
Η realloc()
μεταβάλλει το μέγεθος ενός τμήματος μνήμης που είχε προηγουμένως δεσμευθεί και
στο οποίο έδειχνε ο δείκτης block. Το νέο μέγεθος
καθορίζεται είναι size bytes. Εάν size=0, το
μπλοκ μνήμης απελευθερώνεται και επιστρέφεται το NULL. Η realloc()
διασφαλίζει τα υπάρχοντα περιεχόμενα στη μνήμη και επιστρέφει ένα δείκτη στο
νέο τμήμα μνήμης, ο οποίος μπορεί να είναι είτε ίδιος με το δείκτη block, εάν διατηρηθεί η ίδια αρχή και για το νέο τμήμα μνήμης,
είτε διαφορετικός, στην περίπτωση που το τμήμα μετακινηθεί. Τέλος, εάν ο block είναι NULL η
realloc()
λειτουργεί όπως η malloc().
H λειτουργία της realloc()
αναδεικνύεται μέσα από το παράδειγμα 4.2.
________________________________________________________________________
Στον κώδικα που ακολουθεί αρχικά δεσμεύονται 10 τετράδες bytes για την
αποθήκευση ακεραίων και στη συνέχεια χρησιμοποιείται η realloc() για τη
διεύρυνση του μπλοκ μνήμης στις 20 τετράδες bytes. Από τα αποτελέσματα διαπιστώνεται ότι: α) το νέο μπλοκ μνήμης μετακινήθηκε σε
άλλο σημείο της μνήμης και β) τα περιεχόμενα του αρχικού μπλοκ διατηρήθηκαν
αναλλοίωτα.
#include <stdio.h> #include <alloc.h> void main() { int
*pstr,*pstr2; pstr = (int *) malloc(10*sizeof(int)); *pstr=35; *(pstr+1)=-12; printf( "Before realloc:\n\t*pstr=%d\n\tAddress is
%d\n", *pstr, pstr); pstr2=(int *)realloc(pstr, 20*sizeof(int)); /* Θα μπορούσε να χρησιμοποιηθεί εκ νέου ο pstr, δηλαδή pstr=(int
*)realloc(pstr, 20*sizeof(int)); */ printf( "After realloc:\n\t*pstr2=%d\n\tAddress is
%d\n", *pstr2, pstr2); free(pstr2); free(pstr); } |
________________________________________________________________________
Ένας πίνακας δεικτών (array of pointers) ορίζεται ως εξής:
<τύπος δεδομένων δείκτη> *<όνομα πίνακα>[μέγεθος];
Η πρόταση
char *name[3];
ορίζει τον πίνακα name τριών
θέσεων, τα στοιχεία του οποίου είναι δείκτες σε χαρακτήρα. Με αυτόν τον τρόπο
τα στοιχεία του πίνακα δείχνουν σε αλφαριθμητικά και ο δείκτης (index) του πίνακα
επιλέγει ένα αλφαριθμητικό. Η λειτουργία του πίνακα αυτού θα περιγραφεί με τη
βοήθεια του ακόλουθου παραδείγματος ενώ περισσότερα στοιχεία για τη χρήση
δεικτών σε αλφαριθμητικά θα δοθούν στην §4.6.
________________________________________________________________________
Να γίνει αναλυτική περιγραφή του ακόλουθου προγράμματος και αιτιολόγηση των
αποτελεσμάτων.
#include<stdio.h> void main() { int i; char *name[3] = {
"Francois","James","Mahesh" }; char *tmp; printf( "\nAddresses of pointers:\n"); for (i=0; i<3; i++)
printf( "&name[%d]=%d
",i,&name[i] ); printf( "\n\nAddresses of first character:\n"); for (i=0; i<3; i++)
printf( "&name[%d][0]=%d
",i,name[i] ); printf( "\n\nContents of strings:\n"); for (i=0; i<3; i++)
printf( "name[%d]=%s
",i,name[i] ); tmp=name[0]; // Aντιμετάθεση
των name[0]
και name[2] name[0]=name[2]; name[2]=tmp; printf( "\n\nContents of strings after
shift:\n"); for (i=0; i<3; i++)
printf( "name[%d]=%s
",i,name[i] ); } |
Ø
Αρχικά δημιουργείται ο πίνακας δεικτών name με στοιχεία
τους δείκτες χαρακτήρα, name[0], name[1], name[2], οι οποίοι
αποθηκεύονται στα bytes 1245048-1245051, 1245052-1245055 και 1245056-1245059, αντίστοιχα.
Ø
Σε κάθε δείκτη αποδίδεται η διεύθυνση του
πρώτου byte ενός αλφαριθμητικού, τα οποία αποθηκεύονται σε διαφορετικό τμήμα της
μνήμης. Ειδικότερα, στο δείκτη name[0] αντιστοιχεί
το αλφαριθμητικό "Francois", 8
χαρακτήρων, το οποίο αποθηκεύεται στις διευθύνσεις 4239716-4239723, ενώ στο byte 4239724 αποθηκεύεται ο μηδενικός χαρακτήρας τερματισμού του αλφαριθμητικού.
Κατ’ αντίστοιχο τρόπο ο name[1] δείχνει στο
"James" και ο name[2] στο "Mahesh".
Ø Mε τη βοήθεια
του δείκτη χαρακτήρα temp
ανταλλάσονται οι διευθύνσεις των name[0] και name[2]. Στο τέλος
του προγράμματος ο name[0] δείχνει στο
"Mahesh" και ο name[2] στο "Francois".
________________________________________________________________________
Ο δείκτης σε δείκτη είναι μία
μορφή έμμεσης αναφοράς σε δεδομένα.
Στην περίπτωση ενός κοινού δείκτη, η τιμή του δείκτη είναι η διεύθυνση μίας
«κανονικής» μεταβλητής. Στην περίπτωση ενός δείκτη σε δείκτη, το περιεχόμενο
του πρώτου δείκτη είναι η διεύθυνση του δεύτερου δείκτη, ο οποίος δείχνει στην
κανονική μεταβλητή.
Η έμμεση
αναφορά μπορεί να λάβει ένθεση οιουδήποτε βάθους (δείκτης σε δείκτη σε δείκτη
κ.λ.π.), ωστόσο θα πρέπει να αποφεύγονται οι υπερβολές γιατί ο κώδικας αφενός
μεν θα γίνει δυσανάγνωστος αφετέρου δε θα είναι επιρρεπής σε σφάλματα.
Για την
ανάλυση της λειτουργίας των δεικτών σε δείκτες θα χρησιμοποιηθεί το ακόλουθο
πρόγραμμα, όπου μελετάται η περίπτωση δεικτών σε αλφαριθμητικά:
#include
<stdio.h> void main() { 1 char **name; //
pointer-to-(pointers-to char) 8 free(name); } |
Ø Γραμμή 1: Δηλώνεται
ένας δείκτης που δείχνει σε μία λίστα δεικτών σε χαρακτήρα.
Ø Γραμμή 2: Δεσμεύεται ένα μπλοκ μνήμης, επαρκές για
3 δείκτες σε χαρακτήρα. Στη
συνέχεια ανατίθεται στο name ένας
δείκτης σε αυτό το μπλοκ. Επομένως ο name δείχνει στη
δεσμευμένη μνήμη και όχι το **name.
Ø Γραμμές 3-5: Κάθε
στοιχείο του πίνακα name δείχνει σε μία αλφαριθμητική σταθερά. Τα τρία αλφαριθμητικά δεν είναι
κατ’ ανάγκη τοποθετημένα διαδοχικά στη μνήμη:
Ø Γραμμή 6: Χρησιμοποιείται ο πρώτος δείκτης (index