Ενδεικτικές Λύσεις σε Επιλεγμένα Θέματα της C++ class ListNode public: T data; ListNode<T> * next; ListNode(const ListNode<T> & src) ; data = src.data; if (src.next!=null) next = new ListNode<T> ((const ListNode<T>) * src.next); else next = NULL; return; Αναδρομική λύση (πιο σύντομος κώδικας) class List protected: ListNode<T> * start; public: List( const List<T> & src) ; if (src.start == NULL) start = NULL; return; else start = new ListNode<T>( src.start ); Δώστε τον κώδικα για τον κατασκευαστή αντιγραφής της παραπάνω κλάσης ListNode έτσι ώστε να δημιουργείται πλήρες αντίγραφο των περιεχομένων που δείχνει ο κόμβος src. Βοήθεια: μπορεί να γίνει αναδρομικά. Η κλάση ListNode χρησιμεύει στην κατασκευή της λίστας List και ο κατασκευαστής αντιγραφής καλείται για να κάνει deep copy μιας λίστας, όπως φαίνεται.
Συμπληρώστε τον κώδικα ώστε να δουλεύει το κύριο πρόγραμμα παρακάτω: class Shape public: virtual void draw () = 0; virtual void print () ; class Box : public Shape public: void draw () ; void print () cout << I am a box ; ; Η Shape έχει μια pure virtual μέθοδο (draw), η οποία πρέπει οπωσδήποτε να οριστεί σε κάποια παράγωγη κλάση πριν μπορέσει να δημιουργηθεί κάποιο στιγμιότυπο της τελευταίας. void main () Shape *a = new Box(); a->print(); // τυπώνει I am a box char buf[] = a, b ; Ποιο θα είναι το αποτέλεσμα τις πράξης: cout << *( (long *) ( (void*) buf ) +1); α) Τυπώνει το χαρακτήρα b. β) Τυπώνει τον ASCII κωδικό (αριθμό) που αντιστοιχεί στο χαρακτήρα b. γ) Τυπώνει κάτι απροσδιόριστο δ) Παράγει compilation error Απάντηση: (γ) Δικαιολογήστε την απάντησή σας: Το buf μετατρέπεται σε δείκτη σε long που μετά αυξάνεται κατά 4 ή 8 bytes βγαίνοντας έξω από τα όρια του αρχικού πίνακα (2 bytes). (void*)buf A : είναι πλέον δείκτης σε απροσδιόριστου μεγέθους δεδομένα (η επαύξηση ενός τέτοιου pointer γίνεται κατά ένα byte). ((long*)a) B : είναι πλέον δείκτης σε δεδομένα τύπου long, άρα με δεδομένη μια υλοποίηση του long με 4 ή 8 bytes (ελάχιστο 4), κάθε επόμενος αριθμός long σε έναν πίνακα (δηλ. σε συνεχόμενες θέσεις μνήμης), απέχει από τον προηγούμενο τουλάχιστο 4 bytes. B+1 C : ο δείκτης έχει προχωρήσει κατά ένα «βήμα», όπου το βήμα είναι ίσο με το μέγεθος του τύπου του δείκτη (εδώ είναι «δείκτης σε long»), άρα τουλάχιστο 4 bytes. *(C) D : παίρνουμε το περιεχόμενο του δείκτη, το οποίο ξεκινάει τουλάχιστο 4 bytes μακριά από τη διεύθυνση buf και άρα δε γνωρίζουμε τι υπάρχει εκεί ή αν μπορούμε καν να το διαβάσουμε (πιθανό access violation).
Ποια είναι η διαφορά μεταξύ των δύο σημειωμένων γραμμών; unsigned int number = 12002; ofstream fs = ofstream("a.out", ios_base::out ); if (!fs.bad()) fs.write(reinterpret_cast<char*>(&number), sizeof(int)); // ΓΡΑΜΜΗ Α fs << number; // ΓΡΑΜΜΗ Β fs.close(); Έστω ότι sizeof(int) 4. Τότε το αρχείο θα περιέχει: Α) 0x00 0x00 0x2E 0xE2, που είναι τα 4 bytes του 12002 σε δεκαεξαδική μορφή (0x00002EE2) B) 0x31 0x32 0x30 0x30 0x32, που είναι οι κωδικοί ASCII των 5 χαρακτήρων του 12002 Η γραμμή Α γράφει με δυαδικό τρόπο τα 2 ή 4 bytes (sizeof(int)) που αναπαριστούν το number Η γραμμή Β μετατρέπει τον αριθμό σε συμβολοσειρά και γράφει σε μορφή κειμένου το 12002
Δίνεται η κλάση: class Container protected: T * storage; size_t num_items; size_t storage_size; public: virtual void operator += (const T item) virtual ~Container() Container() : storage(nullptr), num_items(0), storage_size(0) T & operator [] (size_t index) if (index < num_items) return storage[index]; size_t size() return num_items; ; Επεκτείνετε κατάλληλα την Container, ώστε ο παρακάτω κώδικας να δουλεύει χωρίς σφάλματα και να παράγει το αποτέλεσμα που φαίνεται: void main(int argc, char* argv[]) Container<long> * store = new Box(); *store+=10l; *store+=20l; *store+=30l; for (size_t i=0; i<store->size(); i++) std::cout << (*store)[i] << " "; std::cout << std::endl; delete store; Αποτέλεσμα της εκτέλεσης: 10 20 30 // Συμπληρώστε την αρχικοποίηση // αντικειμένου της δικής σας κλάσης Γράψτε εδώ τη δήλωση της δικής σας κλάσης. Δεν επιτρέπεται να αλλοιώσετε την Container, ούτε να χρησιμοποιήσετε κλάσεις της STL. H Container έχει μεν υλοποιημένο τον τελεστή +=, αλλά δεν κάνει τίποτα. Η παράγωγη κλάση αυτής (εδώ θα την πούμε Box ), πρέπει να υλοποιήσει πλήρως όλη τη διαδικασία εισαγωγής στοιχείων σε δυναμική γραμμική συλλογή. Επιπλέον, πρέπει να φτιάξουμε έναν καταστροφέα ο οποίος να μπορεί να διαγράφει την περιοχή μνήμης storage, αν αυτή έχει δεσμευθεί. Παρατηρήστε ότι ο καταστροφέας της Container είναι virtual, ώστε να μπορέσει να κληθεί σωστά ο καταστροφέας της Box. Για την παράγωγη κλάση πρέπει να ισχύουν τα ακόλουθα: - Πρέπει να αρχικοποιεί προαιρετικά τον πίνακα storage σε ένα προκαθορισμένο μέγεθος. Αυτό δεν είναι απαραίτητο όμως και για οικονομία, συνήθως δεν αρχικοποιούμε δυναμικές συλλογές αν δεν αρχίσουμε να τις γεμίζουμε. Στην προτεινόμενη υλοποίηση, κρατάμε τον default constructor της βασικής κλάσης και δεσμεύουμε μνήμη μόνο όταν χρειαστεί. - Πρέπει να αποδεσμεύει μνήμη της storage σε έναν δικό της καταστροφέα, μόνο στην περίπτωση που η storage δεν είναι NULL. - Πρέπει να μπορεί να χειριστεί εισαγωγές στοιχείων τυχαίου πλήθους με τον τελεστή +=. - Για την υλοποίηση του +=, χρειάζεται να προσέξουμε τα ακόλουθα: - Αν ο χώρος storage είναι μη εκχωρημένος, τον δημιουργούμε ίσο με έναν αριθμό στοιχείων της αρεσκείας μας. - Αν num_items < storage_size, μπορούμε να εισάγουμε άφοβα ένα νέο στοιχείο στη θέση num_items. - Αν num_items == storage_size (μεγαλύτερο δε θα είναι αφού πάντα αυξάνει κατά 1), πρέπει να επεκτείνουμε το storage
class Box : public Container<T> public: void operator += (const T item) if (storage_size==0) storage_size = 2; storage = new T[storage_size]; else if (storage_size==num_items) storage_size*=2; T * buffer = new T[storage_size]; memcpy(buffer,storage,sizeof(t)*storage_size/2); delete [] storage; storage = buffer; storage[num_items++] = item; Ο έλεγχος storage_size==0 εδώ είναι ισοδύναμος με τον storage==nullptr Αν δεν έχει δεσμευθεί το storage, δέσμευσε το χώρο και ενημέρωσε το storage_size Αν φτάσαμε στο όριο των στοιχείων του storage: - Όρισε το νέο επιθυμητό μέγεθος - Δέσμευσε έναν καινούριο χώρο (εδώ διπλάσιο) - Αντέγραψε τα παλιά δεδομένα στην αρχή του νέου χώρου - Διέγραψε τον παλιό χώρο και κράτησε ως storage τον καινούριο Πρόσθεσε στην επόμενη διαθέσιμη θέση το νέο στοιχείο ; ~Box() if (storage!=nullptr) delete [] storage; Διαγράφουμε μόνο όταν έχει δεσμευθεί η μνήμη Παρατήρηση: η memcpy θα μπορούσε να αντικατασταθεί από ένα loop αντιγραφής στοιχείων 1 προς 1.
// Η κλάση Counter κατασκευάζεται με βάση ένα std::vector // από δεδομένα οποιουδήποτε τύπου και αυτό που κάνει είναι να αναφέρει // πόσες φορές υπάρχει ένα συγκεκριμένο στοιχείο μέσα στο vector // κατά τη στιγμή της δημιουργίας ενός στιγμιοτύπου Counter. // class Counter private: std::unordered_map<t,size_t> histogram; public: // Μοναδικός κατασκευαστής Counter(std::vector<T> & input); // Συμπληρώστε πεδία που χρειάζεται η κλάση Στην συγκεκριμένη έκδοση του προβλήματος (εξεταστική Ιουνίου 2014), ο τύπος και το εύρος τιμών Τ δεν είναι γνωστά. Στην παραλλαγή του θέματος της εξεταστικής Σεπτεμβρίου 2014, είχαμε μικρό εύρος τιμών (256) και δεδομένο τύπο (char), οπότε υπήρχε και πιο απλή υλοποίηση, με σταθερό πίνακα τύπου size_t μήκους 256 για 256 ανεξάρτητους μετρητές. Πρέπει να δημιουργήσουμε μια δική μας δομή που να φυλάει δεδομένα για την καταμέτρηση. Η δημιουργία γίνεται κατά την αρχικοποίηση του Counter και δε μπορούμε να θεωρήσουμε ότι το input παραμένει αναλλοίωτο μετά από αυτή τη στιγμή. Χρειαζόμαστε μια «αραιή» συλλογή που να συνδέει ένα κλειδί (τιμή του T) με έναν πληθυσμό. ; // Αναφέρει πόσες φορές ένα συγκεκριμένο στοιχείο βρέθηκε μέσα στο vector<t> // input. Αν το στοιχείο δεν υπάρχει, αναφέρει 0, αλλιώς το αριθμό των φορών // που το έχει συναντήσει size_t GetCount(T item); Counter<T>::Counter(std::vector<T> & input) size_t num = input.size(); for ( size_t i=0; i<num; i++) histogram[input[i]]++; size_t Counter<T>::GetCount(T item) std::unordered_map<t,size_t>::iterator got = histogram.find(item); if (got == histogram.end()) return 0; else return got->second; ; Ο τελεστής [] του unordered_map, αν δεν υπάρχει το ζεύγος με κλειδί Τ, το δημιουργεί (καλεί «default constructor» για το size_t, δηλαδή το θέτει ίσο με 0, αφού το size_t είναι βασικός τύπος και όχι κλάση) και μας επιστρέφει αναφορά στο pair.second, δηλαδή στο πεδίο τύπου size_t. Αν υπάρχει ζεύγος με κλειδί Τ, μας επιστρέφει και πάλι το second του ζεύγους, οπότε πάντα ισχύει ο τελεστής ++ Αν δε βρεθεί το στοιχείο, επιστρέφουμε 0, αλλιώς το second του ζεύγους που επιστρέφει ο Iterator. Προσοχή: εδώ θα μπορούσαμε να καλέσουμε ακόμα και απλά: return histogram[item] γιατί αυτό που θα γίνει είναι να φτιάξει μια νέα εγγραφή με κλειδί item αν το item δεν υπάρχει και να του βάλει τιμή 0, οπότε και πάλι το αποτέλεσμα θα
Ενδεικτική χρήση: void main(int argc, char* argv[]) vector<long> data; data.push_back(-10); data.push_back(1); data.push_back(1); ήταν σωστό. Όμως, σε αυτή την περίπτωση για κάθε «άστοχη» κλήση της GetCount (δηλαδή για κλειδιά που δεν υπάρχουν ήδη στο histogram), θα επεκτείνεται χωρίς λόγο η συλλογή μας, με αποτέλεσμα κάποια στιγμή να γίνει βαριά, χωρίς λόγο. Κατά αντιστοιχία, θα μπορούσαμε να χρησιμοποιήσουμε μηχανισμό find/insert και στον κατασκευαστή. Counter<long> cnt = Counter<long>(data); size_t found = cnt.getcount(-10); found = cnt.getcount(0); found = cnt.getcount(1); // found==1 // found==0 // found==2 Πώς θα δεσμεύσω 100 στοιχεία τύπου string* και θα αναθέσω το αποτέλεσμα σε μια αντίστοιχη μεταβλητή με όνομα mydata; mydata = string ** mydata = new string * [100]; string ** mydata = (string**) new void * [100]; string ** mydata = (string**)malloc(sizeof(string*)*100); string ** mydata = (string**)malloc(sizeof(void*)*100); Δεσμεύουμε έναν πίνακα από δείκτες σε string. Όχι έναν πίνακα από string. Εναλλακτικά, μπορούμε να δεσμεύσουμε και με την malloc την μνήμη. Θυμόμαστε να αποδεσμεύσουμε τον πίνακα με τη free() σε αυτή την περίπτωση. Όπως βλέπετε, είναι αδιάφορο τι τύπου δείκτη θα δεσμεύσουμε πραγματικά, αφού το μέγεθος ενός δείκτη είναι δεδομένο (θέση μνήμης). Πρέπει όμως στη συνέχεια να κάνουμε το απαραίτητο cast στον τύπο της μεταβλητής. Δίνεται η ακόλουθη κλάση: class node_t protected: char data; public: node_t(const char letter) : next(0) data = letter; class node_t * next; ;
Συμπληρώστε τον απαραίτητο κώδικα στις γραμμές που δίνονται ώστε να μετασχηματιστεί το περιεχόμενο του data στον παρακάτω κώδικα από αυτό που φαίνεται στο σχήμα 1Α σε αυτό του 1Β, χωρίς να προκαλέσετε διαρροή μνήμης: (25 μονάδες) void main() node_t ** data = new node_t*[4]; // γέμισμα της δομής, όπως φαίνεται στο σχήμα 1Α. // σας δίνεται έτοιμο... // Απάντηση στο ερώτημα: data[1] = data[0]; data[0] = 0; data[3] = data[2]; data[2] = new node_t( E ); delete data[3]->next; data[3]->next = data[1]->next; Ο πίνακας data αποθηκεύει δείκτες σε node_t αντικείμενα. Η μετάθεση των ακολουθιών ΑΒ και CD από τη θέση 0 στην 1 και τη 2 στην 3 αντίστοιχα γίνεται πολύ απλά βάζοντας το δείκτη στη νέα θέση να δείχνει στο ίδιο στοιχείο με την παλιά θέση. Στη συνέχεια, μηδενίζουμε το δείκτη data[0]. Προσοχή: ΔΕ διαγράφουμε το δείκτη, αφού η μνήμη δείχνεται πλέον από τη θέση data[1]! Αν το κάνουμε αυτό θα χάσουμε και τα δεδομένα του data[1]. Εναλλακτικά, αντί για data[χ]->next, μπορούμε να γράψουμε (*data[3]).next 0 0 0 Α C Α E C 0 Β D Β 0 0 Σχήμα 1Α 0 Σχήμα 1Β
char * d = You: 0 points ; while (*(++d)!=0) std::cout << * ; Τι θα τυπωθεί; ************ (12 αστεράκια) Η επανάληψη θα διακοπεί όταν συναντηθεί το μηδέν, ή αλλιώς ο χαρακτήρας \0. Προσοχή: ο χαρακτήρας 0 (ASCII code 48) δεν είναι ίσος με το μηδέν ή NULL ή nullptr ή \0 (ASCII code 0). Πάντα, ένας πίνακας χαρακτήρων που παριστάνει μια έγκυρη συμβολοσειρά, ειδικά αν είναι στατικά δοσμένη όπως εδώ, τερματίζεται με το 0, γι αυτό και λέγεται «nullterminated string». class A public: int number; A(int d) : number(d) ; A & operator = (const A & right) number = 2 * right.number; return *this; ; Τι θα συμβεί στις παρακάτω περιπτώσεις; A a(1); A * p_a; A a2; p_a = new A(a); // (α) // (β) // (γ) // (δ) std::cout << (p_a == &a); // (ε) a = 2; // (στ) α) Δεσμεύεται και αρχικοποιείται μια μεταβλητή τύπου Α στην call stack καλώντας τον δηλωμένο κατασκευαστή. β) Δηλώνεται ένας δείκτης σε A. Δεν αρχικοποιείται.
γ) Επιχειρείται δήλωση και αρχικοποίηση αντικειμένου Α με χρήση του default κατασκευαστή. Ο κατασκευαστής αυτός όμως έχει καταργηθεί, επειδή ορίσαμε δικό μας κατασκευαστή για την κλάση ο οποίος απαιτεί όρισμα compilation error. δ) Δίνουμε αρχική τιμή στο δείκτη p_a καλώντας τον προκατασκευασμένο copy constructor ε) Τυπώνει 0 (false), αφού οι δύο διευθύνσεις δεν είναι ίδιες. στ) Δημιουργείται προσωρινό αντικείμενο με κλήση του κατασκευαστή A(int) αφού υπάρχει κατασκευαστής που δέχεται ως όρισμα τον τύπο του δεδομένου (int 2). Στη συνέχεια, καλείται ο τελεστής αντιγραφής για το a με αποτέλεσμα να πάρει το πεδίο a.number την τιμή 4.