υναµική διαχείριση µνήµης στη C++ Στην ενότητα αυτή θα µελετηθούν τα εξής επιµέρους θέµατα: είκτες στη C++ Οι τελεστές new και delete Destructors Ορισµός τελεστών κλάσεων Ο δείκτης this ΕΠΛ 132 Αρχές Προγραµµατισµού ΙΙ 1 είκτες Η έννοια του δείκτη εφαρµόζεται στην C++ όπως και στη C προσφέροντας µηχανισµό µέσω του οποίου µπορούµε να προσπελάσουµε αντικείµενa µέσω της διεύθυνσή τους, για πέρασµα παραµέτρων από την γραµµή εντολής προς ένα πρόγραµµα, για την δυναµική δηµιουργία αντικειµένων. Η χρήση τους για τους δύο πρώτους στόχους γίνεται όπως και στη C. ΕΠΛ 132 Αρχές Προγραµµατισµού ΙΙ 2 1
είκτες ηλώσεις δεικτών: int *iptr; char *s; Time *tptr; // o iptr είναι δείκτης σε int // ο s είναι δείκτης σε char // ο tptr είναι δείκτης σε // αντικείµενο τύπου class Time. ηλώσεις δεικτών µε αρχικοποίηση: int i = 1; char c = 'y'; int *ptr = &i; char *t = &c; ΕΠΛ 132 Αρχές Προγραµµατισµού ΙΙ 3 int i = 1; char c = 'y'; int *ptr = &i; char *t = &c Παράσταση στη µνήµη t points to address of a character i 1 c 'y' t ptr Ποιο θα ήταν το αποτέλεσµα των πιο κάτω εντολών; cout << *ptr << endl; cin >> *t; ptr points to the address of an integer ΕΠΛ 132 Αρχές Προγραµµατισµού ΙΙ 4 2
Σταθερές και δείκτες Ένας σταθερός δείκτης είναι ένας δείκτης του οποίου η τιµή δεν µπορεί να αλλαχθεί (δηλαδή δεν µπορούµε να αλλάξουµε τη διεύθυνση στην οποία δείχνει ο δείκτης). char c = 'c'; const char d = 'd'; char * const ptr1 = &c; ptr1 = &d; // illegal Ένας δείκτης σε µια σταθερά τιµή είναι δείκτης που δείχνει σε ένα αντικείµενο του οποίου η τιµή δεν µπορεί να αλλάξει. const char *ptr2 = &d; *ptr2 = 'e'; // illegal ΕΠΛ 132 Αρχές Προγραµµατισµού ΙΙ 5 υναµικά αντικείµενα Κατά τη δυναµική διαχείριση µνήµης µπορούµε να δηµιουργούµε αντικείµενα δυναµικά, µέσω αιτήσεων για παροχή µνήµης. αυτό γίνεται µέ τον τελεστή new. υναµικά αντικείµενα έχουν εµβέλεια εκτός από την συνάρτηση στην οποία ορίζονται. Mνήµη που χρησιµοποιείται από δυναµικά αντικείµενα µπορεί να επιστραφεί µέσω του τελεστή delete. ΕΠΛ 132 Αρχές Προγραµµατισµού ΙΙ 6 3
O τελεστής new Εφαρµογή του τελεστή ζητά ελεύθερη µνήµη από το σύστηµα, προσδιορίζονται τον τύπο και το πλήθος των αντικειµένων που θα δηµιουργηθούν. Αν υπάρχει διαθέσιµη µνήµη, η πράξη αυτή θα έχει ως αποτέλεσµα την επιστροφή δείκτη προς το κοµµάτι µνήµης που έχει παραχωρηθεί. ΕΠΛ 132 Αρχές Προγραµµατισµού ΙΙ 7 Χρήση new (1) Σύνταξη Ptr = new SomeType ; όπου ο Ptr έχει ορισθεί ως δείκτης τύπου SomeType Η νεοδηµιουργηθείσα µνήµη δεν αρχικοποιείται, εκτός αν υπάρχει default constructor του τύπου SomeType. Παράδειγµα: int *iptr = new int; Time *tptr = new Time; iptr Uninitialized int object trptr 0,0,0 0/1 Rational Αντικείµενο object Time with default µε την default initialization αρχικοποίηση ΕΠΛ 132 Αρχές Προγραµµατισµού ΙΙ 8 4
Χρήση του new (2) Σύνταξη SomeType *Ptr = new SomeType(ParameterList); Αρχικοποίηση Η µνήµη που επιστρέφεται από την πράξη αυτή αρχικοποιείται δίνοντας τιµές στον κατασκευαστή της κλάσης SomeType Παράδειγµα: int *iptr = new int(10); Time *tptr = new Time(16,15,0); iptr Αντικείµενο int µε ρητή Uninitialized αρχικοποίηση int object 10 trptr 16,15,0 0/1 Αντικείµενο Rational object Time with µεdefault ρητή αρχικοποίηση initialization ΕΠΛ 132 Αρχές Προγραµµατισµού ΙΙ 9 Xρήση new (3) Σύνταξη P = new SomeType [Expression] ; όπου ο P είναι δείκτης του τύπου SomeType Expression είναι ο αριθµός των συνεχόµενων αντικειµένων τύπου SomeType που θέλουµε να δηµιουργηθούν φτιάχνουµε λίστα Λόγω της σύνταξης που µας παρέχουν οι δείκτες ο P µπορεί να θεωρηθεί και να χρησιµοποιηθεί ως πίνακας. ΕΠΛ 132 Αρχές Προγραµµατισµού ΙΙ 10 5
int *A = new int [3]; Time *T = new Time[2]; A[1] = 5; Time r(1, 1, 1); T[0] = r; Παράδειγµα ΕΠΛ 132 Αρχές Προγραµµατισµού ΙΙ 11 ηµιουργία πινάκων Έστω διαδικασίες Getlist, SortList, και DisplayList, που επεξεργάζονται πίνακες, το ποιο κάτω πρόγραµµα, δηµιουργεί ένα πίνακα µεγέθους που προσδιορίζεται από το χρήστη, λαµβάνει τα στοιχεία του, τα ταξινοµεί και τον εµφανίζει στην οθόνη. cout << "Enter list size: "; int n; cin >> n; int *A = new int[n]; GetList(A, n); SelectionSort(A, n); DisplayList(A, n); ΕΠΛ 132 Αρχές Προγραµµατισµού ΙΙ 12 6
Ο τελεστής delete Σύνταξη delete P; // used if storage came from new delete [] P; // used if storage came from new[] Η µνήµη που δείχνεται από τον P επιστρέφεται. int n; cout << "Enter list size: "; cin >> n; int *A = new int[n]; GetList(A, n); SelectionSort(A, n); DisplayList(A, n); delete [] A; ΕΠΛ 132 Αρχές Προγραµµατισµού ΙΙ 13 Αντιγραφή πινάκων int *A = new int[5]; for (int i = 0; i < 5; ++i) A[i] = i; int *B = A; A B 0 1 2 3 4 delete [] A; A B Locations do not belong to program? ΕΠΛ 132 Αρχές Προγραµµατισµού ΙΙ 14 7
ιαρροή Μνήµης int *A = new int [5]; for (int i = 0; i < 5; ++i) A[i] = i; A 0 1 2 3 4 A = new int [5]; These locations cannot be accessed by program A 0 1 2 3 4 ΕΠΛ 132 Αρχές Προγραµµατισµού ΙΙ 15 Παράδειγµα: υναµική λίστα Θα δηµιουργήσουµε κλάση IntList που υλοποιεί δυναµικές λίστες. Η διασύνδεση της κλάσης αυτής θέλουµε να µοιάζει µε αυτή ενός πίνακα, π.χ. να αναφερόµαστε στο i-οστό αντικείµενο ενός αντικειµένου Α της κλάσης αυτής ως Α[i]. Εποµένως θα πρέπει η κλάση να κάνει overload τον τελεστή []. Από την άλλη, αντικείµενα της κλάσης µας δεν θα έχουν τους γνωστούς περιορισµούς πινάκων: η κλάση µας θα επιτρέπει ανάθεση αντικειµένων της κλάσης (=> θα κάνουµε overload τον τελεστή =,) αντικείµενα της κλάσης µπορούν να είναι το αποτέλεσµα επιστροφής διαδικασιών,... Επίσης η κλάσης µας θα προσφέρει τις πιο κάτω συµπεριφορές: δηµιουργία νέων λίστών µε default τιµές (constructors) δυνατότητες εισαγωγής και εξαγωγής στοιχείων επιστροφή του µεγέθους της λίστας και αλλαγή του µεγέθους της λίστας. ΕΠΛ 132 Αρχές Προγραµµατισµού ΙΙ 16 8
Παραδείγµατα χρήσης της IntList IntList A(5, 1); IntList B(10, 2); IntList C(5, 4); for (int i = 0, i < A.size(); ++i) { A[i] = C[i]; cout << A << endl; // [ 4 4 4 4 4 ] A = B; A[1] = 5; cout << A << endl; // [ 2 5 2 2 2 2 2 2 2 2 ] ΕΠΛ 132 Αρχές Προγραµµατισµού ΙΙ 17 Η κλάση IntList class IntList { public: // κατασκευαστές IntList(int n = 10, int val = 0); IntList(IntList &A); // destructor ~IntList(); // συνάρτηση που επιστρέφει το µήκος λίστας int size(); // τελεστής ανάθεσης = IntList & operator=(intlist &A); ΕΠΛ 132 Αρχές Προγραµµατισµού ΙΙ 18 9
Η κλάση IntList (συν.) // τελεστής [] int& operator[](int i); // συνάρτηση αλλαγής µεγέθους λίστας void resize(int n = 0, int val = 0); // συνάρτηση εισαγωγής στοιχείου στο τέλος void push_back(int val); private: // δεδοµένα κλάσης int *Values; // δείκτης προς στοιχεία int NumberValues; // µήκος λίστας ; ΕΠΛ 132 Αρχές Προγραµµατισµού ΙΙ 19 Η κλάση IntList (συν.) // Βοηθητικές συναρτήσεις ostream& operator<<(ostream &sout, IntList &A); istream& operator>>(istream &sin, IntList &A); ΕΠΛ 132 Αρχές Προγραµµατισµού ΙΙ 20 10
Κατασκευαστής 1 IntList::IntList(int n, int val) { assert(n > 0); NumberValues = n; Values = new int [n]; assert(values); for (int i = 0; i < n; ++i) { Values[i] = val; συνάρτηση από τη βιβλιοθήκη assert.h. Αν η παράµετρος της πάρει την τιµή false η εκτέλεση του προγράµµατος τερµατίζεται δίνοντας κατάλληλο µήνυµα προς τον χρήση ΕΠΛ 132 Αρχές Προγραµµατισµού ΙΙ 21 Κατασκευαστής 2 Θέλουµε να φτιάξουµε κατασκευαστή ο οποίος παίρνει ως παράµετρο αντικείµενο και δηµιουργεί καινούριο αντικείµενο, αντίγραφο του παλιού. Αυτή η δυνατότητα παρέχεται για κάθε κλάση στη C++. ηλαδή, για κάθε κλάση Class SomeClass o πιο κάτω κώδικας SomeClass t;... SomeClass r(t); έχει ως αποτέλεσµα να δηµιουργήσει δύο αντικείµενα της κλάσης SomeClass όπου το δεύτερο (r) είναι αντίγραφο του πρώτου (t). ΕΠΛ 132 Αρχές Προγραµµατισµού ΙΙ 22 11
Κατασκευαστής 2 Έστω ότι θέλουµε να φτιάξουµε αντίγραφο κάποιου αντικειµένου της κλάσης IntList. Μπορεί αυτό να επιτευχθεί µέσω του default κατασκευαστή αντιγραφής; Εκτελώντας IntList A(3, 1); 3 A IntList B(A); 1 2 1 και στη συνέχεια A[2] = 2; Τότε B 3 το B[2] έχει αλλάξει! ενώ εµείς θα αναµέναµε ότι το B είναι ένα ανεξάρτητο αντίγραφο του Α. Εποµένως... ΕΠΛ 132 Αρχές Προγραµµατισµού ΙΙ 23 Τελεστές και δυναµικά αντικείµενα Οταν µια κλάση περιέχει πεδίο το οποίο δείχνει προς δυναµικά δεσµευµένη µνήµη τότε για αυτή την κλάση τυπικά θα πρέπει να ορίσουµε ξανά τη διαδικασία... Κατασκευαστής αντιγραφής Κατασκευαστής ο οποίος κτίζει ένα αντικείµενο ως αντίγραφο αντικειµένου του ίδιου τύπου. όπως και τις διαδικασίες... Destructor Ένας αντί-κατασκευαστής ο οποίος αποδεσµεύει τη δυναµικά δεσµευµένη µνήµη που περιέχεται µέσα σε αντικείµενα της κλάσης (όταν αυτά δεν είναι πια χρησιµοποιήσιµα). Τελεστής ανάθεσης Αλλάζει την τιµή ενός αντικειµένου µε βάση ένα άλλο αντικείµενο του ίδιου τύπου. ΕΠΛ 132 Αρχές Προγραµµατισµού ΙΙ 24 12
Κατασκευαστής αντιγραφής IntList::IntList(IntList &A) { NumberValues = A.size(); Values = new int [NumberValues]; assert(values); for (int i = 0; i < NumberValues; ++i) Values[i] = A[i]; Τι είδους subscripting εκτελείται εδώ; ΕΠΛ 132 Αρχές Προγραµµατισµού ΙΙ 25 Destructor Τι συµβαίνει όταν ένα αντικείµενο τύπου IntList βγεί εκτός της εµβέλειας ενός προγράµµατος; Aν δεν έχει προγραµµατιστεί τίποτα τότε θα έχουµε διαρροή µνήµης. Εναλλακτικά µπορούµε να ορίσουµε ένα destructor για την κλάση. Ο destructor καλείται αυτόµατα όταν το αντικείµενο πάψει να υπάρχει για το πρόγραµµα. Ο destructor της κλάσης ClassName πρέπει να οριστεί µε το όνοµα ~ClassName. Ένας destructor δεν µπορεί να πάρει καµιά παράµετρο και δεν επιστρέφει κανένα αποτέλεσµα. IntList::~IntList() { delete [] Values; Προσοχή στο ~ ΕΠΛ 132 Αρχές Προγραµµατισµού ΙΙ 26 13
Ο Τελεστής [] και ο Τελεστής Ανάθεσης Ο τελεστής []: int& IntList::operator[](int i) { assert((i >= 0) && (i < size())); return Values[i]; Ο τελεστής ανάθεσης: Θέλουµε να κάνουµε overload τον τελεστή = έτσι ώστε η ανάθεση A=B να έχει ως αποτέλεσµα την αντιγραφή της λίστας Β στη λίστα Α. Αλγόριθµος 1: Επέστρεψε την υπάρχουσα δυναµικά δεσµευµένη µνήµη. έσµευσε κατάλληλη ποσότητα µνήµης. Αντίγραψε τα περιεχόµενα του ενός αντικειµένου στο άλλο. ΕΠΛ 132 Αρχές Προγραµµατισµού ΙΙ 27 Προσπάθεια 1 IntList& ΙntList::operator=(IntList &A) { NumberValues = A.size(); delete [] Values; Values = new int [NumberValues ]; assert(values); for (int i = 0; i < A.size(); ++i) Values[i] = A[i]; return A; Ποιο είναι το αποτέλεσµα του πιο κάτω προγράµµατος; C(5,1); C = C; ΕΠΛ 132 Αρχές Προγραµµατισµού ΙΙ 28 14
Ο δείκτης this Σε οποιαδήποτε µέθοδο ή τελεστή ενός αντικειµένου ο δείκτης this δείχνει προς το αντικείµενο που την περιέχει. Έτσι για παράδειγµα, αντί του int IntList::size() { return NumberValues; ισοδύναµα µπορούµε να γράψουµε int IntList::size() { return this->numbervalues; ΕΠΛ 132 Αρχές Προγραµµατισµού ΙΙ 29 Τελεστής ανάθεσης Προσπάθεια 2 IntList& IntList::operator=(IntList &A) { if (this!= &A) { delete [] Values; NumberValues = A.size(); Values = new int [A.size()]; assert(values); for (int i = 0; i < A.size(); ++i) { Values[i] = A[i]; return *this; ΕΠΛ 132 Αρχές Προγραµµατισµού ΙΙ 30 15
Τελεστές για Ι/O Μήπως θα έπρεπε οι τελεστές << και >> να οριστούν ως µέλη της συνάρτησης Intlist; class IntList { //... ostream& operator<<(ostream &sout); //... ; H απάντηση εξαρτάται από το πως θέλουµε να καλούµε τους τελεστές αυτούς: Έστω IntList A(5,1); Επιλογή 1: Ορισµός << ως µέλους συνάρτησης: A << cout; //(λιγότερο φυσικό) Επιλογή 2: Ορισµός << ως βοηθητικής συνάρτησης: cout << A; // (πιο φυσικό) ΕΠΛ 132 Αρχές Προγραµµατισµού ΙΙ 31 Ο τελεστής << ostream& operator<<ostream &sout, IntList &A){ sout << "[ "; for (int i = 0; i < A.size(); ++i) { sout << A[i] << " "; sout << "]"; return sout; ΕΠΛ 132 Αρχές Προγραµµατισµού ΙΙ 32 16