Εισαγωγή στη διαχείριση μνήμης |
Όλες οι μεταβλητές που γνωρίσαμε μέχρι τώρα είναι είτε στατικές είτε αυτόματες. Οι στατικές καταλαμβάνουν μόνιμο χώρο στη μνήμη καθ' όλη τη διάρκεια εκτέλεσης του προγράμματος ενώ οι αυτόματες καταλαμβάνουν προσωρινό χώρο στη στοίβα κατά τη διάρκεια της κλήσης μιάς συνάρτησης. Η διαχείριση της μνήμης και στις δύο περιπτώσεις είναι δουλειά του μεταφραστή. Οι μόνες λεπτομέρειες που μας αφορούν είναι θέματα όπως το μέγεθος της στοίβας και ο απαιτούμενος χώρος γιά κάθε μεταβλητή. Στα προγράμματα που ακολουθούν θα παρουσιάσουμε τεχνικές όπου η διαχείριση μέρους της μνήμης γίνεται άμεσα από τον προγραμματιστή και όχι έμμεσα, δηλαδή μέσω του μεταφραστή. Η διαχείριση αυτή επιτρέπει την δημιουργία των λεγόμενων δυναμικών δομών δεδομένων, δηλαδή ομάδων μεταβλητών οι οποίες δεν καταλαμβάνουν χώρο μνήμης που τους έχει διατεθεί από τον μεταφραστή στην αρχή του προγράμματος. Αντίθετα οι μεταβλητές δημιουργούνται κατά την εκτέλεση του προγράμματος και ο αντίστοιχος αποθηκευτικός χώρος διατίθεται κατά τη δημιουργία τους με τη βοήθεια ειδικών συναρτήσεων βιβλιοθήκης που καλεί ρητά ο προγραμματιστής. Επίσης στη διάρκεια του προγράμματος οι ίδιες μεταβλητές μπορεί να απαλειφθούν και ο χώρος της μνήμης που τους είχε διατεθεί να ελευθερωθεί ή και να χρησιμοποιηθεί από νέες μεταβλητές. |
Ο σωρός (HEAP) και τα τμήματα (SEGMENTS) |
Κάθε μεταφραστής C υπακούει σε ορισμένους περιορισμούς που είναι αποτέλεσμα του περιβάλλοντος στο οποίο αναφέρεται, δηλαδή του λειτουργικού συστήματος αλλά και του τύπου του υπολογιστή. Οι περιορισμοί αυτοί αφορούν στο μέγεθος του εκτελέσιμου κώδικα, στο μέγεθος του κειμένου του προγράμματος, στον αριθμό των αρχείων που επιτρέπεται να είναι ταυτόχρονα ανοικτά κ.λπ. Στη περίπτωση των ΙΒΜ-συμβατών προσωπικών υπολογιστών οι περισσότεροι μεταφραστές έχουν σχεδιαστεί με βάση την αρχιτεκτονική ενός μικροεπεξεργαστή 16 bits αλλά υποστηρίζουν διάφορα (δύο, τρία ή τέσσερα) μοντέλα μνήμης. Ο χώρος διευθύνσεων των μικροεπεξεργαστών 16 bits είναι 64 Kbytes. Αυτός ο χώρος λέγεται ένα τμήμα μνήμης (memory segment) και όλες οι διευθύνσεις αυτού του χώρου είναι άμεσα προσβάσιμες. Αν προς στιγμή παραλείψουμε τη παρέμβαση του MS-DOS ο περιορισμός του χώρου διευθύνσεων σημαίνει οτι ο εκτελέσιμος κώδικας και τα δεδομένα πρέπει να βρίσκονται μέσα στα όρια ενός τμήματος. Οι διαστάσεις ενός τμήματος ορίζουν το μικρότερο δυνατό μοντέλο μνήμης των μεταφραστών C, το οποίο και παράγει και τον πιό "γρήγορο" κώδικα. Το MS-DOS (μέχρι την έκδοση 6 τουλάχιστο) επιτρέπει την έμμεση (λογική) πρόσβαση σε συνολικά 640 Kbytes μνήμης, δηλαδή σε 10 τμήματα, με τη βοήθεια ειδικών κλήσεων στο λειτουργικό σύστημα. ορίζοντας έτσι το "μεγάλο μοντέλο μνήμης" (large memory model). Οι μεταφραστές C κάνουν χρήση αυτής της δυνατότητας με διαφορους τρόπους. Πρώτα μπορούμε να ξεχωρίσουμε το τμήμα μνήμης εντολών από το τμήμα μνήμης δεδομένων. Επιπλέον μπορούμε να ορίσουμε πολλαπλά τμήματα γιά πρόγραμμα ή δεδομένα, οδηγούμενοι έτσι στα μεγαλύτερα μοντέλα μνήμης των μεταφραστών της C. Τα προγράμματα έχουν περισσότερο χώρο αλλά γίνονται λίγο πιό "αργά". Ο σωρός αποτελεί ένα τέτοιο παράδειγμα: είναι ένα ξεχωριστό τμήμα μεγέθους 64 Kbytes στο οποίο το πρόγραμμα έχει πρόσβαση μέσω των συναρτήσεων διαχείρισης μνήμης. Το σύστημα εκτέλεσης του προγράμματος, σε συνεργασία με το λειτουργικό σύστημα, είναι υπεύθυνο γιά την δέσμευση των θέσεων μνήμης και κρατά λογαριασμό που βρίσκονται τα δεδομένα και ποιές θέσεις είναι κενές. Ακόμη μπορεί να αποδεσμεύσει θέσεις που είχαν δεσμευτεί, εάν αυτό ζητηθεί από το πρόγραμμα με κατάλληλη κλήση συνάρτησης. Με τη βοήθεια της δυναμικής διαχείρισης μνήμης μπορούμε να χρησιμοποιήσουμε μικρά μοντέλα μνήμης και να παραπέμψουμε το μεγαλύτερο όγκο δεδομένων στο σωρό. Έτσι είναι συνηθισμένο να δεσμεύουμε δυναμικά χώρο γιά πίνακες αριθμών, πίνακες εγγραφών, λίστες, δένδρα κ.λ.π. Ακόμη, με προσεκτικό προγραμματισμό, μπορούμε να υπερβούμε το μέγεθος του σωρού αφού ο ίδιος χώρος μνήμης μπορεί να δεσμευθεί γιά διαφορετικά δεδομένα σε διαφορετικές στιγμές του προγράμματος. Οι τελευταίες εκδόσεις του MS-DOS και τα MS-Windows, καθώς φυσικά και τα μεγαλύτερα λειτουργικά συστήματα όπως το UNIX, επιτρέπουν σχεδόν απεριόριστο χώρο λογικών διευθύνσεων μνήμης. Επιπλέον οι μικροεπεξεργαστές 32bits έχουν χώρο φυσικών διευθύνσεων της τάξης των 2 Gbytes. Επομένως πολλά από τα προβλήματα των μοντέλων μνήμης των μεταφραστών C δεν συναντώνται σε μεταφραστές μεγαλύτερων ή νεότερων συστημάτων. Όμως αν και τα προβλήματα οικονομίας μνήμης δεν είναι τόσο έντονα η δυναμική διαχείριση μνήμης είναι απαραίτητη γιά την ορθολογική διαχείριση μνήμης και την ανάπτυξη δυναμικών δομών δεδομένων. |
Δυναμική δέσμευση μνήμης |
Ας ξεκινήσουμε με ένα πρώτο παράδειγμα που βρίσκεται στο πρόγραμμα του αρχείου DYNLIST.C. Ξεκινούμε με τη δήλωση μιάς εγγραφής με όνομα τύπου animal που περιέχει στοιχεία γιά σκυλιά. Δεν ορίζουμε μεταβλητές του τύπου εγγραφής aninal αλλά δηλώνουμε τρείς δείκτες σε τέτοια εγγραφή, με ονόματα pet1, pet2 και pet3. Διαπιστώνουμε οτι στην εκκίνηση του προγράμματος ο μεταφραστής δεν έχει καμμία μεταβλητή γιά να της διαθέσει χώρο στη μνήμη, άρα το πρόγραμμά μας κατ' αρχήν δεν χρησιμοποιεί καθόλου μνήμη γιά δεδομένα. Η πρώτη εκτελέσιμη πρόταση περιέχει την κλήση της συνάρτησης malloc (MEMory ALLOCation, δέσμευση μνήμης). Η συνάρτηση αυτή δεσμεύει χώρο στη μνήμη γιά να αποθηκευτευτούν δεδομένα και επιστρέφει ως αποτέλεσμα της κλήσης (με return δηλαδή) τη διεύθυνση της θέσης μνήμης που δέσμευσε. Εξ' ορισμού η συνάρτηση malloc δεσμεύει χώρο ικανό γιά την αποθήκευση χαρακτήρων, δηλαδή απλά bytes. Η δήλωση προτύπου της συνάρτησης είναι char *malloc(unsigned int size); Επομένως η malloc δεσμεύει size αριθμό bytes και επιστρέφει με return την διεύθυνση του πρώτου byte σε μορφή δείκτη σε χαρακτήρα. Σε περίπτωση ανεπιυχούς δέσμευσης επιστρέφεται ο μηδενικός δείκτης. Αν εξετάσουμε την πραγματική παράμετρο της συνάρτησης malloc θα διαπιστώσουμε οτι η τυπική παράμετρος size συνδέεται μέσω της πραγματικής παραμέτρου με το αποτέλεσμα της κλήσης της συνάρτησης sizeof sizeof(struct animal) Η συνάρτηση αυτή υπολογίζει και επιστρέφει με return τον αριθμό των bytes που αντιστοιχούν στον τύπο δεδομένων που βρίσκεται μέσα στις παρενθέσεις. Έτσι, για παράδειγμα, η κλήση sizeof(char) επιστέφει την τιμή 1, η κλήση sizeof(int) επιστρέφει την τιμή 2, κ.λ.π. Επομένως η πραγματική παράμετρος έχει την τιμή 52, όσα δηλαδή είναι τα bytes που απαιτούνται γιά μία εγγραφή του τύπου animal. Η άλλη περιπλοκή βρίσκεται στο γεγονός οτι ο δείκτης που επιστρέφει η συνάρτηση malloc είναι δείκτης προς χαρακτήρα. Όπως έχουμε πεί, γιά να εκτελεστεί ορθά η αριθμητική δεικτών πρέπει να καθορίζεται σε τι τύπο δεδομένων δείχνει ο κάθε δείκτης. Επομένως πρέπει ο δείκτης που επεστράφει να μετατραπεί σε δείκτη προς εγγραφή τύπου animal, όπως δηλαδή ο δείκτης pet1 στον οποίο εκχωρείται το αποτέλεσμα της κλήσης της συνάρτησης. Αυτή η μετατροπή πραγματοποιείται με την έκφραση (struct animal *)... η οποία είναι μιά γενίκευση των ρητών μετατροπών τύπων δεδομένων που είχαμε συνατήσει στα πρώτα κεφάλαια. Η παραπάνω έκφραση σημαίνει οτι τα δεδομένα που ακολουθούν πρέπει να εκληφθούν ως δείκτης σε εγγραφή του τύπου animal. Είναι προφανές οτι η μετατροπή μπορεί να γίνει για οποιοδήποτε τύπο δεδεμένου και όχι μόνο γιά δείκτες. Η αλήθεια είναι οτι οι περισσότεροι μεταφραστές θα πραγματοποιούσαν την αναγκαία μετατροπή ακόμη και χωρίς τη παραπάνω έκφραση, εξ αιτίας της εκχώρησης στο δείκτη pet1 που ακολουθεί. Αλλά είναι σωστό όλες οι μετατροπές να γίνονται ρητά γιά λόγους αναγνωσιμότητας του προγράμματος. Ας ανακεφαλαιώσουμε: η συνάρτηση malloc δεσμεύει στο σωρό 52 bytes που απαιτούνται γιά την αποθήκευση μιάς εγγραφής του τύπου animal. Στη συνέχεια επιστρέφει την διεύθυνση του πρώτου byte του χώρου μνήμης που δεσμεύτηκε στον δείκτη pet1. Σχηματικά στη μνήμη έχουμε |
![]() |
Πρόσβαση στο σωρό |
Η πρόσβαση στην εγγραφή που μόλις δημιουργήσαμε γίνεται με τη βοήθεια των τεχνικών που συνατήσαμε στη χρήση δεικτών σε εγγραφές. Oι εκχωρήσεις που ακολουθούν περιέχουν τις εκφράσεις pet1->name, pet1->breed και pet1->age αναφέρονται στα πεδία της εγγραφής που δείχνεται από τον δείκτη pet1, δηλαδή της εγγραφής που μόλις δημιουργήθηκε στο δεσμευμένο χώρο του σωρού. Κατά τα άλλα πρόκειται γιά απλές συνηθισμένα πεδία μιάς εγγραφής. Με την εκχώρηση pet2 = pet1; ο δείκτης pet2 παίρνει και αυτός τη διεύθυνση που περιέχει ο pet1 άρα δείχνει και αυτός στη μοναδική εγγραφή που βρίσκεται στο σωρό. Mετά τις εκχωρήσεις αυτές η κατάσταση στη μνήμη είναι καπως έτσι |
![]() |
Αφού τώρα η εγγραφή δείχνεται από δύο δείκτες ο δείκτης pet1 μπορεί να χρησιμοποιηθεί γιά τη δημιουργία μιάς νέας εγγραφής με τη συνάρτηση malloc. Προσοχή, εάν ο δείκτης pet1 έπαιρνε μιά καινούργια τιμή από τη malloc χωρίς προηγούμενα να έχουμε σώσει την τρέχουσα τιμή του στο δείκτη pet2 το αποτέλεσμα θα ήταν να δημιουργήσουμε μεν μιά καινούργια εγγραφή αλλά να χάσουμε τη διεύθυνση της προηγούμενης εγγραφής, θα είχαμε δηλαδή μιά "αδέσπουη" εγγραφή αφού ο μοναδικός τρόπος πρόσβασης σ' αυτή είναι μέσω των δεικτών. Φυσικά θα ήταν πολύ πιό απλό να καλέσουμε τη συνάρτηση malloc και να εκχωρήσουμε το αποτέλεσμα της κατ' ευθείαν στο δείκτη pet2. Ακολουθεί μιά τριάδα εκχωρήσεων έτσι ώστε και η νέα εγγραφή να έχει κάποια δεδομένα. Στη συνέχεια δεσμεύεται χώρος γιά μιά νέα εγγραφή που δείχνεται από το δείκτη pet3. Μετά τη νέα σειρά εκχωρήσεων η μνήμη έχει ως εξής |
![]() |
Οι εκτυπώσεις που ακολουυθούν απλά εμφανίζουν τα περιεχόμενα των πεδίων Frank is a Labrador Retriever, and is 3 years old. General is a Mixed Breed, and is 1 years old. Krystal is a German Shepherd, and is 4 years old. Αν και δεν παρουσιάζεται στο παραπάνω παράδειγμα είναι δυνατό να δημιουργήσουμε δυναμικά και πολύ απλούς τύπους δεδομένων, όπως ένας αριθμός ή ένας χαρακτήρας. Όμως αυτή η πρακτική είναι πολυ δαπανηρή, σε χώρο (3 bytes γιά ένα χαρακτήρα), σε χρόνο (έμμεση διευθυνσιοδότηση), σε μέγεθος εκτελέσιμου κώδικα κ.λπ. Επομένως ο γενικός κανόνας είναι οτι όταν μπορούμε να προγραμματίσουμε με στατικές ή αυτόματες μεταβλητές μόνο τότε δεν υπάρχει λόγος να χρησιμοποιούμε δυναμική δέσμευση μνήμης. |
Δυναμική αποδέσμευση μνήμης |
Οι τελευταίες σειρές του προγράμματος δίνουν παραδείγματα χρήσης της συνάρτησης free. Η συνάρτηση αυτή εκτελεί την αντίθετη λειτουργία από την malloc. Δέχεται ως παράμετρο τη διεύθυνση μιάς περιοχής μνήμης και αποδεσμεύει αυτή τη περιοχή, δηλαδή ειδοποιεί το σύστημα εκτέλεσης οτι η συγκεκριμένη περιοχή είναι και πάλι ελεύθερη γιά να διατεθεί σε νέα κλήση της συνάρτησης malloc. Εννοείται οτι μετά την εκτέλεση της free ο δείκτης που χρησιμοποιήθηκε ως παράμετρος δεν έχει πλέον καθορισμένη τιμή και άρα δεν υπάρχει τρόπος προσπέλασης στα δεδομένα που περιέχονται στις θέσεις μνήμης που αποδεσμεύονται. Η τελευταία ομάδα προτάσεων περιέχει και δύο παραδείγματα συνηθισμένων προγραμματιστικών λαθών. Πρώτο λάθος: ακριβώς πριν από τις προτάσεις που περιέχουν τη συνάρτηση free υπάρχει η εκχώρηση των περιεχομένων του δείκτη pet3 στο δείκτη pet1. Τώρα η εγγραφή που δείχνει ο δείκτης pet3 έχει δύο δείκτες αλλά η εγγραφή που έδειχνε ο pet1 δεν έχει κενένα δείκτη, είναι δηλαδή "αδέσποτη". Τα δεδομένα έχουν χαθεί και δεν υπάρχει τρόπος πρόσβασης, ούτε καν τρόπος αποδέσμευσης του χώρου μνήμης. Δεύτερο λάθος: η τελευταία πρόταση προσπαθεί να αποδεσμεύσει χώρο που είναι ήδη αποδεσμευμένος μέσω της πρότασης free(pet3). Αυτή η πρόταση θα προκαλέσει λάθος εκτέλεσης. Μιά τελευταία σημείωση: ο σωρός καθαρίζεται πλήρως με τον τερματισμό του προγράμματος. |
![]() |
![]() |
![]() |