Download as pdf or txt
Download as pdf or txt
You are on page 1of 186

Εισαγωγή στον

Αντικειμενοστραφή
Προγραμματισμό με C++
#1
Τμήμα Πληροφορικής και Τηλεπικοινωνιών (Άρτα)
Πανεπιστήμιο Ιωαννίνων
Γκόγκος Χρήστος

V 1.0

Δομημένος προγραμματισμός και


αντικειμενοστραφής προγραμματισμός
• Δομημένος προγραμματισμός (διαδικασιακός προγραμματισμός)
• Ο προγραμματισμός στηρίζεται σε καλά ορισμένες δομές ελέγχου
• Δομή ακολουθίας, δομές ελέγχου (if, switch), δομές επανάληψης (π.χ. while), εκφράσεις
και εκχωρήσεις
• Δεδομένα (μεταβλητές, πίνακες, εγγραφές) είναι ξεχωριστά από τις λειτουργίες σε αυτά
• Αντικειμενοστραφής προγραμματισμός
• Αναπτύχθηκε ως μια επέκταση του δομημένου προγραμματισμού
• Ο αντικειμενοστραφής προγραμματισμός βασίζεται στην έννοια του
αντικειμένου
• Τα αντικείμενα ομαδοποιούν δεδομένα και τις λειτουργίες πάνω σε αυτά τα δεδομένα
• Επιτρέπουν την απόκρυψη πληροφορίας (information hiding) που οδηγεί σε
προγράμματα που είναι ευκολότερο να αναπτυχθούν και να συντηρηθούν

2
Βασικές έννοιες Αντικειμενοστραφούς
Προγραμματισμού
• Μια βασική έννοια των αντικειμενοστραφών γλωσσών είναι η
ενθυλάκωση δεδομένων και συναρτήσεων μαζί σε μονάδες που
ονομάζονται αντικείμενα.
• Ένα αντικείμενο αποτελείται από:
• Όνομα: αποτελεί τον τρόπο με τον οποίο γίνεται η αναφορά στα αντικείμενα μέσα
στο πρόγραμμα.
• Μέλη δεδομένα (member data): τα δεδομένα που περιέχονται σε ένα αντικείμενο.
• Μέλη συναρτήσεις (member functions): συναρτήσεις που επιδρούν στα δεδομένα
του αντικειμένου.
• Διεπαφή (interface): Καθορίζει τους τρόπους με τους οποίους ο προγραμματιστής
μπορεί απευθείας να προσπελαύνει μέλη δεδομένα και μέλη συναρτήσεις ενός
αντικειμένου.

Κλάσεις
• Η κλάση είναι μια ακόμα θεμελιώδης έννοια στον
αντικειμενοστραφή προγραμματισμό και μπορεί να περιγραφεί ως
το «αρχιτεκτονικό σχέδιο» που προσδιορίζει ένα νέο τύπο
αντικειμένων.
• Η κλάση καθορίζει:
• τα δεδομένα, τις συναρτήσεις και τη διεπαφή των αντικειμένων της κλάσης.
• το πως τα αντικείμενα μιας κλάσης συμπεριφέρονται παρέχοντας κώδικα
που υλοποιεί τις συναρτήσεις που σχετίζονται με την κλάση.
• Ο προγραμματιστής μπορεί να δημιουργεί ένα ή περισσότερα
αντικείμενα μιας κλάσης

4
Πως ορίζεται και χρησιμοποιείται μια κλάση
σε ένα πρόγραμμα;
• Δήλωση της κλάσης: επιλογή του τι θα αποθηκεύουν (μέλη
δεδομένων) και πως θα συμπεριφέρονται (μέλη συναρτήσεων) τα
αντικείμενα της κλάσης.
• Ορισμός των μελών συναρτήσεων: παρέχεται υλοποίηση για κάθε
μέλος συνάρτηση της κλάσης.
• Χρήση της κλάσης για τη δημιουργία αντικειμένων: δήλωση νέων
στιγμιοτύπων αντικειμένων της κλάσης όπως δηλώνονται και οι
απλές μεταβλητές.

Παράδειγμα: Δήλωση κλάσης

class Circle {
public: // διεπαφή (interface)
void SetRadius(double r); // θέτει το μέλος δεδομένων radius στην τιμή r
double AreaOf(); // επιστρέφει το εμβαδόν του κύκλου
private:
double radius; // ακτίνα του κύκλου
};

6
Ορισμός των συναρτήσεων μελών
• Υπάρχουν 2 τρόποι με τους οποίους μπορεί να καθοριστεί ο κώδικας που
περιέχουν οι συναρτήσεις μέλη μιας κλάσης:
• Μέσα στη δήλωση της κλάσης
• Μετά τη δήλωση της κλάσης
• Η αναφορά σε μια συνάρτηση μέλος γίνεται ως εξής:
όνομαΚλάσης::όνομαΣυνάρτησηςΜέλους
• Το αναγνωριστικό όνομαΚλάσης::όνομαΣυνάρτησηςΜέλους αναφέρεται
στη συνάρτηση μέλος όνομαΣυνάρτησηςΜέλους της κλάσης
όνομαΚλάσης
• Ο τελεστής :: ονομάζεται τελεστής προσδιορισμού εμβέλειας (scope
resolution operator)
• Μετά τη δήλωση της κλάσης, οι συναρτήσεις μέλη ορίζονται όπως
οποιαδήποτε άλλη συνάρτηση.
7

Παράδειγμα: ορισμός υλοποιήσεων


συναρτήσεων μελών εκτός της κλάσης
class Circle
{
public: // διεπαφή (interface)
void SetRadius(double r); // θέτει το μέλος δεδομένων radius στην τιμή r
double AreaOf(); // επιστρέφει το εμβαδόν του κύκλου
private:
double radius; // ακτίνα του κύκλου
};

// υλοποιήσεις συναρτήσεων μελών


void Circle::SetRadius(double r) {
radius = r;
}
double Circle::AreaOf() {
return (3.14 * radius * radius);
}
8
Παράδειγμα: ορισμός υλοποιήσεων
συναρτήσεων μελών εντός της κλάσης
class Circle
{
public:
void SetRadius(double r) {
radius = r;
}
double AreaOf() {
return (3.14 * radius * radius);
}
private:
double radius; // ακτίνα του κύκλου
};

Αντικείμενα
• Από τη στιγμή που μια κλάση έχει δηλωθεί και οριστεί, μπορούν να δηλώνονται
και να χρησιμοποιούνται αντικείμενα της κλάσης όπως οποιοσδήποτε άλλος
τύπος δεδομένων
• Ο προγραμματιστής μπορεί να δηλώνει ένα αντικείμενο με τον ακόλουθο τρόπο:
• ΌνομαΚλάσης όνομαΑντικειμένου;
• Η παραπάνω δήλωση δημιουργεί ένα αντικείμενο βάσει των «οδηγιών» που
περιέχονται στην κλάση ΌνομαΚλάσης και το αντικείμενο μπορεί να αναφερθεί
με το αναγνωριστικό όνομαΑντικειμένου
• Ο τελεστής . (dot operator) μπορεί να χρησιμοποιηθεί για να προσπελαστούν τα
δημόσια μέλη ενός αντικειμένου.
• Η μορφή με την οποία γίνεται η αναφορά στα μέλη ενός αντικειμένου είναι:
• όνομαΑντικειμένου.ΜέλοςΣυνάρτηση()
• όνομαΑντικειμένου.ΜέλοςΔεδομένων

10
Αντικείμενα

int main() {
Circle C1;
Circle C2;

C1.SetRadius(1); The area of C1 is 3.14


C2.SetRadius(10); The area of C2 is 314
cout<<"The area of C1 is "<<C1.AreaOf()<<endl; The area of C1 is 12.56
cout<<"The area of C2 is "<<C2.AreaOf()<<endl; The area of C2 is 28.26

C1.SetRadius(2);
C2.SetRadius(3);
cout<<"The area of C1 is "<<C1.AreaOf()<<endl;
cout<<"The area of C2 is "<<C2.AreaOf()<<endl;
return 0;
}
11

Ο κώδικας συνολικά
• Ο κώδικας του sample1.cpp
• Δηλώνει την κλάση Circle και ορίζει τα μέλη της και τη διεπαφή της.
• Ορίζει την υλοποίηση των συναρτήσεων μελών της κλάσης Circle.
• Δηλώνει 2 αντικείμενα της κλάσης Circle με ονόματα C1 και C2.
• Χρησιμοποιεί τις διεπαφές των C1 και C2 για να αποθηκεύσει τις ακτίνες των
2 κύκλων και στη συνέχεια για να υπολογίσει το εμβαδό τους.

https://github.com/chgogos/oop/blob/master/various/COP3330/lect1/sample1.cpp

https://github.com/chgogos/oop/tree/master/various/COP3330/lect1/sample1 έκδοση με διαμέριση κώδικα

12
Σύνοψη
• Ένα αντικείμενο είναι μια μονάδα που ενθυλακώνει δεδομένα και
συναρτήσεις. Έχει 4 στοιχεία: όνομα, μέλη δεδομένα, μέλη συναρτήσεις
και διεπαφή.
• Μια κλάση καθορίζει την ορισμένη από το χρήστη μορφή των
αντικειμένων.
• Η χρήση των αντικειμένων σε ένα C++ πρόγραμμα ακολουθεί τη σειρά:
δήλωση, ορισμός και χρήση.
• Ο τελεστής :: χρησιμοποιείται έτσι ώστε να οριστούν οι συναρτήσεις μιας
κλάσης εκτός της κλάσης.
• Ο τελεστής . χρησιμοποιείται για να κληθεί μια συνάρτηση μέλος ή να
προσπελαστούν τα μέλη δεδομένων ενός αντικειμένου.

13

Επίπεδα προστασίας και


κατασκευαστές
#2
Τμήμα Πληροφορικής και Τηλεπικοινωνιών (Άρτα)
Πανεπιστήμιο Ιωαννίνων
Γκόγκος Χρήστος

V 1.01
Επίπεδα προστασίας
• Όταν σχεδιάζουμε μια κλάση μπορούμε να ελέγξουμε το πως θα προσπελαύνονται τα μέλη δεδομένα και
τα μέλη συναρτήσεις της κλάσης.
• Σε κάθε μέλος της κλάσης ανατίθεται ένα επίπεδο προστασίας:
• Δημόσια μέλη: Δεδομένα και συναρτήσεις των αντικειμένων που μπορούν να προσπελαστούν τόσο εκτός όσο και εντός
των μελών συναρτήσεων της κλάσης.
• Ιδιωτικά μέλη: Δεδομένα και συναρτήσεις των αντικειμένων που μπορούν να προσπελαστούν μόνο εντός των μελών
συναρτήσεων της κλάσης.
• Δηλαδή, ο κώδικας που υλοποιεί την κλάση έχει πρόσβαση στα ιδιωτικά δεδομένα και συναρτήσεις, ενώ ο κώδικας που χρησιμοποιεί
την κλάση δεν έχει πρόσβαση στα ιδιωτικά δεδομένα και συναρτήσεις

• Ο κύριος στόχος των επιπέδων προστασίας είναι να επιτρέπει στους προγραμματιστές να παρέχουν μια
δημόσια διεπαφή για την κλάση, ενώ παράλληλα να είναι σε θέση να αλλάζουν την υλοποίηση της κλάσης
χωρίς να δημιουργούνται προβλήματα σε κώδικα που χρησιμοποιεί την κλάση.
• Πρόκειται για έναν μηχανισμό μέσω του οποίου υλοποιείται η απόκρυψη πληροφορίας.

#include <iostream>
sample1.cpp
class Fraction {
public:
void SetNumerator(int n);
void SetDenominator(int d);
double ToDecimal();
• Έξοδος προγράμματος:
private:
int numer;
Decimal: 0.5
• Τι θα συμβεί αν στη main
int denom;
};
void Fraction::SetNumerator(int n) {
numer = n; // παρατηρήστε ότι προσπελαύνουμε το ιδιωτικό μέλος numer εδώ αφαιρέσουμε τα σχόλια από
}
void Fraction::SetDenominator(int d) {
την εντολή:
if (d != 0) F.numer = 1;
denom = d;
else $ g++ sample1.cpp
denom = 1;
}
sample1.cpp: In function 'int main()':
double Fraction::ToDecimal() { sample1.cpp:37:7: error: 'int Fraction::numer' is private within this context
return (double)numer / denom; F.numer = 1;
} ^~~~~
int main() { sample1.cpp:11:9: note: declared private here
Fraction F; int numer;
// F.numer = 1;
F.SetNumerator(1);
^~~~~
F.SetDenominator(2);
std::cout << "Decimal: " << F.ToDecimal() << std::endl;
return 0;
} 3
https://github.com/chgogos/oop/blob/master/various/COP3330/lect2/sample1.cpp
#include <iostream>

class Fraction { sample2.cpp


public:
void SetNumerator(int n); • Έξοδος προγράμματος:
void SetDenominator(int d);
double ToDecimal(); Decimal: Hello!!!
private: 0.5
void PrintHello();
int numer;
int denom; • Τι θα συμβεί αν στη main
};
void Fraction::SetNumerator(int n) {
αφαιρέσουμε τα σχόλια από την
}
numer = n; // παρατηρήστε ότι προσπελαύνουμε το ιδιωτικό μέλος numer εδώ εντολή:
void Fraction::SetDenominator(int d) { F.PrintHello();
if (d != 0) denom = d; else denom = 1;
} $ g++ sample2.cpp
double Fraction::ToDecimal() { sample2.cpp: In function 'int main()':
PrintHello(); // ok, διότι καλείται μέσα από την κλάση
return (double)numer / denom;
sample2.cpp:44:18: error: 'void Fraction::PrintHello()' is private within this context
} F.PrintHello();
void Fraction::PrintHello() { ^
std::cout << "Hello!!!" << std::endl; sample2.cpp:35:6: note: declared private here
} void Fraction::PrintHello()
int main() { ^~~~~~~~
Fraction F;
// F.PrintHello(); // δεν γίνεται διότι η PrintHello είναι ιδιωτικό μέλος της κλάσης
F.SetNumerator(1);
F.SetDenominator(2);
std::cout << "Decimal: " << F.ToDecimal() << std::endl;
return 0;
}
https://github.com/chgogos/oop/blob/master/various/COP3330/lect2/sample2.cpp 4

Περισσότερα για τα επίπεδα προστασίας


• Λόγοι για την εφαρμογή της απόκρυψης πληροφορίας (ιδιωτικά
μέλη):
• Καθιστά τη διεπαφή απλούστερη για τον χρήστη.
• Εφαρμογή της αρχής του ελάχιστου προνομίου (need to know).
• Μεγαλύτερη ασφάλεια. Μικρότερη πιθανότητα κακής χρήσης (από λάθος ή
κακόβουλα).
• Ευκολία αλλαγής υλοποίησης της κλάσης χωρίς να επηρεάζονται άλλα
τμήματα κώδικα που τη χρησιμοποιούν.
• Επιπλέον σημαντικές πληροφορίες:
• Αν δεν ορίζεται ρητά επίπεδο προστασίας, τα μέλη είναι ιδιωτικά.
• Κατά το σχεδιασμό της κλάσης θα πρέπει να ορίζονται ως ιδιωτικά τα μέλη
που δεν ανήκουν στη διεπαφή της.

5
Κατασκευαστές (1/2)
• Ο κατασκευαστής είναι μια ειδική συνάρτηση μέλος που συνήθως
έχει ως σκοπό την αρχικοποίηση των μελών ενός αντικειμένου.
• Ο κατασκευαστής είναι εύκολο να αναγνωριστεί διότι:
• Έχει το ίδιο όνομα με την κλάση. class Circle {

• Δεν έχει τύπο επιστροφής. public:


Circle(); // αυτός είναι ένας κατασκευαστής
Circle(double r); // και αυτός είναι ένας κατασκευαστής
• Παράδειγμα Circle: void setCenter(double x, double y);
void SetRadius(double r);
Circle C1; void Draw();
double AreaOf();
private:
double radius;
double center_x;
double center_y;
};

https://github.com/chgogos/oop/blob/master/various/COP3330/lect2/circle.cpp 6

Κατασκευαστές (2/2)
• Οι κατασκευαστές είναι συναρτήσεις, συνεπώς μπορούν να περιέχουν
οποιοδήποτε κώδικα όπως και οι άλλες συναρτήσεις.
• Τι είναι ιδιαίτερο για τους κατασκευαστές;
• Καλούνται αυτόματα όταν δημιουργείται ένα στιγμιότυπο ενός αντικειμένου (π.χ.
Circle C1;)
• Η συνάρτηση κατασκευαστής δεν μπορεί να κληθεί όπως οι άλλες συναρτήσεις
χρησιμοποιώντας τον τελεστή . (π.χ. C1.Circle();)
• Ο όρος προκαθορισμένος κατασκευαστής (default constructor)
αναφέρεται σε έναν κατασκευαστή χωρίς παραμέτρους.
• Κάθε κλάση θα πρέπει να έχει έναν κατασκευαστή. Αν δεν γραφεί από τον
προγραμματιστή τότε ένας κενός προκαθορισμένος κατασκευαστής
δημιουργείται από τη γλώσσα.

7
Πέρασμα παραμέτρων σε έναν
κατασκευαστή
• Γίνεται απλά προσθέτοντας (…) ως τμήμα της δημιουργίας του
στιγμιοτύπου του αντικειμένου
Circle C1(5); /* δηλώνει ένα νέο αντικείμενο Circle με το όρισμα 5 να περνά στο
δεύτερο κατασκευαστή της Circle στη παράμετρο r */
class Circle {
public:
Circle(); // αυτός είναι ένας κατασκευαστής
Circle(double r); // και αυτός είναι ένας κατασκευαστής
void setCenter(double x, double y);
void SetRadius(double r);
void Draw();
double AreaOf();
private:
double radius;
double center_x;
double center_y;
};

Δείτε και το παράδειγμα με την κλάση Fraction:


https://github.com/chgogos/oop/blob/master/various/COP3330/lect2/sample3.cpp 8

Σύνοψη (ερωτήσεις)
• Για ποια επίπεδα προστασίας μιλήσαμε;
• Γιατί χρησιμοποιούμε επίπεδα προστασίας;
• Ποιο τμήμα του προγράμματος μπορεί να προσπελάσει τα ιδιωτικά μέλη
(δεδομένα ή συναρτήσεις);
• Ποιο τμήμα του προγράμματος δεν μπορεί να προσπελάσει τα ιδιωτικά μέλη;
• Τι συμβαίνει όταν ένα πρόγραμμα προσπαθεί να προσπελάσει με μη έγκυρο
τρόπο ιδιωτικά μέλη της κλάσης;
• Τι είναι ένας κατασκευαστής;
• Πως καταλαβαίνουμε ότι μια συνάρτηση είναι κατασκευαστής μιας κλάσης;
• Τι είναι ο προκαθορισμένος κατασκευαστής;
• Πως περνάμε ορίσματα σε έναν κατασκευαστή;
9
Περιβάλλον Unix και
μεταγλώττιση
#3
Τμήμα Πληροφορικής και Τηλεπικοινωνιών (Άρτα)
Πανεπιστήμιο Ιωαννίνων
Γκόγκος Χρήστος

V 1.0

Πώς μπορώ να έχω πρόσβαση σε περιβάλλον


Unix;
• Εγκατάσταση διανομής Linux (προτεινόμενη διανομή: Ubuntu 18.04 LTS).
• Εγκατάσταση διανομής Linux παράλληλα με διατήρηση εγκατάστασης
Windows (dual boot).
• Εκτέλεση διανομής Linux από Live CD.
• Εγκατάσταση Cygwin σε υπολογιστή με εγκατάσταση Windows.
• Εγκατάσταση WSL (Windows Subsystem for Linux) σε υπολογιστή με
εγκατάσταση Windows 10.
• Εγκατάσταση Linux Virtual Machine σε VirtualBox ή VMWare.
• Vagrant.
• Docker.
• ssh σε υπολογιστή με εγκατεστημένη μια διανομή Linux.

2
Βασικές εντολές στο Linux
ls Λίστα με όλα τα αρχεία και τους καταλόγους στον τρέχοντα κατάλογο
cd <dir> Αλλαγή του τρέχοντος καταλόγου σε <dir>
cd .. Αλλαγή του τρέχοντος καταλόγου ένα επίπεδο πάνω στην ιεραρχία καταλόγων
cd ~ Αλλαγή του τρέχοντος καταλόγου στο home κατάλογο
mkdir <dir> Δημιουργία του υποκαταλόγου <dir> στον τρέχοντα κατάλογο
rm <filename> Διαγραφή του αρχείου <filename>
rm -r <dir> Διαγραφή του καταλόγου <dir> των περιεχομένων του και όλων των υποκαταλόγων του
cat <filename> Έξοδος του κειμένου του αρχείου <filename> στην οθόνη
chmod +x <filename> Αλλαγή των ιδιοτήτων του αρχείου <filename> έτσι ώστε να επιτρέπεται η εκτέλεσή του.
more <filename> Παρόμοιο με το cat, επιτρέπει σκρολάρισμα της οθόνης
man <keyword> Εμφάνιση της σελίδας του εγχειριδίου (manual) για διάφορες προγραμματιστικές έννοιες
και έννοιες του unix

Εγκατάσταση gcc σε Ubuntu 18.04


$ sudo apt update
$ sudo apt install build-essential
$ sudo apt-get install manpages-dev
$ g++ --version
g++ (Ubuntu 7.5.0-3ubuntu1~18.04) 7.5.0
Copyright (C) 2017 Free Software Foundation, Inc.
This is free software; see the source for copying conditions. There is NO
warranty; not even for MERCHANTABILITY or FITNESS FOR A PARTICULAR
PURPOSE.

4
Projects πολλαπλών αρχείων (1/2)
• Αν και είναι δυνατόν οποιοδήποτε πρόγραμμα να γραφεί σε ένα
μόνο αρχείο, συχνά τα προγράμματα διαμερίζονται σε πολλά αρχεία:
• Αρχείο επικεφαλίδας (header): περιέχει τη δήλωση της κλάσης, συνήθως
έχει ως επέκταση .hpp (π.χ. circle.hpp).
• Αρχείο υλοποίησης: περιέχει τον ορισμό της κλάσης, δηλαδή της υλοποίησης
των συναρτήσεων μελών της κλάσης, συνήθως έχει ως επέκταση .cpp (π.χ.
circle.cpp).
• Αρχείο οδηγός: περιέχει την main() και πιθανώς άλλες συναρτήσεις που
χρησιμοποιούνται στο πρόγραμμα, συνήθως έχει ως επέκταση .cpp (π.χ.
main.cpp).

Projects πολλαπλών αρχείων (2/2)


• Λόγοι για τους οποίους χρησιμοποιούνται πολλαπλά αρχεία:
• Επιτρέπει να γίνονται αλλαγές στην υλοποίηση των κλάσεων χωρίς να
επηρεάζεται η διεπαφή της κλάσης.
• Επιτρέπει σε μια κλάση να χρησιμοποιείται από πολλά προγράμματα
οδηγούς.
• Αυξάνει τις ευκαιρίες για επαναχρησιμοποίηση κώδικα.
• Ο κώδικας γίνεται περισσότερο τμηματικός (modular).
• Στην πράξη, πολλές φορές, δεν είναι δυνατόν να γραφεί κώδικας για ένα
μεγάλο έργο λογισμικού σε ένα μόνο αρχείο.

6
Μεταγλώττιση
• Μεταγλώττιση είναι η διαδικασία μετατροπής ενός αρχείου από τη
C++ γλώσσα σε γλώσσα που μπορεί να «καταλάβει» ο υπολογιστής,
δηλαδή σε γλώσσα μηχανής.
• Η μεταγλώττιση εμπεριέχει δύο κύριες φάσεις:
• φάση μεταγλώττισης που είναι υπεύθυνη για τη μετάφραση της γλώσσας.
• φάση σύνδεσης που είναι υπεύθυνη για την επίλυση των αναφορών μεταξύ
επιμέρους αρχείων.

Φάση μεταγλώττισης (compile phase)


• Πραγματοποιεί τις οδηγίες του προεπεξεργαστή (preprocessor directives)
όπως είναι οι: #include, #define κ.λπ.
• Ελέγχει τη σύνταξη του προγράμματος.
• Ελέγχει ότι οι συναρτήσεις και οι μεταβλητές έχουν δηλωθεί πριν τη χρήση
τους.
• Δεν ελέγχει ότι οι συναρτήσεις που χρησιμοποιούνται έχουν οριστεί.
• Δημιουργεί αναπαράσταση κώδικα μηχανής σε μια μορφή που ονομάζεται
αντικείμενο αρχείο (object file). Πρόκειται για ένα αρχείο με όνομα που
έχει ως επέκταση .o
• To αντικείμενο αρχείο δεν είναι εκτελέσιμο και μπορεί να περιέχει χρήση
συναρτήσεων οι οποίες δεν έχουν οριστεί (δηλαδή δεν υπάρχει
υλοποίηση για αυτές).

8
Φάση σύνδεσης (link phase)
• Συνδέει τα αρχεία αντικείμενα σε ένα εκτελέσιμο πρόγραμμα.
• Η φάση σύνδεσης είναι η φάση κατά την οποία οι κλήσεις
συναρτήσεων συνδέονται με τους ορισμούς (υλοποιήσεις) των
συναρτήσεων, και ο μεταγλωττιστής ελέγχει ότι υπάρχει ένας και
μόνο ένας ορισμός για κάθε συνάρτηση που καλείται.
• Όλες οι συναρτήσεις μέλη θα πρέπει να έχουν οριστεί κατά τη
διάρκεια αυτής της φάσης.
• Θα πρέπει να υπάρχει μια main() συνάρτηση έτσι ώστε το εκτελέσιμο
να γνωρίζει από που να ξεκινήσει.

Παράδειγμα μεταγλώττισης project με


διαμέριση αρχείων
• g++ -c Fraction.cpp
• πραγματοποιεί τη φάση μεταγλώττισης στο αρχείο Fraction.cpp και δημιουργεί το
αντικείμενο αρχείο Fraction.o
• g++ -c main.cpp
• πραγματοποιεί τη φάση μεταγλώττισης στο αρχείο main.cpp και δημιουργεί το
αντικείμενο αρχείο main.o
• g++ Fraction.o main.o -o main
• συνδέει τα αρχεία αντικείμενα, ο κώδικας στο main.o που καλεί μέλη συναρτήσεις της
κλάσης Fraction, συνδέεται με τους ορισμούς (υλοποίησεις των μελών συνατήσεων)
που έχουν μεταγλωττιστεί στο αρχείο Fraction.o
• Συντόμευση που πραγματοποιεί μεταγλώττιση και σύνδεση σε ένα βήμα:
• g++ main.cpp Fraction.cpp -o main
https://github.com/chgogos/oop/blob/master/various/COP3330/lect3/Fraction.cpp
https://github.com/chgogos/oop/blob/master/various/COP3330/lect3/Fraction.hpp
10
https://github.com/chgogos/oop/blob/master/various/COP3330/lect3/main.cpp
Η δεσμευμένη λέξη friend,
κατασκευαστές μετατροπής,
καταστροφείς
#4
Τμήμα Πληροφορικής και Τηλεπικοινωνιών
Πανεπιστήμιο Ιωαννίνων (Άρτα)
Γκόγκος Χρήστος

Παράδειγμα κίνητρο (1/3)


• Έστω ότι επιθυμούμε να γράψουμε μια συνάρτηση Equals που να συγκρίνει 2 αντικείμενα
Fraction και να τη χρησιμοποιήσουμε όπως στη συνέχεια:
int main() {
Fraction f1(1, 2);
Fraction f2(2, 4);
if (Equals(f1, f2))
cout << "The fractions are equal" << endl;
return 0;
}

• Μια πιθανή υλοποίηση της συνάρτησης Equals είναι η ακόλουθη:


bool Equals(Fraction x, Fraction y) {
if (x.GetNumerator() * y.GetDenominator() == y.GetNumerator() * x.GetDenominator())
return true;
else
return false;
}
2
Παράδειγμα κίνητρο (2/3)
$ g++ Fraction.cpp main2.cpp -o main
main2.cpp: In function 'bool Equals(Fraction, Fraction)':
main2.cpp:8:11: error: 'int Fraction::numer' is private within this context
if (x.numer * y.denom == y.numer * x.denom)
^~~~~
In file included from main2.cpp:2:
Fraction.hpp:14:9: note: declared private here
• Τι θα συνέβαινε αν επιθυμούμε να int numer;

γράψουμε τη συνάρτηση Equals ως εξής:


^~~~~
main2.cpp:8:21: error: 'int Fraction::denom' is private within this context
if (x.numer * y.denom == y.numer * x.denom)
^~~~~
In file included from main2.cpp:2:
bool Equals(Fraction x, Fraction y) { Fraction.hpp:15:9: note: declared private here
if (x.numer * y.denom == int denom;
y.numer * x.denom) ^~~~~
return true; main2.cpp:8:32: error: 'int Fraction::numer' is private within this context
else if (x.numer * y.denom == y.numer * x.denom)
^~~~~
return false;
In file included from main2.cpp:2:
} Fraction.hpp:14:9: note: declared private here
int numer;
^~~~~
main2.cpp:8:42: error: 'int Fraction::denom' is private within this context
if (x.numer * y.denom == y.numer * x.denom)
Καθώς η συνάρτηση Equals δεν είναι συνάρτηση μέλος ^~~~~
της Fraction, δεν μπορεί να προσπελαύνει απευθείας τις In file included from main2.cpp:2:
Fraction.hpp:15:9: note: declared private here
ιδιωτικές συναρτήσεις μέλη. int denom;
^~~~~

Παράδειγμα κίνητρο (3/3)


• Ωστόσο, συναρτήσεις όπως η Equals ενδέχεται να χρειάζεται να
προσπευλαύνουν ιδιωτικά μέλη συχνά, οπότε θα θέλαμε να έχουμε
τη δυνατότητα να προσπελαύνουμε απευθείας ιδιωτικά μέλη της
κλάσης.

https://github.com/chgogos/oop/tree/master/various/COP3330/lect4/sample1

• Μια λύση στη C++ είναι η χρήση της δεσμευμένης λέξης friend.

4
Η δεσμευμένη λέξη friend
• Η δεσμευμένη λέξη friend επιτρέπει σε μια κλάση να χορηγήσει πλήρη
πρόσβαση σε μια εξωτερική οντότητα.
• Πλήρης πρόσβαση σημαίνει δυνατότητα προσπέλασης και των ιδιωτικών μελών της
κλάσης.
• Εξωτερική οντότητα μπορεί να είναι μια συνάρτηση, ή ακόμα και μια άλλη κλάση.
• Για να αποδοθεί η ιδιότητα friend, θα πρέπει να προστεθεί η δήλωση
friend μέσα στη δήλωση της κλάσης, και να ακολουθήσει η αντίστοιχη
οντότητα.
• Μια friend οντότητα δεν είναι ούτε δημόσια ούτε ιδιωτική, καθώς δεν πρόκειται για
μέλος της κλάσης, οπότε δεν έχει σημασία αν θα τοποθετηθεί σε τμήμα public ή
private της κλάσης.
• Μια friend συνάρτηση σε μια κλάση έχει πλήρη πρόσβαση στα ιδιωτικά μέλη της
κλάσης.

Παράδειγμα με friend συναρτήσεις (1/2)


• Στο παράδειγμα που ακολουθεί:
• Ορίζεται η friend συνάρτηση Equals()
• Ορίζεται η friend συνάρτηση Add() που προσθέτει δύο κλάσματα και
επιστρέφει το αποτέλεσμα.
• Στο αρχείο main.cpp βρίσκεται το πρόγραμμα οδηγός.

https://github.com/chgogos/oop/tree/master/various/COP3330/lect4/sample2_friend

6
Παράδειγμα με friend συναρτήσεις (2/2)
class Fraction #include <iostream>
{ #include "Fraction.hpp"
... using namespace std;
friend bool Equals(Fraction x, Fraction y); int main() {
friend Fraction Add(Fraction x, Fraction y); Fraction f1(1, 2);
}; Fraction f2(2, 4);
if (Equals(f1, f2))
Fraction.hpp cout << "The fractions are equal" << endl;
Fraction f3 = Add(f1, f2);
... f3.Show();
bool Equals(Fraction x, Fraction y) { return 0;
if (x.numer * y.denom == y.numer * x.denom) }
return true;
else main.cpp
return false;
}
$ g++ Fraction.cpp main.cpp -o main
Fraction Add(Fraction x, Fraction y) { $ ./main
int num = x.numer * y.denom + y.numer * x.denom; The fractions are equal
int denom = x.denom * y.denom;
8/8
Fraction answer(num, denom);
return answer;
}
7
Fraction.cpp

Χρήση συνάρτησης μέλους αντί για friend


συνάρτηση
• Όταν μια συνάρτηση πραγματοποιεί επεξεργασία σε δύο αντικείμενα,
συχνά είναι βολικό να περνάμε ως παραμέτρους και τα δύο αντικείμενα
και να κάνουμε τη συνάρτηση friend.
• Μια άλλη επιλογή είναι να χρησιμοποιήσουμε μια συνάρτηση μέλους
αλλά σε αυτή την περίπτωση ένα από τα αντικείμενα θα πρέπει να είναι
το καλών αντικείμενο (calling object).
• Παράδειγμα: Η συνάρτηση Equals() θα μπορούσε να οριστεί ως συνάρτηση μέλος
της κλάσης Fraction, και να κληθεί ως εξής:
if (f1.Equals(f2))
cout << "The fractions are equal" << endl;
• Στο παραπάνω κώδικα, f1 είναι το καλών αντικείμενο (δηλαδή το αντικείμενο που
καλεί τη συνάρτηση μέλος) και το f2 περνά στη συνάρτηση Equals της f1 ως όρισμα.

8
Παράδειγμα με συναρτήσεις μέλη αντί για
friend συναρτήσεις
bool Fraction::Equals(Fraction other) {
class Fraction { Fraction.hpp return numer * other.denom == other.numer * denom;
public: }
...
bool Equals(Fraction other); Fraction Fraction::Add(Fraction other) {
Fraction Add(Fraction other); int n = numer * other.denom + other.numer * denom;
... int d = denom * other.denom;
}; return Fraction(n, d);
main.cpp }
#include <iostream>
#include "Fraction.hpp"
Fraction.cpp
using namespace std;
int main() {
Fraction f1(1, 2); The fractions are equal
Fraction f2(2, 4);
if (f1.Equals(f2))
11/14
cout << "The fractions are equal" << endl; Decimal value: 0.785714
f2.SetValue(2, 7);
Fraction f3 = f1.Add(f2);
f3.Show(); https://github.com/chgogos/oop/tree/master/
cout << "Decimal value: " << f3.Evaluate() << endl;
various/COP3330/lect4/sample3_member
return 0;
} 9

Συναρτήσεις μέλη vs. φίλες συναρτήσεις


• Η επιλογή του εάν θα χρησιμοποιηθεί συνάρτηση μέλος ή friend
συνάρτηση είναι θέμα προγραμματιστικού στυλ.
• Διαφορετικοί προγραμματιστές μπορεί να έχουν διαφορετικές απόψεις
για το θέμα. Στη συνέχεια παρουσιάζονται για σύγκριση των δύο κλήσεων
ο ένας κώδικας ο ένας μετά τον άλλο:
f3 = Add(f1,f2); // κλήση στη friend συνάρτηση Add
f3 = f1.Add(f2); // κλήση στη συνάρτηση μέλος Add της κλάσης Fraction
• Ωστόσο, αξίζει να σημειωθεί ότι οι δύο παραπάνω κλήσεις δεν είναι
πάντα ισοδύναμες.
• Στην έκδοση με τις friend συναρτήσεις η Add λαμβάνει αντίγραφα από τα f1 και f2
(συνεπώς η συνάρτηση δεν μπορεί να αλλάξει τα αρχικά αντικείμενα).
• Στην έκδοση με τη συνάρτηση μέλος, η συνάρτηση Add μπορεί να αλλάξει το
αντικείμενο f1.
10
Κατασκευαστές μετατροπής
• Ορισμένοι ενσωματωμένοι τύποι μπορούν να πραγματοποιήσουν
αυτόματη μετατροπή τύπων όπως:
int x = 5;
double y = 4.5, z = 1.2;
y = x; // έγκυρο, αυτόματη μετατροπή
z = x + y; // έγκυρο, αυτόματη μετατροπή

• Παρόμοια λειτουργικότητα μπορεί να προστεθεί και σε κλάσεις που


δημιουργούμε χρησιμοποιώντας τους λεγόμενους κατασκευαστές
μετατροπής (conversion constructors).

11

Κατασκευαστές μετατροπής
• Ένας κατασκευαστής μετατροπής είναι ένας κατασκευαστής με μια παράμετρο.
• Καθώς ο κατασκευστής δημιουργεί/αρχικοποιεί ένα νέο αντικείμενο, μπορούμε να
χρησιμοποιήσουμε έναν κατασκευαστή μετατροπής για να μετατρέψουμε μια μεταβλητή με
τύπο τον τύπο της παραμέτρου σε ένα νέο αντικείμενο.
• Παράδειγμα κατασκευαστή μετατροπής:
Fraction(int n); /* μπορεί να χρησιμοποιηθεί για να μετατρέψει έναν ακέραιο σε
Fraction, αρχικοποιεί το κλάσμα σε n/1 */

• Ο παραπάνω κατασκευαστής μπορεί να χρησιμοποιηθεί για να


πραγματοποιούνται αυτόματες μετατροπές τύπων όπως οι ακόλουθες:
Fraction f1, f2;
f1 = Fraction(4) /* ρητή κλήση του κατασκευαστή. Δημιουργείται το
κλάσμα 4/1 και ανατίθεται στο f1 */
f2 = 10; /* υπονοούμενη κλήση του κατασκευαστή μετατροπής.
Ισοδύναμο με f2 = Fraction(10); */
f1 = Add(f2, 5); /* ο κατασκευαστής μετατροπής μετατρέπει το 5 σε 5/1 */

12
Κατασκευαστής μετατροπής
• Ένας κατασκευαστής με πολλές παραμέτρους μπορεί να είναι
κατασκευαστής μετατροπής αν όλες, πλην μιας, από τις
παραμέτρους είναι προαιρετικές (optional):
Fraction(int n, int d=1);
• Η αυτόματη μετατροπή τύπων από κατασκευαστές μπορεί να
αποτραπεί χρησιμοποιώντας τη δεσμευμένη λέξη explicit στην αρχή
της δήλωσης:
explicit Fraction(int n);
• Ο ανωτέρω κατασκευαστής δεν θα πραγματοποιεί πλέον αυτόματη
μετατροπή ακεραίων σε Fraction.
13

Παράδειγμα με κατασκευαστή μετατροπής


class Fraction { Fraction.hpp #include <iostream> main.cpp
public: #include "Fraction.hpp"
Fraction(); using namespace std;
Fraction(int n, int d = 1); int main() {
... // ρητές κλήσεις κατασκευαστών
}; Fraction f1;
f1.Show();
Fraction f2(2);
f2.Show();
... Fraction.cpp Fraction f3(3, 4);
Fraction::Fraction() {
f3.Show();
cout << "Fraction()" << endl;
// υπονοούμενη κλήση κατασκευαστή
numer = 0;
f1 = 4;
denom = 1;
f1.Show();
}
return 0;
Fraction::Fraction(int n, int d) {
}
cout << "Fraction(int, int)" << endl;
numer = n; Fraction()
denom = d; 0/1
} Fraction(int, int)
2/1
... Fraction(int, int)
3/4
Fraction(int, int)
4/1

https://github.com/chgogos/oop/tree/master/various/COP3330/lect4/sample4_conversion 14
Καταστροφείς (destructors)
• Eπιπρόσθετα στις ειδικές συναρτήσεις των κατασκευαστών, κάθε κλάση διαθέτει και
μια ειδική συνάρτηση που ονομάζεται καταστροφέας (destructor).
• Ο καταστροφέας μοιάζει με τον προκαθορισμένο κατασκευαστή (δηλαδή τον
κατασκευαστή χωρίς παραμέτρους) αλλά έχει το σύμβολο ~ πριν το όνομά του.
• Οι καταστροφείς δεν μπορούν να έχουν παραμέτρους, άρα υπάρχει ένας μόνο
καταστροφέας ανά κλάση.
• Για παράδειγμα ο καταστροφέας της κλάσης Fraction θα είναι: ~Fraction();
• Όπως και με τους κατασκευαστές, οι καταστροφείς καλούνται αυτόματα (όχι ρητά).
• Οι καταστροφείς καλούνται αυτόματα ακριβώς πριν το αντικείμενο αποδεσμευτεί από
το σύστημα, συνήθως όταν βγαίνει εκτός εμβέλειας (δηλαδή, όταν δεν είναι πλέον
προσπελάσιμο από τον προγραμματιστή).
• Η τυπική εργασία ενός καταστροφέα είναι να πραγματοποιεί όποιες εργασίες
αποδέσμευσης πόρων (συνήθως μνήμης) απαιτούνται, πριν το αντικείμενο
αποδεσμευτεί.

15

Παράδειγμα καταστροφέα
#include <iostream>
#include <string>
using std::string;
class Thing {
public:
Thing(const char *n);
~Thing();
private:
string name;
Constructor running for T1
}; Hello
Thing::Thing(const char *n) {
name = n;
Constructor running for Tfoo
std::cout << "Constructor running for " << name << std::endl; Destructor running for Tfoo
}
Thing::~Thing() {
Constructor running for T2
std::cout << "Destructor running for " << name << std::endl; Destructor running for T2
}
void foo() {
Destructor running for T1
Thing TFoo("Tfoo");
}
int main() {
Thing T1("T1");
std::cout << "Hello" << std::endl;
foo();
Thing T2("T2");
return 0;
}
https://github.com/chgogos/oop/blob/master/various/COP3330/lect4/sample5_destructor/destructor.cpp 16
Ερωτήσεις σύνοψης
• Ποιος είναι ο ρόλος της δεσμευμένης λέξη friend σε μια κλάση;
• Μια συνάρτηση που έχει γίνει friend σε μια κλάση είναι public ή
private;
• Ποια είναι η διαφορά ανάμεσα στις δύο ακόλουθες κλήσεις:
• f3 = f1.Add(f2);
• f3 = Add(f1,f2);
• Τι είναι ο κατασκευαστής μετατροπής;
• Πώς ακυρώνουμε τις αυτόματες μετατροπές που γίνονται από έναν
κατασκευαστή μετατροπής;

17

Η δεσμευμένη λέξη const


#5
Τμήμα Πληροφορικής και Τηλεπικοινωνιών
Πανεπιστήμιο Ιωαννίνων (Άρτα)
Γκόγκος Χρήστος
const
• Γενικά, η δεσμευμένη λέξη const εφαρμόζεται σε αναγνωριστικά από τον
προγραμματιστή προκειμένου να δηλώσει την πρόθεσή του ότι το αναγνωριστικό δεν
θα πρέπει να χρησιμοποιείται για να αλλάξει δεδομένα στα οποία αναφέρεται.
• Ο μεταγλωττιστής επιβάλλει την πρόθεση του προγραμματιστή, για παράδειγμα
(t1.cpp):
g++ t1.cpp
t1.cpp:4:5: error: cannot assign to variable 'x' with const-qualified type 'const int'
int main()
x = 2;
{
~^
const int x = 5;
t1.cpp:3:13: note: variable 'x' declared const here
x = 2;
const int x = 5;
}
~~~~~~~~~~^~~~~
1 error generated.

• Στο παραπάνω παράδειγμα, το x είναι ένα αναγνωριστικό που αναφέρεται σε ακέραια


δεδομένα στη μνήμη και ο μεταγλωττιστής επιβάλλει ότι το x δεν θα πρέπει να
χρησιμοποιείται με οποιοδήποτε τρόπο που θα οδηγούσε σε αλλαγή των δεδομένων.

https://github.com/chgogos/oop/blob/master/various/COP3330/lect5/t1.cpp 2

const
• Σε ένα άλλο παράδειγμα (t2.cpp):
int main()
g++ t2.cpp
{
t2.cpp:6:11: error: assigning to 'int *' from incompatible type 'const int *'
const int x = 5;
x_ptr = & x;
int *x_ptr;
^~~
x_ptr = & x;
1 error generated.
}

• Στο παραπάνω παράδειγμα, ο μεταγλωττιστής δεν επιτρέπει στο


δείκτη x_ptr να δείξει προς τη διεύθυνση του x (που είναι τύπου
const int). Αυτό συμβαίνει διότι ο x_ptr θα μπορούσε να
χρησιμοποιηθεί για να αλλάξει τα δεδομένα που είναι
αποθηκευμένα στο x.

https://github.com/chgogos/oop/blob/master/various/COP3330/lect5/t2.cpp 3
Επισκόπηση: const παράμετροι
• Στη C++ υπάρχουν δύο τρόποι με τους οποίους μπορούν να περάσουν
ορίσματα στις συναρτήσεις:
• Πέρασμα με τιμή (call by value): Ένα αντίγραφο του ορίσματος δημιουργείται και η
συνάρτηση επενεργεί σε αυτό το αντίγραφο.
• Πέρασμα με αναφορά (call by reference): Δεν δημιουργείται αντίγραφο, η
παράμετρος της συνάρτησης είναι ένα αναγνωριστικό που αναφέρεται στα ίδια
δεδομένα με το όρισμα (η παράμετρος λειτουργεί ως ένα ψευδώνυμο για το
όρισμα).
• Η μέθοδος που χρησιμοποιείται για το πέρασμα των ορισμάτων
υποδηλώνεται από τις παραμέτρους της συνάρτησης:
void foo(int x); // το x περνά με τιμή
void foo(int &x); // το x περνά με αναφορά
• Ποια είναι η κύρια διαφορά ανάμεσα στους δύο αυτούς μηχανισμούς
περάσματος παραμέτρων (δείτε το t3.cpp);

https://github.com/chgogos/oop/blob/master/various/COP3330/lect5/t3.cpp 4

Επισκόπηση: const παράμετροι


• Ο προγραμματιστής μπορεί να προσθέτει τη δεσμευμένη λέξη const και στους
δύο μηχανισμούς περάσματος παραμέτρων (t4.cpp):
void foo(const int x); /* το x είναι μια παράμετρος εισόδου
(μόνο για ανάγνωση), δεν μπορεί να τροποποιηθεί εντός της foo */
void foo(const int &x); /* το x είναι μια παράμετρος εισόδου
(μόνο για ανάγνωση), δεν μπορεί να τροποποιηθεί εντός της foo */
• Ποια είναι η διαφορά ανάμεσα στους δύο αυτούς μηχανισμούς περάσματος
παραμέτρων;
• Το πέρασμα με τιμή πρέπει να κατασκευάσει ένα αντίγραφο.
• Το πέρασμα με αναφορά δεν κατασκευάζει αντίγραφο (απλά περνά την αναφορά της
πραγματικής παραμέτρου στο υποπρόγραμμα)
• Η αναφορά μιας μεταβλητής είναι απλά ένας δείκτης προς τη μεταβλητή (8 bytes)
• Το πέρασμα με const αναφορά είναι πολύ χρήσιμο όταν η παράμετρος είναι μια μεγάλη δομή
δεδομένων έτσι ώστε να μειώσει την επιβάρυνση που θα δημιουργούταν από την κατασκευή ενός
αντιγράφου.
• Δείτε το t4.cpp.

https://github.com/chgogos/oop/blob/master/various/COP3330/lect5/t4.cpp 5
Πέρασμα αντικειμένων με const αναφορά
• Τα αντικείμενα μπορούν επίσης να περάσουν με const αναφορά
έτσι ώστε να αποφευχθεί η επιβάρυνση αντιγραφής:
friend Fraction Add(const Fraction& f1, const Fraction& f2);

• Όπως και με τους άλλους τύπους δεδομένων, ο μεταγλωττιστής


διασφαλίζει ότι ένα αντικείμενο που περνά με const αναφορά δεν
θα χρησιμοποιηθεί με τέτοιο τρόπο που θα μπορούσε να αλλάξει τα
μέλη δεδομένα του.

const συνάρτηση μέλος


• Οποιαδήποτε κλήση σε μια συνάρτηση μέλος διαθέτει το «καλών
αντικείμενο»
Fraction f1; // ένα αντικείμενο Fraction
f1.Evaluate(); // το f1 είναι το καλών αντικείμενο
• Καθώς η συνάρτηση μέλος έχει πρόσβαση στα δεδομένα του καλούντος
αντικειμένου, μπορεί να θέλουμε να διασφαλίσουμε ότι το καλών
αντικείμενο δεν πρόκειται ποτέ να αλλάξει από τη συνάρτηση μέλος.
• Τέτοιες συναρτήσεις ονομάζονται const συναρτήσεις μέλη και
υποδηλώνονται από τη χρήση της δεσμευμένης λέξης const μετά τη
δήλωση της συνάρτησης καθώς και στον ορισμό της υλοποίησης της
συνάρτησης.
• Δείτε το t5.cpp.
https://github.com/chgogos/oop/blob/master/various/COP3330/lect5/t5.cpp 7
const αντικείμενα
• const μεταβλητές είναι εκείνες οι μεταβλητές που έχουν μόνο μια τιμή
(αυτή με την οποία αρχικοποιήθηκαν).
const int SIZE = 10;
const double PI = 3.1415;
• Τα αντικείμενα μπορούν να δηλωθούν με παρόμοιο τρόπο ως const. Ο
κατασκευαστής αρχικοποιεί τα const αντικείμενα, αλλά μετά από αυτό, τα
μέλη δεδομένα του αντικειμένου δεν θα μπορούν να αλλάξουν.
const Fraction ZERO; // το κλάσμα ορίζεται σε 0/1 και δεν αλλάζει
const Fraction FIXED(3,4);// το κλάσμα ορίζεται σε 3/4 και δεν αλλάζει
• Για να διασφαλιστεί ότι ένα αντικείμενο δεν μπορεί να αλλάξει, ο
μεταγλωττιστής επιβάλλει ότι ένα const αντικείμενο μπορεί να καλεί μόνο
const συναρτήσεις μέλη.
• Δείτε το παράδειγμα const_fraction.

https://github.com/chgogos/oop/tree/master/various/COP3330/lect5/const_fraction 8

const μέλη δεδομένων


• Τα μέλη δεδομένων μιας κλάσης μπορούν επίσης να δηλωθούν ως const,
αλλά υπάρχουν ορισμένοι συντακτικοί κανόνες που περιπλέκουν την
κατάσταση.
• Γνωρίζουμε ότι όταν μια μεταβλητή δηλώνεται με το const σε ένα μπλοκ
κώδικα, θα πρέπει να αρχικοποιείται στην ίδια γραμμή:
const int SIZE = 10;
• Ωστόσο, δεν είναι συντακτικά σωστό να αρχικοποιούμε μεταβλητές μέλη
δεδομένων στις γραμμές κώδικα στις οποίες δηλώνονται στην κλάση
(παρατήρηση: από την έκδοση C++11 και μετά αυτό επιτρέπεται).
• Αλλά μια const δήλωση μεταβλητής δεν μπορεί «να σπάσει» σε ένα
κανονικό τμήμα κώδικα σε 2 γραμμές (δήλωση και ανάθεση).
• Επίσης, η απόπειρα να αναθέσουμε τιμή στην const μεταβλητή μέλος
μέσα σε έναν κατασκευαστή δεν θα λειτουργήσει.
9
Λίστα αρχικοποίησης
• Μπορούμε να χρησιμοποιήσουμε μια ειδική περιοχή του κατασκευαστή
που ονομάζεται λίστα αρχικοποίησης (initialization list) έτσι ώστε να
υπερκεράσουμε το πρόβλημα της αρχικοποίησης const μελών
αντικειμένων.
• Οι λίστες αρχικοποίησης έχουν την ακόλουθη μορφή:
classname::classname(int p1, int p2): const_member_var1(p1), member_var2(p2)

{
// κώδικας
}
• Η παραπάνω λίστα αρχικοποίησης θα θέσει το const_member_var1 και
το member_var2 στις τιμές που περνάνε στον κατασκευαστή ως p1 και p2
αντίστοιχα.

10

Ερωτήσεις σύνοψης
• Περιγράψτε το πέρασμα με τιμή και το πέρασμα με αναφορά.
• Γιατί χρησιμοποιούμε το πέρασμα με αναφορά;
• Σε τι αναφέρεται ο όρος «καλών αντικείμενο»;
• Τι είναι μια const συνάρτηση μέλος;
• Τι είναι ένα const αντικείμενο;
• Πώς το αρχικοποιούμε;
• Τι είναι const μέλος δεδομένων;
• Πώς το αρχικοπoιούμε;
• Μέσω ποιου μηχανισμού το const επιβάλλεται;
11
Υπερφόρτωση τελεστών
#6
Τμήμα Πληροφορικής και Τηλεπικοινωνιών
Πανεπιστήμιο Ιωαννίνων (Άρτα)
Γκόγκος Χρήστος

Τελεστές
• Υπάρχουν πολλοί διαθέσιμοι τελεστές (+, -, *, /, ==, !=, <<, >>) που
λειτουργούν με τους ενσωματωμένους τύπους (π.χ. int, double).
• Μπορούμε να φανταστούμε ότι οι τελεστές αυτοί υλοποιούνται με
κλήσεις συναρτήσεων που γίνονται αυτόματα από τη C++:
• Για παράδειγμα, η εντολή:
int x = 1 + 2;
• Μπορεί να αντικατασταθεί αυτόματα από μια κλήση συνάρτησης όπως η
ακόλουθη:
int x = operator+(1,2);
• Με το ακόλουθο πρωτότυπο:
int operator+(int p1, int p2); /* επιστρέφει το άθροισμα των
p1 και p2 */

2
Τελεστές
• Ένα ακόμα παράδειγμα:
double var = 1.1;
double x = 4.3 * 2.1 - var;
• Θα μπορούσε να αντικατασταθεί αυτόματα από τις εξής κλήσεις
συναρτήσεων:
double x = operator-(operator*(4.3, 2.1), var);
• Με τα ακόλουθα πρωτότυπα:
double operator*(double p1, double p2);
double operator-(double p1, double p2);
• H C++ υποστηρίζει την υπερφόρτωση τελεστών που επιτρέπει σε
γνώριμους τελεστές, όπως το +, να χρησιμοποιούνται με τύπους
ορισμένους από τον χρήστη (όπως είναι τα αντικείμενα).

Παράδειγμα κίνητρο
• Παράδειγμα 1
• Παρατηρήστε τη χρήση των αριθμητικών τελεστών στο ακόλουθο παράδειγμα:
int x = 3, y = 6, z;
float a=3.4, b = 2.1, c;
z = x + y;
c = a / b;
• Έστω ότι επιθυμούμε να πραγματοποιούμε πράξεις με αντικείμενα Fraction με τον ίδιο
γνώριμο τρόπο:
1/2 + 3/4;
2/3 - 1/3;
• Θα μπορούσαμε να το πετύχουμε αυτό χρησιμοποιώντας την κλάση Fraction ως εξής;
Fraction f1(1,2), f2(3,4), f3;
f3 = f1 + f2; /* θα το δεχθεί ο μεταγλωττιστής; */
• Θα πρέπει να είναι σαφές ότι το παραπάνω δεν έχει νόημα για τον μεταγλωττιστή απευθείας. Το
Fraction είναι ένας τύπος που έχει οριστεί από τον χρήστη. Πώς θα μπορούσε να γνωρίζει ο
υπολογιστής περί κοινών παρονομαστών κ.λπ.;
• Ωστόσο, θα ήταν καλό να μπορούσαμε να χρησιμοποιούμε τα αντικείμενα Fraction με τον ίδιο τρόπο με
τον οποίο χρησιμοποιούμε και τους άλλους αριθμητικούς τύπους (όπως το int και το double).

4
Παράδειγμα κίνητρο
• Παράδειγμα 2:
• Θεωρείστε την έξοδο του ακόλουθου κώδικα που γνωρίζουμε ότι είναι
έγκυρη:
int x = 5;
cout << x;
• Τι θα συμβεί όμως στην ακόλουθη περίπτωση;
Fraction f(3,4);
cout << “The fraction f = ” << f << endl;
• Πάλι, θα πρέπει να είναι σαφές ότι ο μεταγλωττιστής δεν θα μπορούσε να γνωρίζει πως
το αντικείμενο Fraction θα έπρεπε να εμφανίζεται στην οθόνη.

• Μπορούμε να χρησιμοποιούμε υπερφόρτωση τελεστών για να παρέχουμε


μια συνάρτηση που υλοποιεί την επιθυμητή συμπεριφορά για μια
λειτουργία όπως η παραπάνω.
5

Υπερφόρτωση τελεστών (σύνταξη)


• Η υπερφόρτωση ενός τελεστή είναι απλά μια συνάρτηση. Αυτό σημαίνει ότι θα πρέπει
να έχει τύπο επιστροφής, όνομα και λίστα παραμέτρων.
• Το όνομα της συνάρτησης υπερφόρτωσης ενός τελεστή είναι η σύνθεση της λέξης
operator και του συμβόλου του τελεστή, για παράδειγμα:
operator+, operator++, operator<<, operator==, κ.λπ.
• Άρα, η δήλωση της υπερφόρτωσης ενός τελεστή είναι παρόμοια με μια συνάρτηση με
τη λέξη operator να αποτελεί τμήμα του ονόματος της συνάρτησης:
τύποςΕπιστροφής operatorΣύμβολοΤελεστή(λίστα παραμέτρων);
• Παράδειγμα:
• Η κλάση string έχει υπερφορτωμένο τον τελεστή << έτσι ώστε να μπορεί να παράγει έξοδο
λεκτικών χρησιμοποιώντας το cout. O μεταγλωττιστής θα καλεί αυτόματα τη συνάρτηση
operator<<() όταν χρησιμοποιούμε τον τελεστή << ή θα μπορούμε να καλούμε τη συνάρτηση
ρητά.
• Δείτε το example1.cpp

https://github.com/chgogos/oop/blob/master/various/COP3330/lect6/example1.cpp 6
Λεπτομέρειες σχετικά με την υπερφόρτωση
τελεστών (1/2)
• Η υπερφόρτωση τελεστών έχει τους ακόλουθους περιορισμούς:
• Η υπερφόρτωση ενός τελεστή δεν αλλάζει την προτεραιότητά του.
• a + b * c θα είναι πάντα ισοδύναμο με a + (b * c)
• Η υπερφόρτωση ενός τελεστή δεν αλλάζει την προσεταιριστικότητά του.
• a + b + c θα είναι πάντα ισοδύναμο με (a + b) + c
• Η υπερφόρτωση ενός τελεστή δεν μπορεί να αλλάξει την πληθικότητα του
(δηλαδή, τον αριθμό των τελεστέων του)
• Η συνάρτηση που πραγματοποιεί το a+b θα έχει πάντα 2 παραμέτρους, δεν γίνεται να
γίνει η πράξη a+b+c στη συνάρτηση.
• Δεν είναι δυνατόν να κατασκευαστούν νέοι τελεστές, μόνο νέες εκδόσεις των
ήδη υπαρχόντων.
• Η λειτουργία των τελεστών για τους ενσωματωμένους τύπους δεν αλλάζει.

Λεπτομέρειες σχετικά με την υπερφόρτωση


τελεστών (2/2)
• Ορισμένοι τελεστές μπορούν να γραφούν ως συναρτήσεις μέλη μιας κλάσης.
• Ορισμένοι τελεστές μπορούν να γραφούν ως απλές συναρτήσεις, εκτός κλάσης.
• Συχνά χρησιμοποιείται η δεσμευμένη λέξη friend σε αυτές τις περιπτώσεις.
• Ορισμένοι τελεστές μπορούν να γραφούν και με τους δύο τρόπους και είναι επιλογή του
προγραμματιστή.
• Ένας δυαδικός τελεστής έχει δύο ορίσματα (π.χ. f1+f2).
• Αν γραφεί ως απλή συνάρτηση (που δεν είναι συνάρτηση μέλος της κλάσης), και οι δύο τελεστέοι θα πρέπει
να στέλνονται ως παράμετροι, άρα θα πρόκειται για μια συνάρτηση με δύο παραμέτρους.
• Αν γραφεί ως συνάρτηση μέλος της κλάσης, ο πρώτος τελεστέος θα είναι το καλών αντικείμενο, και ο άλλος
τελεστέος θα στέλνεται ως παράμετρος, άρα θα πρόκειται για συνάρτηση με μια παράμετρο.
• Ένας μοναδιαίος τελεστής έχει ένα τελεστέο (π.χ. f++).
• Αν γραφεί ως απλή συνάρτηση (που δεν είναι συνάρτηση μέλος της κλάσης), ο τελεστέος θα στέλνεται ως
παράμετρος.
• Αν γραφεί ως συνάρτηση μέλος της κλάσης, η συνάρτηση δεν θα δέχεται παράμετρο καθώς θα επενεργεί
στο καλών αντικείμενο.

8
Υπερφόρτωση αριθμητικών τελεστών (1/3)
• Επίδειξη της πρόσθεσης κλασμάτων υπεφορτώνοντας τον τελεστή +.
• Επιθυμούμε να επιτύχουμε το:
Fraction f1, f2(1,2), f3(3,4);
f1 = f2 + f3;
• H δεύτερη σειρά του παραπάνω κώδικα μεταφράζεται σε:
f1 = f2.operator+(f3); // υπερφόρτωση συνάρτησης μέλους

f1 = operator+(f2,f3); // υπερφόρτωση απλής συνάρτησης

Υπερφόρτωση αριθμητικών τελεστών (2/3)


• Υπερφόρτωση του + με συνάρτηση μέλος. Θα πρέπει να
προσθέσουμε την ακόλουθη συνάρτηση στη δήλωση της κλάσης
Fraction:
Fraction operator+(const Fraction& f) const;

• H δήλωση (υλοποίηση) της συνάρτησης μέλους θα είναι:


Fraction Fraction::operator+(const Fraction& f) const {
Fraction r;
r.numerator = (numerator * f.denominator)
+ (f.numerator * denominator);
r.denominator = (denominator * f.denominator);
return r;
}
https://github.com/chgogos/oop/tree/master/various/COP3330/lect6/fraction_member_overload 10
Υπερφόρτωση αριθμητικών τελεστών (3/3)
• Υπερφόρτωση του + με απλή συνάρτηση. Θα πρέπει να ορίσουμε
της συνάρτηση ως φίλη της κλάσης Fraction έτσι ώστε να επιτραπεί η
πρόσβαση στα ιδιωτικά μέλη της κλάσης:
friend Fraction operator+(const Fraction &f1, const Fraction &f2);

• Η δήλωση (υλοποίηση) της συνάρτησης:


Fraction operator+(const Fraction &f1, const Fraction &f2) {
Fraction r;
r.numerator = (f1.numerator * f2.denominator) + (f2.numerator * f1.denominator);
r.denominator = f1.denominator * f2.denominator;
return r;
}

https://github.com/chgogos/oop/tree/master/various/COP3330/lect6/fraction_alone_overload
11

Υπερφόρτωση συγκριτικών τελεστών


• Οι συγκριτικοί τελεστές, επίσης, μπορούν να υπερφορτωθούν είτε μέσω απλών
συναρτήσεων είτε μέσω συναρτήσεων μελών.
• Η συνάρτηση Equals είχε γραφεί ως εξής:
friend bool Equals(const Fraction &x, const Fraction &y);

• Η συνάρτηση αυτή μπορεί να γραφεί υπερφορτώνοντας τον τελεστή ==


friend bool operator==(const Fraction &f1, const Fraction &f2);

• Στη συνέχεια παρουσιάζονται οι κλήσεις των συναρτήσεων για κάθε μια από τις παραπάνω
περιπτώσεις:
Fraction f1, f2;
if (Equals(f1,f2)) cout << “Equals”;

vs.

Fraction f1, f2;


if (f1==f2) cout << “Equals”;

12
Υπερφόρτωση συγκριτικών τελεστών
• Για να μπορούν να χρησιμοποιηθούν όλοι οι συγκριτικοί τελεστές, θα
χρειαστεί να υπερφορτώσουμε και τους 6:
• operator==
• operator!=
• operator<
• operator>
• operator<=
• operator>=
• Εναλλακτικά μπορούν να υπερφορτωθούν μόνο οι operator== και
operator< και να συμπληρωθεί:
#include <utility>
using namespace std::rel_ops;

13

Υπερφόρτωση τελεστών stream (1/4)


• Ο τελεστής << (stream insertion=εισαγωγή ροής) και ο τελεστής >> (stream
extraction=εξαγωγή ροής) δεν λειτουργούν αυτόματα για αντικείμενα νέων
κλάσεων.
• Θα πρέπει να προστεθούν οι συναρτήσεις που πραγματοποιούν υπερφόρτωση
των συγκεκριμένων τελεστών εφόσον επιθυμούμε αυτή τη λειτουργικότητα για
τις κλάσεις που αναπτύσσουμε.
• Για παράδειγμα:
• Στην κλάση string o τελεστής << είναι υπερφορτωμένος ως εξής:
ostream& operator<<(ostream &os, const string& s);

• Ωστόσο, τίθενται ορισμένα ερωτήματα:


• Γιατί η πρώτη παράμετρος είναι ostream&;
• Γιατί η πρώτη παράμετρος δεν είναι const;
• Γιατί η συνάρτηση επιστρέφει ένα ostream&, δεν θα έπρεπε να είναι void;
• Η υπερφορτωμένη συνάρτηση είναι απλή συνάρτηση, θα μπορούσε να είναι συνάρτηση μέλος;

14
Υπερφόρτωση τελεστών stream (2/4)
• Γιατί η πρώτη παράμετρος είναι ostream&;
• Το cout είναι ένα αντικείμενο τύπου ostream. Καθώς η κλήση έχει τη μορφή cout <<
str, η πρώτη παράμετρος στην υπερφόρτωση είναι η cout και η δεύτερη η str.
• Γιατί η πρώτη παράμετρος δεν είναι const;
• To cout αναπαριστά την έξοδο στην οθόνη του Η/Υ. Αλλάζοντας την έξοδο στην
οθόνη σημαίνει ότι η cout θα πρέπει να μπορεί να αλλάξει, άρα δεν μπορεί να είναι
const.
• Γιατί η συνάρτηση επιστρέφει ένα ostream&, δεν θα έπρεπε να είναι void;
• Αυτό συμβαίνει έτσι ώστε να μπορούν να υποστηριχθούν διαδοχικές κλήσεις
(cascading calls). Για παράδειγμα:
• cout << s1 << s2 << s3; ➔ operator<<(cout, s1) << s2 << s3; ➔
cout << s2 << s3; ➔ operator<<(cout, c2) << s3; ➔ cout << s3; ➔
operator<<(cout, s3) ➔ cout; ➔ ;

15

Υπερφόρτωση τελεστών stream (3/4)


• Η υπερφορτωμένη συνάρτηση είναι απλή συνάρτηση, θα μπορούσε
να είναι συνάρτηση μέλος;
• Όχι, διότι η πρώτη παράμετρος είναι τύπου ostream, το καλών αντικείμενο
θα έπρεπε να είναι ostream (συνεπώς η υπερφορτωμένη συνάρτηση θα
έπρεπε να ήταν συνάρτηση μέλος της κλάσης ostream). Καθώς η κλάση
ostream δεν έχει αναπτυχθεί από εμάς αλλά αποτελεί τμήμα της τυπικής
βιβλιοθήκης της γλώσσας, αυτό δεν μπορεί να γίνει.

16
Υπερφόρτωση τελεστών stream (4/4)
• Στη συνέχεια παρουσιάζονται τα prototypes για την υπερφόρτωση
των τελεστών εισαγωγής και εξαγωγής στην κλάση Fraction.
friend ostream& operator<<(ostream& os, const Fraction& f);
friend istream& operator>>(istream& is, Fraction& f);

• Και μια πιθανή υλοποίησή τους:


ostream &operator<<(ostream &os, const Fraction &f) {
os << f.numerator << '/' << f.denominator;
return os;
}
istream &operator>>(istream &is, Fraction &f) {
char slash;
is >> f.numerator >> slash >> f.denominator;
return is;
}
• Εκτελέστε το main2.cpp.

https://github.com/chgogos/oop/tree/master/various/COP3330/lect6/fraction_alone_overload 17

Ερωτήσεις σύνοψης
• Τι είναι η υπερφόρτωση τελεστών;
• Πως μπορεί να γίνει η υπερφόρτωση του τελεστή << σε μια κλάση
που αναπτύσσουμε;
• Αναφέρατε τρεις περιορισμούς της υπερφόρτωσης τελεστών.
• Αναφέρατε δύο πιθανούς τρόπους με τους οποίους μπορεί να γίνει η
υπερφόρτωση του τελεστή +.

18
Σύνθεση (composition)
#7
Τμήμα Πληροφορικής και Τηλεπικοινωνιών (Άρτα)
Πανεπιστήμιο Ιωαννίνων
Γκόγκος Χρήστος

V 1.0

Αντικείμενα ως μέλη δεδομένων


• Τα αντικείμενα είναι ένας συνδυασμός από μέλη δεδομένων, μέλη
συναρτήσεων και μιας διεπαφής (interface).
• Μέλη δεδομένων μιας κλάσης μπορούν να είναι και αντικείμενα
(αντικείμενα μέσα σε αντικείμενα). Για παράδειγμα:
αντικείμενο Glass
class Glass {
int size; int size; 12
Fraction empty; Fraction empty; Fraction object
Fraction full; num: 5
}; den: 11
Fraction full; Fraction object
num: 6
den: 11
2
Σύνθεση
• Η σχέση «αντικείμενο μέσα σε αντικείμενο» ονομάζεται σύνθεση.
• Μπορεί να υλοποιηθεί δηλώνοντας ένα αντικείμενο ή ένα δείκτη ή μια αναφορά ως
μέλος δεδομένων της κλάσης.
• Συχνά αναφέρεται ως σχέση ‘has-a’.
• Για παράδειγμα:
• Glass ‘has-a’ Fraction. (το αντικείμενο Fraction είναι μέλος δεδομένων της κλάσης Glass)
• Car ‘has-a(n)’ Engine. (το αντικείμενο Engine είναι μέλος δεδομένων της κλάσης Car )
• Deck ‘has-a’ collection of Cards. (το αντικείμενο «συλλογή 52 αντικειμένων Card
(τραπουλόχαρτο)» είναι μέλος δεδομένων της κλάσης Deck (τράπουλα).
• Η σύνθεση επιτρέπει στον κώδικα να είναι περισσότερο τμηματικός.
• Μπορούμε να δημιουργούμε μικρότερες κλάσεις και να τις συνδυάζουμε έτσι ώστε να
πετύχουμε συνθετότερη λειτουργικότητα.
• Δείτε το παράδειγμα PokerHand

https://github.com/chgogos/oop/blob/master/various/COP3330/lect7/poker_hand.cpp
3

#include <iostream>
using namespace std;

Κατασκευαστές class small_class {


public:
small_class();
private:

• Όταν δημιουργείται ένα αντικείμενο,


int data;
};

εκτελείται ο κατασκευαστής του, και small_class::small_class() {


cout << "small_class default constructor called " << endl;
επιπλέον καλείται ο κατασκευαστής }

κάθε ενός αντικειμένου που υπάρχει class large_class {


ως μέλος δεδομένων σε αυτό. public:
large_class();
• Η τυπική συμπεριφορά είναι ότι private:

καλείται ο προκαθορισμένος
small_class sc;
};
κατασκευαστής για το μέλος large_class::large_class() {

δεδομένων.
cout << "large_class constructor called " << endl;
}

• Ποιος κατασκευαστής θα κληθεί int main(){


νωρίτερα; large_class obj;
• Πρώτα καλείται ο κατασκευαστής του }

αντικειμένου που περιέχεται ως μέλος


δεδομένων στην κλάση και στη συνέχεια ο
κατασκευαστής της κλάσης. small_class default constructor called
large_class constructor called

https://github.com/chgogos/oop/blob/master/various/COP3330/lect7/sample1.cpp
4
#include <iostream>
using namespace std;
class small_class {

Κατασκευαστές
public:
small_class();
small_class(int);
small_class parameter constructor called
private: large_class parameter constructor called
int data;
};

• Πως αρχικοποιείται ένα αντικείμενο


small_class::small_class() {
cout << "small_class default constructor called " << endl;

μέσα σε ένα αντικείμενο;


}
small_class::small_class(int d) {
data = d;

• Τι θα πρέπει να γίνει έτσι ώστε να cout << "small_class parameter constructor called " << endl;
}

χρησιμοποιηθεί ένας κατασκευαστής class large_class {


public:
(εκτός από τον προκαθορισμένο) για large_class();
large_class(int);
ένα μέλος δεδομένων; private:
small_class sc;
• Μπορεί να χρησιμοποιηθεί λίστα };
large_class::large_class() {
αρχικοποίησης: cout << "large_class default constructor called " << endl;
}
• Η λύση αυτή έχει περιορισμούς. Μπορεί large_class::large_class(int d) : sc(d) {
να χρειαστεί να καλέσουμε τον cout << "large_class parameter constructor called " << endl;
}
κατασκευαστή μέσα στον κατασκευαστή int main() {
της large κλάσης. large_class obj(1);
}
https://github.com/chgogos/oop/blob/master/various/COP3330/lect7/sample2.cpp
https://github.com/chgogos/oop/blob/master/various/COP3330/lect7/sample3.cpp
sample2.cpp 5

#include <iostream>
using namespace std;

Τελεστής τελεία class Circle {


public:
Circle(int);

(dot operator) float radius;


};
Circle::Circle(int r) { radius = r; }

• Αν ένα αντικείμενο που


class Square {
public:

είναι μέλος δεδομένων Square(int);


float getlength();
SH Circle Radius: 1
SH Square Length: 2
ενός άλλου αντικειμένου private:
float length;
έχει δημόσια μέλη };
Square::Square(int l) {length = l; }
(δεδομένα ή συναρτήσεις), float Square::getlength() {

τότε μπορούμε να τα
return (length);
}

προσπελάσουμε μέσω του class ShapeHolder {


public:
τελεστή τελείας. ShapeHolder();
Circle c1; Square s1;
};
ShapeHolder::ShapeHolder() : c1(1), s1(2) {}
https://github.com/chgogos/oop/blob/master/ int main() {
various/COP3330/lect7/sample4.cpp ShapeHolder s1;
cout << "SH Circle Radius: " << s1.c1.radius << endl;
cout << "SH Square Length: " << s1.s1.getlength() << endl;
return 0;
} 6
Ερωτήσεις σύνοψης
• Τι είναι η σύνθεση (composition);
• Πότε αρχικοποιείται ένα αντικείμενο μέλος δεδομένων;
• Ποιος κατασκευαστής χρησιμοποιείται για να αρχικοποιήσει ένα
αντικείμενο μέλος δεδομένων αν δεν επέμβουμε;
• Πως μπορώ να αρχικοποιήσω ένα αντικείμενο μέλος δεδομένων με
ένα συγκεκριμένο κατασκευαστή;
• Τι μπορεί να σημαίνει η ακόλουθη κλήση;
obj1.dm.fun();

Πίνακες αντικειμένων
#8
Τμήμα Πληροφορικής και Τηλεπικοινωνιών (Άρτα)
Πανεπιστήμιο Ιωαννίνων
Γκόγκος Χρήστος

V 1.0
Πίνακες: επισκόπηση
• Ένας πίνακας είναι μια δεικτοδοτημένη συλλογή από στοιχεία
δεδομένων του ίδιου τύπου:
int aaa[10];

• 10 στοιχεία τύπου int. Κάθε θέση του πίνακα είναι ένα αντικείμενο.
• Ποιο είναι το εύρος αποδεκτών τιμών για το δείκτη;
• Τι αναπαριστούν τα 10 στοιχεία του πίνακα;

Δήλωση πινάκων με αντικείμενα


• Η δήλωση πινάκων με αντικείμενα είναι παρόμοια με τη δήλωση
πινάκων για ενσωματωμένους τύπους.
Fraction rationals[20];
Complex nums[50];
Hydrant fireplugs[10];

• Σε κάθε θέση του πίνακα υπάρχει ένα αντικείμενο.


Η δήλωση:
Fraction rationals[20];
δηλώνει 20 αντικείμενα Fraction: rationals[0], rationals[1], … ,
rationals[19]

3
Αρχικοποίηση πινάκων αντικειμένων
• Παρόμοια με τη δήλωση ενός πίνακα ακεραίων.
• Δεν χρειάζεται να κάνουμε τίποτα για να χρησιμοποιηθεί ο προκαθορισμένος
κατασκευαστής.
int x;
Fraction num;
Fraction nums[4];
• Για να αρχικοποιήσουμε με συγκεκριμένο τρόπο, καλούμε ρητά τον κατάλληλο
κατασκευαστή.
int x(10);
Fraction f(10,20);
• Πώς γίνεται να αρχικοποιήσουμε έναν πίνακα αντικειμένων; Χρειαζόμαστε έναν τρόπο έτσι
ώστε να καθοριστούν διαφορετικοί κατασκευαστές για διαφορετικά αντικείμενα.
Fraction numlist[3]={Fraction(2,4), Fraction(5), Fraction()};

• το numlist[0] αρχικοποιείται με τον κατασκευαστή Fraction(2,4)


• το numlist[1] αρχικοποιείται με τον κατασκευαστή Fraction(5)
• το numlist[2] αρχικοποιείται με τον κατασκευαστή Fraction()

Χρήση του πίνακα των αντικειμένων


• Η δεικτοδότηση λειτουργεί με τον ίδιο τρόπο όπως και στους πίνακες
πρωτογενών τύπων δεδομένων.
• Η αναφορά σε κάθε αντικείμενο του πίνακα γίνεται ως arrayName[index];
• O τελεστής τελεία λειτουργεί με τον ίδιο τρόπο όπως και με τα απλά ονόματα
αντικειμένων.
όνομαΑντικειμένου.όνομαΜέλους
• Το όνομαΑντικειμένου λαμβάνει τη μορφή ενός στοιχείου του πίνακα:
όνομαΠίνακα[δείκτης].όνομαΜέλους
• Παράδειγμα:
Fraction rationals[20];
...
rationals[2].Show();
rationals[6].Input();
for(int i=0;i<20;i++)
rationals[i].SetVal(1,2);

5
Δείκτες
#9
Τμήμα Πληροφορικής και Τηλεπικοινωνιών (Άρτα)
Πανεπιστήμιο Ιωαννίνων
Γκόγκος Χρήστος

V 1.0

Μνήμη
Memory
• Η κύρια μνήμη του υπολογιστή
χρησιμοποιείται για να 0x00000000
αποθηκεύει κώδικα (το ίδιο το 0x00000001
πρόγραμμα) και μεταβλητές του 0x00000002
προγράμματος. …

• Η μνήμη μπορεί να θεωρηθεί ως


ένας τεράστιος πίνακας.
• Η αναφορά σε κάθε byte της
μνήμης μπορεί να γίνει με τη
χρήση μιας «διεύθυνσης» (σε έναν
32-bit υπολογιστή η διεύθυνση
είναι 32 bits, σε έναν 64-bit
υπολογιστή η διεύθυνση είναι 64 0xffffffff
bits)

2
Μνήμη
• Κάθε μεταβλητή αποθηκεύεται
κάπου στη μνήμη, συσχετίζεται δε #include <iostream>
με μια διεύθυνση μνήμης που using namespace std;
επιτρέπει την πρόσβαση στη
μεταβλητή. int main() {
int i;
• Διάφοροι τύποι μεταβλητών char c;
καταλαμβάνουν διαφορετικό χώρο char *ptr = &c;
στη μνήμη: cout << "size of int variable = " << sizeof(i) << endl;
• char (1 byte) cout << "size of char variable = " << sizeof(c) << endl;
cout << "size of char* variable = " << sizeof(ptr) << endl;
• int (4 bytes) return 0;
• float (4 bytes) }
• double (8 bytes)
• Η συνάρτηση sizeof() size of int variable = 4

επιστρέφει το μέγεθος κάθε


size of char variable = 1
size of char* variable = 8
μεταβλητής.
• Δείτε το sizeof.cpp
https://github.com/chgogos/oop/blob/master/various/COP3330/lect9/sizeof.cpp
3

Μνήμη
Memory
• Τα μέλη δεδομένων ενός αντικειμένου
αποθηκεύονται και αυτά στη μνήμη. 0x00000000
int main() 0x00000001
{ y 0x00000002
int y; y

Fraction f; y
… y
} f.numer
f.numer
• Η μνήμη χωρίζεται σε διαφορετικά τμήματα: f.numer
• Η κάθε μεταβλητή αποθηκεύεται f.numer
καταλαμβάνοντας διαφορετική περιοχή. f.denom
• στατική μνήμη (static memory): καθολικές f.denom
μεταβλητές f.denom
• μνήμη στοίβας (stack memory): τοπικές
μεταβλητές f.denom
• μνήμη σωρού (heap memory): δυναμικά
δεσμευμένη μνήμη 0xffffffff

4
Δείκτες
• Κάθε μεταβλητή (αντικείμενο, μέλος δεδομένων, κ.λπ.)
αποθηκεύεται σε μια θέση στη μνήμη.
• Κάθε θέση μνήμης αντιστοιχείται σε έναν μοναδικό αριθμό που
ονομάζεται διεύθυνση της θέσης μνήμης.
• Ένας δείκτης είναι μια μεταβλητή που περιέχει μια θέση μνήμης.
• Ένας δείκτης μπορεί να χρησιμοποιηθεί για να αποθηκεύσει τη θέση
ενός αντικειμένου ή μιας μεταβλητής στη μνήμη.
• Στη συνέχεια μπορούμε να κάνουμε «αποαναφορά» (dereference)
ενός δείκτη έτσι ώστε να αποκτήσουμε απευθείας πρόσβαση στο
αντικείμενο ή στην μεταβλητή που δείχνει ο δείκτης.

Δείκτες Memory

100
• Κάθε διεύθυνση μνήμης
1 byte
101

αναφέρεται στη θέση ενός


102
103

απλού byte δεδομένων και οι 104


105

συνεχόμενες διευθύνσεις 106


107
μνήμης αναφέρονται σε 108
109
συνεχόμενα bytes. 110
111

• Η μνήμη μπορεί να θεωρηθεί ως


112
113

ένας τεράστιος πίνακας 114


115
χαρακτήρων. 116
117
118
119
120
121
6
Δείκτες Memory

• Ένας δείκτης καταλαμβάνει χώρο


100
4 101
στη μνήμη, όπως οποιαδήποτε val
bytes 102
άλλη μεταβλητή. 103
104
• Καθώς ένας δείκτης κρατά μια 105

διεύθυνση και τα 64bit συστήματα 8


106
107 void foo() {
χρησιμοποιούν διευθύνσεις 64bits, valptr
bytes 108 int val;
χρειάζονται 8bytes για να 109
int *valptr;
αναπαραστήσει κάθε διεύθυνση 110
111 …
(64bits * 1byte/8bits = 8 bytes). 112 }
113
• Όλοι οι δείκτες καταλαμβάνουν 114
τον ίδιο χώρο μνήμης ανεξάρτητα 115

από τον τύπο τους (char*, 116


117
int*, double*) 118
119
120
121
7

Δείκτες Memory

• Ένας δείκτης σε δείκτη κρατά τη


100
4 101
διεύθυνση ενός δείκτη του ίδιου val
bytes 102
τύπου και συνεπώς καταλαμβάνει 103
τον ίδιο χώρο μνήμης όπως ένας 104
105
δείκτης. 8
106
107 void foo() {
• Ένας δείκτης σε ακέραιο, int*, valptr
bytes 108 int val;
κρατά τη διεύθυνση στην οποία 109
int *valptr;
βρίσκεται ένας int. 110
111 int **valptrptr;
• Ένας δείκτης int**, κρατά τη 112 …
διεύθυνση στην οποία βρίσκεται 113
}
ένας δείκτης int*. 8
114
115
valptrptr
• Ένας δείκτης int***, κρατά τη bytes 116
διεύθυνση στην οποία βρίσκεται ένας 117
δείκτης int**. 118
• κ.ο.κ. 119
120
121
8
Δείκτες Memory

• Όταν ο μεταγλωττιστής
100
val 101
16
πραγματοποιεί μια ανάθεση 102
103
void foo() {
τιμής, μεταβαίνει στη 104
int val;
int *valptr;
διεύθυνση μιας μεταβλητής και
105
106 int **valptrptr;
ενημερώνει την τιμή που είναι valptr 107
108
αποθηκευμένη σε αυτή. 109 val = 16;
110 valptr = &val;
• Γενικότερα, όταν ο 111
112
*valptr = 5;
προγραμματιστής γράφει το 113

όνομα x μιας μεταβλητής, αυτό valptrptr


114
115
valptrptr = &valptr;
*valptrptr = NULL;
μπορεί να ερμηνευτεί ως «η 116

τιμή στη θέση μνήμης που έχει


117
118 }
δεσμευτεί για το x». 119
120
121
9

Δείκτες Memory

100
• Τοποθετώντας τον τελεστή & val 16 101
εμπρός από το όνομα μιας 102
103
void foo() {
int val;
μεταβλητής, μπορούμε να 104
105 int *valptr;
αναφερθούμε στη διεύθυνση μιας 106 int **valptrptr;
μεταβλητής αντί για την τιμή της. valptr 107
100 108
109 val = 16;
• Συνεπώς &x σημαίνει «η 110 valptr = &val;
διεύθυνση της θέσης μνήμης που
111 *valptr = 5;
112

έχει δεσμευθεί για το x». 113


114 valptrptr = &valptr;
• Η διεύθυνση της μεταβλητής είναι valptrptr 115 *valptrptr = NULL;
116

η διεύθυνση του πρώτου byte που 117


118 }
καταλαμβάνει στη μνήμη. 119
120
121
10
Δείκτες Memory

• Για μεταβλητές που είναι δείκτες, ο


100

τελεστής * επιτρέπει στον val 5 101


102 void foo() {
προγραμματιστή να προσπελάσει την 103
τιμή της διεύθυνσης μνήμης που περιέχει 104
int val;
ο δείκτης. 105 int *valptr;
106 int **valptrptr;
• Η διαδικασία αυτή καλείται αποαναφορά 107
(dereferencing) του δείκτη και είναι valptr 100 108
επίσης γνωστή ως έμμεση αναφορά 109 val = 16;
(indirection). 110 valptr = &val;
111
• H έμμεση αναφορά είναι ο λόγος για τον 112
*valptr = 5;
οποίο οι δείκτες έχουν τύπο, καθώς αν 113
και όλοι οι δείκτες περιέχουν 114 valptrptr = &valptr;
διευθύνσεις, ο μεταγλωττιστής θα πρέπει valptrptr 115 *valptrptr = NULL;
να γνωρίζει τον τύπο που είναι 116
αποθηκευμένος σε μια διεύθυνση έτσι
117
}
ώστε να μπορεί να προσπελάσει την τιμή
118
119
στην οποία δείχνει ο δείκτης. 120
121
11

Δείκτες Memory

100
• Καθώς και ένας δείκτη σε δείκτη val 5 101

αποθηκεύει μια διεύθυνση, void foo() {


102
103 int val;
μπορεί να χρησιμοποιηθεί 104
105 int *valptr;
σχεδόν όπως ένας δείκτης. valptr 100
106
107
int **valptrptr;

• Ο επιπλέον δείκτης απλά


108
109 val = 16;

σημαίνει ότι η τιμή της


110 valptr = &val;
111 *valptr = 5;
διεύθυνσης είναι η διεύθυση
112
113

ενός int* και όχι ενός int. valptrptr 104


114
115
valptrptr = &valptr;
*valptrptr = NULL;
116
117
118 }
119
120
121
12
Δείκτες Memory

100
• Η ειδική θέση μνήμης NULL val 5 101

(0x0) είναι δεσμευμένη για να void foo() {


102
103 int val;
υποδηλώσει μνήμη που δεν 104
105 int *valptr;
μπορεί να χρησιμοποιηθεί. valptr 0
106
107
int **valptrptr;

• Προτείνεται οι δείκτες να
108
109 val = 16;

αρχικοποιούνται στην τιμή


110 valptr = &val;
111 *valptr = 5;
112
NULL. 113
114 valptrptr = &valptr;
valptrptr 115 *valptrptr = NULL;
104 116
117
118 }
https://github.com/chgogos/oop/blob/master 119
/various/COP3330/lect9/sample1.cpp 120
121
13

Τοπικές μεταβλητές
• Οι τοπικές μεταβλητές καταλαμβάνουν χώρο στη στοίβα.
• Η διάρκεια ζωής μιας τοπικής μεταβλητής καθορίζεται από την
συνάρτηση στην οποία δηλώνεται.
• Η μεταβλητή δημιουργείται όταν καλείται η συνάρτηση
• Όταν η συνάρτηση επιστρέφει, όλες οι τοπικές μεταβλητές παύουν να
υπάρχουν (η δε μνήμη που καταλάμβαναν επιστρέφει στο σύστημα).
• Προσοχή στη χρήση δεικτών που δείχνουν σε τοπικές μεταβλητές
συναρτήσεων
• Δεν θα πρέπει ποτέ μια συνάρτηση να επιστρέφει έναν δείκτη σε τοπική μεταβλητή.

https://github.com/chgogos/oop/blob/master/various/COP3330/lect9/sample2.cpp

14
Πίνακες και αριθμητική δεικτών
Memory
‘a’
• Όταν ένας πίνακας δηλώνεται, ο
100
my_array ‘b’ 101
μεταγλωττιστής δεσμεύει στη ‘c’ 102
103
μνήμη το χώρο μνήμης που 104

απαιτείται για όλα τα στοιχεία


105
106
του πίνακα. 107
108
void foo() {
char my_array[3] = {‘a’,
• Το όνομα του πίνακα λειτουργεί 109
110
‘b’, ‘c’};
char *ptr;
πλέον ως ένας δείκτης προς το 111
112 ptr = my_array;
πρώτο στοιχείο του πίνακα, αν 113 }

και αυτό είναι μια «βολική 114


115
θεώρηση» καθώς δεν υπάρχει 116

αποθηκευμένος πραγματικά
117
118
ένας δείκτης στη μνήμη. 119
120
121
15

Πίνακες και αριθμητική δεικτών


Memory
‘a’
• Ένας «πραγματικός» δείκτης
100
my_array ‘b’ 101
δεσμεύει μνήμη στην οποία ‘c’ 102

μπορεί να αποθηκευτεί μια


103
104
διεύθυνση μνήμης. 105
106
ptr 100
• Όταν ένας πίνακας αναφέρεται σε 107
108
void foo() {
char my_array[3] = {‘a’,
ένα πρόγραμμα χωρίς τον τελεστή 109 ‘b’, ‘c’};
[], ο μεταγλωττιστής «θεωρεί» ότι 110
111
char *ptr;
το όνομα του πίνακα αναφέρεται 112 ptr = my_array;
}
σε έναν δείκτη που περιέχει τη 113
114
διεύθυνση του πρώτου στοιχείου 115

του πίνακα. Έτσι μπορεί να


116
117
ανατεθεί σε έναν δείκτη ο πίνακας. 118
119
120
121
16
Πίνακες και αριθμητική δεικτών
Memory
‘b’
• Οι δείκτες μπορούν να τροποποιηθούν με τους ‘a’
100
τελεστές +, -, ++, --. Μια βασική διαφορά my_array 101
μεταξύ ενός δείκτη και ενός ακεραίου είναι ότι η ‘d’ 102
εντολή ptr = ptr + num, μεταφράζεται σε 103
ptr = ptr + num * (το μέγεθος του 104
τύπου στον οποίο δείχνει ο δείκτης). 105
106 void foo() {
• Η αλλαγή της τιμής ενός δείκτη με αριθμητικές ptr 101
107
char my_array[3] = {‘a’, ‘b’, ‘c’};
πράξεις είναι γνωστή ως αριθμητική δεικτών. char *ptr;
Μεταξύ άλλων, είναι χρήσιμη για να διασχίσουμε 108 ptr = my_array;
έναν πίνακα. 109
ptr = ptr + 1;
110
• Η αριθμητική δεικτών και η αποαναφορά μπορούν 111
*ptr = *my_array;
*(ptr+1) = ‘d’;
να συνδυαστούν σε μια εντολή για να 112 ptr[-1] = ‘b’;
προσπελαστούν θέσεις σε διάφορες αποστάσεις 113 }
από την αρχή του πίνακα. Όταν ο μεταγλωττιστής 114
συναντά έναν δεικτοδοτημένο πίνακα, η εντολή 115
μεταφράζεται από array[index] σε 116
*(array+index) 117
https://github.com/chgogos/oop/blob/master/various/ 118
119
COP3330/lect9/sample3.cpp 120
121
https://github.com/chgogos/oop/blob/master/various/
COP3330/lect9/sample4.cpp 17

Επιπλέον πληροφορίες για τους δείκτες


• Ένας δείκτης μπορεί να ανατεθεί σε έναν άλλο δείκτη αν είναι του
ίδιου τύπου ή αν γίνει μετατροπή τύπου (casting).
• Οποιοσδήποτε δείκτης μπορεί να ανατεθεί σε δείκτη void*.
• Η αποαναφορά ενός μη αρχικοποιημένου δείκτη ή ενός NULL δείκτη
θα προκαλέσει σφάλμα κατάτμησης (segmentation fault).
https://github.com/chgogos/oop/blob/master/various/COP3330/lect9/sample5.cpp

18
Αναφορές (references)
• Οι αναφορές είναι μια υποκατηγορία δεικτών που χρησιμοποιείται στη C++ και όχι στη C.
• Πρόκειται για ένα περιορισμένων δυνατοτήτων δείκτη που μπορεί να δείχνει προς ένα μόνο
αντικείμενο ή μια μόνο μεταβλητή.
• Δηλώνεται χρησιμοποιώντας τον τελεστή & και όχι το * (int &ref).
• Δεν μπορεί να υπάρχει αναφορά σε αναφορά.
• Μια αναφορά πρέπει να αρχικοποιείται κατά τη δήλωση της.
• Από τη στιγμή που αρχικοποιείται μια αναφορά δεν μπορεί να αλλάξει τιμή προς ένα άλλο
αντικείμενο ή μια άλλη μεταβλητή.
• Δεν μπορεί να εφαρμοστεί αριθμητική δεικτών σε αναφορές.
• Οι αναφορές πραγματοποιούν αυτόματη αποαναφορά και συνεπώς μόνο η τιμή της μεταβλητής
που αναφέρεται είναι προσβάσιμη.
• Μια αναφορά λειτουργεί ως δεύτερο όνομα ή ως ψευδώνυμο για μια μεταβλητή ή ένα
αντικείμενο.
https://github.com/chgogos/oop/blob/master/various/COP3330/lect9/sample6.cpp
https://github.com/chgogos/oop/blob/master/various/COP3330/lect9/sample7.cpp
https://github.com/chgogos/oop/blob/master/various/COP3330/lect9/sample8.cpp
19
https://github.com/chgogos/oop/blob/master/various/COP3330/lect9/sample9.cpp

Πειραματισμός με δείκτες: pythontutor

http://pythontutor.com/cpp.html
20
Πειραματισμός με δείκτες: skorbut

https://github.com/fredoverflow/skorbut-release 21

Ερωτήσεις σύνοψης
• Τι αποθηκεύεται στο a όταν υπάρχει η δήλωση int* a;
• Τι αποθηκεύεται στο a όταν υπάρχει η δήλωση int** a;
• Γιατί δεν θα πρέπει να επιστρέφουμε έναν δείκτη προς μια τοπική μεταβλητή;
• Ποια είναι η μορφή με την οποία με δείκτη μπορούμε να αναφερθούμε στο στοιχείο a[100];
• Τι σημαίνει ο δείκτης NULL;
• Ποια είναι η σχέση ανάμεσα σε δείκτες και αναφορές;
• Ποια είναι τα μεγέθη των p1, p2 και p3 στον ακόλουθο κώδικα;
int *p1;
char *p2;
Fraction *p3;
• Ποια είναι η έξοδος του ακόλουθου κώδικα;
int a = 5;
int *ptr = &a;
cout << ptr;
cout << *ptr;

22
Δυναμική δέσμευση μνήμης
#10
Τμήμα Πληροφορικής και Τηλεπικοινωνιών (Άρτα)
Πανεπιστήμιο Ιωαννίνων
Γκόγκος Χρήστος

V 1.0

Δέσμευση μνήμης
• Υπάρχουν δύο τύποι δέσμευσης μνήμης
• Στατική δέσμευση μνήμης - πραγματοποιείται αυτόματα από το
μεταγλωττιστή (υπονοούμενη=implicitly).
• καθολικές μεταβλητές: η μνήμη δεσμεύεται στην αρχή του προγράμματος και
απελευθερώνεται όταν το πρόγραμμα τερματίζει, συνεπώς οι καθολικές μεταβλητές
είναι διαθέσιμες σε όλη τη διάρκεια εκτέλεσης του προγράμματος.
• Μπορούν να προσπελαστούν από οποιοδήποτε σημείο του προγράμματος.
• τοπικές μεταβλητές (δηλώνονται σε μια συνάρτηση): η μνήμη δεσμεύεται όταν η
συνάρτηση ξεκινά και ελευθερώνεται όταν η συνάρτηση επιστρέφει.
• Μια τοπική μεταβλητή δεν μπορεί να προσπελαστεί από μια άλλη συνάρτηση.
• Η δέσμευση και η αποδέσμευση μνήμης γίνεται αυτόματα (υπονοούμενα).
• Η μη αναγκαιότητα διαχείρισης της μνήμη είναι βολική αλλά έχει περιορισμούς:
• Για παράδειγμα στη στατική δέσμευση μνήμης το μέγεθος των πινάκων πρέπει να είναι
καθορισμένο κατά τη δήλωση τους.

2
Δέσμευση μνήμης
• Ο δεύτερος τύπος δέσμευσης μνήμης είναι η δυναμική δέσμευση.
• Δυναμική δέσμευση μνήμης - πραγματοποιείται από τον προγραμματιστή με
σαφή (explicitly) τρόπο.
• Ο προγραμματιστής με σαφείς εντολές ζητά από το σύστημα να δεσμεύσει μνήμη και
να επιστρέψει την αρχική διεύθυνση από τη μνήμη που έχει δεσμευθεί. Η διεύθυνση
αυτή μπορεί να χρησιμοποιηθεί από τον προγραμματιστή για να προσπελάσει τη μνήμη
που έχει δεσμευθεί.
• Όταν πλέον δεν χρειαζόμαστε τη μνήμη που έχει δεσμευθεί, η μνήμη θα πρέπει να
ελευθερωθεί με σαφείς εντολές που θα δώσει ο προγραμματιστής.

Δέσμευση μνήμης: ο τελεστής new


• Ο τελεστής new χρησιμοποιείται για να δεσμεύσει δυναμικά μνήμη.
• Μπορεί να χρησιμοποιηθεί για να δεσμεύσει μια απλή μεταβλητή ή έναν πίνακα
μεταβλητών.
• Ο τελεστής new επιστρέφει έναν δείκτη προς τον τύπο δεδομένων που έχει
δεσμευθεί. Παραδείγματα:
char *my_char_ptr = new char;
int *my_int_array = new int[20];
Fraction *f = new Fraction(1,2);
• Πριν την ανάθεση τιμής ο δείκτης μπορεί να δείχνει ή να μην δείχνει σε έγκυρη
θέση μνήμης.
• Μετά την ανάθεση τιμής ο δείκτης δείχνει σε έγκυρη θέση μνήμης εφόσον η
δέσμευση μνήμης επιτύχει.
https://github.com/chgogos/oop/blob/master/various/COP3330/lect10/sample1.cpp
https://github.com/chgogos/oop/blob/master/various/COP3330/lect10/sample2.cpp

4
Απελευθέρωση μνήμης: ο τελεστής delete
• O τελεστής delete χρησιμοποιείται για να ελευθερώσει μνήμη που έχει
δεσμευθεί με τον τελεστή new.
• O τελεστής delete θα πρέπει να καλείται σε έναν δείκτη προς μια δεσμευμένη
μνήμη όταν η μνήμη αυτή πλέον δεν χρειάζεται.
• O τελεστής delete μπορεί να διαγράψει μια απλή μεταβλητή ή έναν πίνακα:
delete pointerName;
delete [] arrayName;
• Από τη στιγμή που έχει κληθεί η delete για μια περιοχή μνήμης, δεν θα πρέπει
να γίνονται πλέον προσπελάσεις σε αυτή τη περιοχή μνήμης.
• Η σύμβαση είναι να θέτουμε το δείκτη προς μια μνήμη που έχει διαγραφεί σε
NULL.
• Κάθε new θα πρέπει να έχει το αντίστοιχο delete (αλλιώς το πρόγραμμα έχει διαρροή
μνήμης).
• Το new και το delete μπορεί να μην βρίσκονται στην ίδια συνάρτηση.
https://github.com/chgogos/oop/blob/master/various/COP3330/lect10/sample3.cpp
https://github.com/chgogos/oop/blob/master/various/COP3330/lect10/sample4.cpp
5

Ο σωρός (heap)
• O σωρός είναι μια μεγάλη περιοχή μνήμης ελεγχόμενη από το σύστημα
χρόνου εκτέλεσης που είναι υπεύθυνο να εξυπηρετεί αιτήματα
δέσμευσης και αποδέσμευσης δυναμικής μνήμης.
• Είναι δυνατόν να δεσμευθεί δυναμικά μνήμη και στη συνέχει να «χαθεί» ο
δείκτης προς τη μνήμη αυτή. Σε αυτή την περίπτωση έχουμε διαρροή
μνήμης.
• Οι διαρροές μνήμες μπορεί να προκαλέσουν την εξάντληση του χώρου
του σωρού.
• Αν γίνει απόπειρα να δεσμευθεί μνήμη από το σωρό και δεν υπάρχει
αρκετή μνήμη, τότε παράγεται εξαίρεση (exception) που υποδηλώνει το
λάθος.
https://github.com/chgogos/oop/blob/master/various/COP3330/lect10/sample5.cpp

6
Γιατί να χρησιμοποιηθεί δυναμική δέσμευση
μνήμης;
• Επιτρέπει στα δεδομένα (ειδικά στους πίνακες) να λάβουν
μεταβλητά μεγέθη (π.χ. να γίνεται ερώτηση προς το χρήστη για τον
αριθμό των ακεραίων που επιθυμεί να αποθηκεύσει, και στη
συνέχεια να δημιουργεί έναν πίνακα ακεραίων με αυτό ακριβώς το
μέγεθος).
• Επιτρέπει σε μεταβλητές που έχουν δημιουργηθεί τοπικά να
εξακολουθούν να υπάρχουν και μετά την ολοκλήρωση της
συνάρτησης.
• Επιτρέπει τη δημιουργία πολλών δομών δεδομένων που
χρησιμοποιούνται στις Δομές Δεδομένων και στους Αλγορίθμους.
https://github.com/chgogos/oop/blob/master/various/COP3330/lect10/sample6.cpp
https://github.com/chgogos/oop/blob/master/various/COP3330/lect10/sample7.cpp
7

Οι τελεστές . και ->


• Ο τελεστής . χρησιμοποιείται για να προσπελάσουμε τα μέλη ενός
αντικειμένου.
f1.Evaluate();
f1.num=5;
• Πως γίνεται η πρόσβαση στα μέλη ενός αντικειμένου αν διαθέτουμε
μόνο έναν δείκτη προς αυτό;
• Αν έχουμε έναν δείκτη f_ptr = &f;
• θα μπορούσαμε να χρησιμοποιήσουμε: (*f_ptr).Evaluate();
• Ωστόσο, υπάρχει ένας συντομότερος τρόπος: f_ptr->Evaluate();
• Οι δύο παραπάνω τρόποι πρόσβασης στα μέλη ενός αντικειμένου είναι ισοδύναμοι.
https://github.com/chgogos/oop/blob/master/various/COP3330/lect10/sample8.cpp

8
Δέσμευση και αποδέσμευση μνήμης στη C
(λειτουργεί επίσης και στη C++)
• Οι συναρτήσεις malloc και free
• Οι συναρτήσεις malloc και free ορίζονται στο <stdlib.h>
• H malloc είναι παρόμοια με την new, αλλά θα πρέπει να καθοριστεί το
ακριβές μέγεθος μνήμης.
• Επιστρέφει void* (πρέπει να μετατραπεί στον κατάλληλο τύπο δείκτη)
• Η free είναι ισοδύναμη με την delete (ωστόσο, δεν υπάρχει
διαφοροποίηση ανάμεσα στην αποδέσμευση μνήμης για απλή μεταβλητή
και για πίνακα)

Ερωτήσεις σύνοψης
• Σε ποια περιοχή της μνήμης επενεργεί η στατική δέσμευση και σε ποια η
δυναμική δέσμευση μνήμης;
• Πότε θα πρέπει να χρησιμοποιούμε δυναμική δέσμευση μνήμης;
• Πώς γίνεται η δέσμευση και η αποδέσμευση μνήμης για ένα απλό στοιχείο
δεδομένων και πως για έναν πίνακα;
• Πότε παρατηρείται διαρροή μνήμης;
• Ποια είναι η αντίστοιχη εντολή της new με την οποία στη C γίνεται
δέσμευση μνήμης;
• Ποια είναι η αντίστοιχη εντολή της delete με την οποία στη C γίνεται
αποδέσμευση μνήμης;
• Με τι μπορεί να αντικατασταθεί ο τελεστής -> ;
10
Αναδρομή
#11
Τμήμα Πληροφορικής και Τηλεπικοινωνιών (Άρτα)
Πανεπιστήμιο Ιωαννίνων
Γκόγκος Χρήστος

V 1.0

Διάσπαση (αποσύνθεση) προβλήματος


• Η διάσπαση προβλήματος (problem decomposition) είναι μια από τις
βασικές τεχνικές επίλυσης προβλημάτων στον προγραμματισμό.
Προκειμένου ένα μεγάλο πρόβλημα να γίνει ευκολότερα
διαχειρίσιμο διασπάται σε μικρότερα και ευκολότερα προβλήματα. Η
λύση του μεγάλου προβλήματος προκύπτει συνδυάζοντας τις λύσεις
των μικρότερων προβλημάτων.
• Παράδειγμα 1:
• Πρόβλημα: Ταξινόμησε την ακολουθία Α[1..Ν]
• Διάσπασε σε:
• Υποπρόβλημα 1: Ταξινόμησε την υποακολουθία Α[2..Ν] (μικρότερο πρόβλημα)
• Υποπρόβλημα 2: Τοποθέτησε το Α[1] στη σωστή θέση στην ταξινομημένη
υποακολουθία Α[2..Ν] (ευκολότερο πρόβλημα)

2
Διάσπαση προβλήματος
• Παράδειγμα 2:
• Πρόβλημα (μέγεθος = Ν): Υπολογισμός του σ𝑁
𝑖=1 𝑖
3

• Διάσπαση σε:
• Υποπρόβλημα (μεγέθος = Ν-1): Υπολογισμός του Χ = σ𝑁−1
𝑖=1 𝑖
3

• Η λύση είναι Χ + Ν * Ν * Ν
• Παράδειγμα 3:
• Πρόβλημα: Εύρεση του αθροίσματος Α[1..Ν]
• Διάσπαση σε:
• Χ = άθροισμα των Α[2..Ν]
• Η λύση είναι Χ + Α[1]

Διάσπαση προβλήματος και αναδρομή


• Όταν ένα μεγάλο πρόβλημα μπορεί να λυθεί επιλύοντας μικρότερα
προβλήματα της ίδια μορφής τότε η αναδρομή αποτελεί ένα φυσικό
τρόπο για την αντιμετώπιση του προβλήματος.
• Η συγγραφή αναδρομικών συναρτήσεων μπορεί να γίνει ως μια
διαδικασία 4 βημάτων (κατανόηση αναδρομής, πρωτότυπο
αναδρομικής συνάρτησης, βασική περίπτωση, αναδρομική
περίπτωση) που θα εφαρμοστεί στη συνέχεια για το ακόλουθο
παράδειγμα:
• Πρόβλημα: Εύρεση του αθροίσματος Α[1..Ν]
• Διάσπαση σε:
• Χ = άθροισμα των Α[2..Ν]
• Η λύση είναι Χ + Α[1]

4
Συγγραφή αναδρομικών συναρτήσεων (1/4)
• Βήμα 1: κατανόηση του πως ένα πρόβλημα μπορεί να διασπαστεί σε
ένα σύνολο από μικρότερα προβλήματα της ίδιας μορφής καθώς και
πως οι λύσεις των μικρότερων προβλημάτων μπορούν να
συνδυαστούν για να σχηματίσουν τη λύση του μεγαλύτερου
προβλήματος.
• Παράδειγμα:
• Πρόβλημα: Εύρεση του αθροίσματος Α[1..Ν]
• Γενίκευση του προβλήματος στην εύρεση του αθροίσματος του
Α[beg..end]
• Διάσπαση σε:
• Χ = άθροισμα των Α[beg+1..end]
• Η λύση είναι Χ + Α[beg]

Συγγραφή αναδρομικών συναρτήσεων (2/4)


• Βήμα 2: διατύπωση της λύσης του προβλήματος ως μια συνάρτηση που
θα δέχεται τις κατάλληλες παραμέτρους. Η βασική ιδέα είναι ότι θα
πρέπει τόσο το αρχικό πρόβλημα όσο και τα μικρότερα υποπροβλήματα
να χρησιμοποιούν την ίδια συνάρτηση.
• Διατύπωση του προβλήματος ως τη συνάρτηση:
• sum(A, beg, end) είναι το άθροισμα του Α[beg..end] (το αρχικό πρόβλημα)
• sum(A, beg+1, end) είναι το άθροισμα του Α[beg+1..end] (υποπρόβλημα)
• Η συνάρτηση μπορεί να έχει την ακόλουθη μορφή (πρωτότυπο
αναδρομικής συνάρτησης):
int sum(int A[], int beg, int end);

6
Συγγραφή αναδρομικών συναρτήσεων (3/4)
• Βήμα 3: Ορίζεται η βασική περίπτωση (base case) της αναδρομής.
Συνήθως πρόκειται για τις εύκολες περιπτώσεις στις οποίες το
μέγεθος του προβλήματος είναι 0 ή 1.
• int sum(int A[], int beg, int end);
• Η βασική περίπτωση μπορεί να είναι το άθροισμα όταν δεν υπάρχει κάποιο
στοιχείο στον πίνακα που να περιέχεται ανάμεσα στο δείκτη beg και στο
δείκτη end.
if (beg>end) return 0;

Συγγραφή αναδρομικών συναρτήσεων (4/4)


• Βήμα 4: Ορίζεται η αναδρομική περίπτωση, δηλαδή ο συνδυασμός
των λύσεων των μικρότερων προβλημάτων που σχηματίζει τη λύση
στο αρχικό πρόβλημα.
• Διάσπαση σε:
• Χ = άθροισμα των Α[beg+1..end]
• Η λύση είναι Χ + Α[beg]
• Αναδρομική περίπτωση:
X = sum(A, beg + 1, end);
return X + A[beg];

ή απλούστερα:

return A[beg] + sum(A, beg + 1, end);

8
Τελική μορφή αναδρομικής συνάρτησης
• Η αναδρομική συνάρτηση στο σύνολό της είναι η ακόλουθη:

int sum(int A[], int beg, int end)


{
if (beg > end)
return 0;
return A[beg] + sum(A, beg + 1, end);
}

https://github.com/chgogos/oop/blob/master/various/COP3330/lect11/sample1.cpp

Ιχνηλάτηση (tracing) της αναδρομικής


συνάρτησης
• Έστω Α = {1,2,3,4,5};
• sum(A, 0, 4)
• A[0] + sum(A, 1, 4)
• A[1] + sum(A, 2, 4)
• A[2] + sum (A, 3, 4)
• A[3] + sum(A, 4, 4)
• A[4] + sum(A, 5, 4)  η sum(A, 5, 4) επιστρέφει 0
• A[4] + 0 = 5  η sum(A, 4, 4) επιστρέφει 5
• A[3] + 5 = 9  η sum(A, 3, 4) επιστρέφει 9
• A[2] + 9 = 12  η sum(A, 2, 4) επιστρέφει 12
• A[1]+12 = 14  η sum(A, 1, 4) επιστρέφει 14
• A[0] + 14 = 15  η sum(A, 0, 4) επιστρέφει 15
• Σε κάθε αναδρομικό βήμα, το πρόγραμμα είναι ένα βήμα πλησιέστερα στη
βασική περίπτωση, όταν φθάσει στη βασική περίπτωση, αρχίζουν να
σχηματίζονται οι λύσεις σταδιακά για τα μεγαλύτερα προβλήματα
10
Παράδειγμα αναδρομής 2
• Ταξινόμηση της ακολουθίας Α[beg..end]
• Διάσπαση σε:
• Υποπρόβλημα 1: Ταξινόμησε την ακολουθία Α[beg+1..end]
• Υποπρόβλημα 2: Εισήγαγε το A[beg] στην κατάλληλη θέση της ακολουθίας
Α[beg+1..end]
• Πρωτότυπο συνάρτησης:
• void sort(A, beg, end); // ταξινόμησε την ακολουθία A από beg μέχρι end
• Όταν η ακολουθία δεν έχει στοιχεία είναι ταξινομημένη (beg>end)
• Όταν η ακολουθία έχει μόνο ένα στοιχείο είναι ταξινομημένη (beg==end)

11

Παράδειγμα αναδρομής 2
• Βασική περίπτωση:
• if (beg>=end) return;
• Αναδρομική περίπτωση, η ακολουθία έχει περισσότερα από ένα στοιχεία
(beg<end):
• Υποπρόβλημα 1: Ταξινόμησε την ακολουθία A[beg+1..end]
sort(A, beg + 1, end);
• Υποπρόβλημα 2: Εισήγαγε το A[beg] στην ταξινομημένη ακολουθία A[beg+1..end]
tmp = A[beg];
for (i = beg + 1; i <= end; i++) {
if (tmp > A[i])
A[i - 1] = A[i];
else
break;
}
A[i - 1] = tmp; 12
Τελική μορφή αναδρομικής συνάρτησης
• Η αναδρομική συνάρτηση στο σύνολό της είναι η ακόλουθη:
void sort(int A[], int beg, int end)
{
if (beg >= end)
return;
sort(A, beg + 1, end);
int tmp, i;
tmp = A[beg];
for (i = beg + 1; i <= end; i++)
{
if (tmp > A[i])
A[i - 1] = A[i];
else
break;
}
A[i - 1] = tmp;
}

https://github.com/chgogos/oop/blob/master/various/COP3330/lect11/sample2.cpp 13

Αναδρομική σκέψη
• Εντοπισμός της βασικής περίπτωσης (base case) για την οποία
συνήθως είναι προφανής η αντιμετώπισή της.
• Ερώτηση: Αν μπορούμε να επιλύσουμε ένα πρόβλημα μεγέθους Ν-1,
πως μπορούμε να χρησιμοποιήσουμε αυτή τη λύση για να λύσουμε
ένα πρόβλημα μεγέθους Ν;
• Αυτή είναι η αναδρομική περίπτωση (recursive case)

• Βήματα αν η απάντηση είναι ΝΑΙ:


• Εύρεση του κατάλληλου πρωτοτύπου συνάρτησης
• Βασική περίπτωση
• Αναδρομική περίπτωση

14
Αναδρομή και μαθηματική επαγωγή
• Η μαθηματική επαγωγή είναι χρήσιμο εργαλείο για την απόδειξη θεωρημάτων
• Πρώτα αποδεικνύεται η βασική περίπτωση (Ν=1)
• Επίδειξη ότι το θεώρημα ισχύει για κάποιες μικρές τιμές.
• Στη συνέχεια, διατύπωση της επαγωγικής υπόθεσης
• Υπόθεση ότι το θεώρημα είναι αληθές για όλες τις περιπτώσεις μέχρι κάποιο όριο (N=k)
• Απόδειξη ότι το θεώρημα ισχύει για την επόμενη τιμή (N=k+1)
N N+1
• Παράδειγμα: σ𝑁 𝑖=1 𝑖 = 2
• Αναδρομή
• Βασική περίπτωση: γνωρίζουμε πως να επιλύσουμε το πρόβλημα για τη βασική περίπτωση
(Ν=0 ή 1).
• Αναδρομική περίπτωση: Υποθέτοντας ότι μπορούμε να λύσουμε το πρόβλημα για N=k-1,
προσπαθούμε να λύσουμε το πρόβλημα για Ν=k.
• Η αναδρομή είναι βασικά η εφαρμογή της επαγωγής στην επίλυση
προβλημάτων.

15

Αντιγραφή λεκτικών αναδρομικά με τη


συνάρτηση mystrcpy
• void mystrcpy(char *dst, char *src);
• Αντιγραφή του λεκτικού src στο λεκτικό dst.

• Βασική περίπτωση: if (*src == ‘\0’) *dst = *src;


• Αναδρομική περίπτωση: Αν γνωρίζουμε πως να αντιγράψουμε ένα λεκτικό με
έναν λιγότερο χαρακτήρα σε σχέση με το λεκτικό που πραγματικά θέλουμε
να αντιγράψουμε, πώς μπορούμε να χρησιμοποιήσουμε αυτή τη δυνατότητα
για να αντιγράψουμε το πλήρες λεκτικό;
• Αντιγραφή ενός χαρακτήρα *dst = *src
• Αντιγραφή του υπόλοιπου λεκτικού  αναδρομικά με την mystrcpy

16
Αντιγραφή λεκτικών αναδρομικά
void mystrcpy(char *dst, const char *src)
{
if (*src == '\0')
{
*dst = *src;
return;
}
*dst = *src;
mystrcpy(dst + 1, src + 1);
}

https://github.com/chgogos/oop/blob/master/various/COP3330/lect11/sample3.cpp

17

Υπολογισμός μήκους λεκτικών αναδρομικά


με τη συνάρτηση mystrlen
• void mystrlen(char* str);
• Αν γνωρίζουμε πως να μετρήσουμε το μήκος ενός λεκτικού με έναν
λιγότερο χαρακτήρα, τότε αρκεί στην τιμή αυτή να προσθέσουμε 1
για να λάβουμε το μήκος του λεκτικού.
int mystrlen(const char *str)
{
if (*str == '\0')
{
return 0;
}
return 1 + mystrlen(str + 1);
}
https://github.com/chgogos/oop/blob/master/various/COP3330/lect11/sample4.cpp
18
Ερωτήσεις σύνοψης
• Τι είναι η βασική περίπτωση (base case) στην αναδρομή;
• Τι είναι η αναδρομική περίπτωση (recursive case) στην αναδρομή;
• Πως μπορούμε αναδρομικά να βρούμε μια λύση για το εάν μια λέξη είναι
παλινδρομική ή όχι (π.χ. ΣΟΦΟΣ, ΑΝΝΑ); Συμπληρώστε τον κώδικα:
• bool ispalindrome(string word, int front, int back){…}
• Πως μπορούμε αναδρομικά να αναζητήσουμε ένα κλειδί σε μια
ακολουθία αριθμών; Συμπληρώστε τον κώδικα:
• bool search(int A[], int beg, int end, int key) {…}
• Γράψτε αναδρομική συνάρτηση υπολογισμού του παραγοντικού ενός
θετικού ακεραίου αριθμού (n!=1*2*3*…*n).

19

Κατασκευαστής αντιγραφής
και αντιγραφή ανάθεσης
#12
Τμήμα Πληροφορικής και Τηλεπικοινωνιών (Άρτα)
Πανεπιστήμιο Ιωαννίνων
Γκόγκος Χρήστος

V 1.0
Συναρτήσεις που δημιουργούνται αυτόματα
• Οι ακόλουθες δύο συναρτήσεις μέλη που έχουμε ήδη συναντήσει,
κάτω από ορισμένες περιστάσεις δημιουργούνται αυτόματα από τον
μεταγλωττιστή
• Κατασκευαστής: Ένας κενός προκαθορισμένος κατασκευαστής (δηλαδή ένας
κατασκευαστής χωρίς παραμέτρους και με κενό σώμα) δημιουργείται
αυτόματα αν δεν οριστεί κάποιος άλλος κατασκευαστής.
• Καταστροφέας: Ένας κενός καταστροφέας δημιουργείται αν δεν οριστεί
καταστροφέας (επίσης με κενό σώμα).
• Δύο άλλες συναρτήσεις μέλη που επίσης, κάτω από ορισμένες
περιστάσεις δημιουργούνται αυτόματα είναι o κατασκευαστής
αντιγραφής (copy constructor) και o τελεστής ανάθεσης (assignment
operator)
2

Κατασκευαστής αντιγραφής (copy


constructor)
• Ο κατασκευαστής αντιγραφής είναι κατασκευαστής, και συνεπώς:
• Έχει το ίδιο όνομα με την κλάση.
• Δεν έχει τύπο επιστροφής (αν και φαίνεται σαν να επιστρέφει ένα αντικείμενο
κλάσης όταν καλείται ρητά).
• Όπως και με τον κατασκευαστή μετατροπής υπάρχουν περιπτώσεις που ο
copy constructor καλείται υπονοούμενα. Αυτό συμβαίνει:
• Όταν ένα αντικείμενο δηλώνεται να έχει την ίδια τιμή με ένα άλλο αντικείμενο
• Παράδειγμα:
Fraction f1(1,2);
Fraction f2 = f1; // το νέο αντικείμενο αρχικοποιείται ως ένα αντίγραφο του f1
• Όταν ένα αντικείμενο περνά με τιμή (by value) σε μια συνάρτηση
• Όταν ένα αντικείμενο επιστρέφεται με τιμή (by value) από μια συνάρτηση
3
Δήλωση κατασκευαστή αντιγραφής
• Καθώς ο σκοπός ενός κατασκευαστή αντιγραφής είναι να αρχικοποιηθεί ένα νέο
αντικείμενο ως αντίγραφο ενός άλλου αντικειμένου, δέχεται ένα αντικείμενο ως
παράμετρο.
• classname(const classname&)
• Η παράμετρος είναι const (αν και δεν απαιτείται) διότι ο κατασκευαστής
αντιγραφής δεν θα πρέπει να τροποποιεί την παράμετρο.
• Η παράμετρος θα πρέπει να περνά με αναφορά. Αν περνούσε με τιμή τότε λόγω
αναδρομικής κλήσης της ίδιας συνάρτησης, δηλαδή του copy constructor θα
οδηγούμασταν σε stack overflow (ωστόσο, ο μεταγλωττιστής δεν επιτρέπει να
δηλώσουμε σε κατασκευαστή αντιγραφής την παράμετρο να περνά με τιμή).
• Παραδείγματα:
• Fraction(const Fraction &f);
• Mixed(const Mixed& m);

Κατασκευαστής αντιγραφής
• Τι θα πρέπει να κάνει ένας copy constructor;
• Να αντιγράφει τα μέλη δεδομένων από την παράμετρο στο νέο αντικείμενο.
• Παράδειγμα:
Fraction(const Fraction &f) Fraction(const Fraction &f)
{ {
numer = f.numer; ή this->numer = f.numer;
denom = f.denom; this->denom = f.denom;
} }

• Ο copy constructor που δημιουργείται αυτόματα (default copy constructor)


κάνει ακριβώς αυτό.
• Ωστόσο, δεν είναι αυτή η συμπεριφορά που θέλουμε πάντα.

5
«Προβληματικός» προκαθορισμένος
κατασκευαστής αντιγραφής: ρηχή αντιγραφή
• Έστω ότι θέλουμε να αντιγράψουμε ένα αντικείμενο playlist

-ΑΡΧΙΚΟ PLAYLIST-
(Συναρτήσεις)

S S E E E
Song *plist: 0xFFA08
1 2
int array_size: 5
int num_songs: 2

• Η ρηχή αντιγραφή (shallow copy) κάνει ακριβής αντιγραφή όλων των


μελών δεδομένων από το παλιό αντικείμενο στο νέο

Λειτουργία της προκαθορισμένης ρηχής


αντιγραφής
• Στον κατασκευαστή αντιγραφής το αρχικό αντικείμενο είναι η παράμετρος.
• Στο αντίγραφο ορίζονται τα μέλη δεδομένων να είναι ίδια με αυτά του αρχικού
αντικειμένου.
-ΑΡΧΙΚΟ PLAYLIST-
(Συναρτήσεις)

S S E E E
Song *plist: 0xFFA08
1 2
int array_size: 5
int num_songs: 2

-ΑΝΤΙΓΡΑΦΟ- Το πρόβλημα είναι ότι το νέο αντικείμενο μοιράζεται την plist με


(Συναρτήσεις) το παλιό αντικείμενο. Η αντιγραφή που πραγματοποιήθηκε δεν
… ήταν μια «πραγματική αντιγραφή» από την οποία θα προκύπτανε
Song *plist: 0xFFA08 δύο ανεξάρτητα αντίγραφα του αντικειμένου. Αυτός είναι ο λόγος
int array_size: 5 που ονομάζεται «ρηχή αντιγραφή»
int num_songs: 2 7
Βαθιά αντιγραφή (πρέπει να υλοποιηθεί ως
κατασκευαστής αντιγραφής από το χρήστη)
• Ο κατασκευαστής αντιγραφής έχει το αρχικό αντικείμενο ως παράμετρο.
• Τα μέλη δεδομένα που δεν είναι δείκτες ορίζονται να έχουν ίσες τιμές με τα
μέλη δεδομένων του αρχικού αντικειμένου
-ΑΡΧΙΚΟ PLAYLIST-
(Συναρτήσεις)

S S E E E
Song *plist: 0xFFA08
1 2
int array_size: 5
int num_songs: 2

-ΑΝΤΙΓΡΑΦΟ-
(Συναρτήσεις)

Song *plist: -
int array_size: 5
int num_songs: 2 8

Βαθιά αντιγραφή (deep copy)


• Δεσμεύεται νέα μνήμη στην οποία θα δείχνει ο δείκτης του
αντιγράφου.

-ΑΡΧΙΚΟ PLAYLIST-
(Συναρτήσεις)

S S E E E
Song *plist: 0xFFA08
1 2
int array_size: 5
int num_songs: 2

-ΑΝΤΙΓΡΑΦΟ-
(Συναρτήσεις)

Song *plist: 0xFFB74
int array_size: 5
int num_songs: 2 9
Βαθιά αντιγραφή (deep copy)
• Αντιγράφονται τα δεδομένα από την παλιά δυναμική μνήμη στη νέα.

-ΑΡΧΙΚΟ PLAYLIST-
(Συναρτήσεις)

S S E E E
Song *plist: 0xFFA08
1 2
int array_size: 5
int num_songs: 2

-ΑΝΤΙΓΡΑΦΟ-
(Συναρτήσεις)

S S E E E
Song *plist: 0xFFB74
1 2
int array_size: 5
int num_songs: 2 10

Τελεστής ανάθεσης (assignment operator)


• Ο τελεστής ανάθεσης (=) καλείται όταν ένα αντικείμενο εκχωρείται
σε ένα άλλο.
• Μοιάζει με τον κατασκευαστή αντιγραφής αλλά υπάρχουν ορισμένες
βασικές διαφορές:
• Ο τελεστής ανάθεσης είναι μια κανονική συνάρτηση μέλος της κλάσης και όχι
ένας κατασκευαστής, άρα αφορά δύο αντικείμενα που ήδη υπάρχουν και
έχουν αρχικοποιηθεί.
• Ο τελεστής ανάθεσης επιστρέφει την τιμή που του ανατίθεται (έτσι,
επιτρέπονται και διαδοχικές (cascading) κλήσεις του τελεστή ανάθεσης)
Fraction f1(1,2), f2, f3;
f3 = f2 = f1; // τα αντικείμενα f2 και f3 λαμβάνουν την ίδια τιμή.

11
Τελεστής ανάθεσης
• Μορφή:
• classname& operator=(const classname&)
• Παράδειγμα:
• Fraction lhs(1,2), rhs(2,5);
lhs = rhs;
• lhs είναι το καλών αντικείμενο, ενώ rhs είναι η παράμετρος, η συνάρτηση
του τελεστή ανάθεσης αλλάζει το lhs έτσι ώστε να είναι ένα αντίγραφο του
rhs και επιστρέφει μια αναφορά στο lhs μέσω του δείκτη this.

12

Ο δείκτης this
• Σε κάθε αντικείμενο υπάρχει ένας δείκτης που ονομάζεται this
• Είναι σαν να υπάρχει η ακόλουθη δήλωση στα μέλη δεδομένων του
αντικειμένου:
• classname *this;
• Ο δείκτης this δείχνει προς το ίδιο το αντικείμενο.
• Μπορούμε να καλούμε άλλες συναρτήσεις μέλη με την εντολή:
• this->memberFunction();
• Χρησιμοποιώντας το δείκτη this επιστρέφουμε μια αναφορά προς
το ίδιο το αντικείμενο στον τελεστή ανάθεσης.

13
Ρηχή αντιγραφή με ανάθεση
• Έστω ότι αναθέτουμε το αντικείμενο playlist RHS στο LHS.
• LHS = RHS;
-RHS-
(Συναρτήσεις)

S S E E E
Song *plist: 0xFFB74
1 2
int array_size: 5
int num_songs: 2
-LHS-
(Συναρτήσεις)

S S S S E E E
Song *plist: 0xFFA08
1 2 3 4
int array_size: 7
int num_songs: 4
14

Ρηχή αντιγραφή με ανάθεση


• LHS = RHS;
• Ο τελεστής ανάθεσης που αυτόματα δημιουργείται από το μεταγλωττιστή πραγματοποιεί ρηχή
αντιγραφή.
-RHS-
(Συναρτήσεις)

S S E E E
Song *plist: 0xFFB74
1 2
int array_size: 5
int num_songs: 2
-LHS-
(Συναρτήσεις)

Song *plist: 0xFFΒ74
int array_size: 5
int num_songs: 2
15
Βαθιά αντιγραφή με ανάθεση
• LHS = RHS;
• Καθώς ο πίνακας που έχει δεσμεύσει το LHS για plist δεν έχει το σωστό μέγεθος, θα πρέπει να
αποδεσμευθεί η μνήμη του και να δεσμευθεί μνήμη κατάλληλου μεγέθους.
-RHS-
(Συναρτήσεις)

S S E E E
Song *plist: 0xFFB74
1 2
int array_size: 5
int num_songs: 2
-LHS-
(Συναρτήσεις)

Song *plist: 0xFFBCC2
int array_size: 5
int num_songs: 2
16

Βαθιά αντιγραφή με ανάθεση


• LHS = RHS;
• Στη συνέχεια θα πρέπει να αντιγραφούν τα στοιχεία του plist του RHS στο LHS, και επιπλέον
να αντιγραφούν τα υπόλοιπα μέλη δεδομένων.
-RHS-
(Συναρτήσεις)

S S E E E
Song *plist: 0xFFB74
1 2
int array_size: 5
int num_songs: 2
-LHS-
(Συναρτήσεις)

S S E E E
Song *plist: 0xFFBCC2
1 2
int array_size: 5
int num_songs: 2
17
Κατασκευαστής αντιγραφής και αντιγραφή
ανάθεσης
• Αν οριστεί κατασκευαστής αντιγραφής, και δεν οριστεί κάποιος
άλλος κατασκευαστής, ο μεταγλωττιστής δεν θα δημιουργήσει
προκαθορισμένο κατασκευαστή.
• Ο τελεστής ανάθεσης θα πρέπει να είναι συνάρτηση μέλος και όχι
friend συνάρτηση.
• Ο τελεστής ανάθεσης ονομάζεται αλλιώς και τελεστής αντιγραφής
και υλοποιεί την αντιγραφή ανάθεσης (copy assignment).
• Ο τελεστής ανάθεσης πάντα τελειώνει με:
return *this;

18

Κώδικας song και playlist για shallow copy και


deep copy

https://github.com/chgogos/oop/blob/master/various/COP3330/lect12/playlist_shallow_copy.cpp

https://github.com/chgogos/oop/blob/master/various/COP3330/lect12/playlist_deep_copy.cpp

https://github.com/chgogos/oop/blob/master/various/COP3330/lect12/playlist_deleted.cpp

19
Ερωτήσεις σύνοψης
• Ποιο είναι το πρωτότυπο του κατασκευαστή αντιγραφής (copy
constructor);
• Ποια είναι η διαφορά ανάμεσα στη ρηχή αντιγραφή (shallow copy) και στη
βαθιά αντιγραφή (deep copy); Ποια είναι η προκαθορισμένη;
• Πότε καλείται ο κατασκευαστής αντιγραφής για ένα αντικείμενο;
• Γιατί η παράμετρος στον κατασκευαστή αντιγραφής περνά με const
αναφορά; Τι θα συνέβαινε αν περνούσε με τιμή;
• Ποιο είναι το πρωτότυπο για τον τελεστή ανάθεσης (assignment operator);
• Ποιες είναι οι διαφορές ανάμεσα στον κατασκευαστή αντιγραφής και στην
ανάθεση αντιγραφής;
• Τι επιστρέφει o operator=;
20

C-Strings, std::string,
std::string_view και οι τελεστές
[] και &
#13
Τμήμα Πληροφορικής και Τηλεπικοινωνιών (Άρτα)
Πανεπιστήμιο Ιωαννίνων
Γκόγκος Χρήστος

V 1.0
C-strings
• Τα C-strings υλοποιούνται ως πίνακες χαρακτήρων που τερματίζονται με το
χαρακτήρα ‘\0’ (NULL).
buffer

char buffer[5];
strcpy(buffer, “hi!\n”); ‘h’ ‘i' ‘!’ ‘\n’ ‘\0’
cout << buffer;

• Όταν χρησιμοποιούμε τα διπλά εισαγωγικά “”, ο μεταγλωττιστής κατασκευάζει


μια τερματιζόμενη με NULL, const ακολουθία χαρακτήρων την οποία γεμίζει με
τους χαρακτήρες που ο προγραμματιστής έχει επιλέξει.
• Αν ένας πίνακας χαρακτήρων δεν τερματίζει με NULL τότε δεν είναι C-string.

C-string και C++


• Υπάρχουν ενσωματωμένες δυνατότητες χειρισμού C-strings στις standard
βιβλιοθήκες της C++.
• Η βιβλιοθήκη <cstring>
• Περιέχει συναρτήσεις για συνηθισμένες λειτουργίες πάνω σε strings όπως αντιγραφή,
συνένωση, μήκος, αναζήτηση, διάσπαση c-string σε τμήματα και άλλα.
• strcpy(), strcat(), strlen(), strncat(), strcmp(), strncmp(), strstr(),
strtok(), …
• Η βιβλιοθήκη <iostream>
• Περιέχει συναρτήσεις για το χειρισμό I/O των C-strings, όπως οι τελεστές εισαγωγής (<<) και
εξαγωγής (>>), οι συναρτήσεις get(), getline() κ.α.
• char str1[140];
cout << str1; // τελεστής εισαγωγής για c-strings
cin >> str1; // τελεστής εξαγωγής για c-strings, διαβάζει μέχρι τον πρώτο κενό
χαρακτήρα
cin.get(str1, 40, ‘,’); // διαβάζει μέχρι να συναντήσει το διαχωριστικό κόμμα (,)
ή μέχρι να αναγνωστούν 40-1 χαρακτήρες
cin.getline(str1, 40); // διαβάζει μέχρι το διαχωριστικό (το προκαθορισμένο είναι
η αλλαγή γραμμής), απορρίπτει το διαχωριστικό ή μέχρι να αναγνωστούν 40-1 χαρακτήρες

3
Μειονεκτήματα των C-strings
• Σταθερό μέγεθος (ορίζεται όταν δηλώνεται το C-string ως στατικός
πίνακας).
• Το όνομα του C-string λειτουργεί ως δείκτης.
• Τα όρια του πίνακα δεν επιβάλλονται με κάποιο τρόπο.
• Πρέπει να χρησιμοποιούν «άβολες» συναρτήσεις αντί για
διαισθητικά εύκολα κατανοητούς τελεστές.
• strcpy(str1, str2) αντί για str1=str2
• strcmp(str1, str2) αντί για str1==str2
• strcat(str1, str2) αντί για str1+=str2
• Η χρήση του NULL χαρακτήρα μπορεί να δημιουργήσει προβλήματα.

Παραδείγματα κώδικα με C-strings


• https://github.com/chgogos/oop/blob/master/various/COP3330/lect13/sample2.cpp
• https://github.com/chgogos/oop/blob/master/various/COP3330/lect13/sample3.cpp
• https://github.com/chgogos/oop/blob/master/various/COP3330/lect13/sample4.cpp
• https://github.com/chgogos/oop/blob/master/cpp_playground/ex085/c_string1.cpp

5
std::string
• Πλεονεκτήματα std::string έναντι C-strings
• Μεταβλητό μέγεθος
• Εύρεση μήκους σε σταθερό χρόνο (και όχι σε γραμμικό)
• Δεν απαιτούν εντολές διαχείρισης μνήμης
• Αυτόματος χειρισμός των ορίων των λεκτικών
• Διαισθητικά εύκολη ανάθεση τιμής με το = αντί για το strcpy
• Διαισθητικά εύκολη σύγκριση με το == αντί για το strcmp
• Διαισθητικά εύκολη συνένωση λεκτικών με το + αντί για το strcat
• Μετατροπή σε C-string με τη συνάρτηση μέλος c_str()
• Δείτε το string1.cpp
https://github.com/chgogos/oop/blob/master/cpp_playground/ex085/string1.cpp
https://github.com/chgogos/oop/blob/master/cpp_playground/ex085/string2.cpp 6

std::string_view (C++17)
• Στη C++17, με τη χρήση της
std::string_view μπορούν να
αποφευχθούν περιττές const char *s = "ABCDEF";

αντιγραφές λεκτικών που θα


char s2[10];
strcpy(s2, s);
συνέβαιναν με τη std::string string_view sv{s2};
cout << s << " " << s2 << " " << sv << endl;
• Ένα std::string_view μπορεί να s2[0] = '*';
αναφέρεται τόσο σε ένα cout << s << " " << s2 << " " << sv << endl;

std::string όσο και σε ένα C-


string ABCDEF ABCDEF ABCDEF
ABCDEF *BCDEF *BCDEF
• Τα std::string_view έχουν το ίδιο
API με τα std::string
https://github.com/chgogos/oop/blob/master/cpp_playground/ex085/string_view1.cpp
https://github.com/chgogos/oop/blob/master/cpp_playground/ex085/string_view2.cpp
7
Υπερφόρτωση του τελεστή []
• Γίνεται με δύο συναρτήσεις μέλη:
• τύπος_επιστρεφόμενης_τιμής operator[](τύπος_δείκτη index) const;
• τύπος_επιστρεφόμενης_τιμής& operator[](τύπος_δείκτη index);
• Η const συνάρτηση μέλος επιτρέπει την ανάγνωση στοιχείων από
ένα const αντικείμενο.
• Η συνάρτηση μέλος που δεν είναι const επιστρέφει μια αναφορά
στο στοιχείο που μπορεί να τροποποιηθεί.
https://github.com/chgogos/oop/blob/master/various/COP3330/lect13/sample5.cpp

Υπερφόρτωση τελεστή &


• Ο τελεστής & μπορεί να υπερφορτωθεί όπως και οποιοσδήποτε
άλλος τελεστής.

https://github.com/chgogos/oop/blob/master/various/COP3330/lect13/sample6.cpp

9
Ερωτήσεις σύνοψης
• Πως υλοποιείται στη C ένα λεκτικό;
• Τι είναι η συνάρτηση strcpy και ποια επικεφαλίδα πρέπει να γίνει
include έτσι ώστε να μπορεί να χρησιμοποιηθεί;
• Τι επιτυγχάνουμε με τη συνάρτηση getline() του αντικειμένου
cin;
• Γιατί υπάρχουν δύο υπερφορτώσεις συναρτήσεων για το
operator[];

10

Κληρονομικότητα
#14
Τμήμα Πληροφορικής και Τηλεπικοινωνιών
Πανεπιστήμιο Ιωαννίνων (Άρτα)
Γκόγκος Χρήστος
Σχέσεις μεταξύ αντικειμένων
• Δύο βασικές σχέσεις μεταξύ αντικειμένων είναι η σχέση HAS-A και η
σχέση IS-A.
• Παραδείγματα σχέσης HAS-A
• PlayList has-a Song
• PokerHand has-a Card
• Σχέση IS-A
• Ένας ComputerScienceStudent είναι ένας (IS-A) UniversityStudent.
• Ένας UniversityStudent είναι ένας (IS-A) Student.

Εισαγωγή
• Η «κληρονομικότητα» (inheritance) στη C++ επιτρέπει στους
προγραμματιστές να ορίσουν IS-A σχέσεις:
• Μια κλάση μπορεί να οριστεί ως παραγόμενη από μια άλλη κλάση (που
ονομάζεται βασική κλάση).
• Η παραγόμενη κλάση γίνεται ένα είδος/εξειδίκευση της βασικής κλάσης.
• Ένα αντικείμενο της παραγόμενης κλάσης είναι (IS-A) αντικείμενο της βασικής κλάσης.
• Εναλλακτικά, μπορεί να θεωρηθεί ότι η παραγόμενη κλάση είναι ότι είναι η
βασική κλάση και (πιθανά) περισσότερα.
• Αυτό επιτρέπει, σε ορισμένες περιπτώσεις και εφόσον απαιτείται, τη χρήση
της παραγόμενης κλάσης σαν να ήταν η βασική κλάση.

3
Κληρονομικότητα, βασική και παραγόμενη
κλάση
• Η σχέση IS-A υλοποιείται μέσω της κληρονομικότητας χρησιμοποιώντας τον
ακόλουθο τρόπο δήλωσης, που σημαίνει ότι τα αντικείμενα της παραγόμενης
κλάσης είναι αντικείμενα της βασικής κλάσης (μπορούν να χρησιμοποιούν όλες
τις συναρτήσεις της διεπαφής της βασικής κλάσης)
• class derivedClassName: public baseClassName
class Mammal {
public:
void PrintInfo() const;
};
class Cow : public Mammal {
public:
void Sound() const;
};
...
int main() {
Cow C;
C.PrintInfo();
}
https://github.com/chgogos/oop/blob/master/various/COP3330/lect14/sample1.cpp 4

Επίπεδα προστασίας
• Μια παραγόμενη κλάση μπορεί να προσπελάσει όλα τα δημόσια μέλη της
βασικής κλάσης, αλλά δεν μπορεί να προσπελάσει τα ιδιωτικά μέλη της βασικής
κλάσης.
• Αν επιθυμούμε να επιτρέψουμε σε μερικά μέλη της βασικής κλάσης να προσπελαύνονται
από την παραγόμενη κλάση, αλλά να μην προσπελαύνονται από άλλες κλάσεις τότε
χρησιμοποιούμε το επίπεδο προστασίας protected.
• Σύνοψη επιπέδων προστασίας:
• public: μέλη που μπορούν να προσπελαστούν με το όνομά τους στο εσωτερικό
οποιασδήποτε συνάρτησης.
• private: Μέλη που μπορούν να προσπελαστούν στο εσωτερικό συναρτήσεων μελών και
από συναρτήσεις που έχουν δηλωθεί ως φίλες συναρτήσεις (friend) της κλάσης.
• protected: Μέλη που μπορούν να προσπελαστούν στο εσωτερικό συναρτήσεων μελών και
από συναρτήσεις που έχουν δηλωθεί ως φίλες συναρτήσεις (friend) της κλάσης και επίσης
μπορούν να προσπελαστούν στο εσωτερικό των συναρτήσεων μελών της παραγόμενης
κλάσης και στις friend συναρτήσεις της παραγόμενης κλάσης.
https://github.com/chgogos/oop/blob/master/various/COP3330/lect14/sample2.cpp
5
Κατασκευαστές/καταστροφείς
• Καθώς ένα στιγμιότυπο μιας παραγόμενης κλάσης είναι επίσης και
στιγμιότυπο της βασικής κλάσης, τόσο ο κατασκευαστής της βασικής
κλάσης όσο και ο κατασκευαστής της παραγόμενης κλάσης θα
πρέπει να εκτελείται όταν ένα αντικείμενο της παραγόμενης κλάσης
δημιουργείται.
• Οι κατασκευαστές εκτελούνται στη σειρά από τον πλέον γενικό προς
τον πλέον ειδικό.
• Οι καταστροφείς εκτελούνται στη σειρά από τον πλέον ειδικό προς
τον πλέον γενικό.
• Αν οι κατασκευαστές έχουν παραμέτρους, τότε η κλήση τους γίνεται
μέσω της λίστας αρχικοποίησης.
https://github.com/chgogos/oop/blob/master/various/COP3330/lect14/sample3.cpp
https://github.com/chgogos/oop/blob/master/various/COP3330/lect14/sample4.cpp 6

Παράκαμψη (override) συναρτήσεων


• Μια από τις χρησιμότερες δυνατότητες της κληρονομικότητας είναι ότι
επιτρέπει να εξειδικευτεί η συμπεριφορά των παραγόμενων αντικειμένων.
• Μια παραγόμενη κλάση μπορεί να δηλώσει τη δική της έκδοση μιας
συνάρτησης που υπάρχει ήδη στη βασική κλάση από την οποία
κληρονομεί.
• Η συνάρτηση της παραγόμενης κλάσης θα αντικαταστήσει την έκδοση της
συνάρτησης της βασικής κλάσης.
• Μπορούμε να έχουμε πρόσβαση και στη συνάρτηση της βασικής κλάσης
με ρητή κλήση της.
• Αυτό συμβαίνει διότι, βάσει ορισμού, η παραγόμενη κλάση θα πρέπει να είναι
οτιδήποτε είναι η βασική κλάση και πιθανά περισσότερα.
https://github.com/chgogos/oop/blob/master/various/COP3330/lect14/sample5.cpp
https://github.com/chgogos/oop/blob/master/various/COP3330/lect14/sample6.cpp 7
Ερωτήσεις σύνοψης
• Πως δηλώνεται ότι μια νέα κλάση Α έχει ως βασική κλάση την κλάση
Β;
• Ποια είναι η διαφορά των private και protected επιπέδων
προστασίας;
• Πως μπορεί μια παραγόμενη κλάση να κάνει παράκαμψη (override)
μιας συνάρτησης που ορίζεται στη βασική κλάση;
• Ποια είναι η σειρά με την οποία καλούνται κατασκευαστές και
καταστροφείς στην περίπτωση αντικειμένων παραγόμενης κλάσης;

Πολυμορφισμός και ιδεατές


συναρτήσεις
#15
Τμήμα Πληροφορικής και Τηλεπικοινωνιών (Άρτα)
Πανεπιστήμιο Ιωαννίνων
Γκόγκος Χρήστος

V 1.0
Ανάγκη ύπαρξης ιδεατών συναρτήσεων
• Στο παράδειγμα με τους σπουδαστές (lect14/sample5.cpp) για κάθε τύπο σπουδαστή (Student,
Grad και Undergrad) εκτυπώνεται διαφορετική έκθεση προόδου (GradeReport).
• Student s;
s.GradeReport();
Grad g;
g.GradeReport();
Undergrad u;
u.GradeReport();
• Έστω ότι έχουμε μια λίστα με 3000 σπουδαστές και προσπαθούμε με τον ακόλουθο κώδικα να
εμφανίσουμε τις εκθέσεις προόδου για όλους τους σπουδαστές:
• Student list[3000];
for(i=0;i<3000;i++)
list[i].GradeReport();
• Υπάρχουν δύο προβλήματα με τον παραπάνω κώδικα:
• Η λίστα είναι ένας ομογενής πίνακας, πρέπει να χρησιμοποιήσουμε τον πλέον κοινό τύπο (το βασικό τύπο).
Ο βασικός τύπος δεν έχει όλες τις πληροφορίες για τις παραγόμενες κλάσεις Grad και Undergrad
προκειμένου να παράγει την έκθεση προόδου που χρειάζεται.
• Η συνάρτηση GradeReport που καλείται είναι η έκδοση που υπάρχει στην κλάση Student (η γενική
έκθεση προόδου), και όχι η εξειδικευμένη συνάρτηση που υπάρχει σε κάθε παραγόμενη κλάση.

Ανάγκη ύπαρξης ιδεατών συναρτήσεων


• Μια πιθανή λύση είναι να χρησιμοποιηθούν δείκτες έτσι ώστε να δημιουργηθεί μια
ετερογενής λίστα.
• Ένας δείκτης μπορεί να δείχνει προς έναν τύπο δεδομένων
• Ένας δείκτης προς τη βασική κλάση μπορεί να δείχνει προς αντικείμενα που παράγονται από
αυτή τη βασική κλάση καθώς ένα αντικείμενο παραγόμενης κλάσης είναι (IS-A) αντικείμενο και
της βασικής κλάσης.
• Grad gs; Undergrad ugs; Student *ps = &gs; ps = &ugs;
• Ομοίως μια μεταβλητή αναφορά του βασικού τύπου μπορεί να αναφέρεται προς κάποιο
αντικείμενο το οποίο είναι παραγόμενο από τη βασική κλάση.
• Grad gs; Student &rs = gs; Undergrad ugs; Student &rs2 = ugs;
• Συνεπώς, μια λίστα με 3000 σπουδαστές μπορεί να δημιουργηθεί ως εξής:
• Student *list[3000]; // πίνακας με δείκτες προς σπουδαστές
list[0] = &gs; // χρησιμοποιώντας ένα αντικείμενο Grad που έχει ήδη οριστεί
list[1] = &us; // χρησιμοποιώντας ένα αντικείμενο Undergrad που έχει ήδη οριστεί
list[2] = new Grad; // δυναμικά δεσμεύοντας μνήμη για ένα αντικείμενο Grad

3
Ανάγκη ύπαρξης ιδεατών συναρτήσεων
• Διάσχιση της λίστας όλων των σπουδαστών και εκτύπωση της
έκθεσης προόδου για κάθε σπουδαστή.
• Μια πιθανή λύση:
• Student *list[3000];

for (int i=0;i<size;i++)
list[i]->GradeReport();

• Καθώς το κάθε στοιχείο της λίστας δείχνει προς διαφορετικό


αντικείμενο, υπάρχει το ερώτημα σχετικά με το ποια έκδοση της
GradeReport() καλείται στην list[i]->GradeReport();

Ανάγκη ύπαρξης ιδεατών συναρτήσεων


Student *list[3000];

for (int i=0;i<size;i++)
list[i]->GradeReport(); // Ποια έκδοση της GradeReport() καλείται;

• Η απόφαση σχετικά με τη συνάρτηση που θα καλείται λαμβάνεται κατά τη


μεταγλώττιση. Καθώς τα αντικείμενα list[i] είναι τύπου Student*, θα κληθεί η
GradeReport() της κλάσης Student.
• Αυτό συμβαίνει διότι ο μεταγλωττιστής θα πρέπει να λάβει απόφαση για το ποια
συνάρτηση θα καλεί πριν το πρόγραμμα εκτελεστεί (στατική δέσμευση = static binding).
• Για να λυθεί το πρόβλημα χρειαζόμαστε έναν τρόπο με τον οποίο θα γίνει dynamic
binding (δυναμική δέσμευση) κατά την εκτέλεση, δηλαδή η συνάρτηση να γίνεται bind
σε διαφορετικό κώδικα ανάλογα με την περίσταση.
• Οι ιδεατές συναρτήσεις (virtual functions) είναι ο μηχανισμός με τον οποίο μπορεί να επιτευχθεί
ακριβώς αυτό στη C++.

5
Ιδεατές συναρτήσεις
• Όταν μια συνάρτηση δηλωθεί ως virtual, αυτό σημαίνει ότι η συνάρτηση μπορεί
να γίνει bound σε πολλές διαφορετικές συναρτήσεις δυναμικά κατά το χρόνο
εκτέλεσης.
• Ο πολυμορφισμός αναφέρεται στη δυνατότητα να συσχετίσουμε πολλά νοήματα με μια
(ιδεατή) συνάρτηση.
• Παράδειγμα:
• class Student {public: virtual void GradeReport();...};
• Η συνάρτηση GradeReport() μπορεί να αντιστοιχηθεί σε διαφορετικές συναρτήσεις στις
παραγόμενες κλάσεις (το πρόγραμμα θα κοιτά στο αντικείμενο στο οποίο θα δείχνει ο
δείκτης για να αποφασίσει ποια συνάρτηση θα κληθεί) .
Student *sp1, *sp2, *sp3;
Student s; Grad g; Undergrad u;
sp1 = &s; sp2 = &g; sp3 = &u;
sp1->GradeReport(); // εκτελείται η έκδοση του Student
sp2->GradeReport(); // εκτελείται η έκδοση του Grad
sp3->GradeReport(); // εκτελείται η έκδοση του Undergrad
https://github.com/chgogos/oop/blob/master/various/COP3330/lect15/sample1.cpp
6

Η λύση στο αρχικό πρόβλημα


• Δημιουργία μιας ετερογενούς λίστας, χρήση ιδεατής συνάρτησης για
να επιτευχθεί πρόσβαση στις διαφορετικές GradeReport()
συναρτήσεις που βρίσκονται στις διαφορετικές παραγόμενες
κλάσεις.

class Student {public: virtual void GradeReport();…};



Student *list[3000];

for (int i=0;i<size;i++)
list[i]->GradeReport();

7
Καθαρές ιδεατές συναρτήσεις (pure virtual
functions)
• Οι κανονικές συναρτήσεις μέλη, συμπεριλαμβανομένων και των ιδεατών
συναρτήσεων θα πρέπει να έχουν κάποια υλοποίηση.
• Συνεπώς η ιδεατή συνάρτηση GradeReport() θα πρέπει κανονικά να έχει κάποια
υλοποίηση στη βασική κλάση.
• Ωστόσο, αυτό δεν είναι πάντα απαραίτητο (ή εφικτό). Mπορεί να μην υπάρχουν
πολλά που μπορούν να γίνουν στις ιδεατές συναρτήσεις των βασικών κλάσεων
καθώς η όποια λειτουργικότητα θα πρέπει να υλοποιηθεί στις παραγόμενες κλάσεις
όπου θα υπάρχουν και περισσότερες πληροφορίες.
• Η C++ επιτρέπει σε μια ιδεατή συνάρτηση να μην έχει υλοποίηση και σε αυτή τη
περίπτωση η συνάρτηση λέγεται ότι είναι pure virtual, η δε υλοποίηση γίνεται στην
παραγόμενη κλάση.
• Μια ιδεατή συνάρτηση δηλώνεται ως pure τοποθετώντας το =0 στη δήλωσή της.
• virtual void GradeReport() = 0;

Καθαρές ιδεατές συναρτήσεις και κλάσεις


• Όταν ένα αντικείμενο δηλώνεται ως αντικείμενο ενός κανονικού
(concrete) τύπου κλάσης, γνωρίζουμε τα πάντα για το αντικείμενο (τα
μέλη δεδομένων που διαθέτει, τη συμπεριφορά της κάθε συνάρτησης
μέλους).
• Ωστόσο, αν ένα αντικείμενο δηλωθεί ως αντικείμενο μιας κλάσης που
περιέχει μια καθαρή ιδεατή συνάρτηση τότε:
• Το αντικείμενο δεν είναι καλά ορισμένο
• Η συμπεριφορά της καθαρής ιδεατής συνάρτησης δεν είναι ορισμένη.
• Ο μεταγλωττιστής δεν μπορεί να επιτρέψει την ύπαρξη τέτοιων αντικειμένων.
• Η C++ διαφοροποιεί τις κανονικές κλάσεις (concrete classes) από τις
κλάσεις που περιέχουν καθαρές ιδεατές συναρτήσεις και οι οποίες
κλάσεις ονομάζονται αφηρημένες (abstract).

9
Αφηρημένες κλάσεις
• Μια αφηρημένη κλάση είναι μια κλάση με τουλάχιστον μια καθαρή
ιδεατή συνάρτηση.
• Μια αφηρημένη κλάση δεν μπορεί να δημιουργεί στιγμιότυπα.
• Student st; // error, Student is an abstract class, the object
cannot be created
• Μια αφηρημένη κλάση μπορεί να χρησιμοποιηθεί για να δηλωθούν δείκτες
• Ένας δείκτης δεν απαιτεί τη δημιουργία ενός αντικειμένου
• Ένας δείκτης σε μια βασική κλάση μπορεί να δείχνει προς ένα αντικείμενο της
παραγόμενης κλάσης.
• Student *st; // έγκυρο, μπορεί να δείχνει προς αντικείμενο της
παραγόμενης κλάσης
https://github.com/chgogos/oop/blob/master/various/COP3330/lect15/sample2.cpp
https://github.com/chgogos/oop/blob/master/various/COP3330/lect15/sample3.cpp
10

Παράδειγμα
• Αφηρημένη κλάση Employee
• Κλάση Temporary, παραγόμενη κλάση από την Employee
• Κλάση Permanent, παραγόμενη κλάση από την Employee
(αφηρημένη)
• Κλάση Hourly, παραγόμενη κλάση από την Permanent
• Κλάση Salaried, παραγόμενη κλάση από την Permanent

https://github.com/chgogos/oop/tree/master/various/COP3330/lect15/employee

11
Ερωτήσεις σύνοψης
• Τι είναι μια ιδεατή συνάρτηση;
• Τι μπορούμε να πετύχουμε με τις ιδεατές συναρτήσεις;
• Πως ορίζεται μια καθαρή ιδεατή συνάρτηση;
• Τι είναι μια αφηρημένη κλάση;
• Μπορεί μια μεταβλητή να δηλωθεί ότι έχει ως τύπο, τον τύπο μιας
αφηρημένης κλάσης;
• Μπορεί ένας δείκτης να δηλωθεί ότι έχει ως τύπο, τον τύπο μιας
αφηρημένης κλάσης;

12

Templates (πρότυπα)
#16
Τμήμα Πληροφορικής και Τηλεπικοινωνιών (Άρτα)
Πανεπιστήμιο Ιωαννίνων
Γκόγκος Χρήστος

V 1.0
Πρότυπα
• Τα πρότυπα επιτρέπουν την περιγραφή λειτουργιών που μπορούν να
εφαρμοστούν σε πολλαπλούς τύπους δεδομένων.
• Γενερικός προγραμματισμός (generic programming)
• Επαναχρησιμοποίηση κώδικα
• Πρότυπα συναρτήσεων (function templates)
• Επιτρέπουν τον ορισμό λογικής με τέτοιο τρόπο έτσι ώστε η ίδια αλγοριθμική
λογική να μπορεί να εφαρμοστεί σε πολλαπλούς τύπους δεδομένων.
• Πρότυπα κλάσεων (class templates)
• Επιτρέπουν τον ορισμό generic προτύπων κλάσεων που δίνουν τη
δυνατότητα προσάρτησης συγκεκριμένων τύπων δεδομένων έτσι ώστε να
προκύπτουν νέες κλάσεις.

Πρότυπα συναρτήσεων
• Οι συναρτήσεις της C++ λειτουργούν με συγκεκριμένους τύπους δεδομένων.
Συχνά προκύπτει η ανάγκη του να γράψουμε διαφορετικές συναρτήσεις για να
πραγματοποιηθεί η ίδια λειτουργία με διαφορετικούς τύπους δεδομένων.
int maximum(int a, int b, int c) float maximum(float a, float b, float c)
{ {
int max = a; float max = a;
if (b>max) max = b; if (b>max) max = b;
if (c>max) max = c; if (c>max) max = c;
return max; return max;
} }

• Η λογική είναι ακριβώς η ίδια, αλλά ο τύπος δεδομένων είναι διαφορετικός.


• Τα πρότυπα συναρτήσεων επιτρέπουν στη λογική να γραφεί μια φορά και να
χρησιμοποιείται για όλους τους τύπους δεδομένων (generic functions).

3
Πρότυπα συναρτήσεων
• Generic συνάρτηση εύρεσης μεγαλύτερης τιμής από τρεις τιμές.
template <class T> template <typename T>
T maximum(T a, T b, T c) T maximum(T a, T b, T c)
{ {
T max = a;
if (b>max) max = b;
ή T max = a;
if (b>max) max = b;
if (c>max) max = c; if (c>max) max = c;
return max; return max;
} }
• Ένα πρότυπο συνάρτησης δεν αποτελεί μια πλήρως ορισμένη συνάρτηση για το μεταγλωττιστή,
καθώς ο μεταγλωττιστής χρειάζεται να γνωρίζει τον πραγματικό τύπο δεδομένων για να
δημιουργήσει τον αντίστοιχο κώδικα. Συχνά, πρότυπα συναρτήσεων τοποθετούνται σε αρχεία
επικεφαλίδων (.h ή .hpp) για να συμπεριληφθούν σε προγράμματα που χρησιμοποιούν τη
συνάρτηση. Ο μεταγλωττιστής δημιουργεί τον κώδικα της συνάρτησης με βάση την πραγματική
χρήση του προτύπου συνάρτησης.

Χρήση προτύπων συνάρτησης


• Από τη στιγμή που ένα πρότυπο συνάρτησης έχει οριστεί, μπορεί να
χρησιμοποιηθεί περνώντας παραμέτρους πραγματικών τύπων.
template <class T>
T maximum(T a, T b, T c)
{
T max = a;
if (b>max) max = b;
if (c>max) max = c;
return max;
}
int i1, i2, i3;
int m = maximum(i1, i2, i3);

• Η κλήση θα προκαλέσει την κλήση του προτύπου συνάρτησης με T==int. Η δε


τιμή επιστροφής της συνάρτησης θα είναι int.
5
Ένα ακόμα παράδειγμα
template <class T>
void printArray(const T *a, const int count)
{
for(int i=0;i<count;i++)
cout << array[i] << “ ”;
cout << endl;
}

char cc[100];
int ii[100];
double dd[100];
myclass xx[100]; // μπορεί να χρησιμοποιηθεί ακόμα και τύπος ορισμένος από το χρήστη

printArray(cc,100);
printArray(ii,100);
printArray(dd,100);
printArray(xx,100);
6

Χρήση προτύπου συνάρτησης


• Μπορεί οποιοσδήποτε τύπος ορισμένος από το χρήστη να
χρησιμοποιηθεί με ένα πρότυπο συνάρτησης;
• Όχι πάντα, μόνο οι τύποι δεδομένων που υποστηρίζουν όλες τις λειτουργίες
που χρησιμοποιούνται στη συνάρτηση.
• Στο προηγούμενο παράδειγμα αν η κλάση myclass δεν έχει υπερφορτώσει
τον τελεστή <<, τότε η συνάρτηση printArray δεν θα λειτουργήσει για το
συγκεκριμένο τύπο δεδομένων.

7
Πρότυπα κλάσεων (class templates)
• Μερικές φορές είναι χρήσιμο να επιτρέπεται η αποθήκευση τιμών
διαφορετικών τύπων σε μια κλάση.
• Δείτε τα παραδείγματα simplelist1 και simplelist2
https://github.com/chgogos/oop/blob/master/various/COP3330/lect16/simplelist1.h
Λίστα 10
https://github.com/chgogos/oop/blob/master/various/COP3330/lect16/simplelist1.cpp θέσεων με
ακεραίους
https://github.com/chgogos/oop/blob/master/various/COP3330/lect16/main_simplelist1.cpp

https://github.com/chgogos/oop/blob/master/various/COP3330/lect16/simplelist2.cpp Λίστα 10
θέσεων με
https://github.com/chgogos/oop/blob/master/various/COP3330/lect16/simplelist2.cpp ακεραίους ή
πραγματικούς
https://github.com/chgogos/oop/blob/master/various/COP3330/lect16/main_simplelist2.cpp

Πρότυπα κλάσεων
• Η ίδια ιδέα με τα πρότυπα συναρτήσεων εφαρμόζεται και στα
πρότυπα κλάσεων.
• Δείτε το παράδειγμα simplelist3

https://github.com/chgogos/oop/blob/master/various/COP3330/lect16/simplelist3.h

https://github.com/chgogos/oop/blob/master/various/COP3330/lect16/simplelist3.cpp

https://github.com/chgogos/oop/blob/master/various/COP3330/lect16/main_simplelist3.cpp

9
Πρότυπα κλάσεων
• Για να γίνει μια κλάση, πρότυπο κλάσης χρειάζεται να τοποθετηθεί στην
αρχή της δήλωσης της κλάσης ο ακόλουθος κώδικας:
• template<class T> ή template<typename T>
• Εδώ το T είναι απλά μια παράμετρος που υποδηλώνει έναν τύπο.
• Όταν θα δημιουργηθούν αντικείμενα της κλάσης, το T αντικαθίσταται με έναν
πραγματικό τύπο.
• Για να οριστεί μια συνάρτηση μέλος της κλάσης θα πρέπει να
χρησιμοποιηθεί η ακόλουθη σύνταξη:
• className<T>::memberName(…){…}
• Αντίστοιχα, για να δηλωθεί μια μεταβλητή του προτύπου κλάσης θα
πρέπει να χρησιμοποιηθεί η ακόλουθη σύνταξη:
• className<υπαρκτός τύπος δεδομένων> variable;

10

Ένα ακόμα παράδειγμα με πρότυπο κλάσης


• Το πρότυπο MemoryCell μπορεί να template <class Object>
χρησιμοποιηθεί με οποιοδήποτε τύπο class MemoryCell
Object με τις ακόλουθες προϋποθέσεις: {
• Το Object να έχει κατασκευαστή χωρίς private:
παραμέτρους. Object storedValue;
• Το Object να έχει κατασκευαστή
αντιγραφής. public:
• Το Object να έχει τελεστή αντιγραφής. explicit MemoryCell(const Object &initialValue = Object()) :
storedValue(initialValue) {}
• Συμβάσεις const Object &read() const
• Η δήλωση μιας κλάσης προτύπου και ο {
ορισμός της συνήθως συνδυάζονται στο return storedValue;
ίδιο αρχείο.
}
• Δεν είναι εύκολο να διαχωριστούν διότι η
συγγραφή του κώδικα γίνεται πολύπλοκη. void write(const Object &x)
{
• Αυτό αποτελεί διαφορετική σύμβαση σε
σχέση με αυτή που ακολουθείται για άλλες storedValue = x;
κλάσεις και η οποία διαχωρίζει τη δήλωση }
και την υλοποίηση της κλάσης σε };
ξεχωριστά αρχεία.
11
Χρήση του προτύπου κλάσης
• Η κλάση MemoryCell μπορεί να #include <string>
χρησιμοποιηθεί για να #include <iostream>
#include "memorycell.h"
αποθηκεύει τόσο πρωτογενείς
τύπους όσο και τύπους
using namespace std;

κλάσεων. int main()


{
• Η MemoryCell δεν είναι κλάση, MemoryCell<int> m1;
MemoryCell<string> m2("hello");
αλλά είναι ένα πρότυπο κλάσης. m1.write(37);
m2.write(m2.read() + " world");
• Κλάσεις είναι οι: cout << m1.read() << endl << m2.read() << endl;
}
• MemoryCell<int>
• MemoryCell<string>
https://github.com/chgogos/oop/blob/master/various/COP3330/lect16/memorycell.h
https://github.com/chgogos/oop/blob/master/various/COP3330/lect16/main_memorycell.cpp
12

Ερωτήσεις σύνοψης
• Τι είναι τα πρότυπα συναρτήσεων (function templates);
• Τι είναι τα πρότυπα κλάσεων (class templates);
• Τι είναι ο γενερικός προγραμματισμός (generic programming);

13
Συνδεδεμένες λίστες
#17
Τμήμα Πληροφορικής και Τηλεπικοινωνιών (Άρτα)
Πανεπιστήμιο Ιωαννίνων
Γκόγκος Χρήστος

V 1.0

Δομές δεδομένων για την αποθήκευση μιας


συλλογής στοιχείων
• Συχνά υπάρχει η ανάγκη χρήσης δομών δεδομένων για την
αποθήκευση μιας συλλογής στοιχείων.
• Τυπικές λειτουργίες σε αυτές τις δομές είναι η εισαγωγή, η διαγραφή, η
εύρεση μέγιστου στοιχείου, η ενημέρωση κ.λπ.

2
Δομές δεδομένων για την αποθήκευση μιας
συλλογής αντικειμένων
• Τι επιλογές έχουμε;
• Στατικοί πίνακες
• Περιορισμοί:
• Προκαθορισμένη χωρητικότητα, άρα μπορεί να μη γίνεται καλή χρήση του διαθέσιμου χώρου που έχει
δεσμευθεί στη μνήμη.
• Οι λειτουργίες εισαγωγής και διαγραφής μπορεί να έχουν υψηλό κόστος (καθώς γίνονται πολλές αντιγραφές)
αν δεν θέλουμε να αφήνουμε κενά ενδιάμεσα στον πίνακα.
• Πλεονεκτήματα:
• Ευκολία προγραμματισμού.
• Συνεχόμενες θέσεις μνήμης για εύκολη δεικτοδότηση.
• Δυναμικοί πίνακες
• Περιορισμοί:
• Η χωρητικότητα είναι δυναμική, η μνήμη πάλι μπορεί να μη χρησιμοποιείται με τον καλύτερο τρόπο, αλλά σε
ορισμένες περιπτώσεις αποτελεί καλύτερη λύση σε σχέση με τους στατικούς πίνακες.
• Οι λειτουργίες εισαγωγής και διαγραφής μπορεί να έχουν υψηλό κόστος, ειδικά όταν η χωρητικότητα αλλάζει.
• Πλεονεκτήματα:
• Ευκολία προγραμματισμού (ο προγραμματιστής ωστόσο πρέπει να φροντίζει για τη δέσμευση και την
αποδέσμευση μνήμης).
• Συνεχόμενες θέσεις μνήμης για εύκολη δεικτοδότηση.

Δομές δεδομένων για την αποθήκευση μιας


συλλογής αντικειμένων
• Μια ακόμα λύση είναι οι συνδεδεμένες λίστες.
• Πρόκειται για μια πραγματικά δυναμική δομή δεδομένων καθώς κάθε στοιχείο της
δεσμεύεται δυναμικά χρησιμοποιώντας τον τελεστή new όταν αυτό χρειάζεται.
• Η χωρητικότητα της συνδεδεμένης λίστας είναι πάντα ίση με τη μνήμη που έχει
δεσμευθεί (συν κάποια επιβάρυνση για τους δείκτες που χρειάζονται).
• Οι λειτουργίες εισαγωγής και διαγραφής έχουν χαμηλό κόστος.
• Οι θέσεις μνήμης που καταλαμβάνει δεν είναι συνεχόμενες.
• Σημαντικός περιορισμός είναι ότι είτε δεν ορίζεται ο τελεστής [], είτε έχει υψηλό κόστος
χρόνου εκτέλεσης.

• Οι συνδεδεμένες λίστες είναι μια από τις «συνδεδεμένες δομές δεδομένων».


• Άλλες συνδεδεμένες δομές δεδομένων είναι τα δένδρα, τα γραφήματα κ.α.

4
Συνδεδεμένες λίστες και πίνακες
s

• Ένας πίνακας λεκτικών: 0 “abc”


• string s[5]; 1 “white”
• s[0]=“abc”;
• s[1]=“white”; 2 “black”
• s[2]=“black”; 3
• Μια συνδεδεμένη λίστα λεκτικών: 4
• Κάθε στοιχείο της λίστας έχει δύο πεδία
• Ένα πεδίο string.
• Ένα δείκτη που δείχνει προς το επόμενο στοιχείο της λίστας.
head
class listnode
{
public: “abc” “white” “black” NULL
string item;
listnode *next;
};

Συνδεδεμένες λίστες
• Μειώνεται η σπατάλη μνήμης (απαίτηση επιπλέον χώρου μόνο για
τους δείκτες).
• Κάθε κόμβος της λίστας δεσμεύεται δυναμικά με τον τελεστή new.
• Υπάρχουν πολλές παραλλαγές συνδεδεμένων λιστών:
• Απλά συνδεδεμένη λίστα head
• Διπλά συνδεδεμένη λίστα
•… “abc” “white” “black” NULL

head

NULL “abc” “white” “black” NULL

tail 6
Μια διπλά συνδεδεμένη λίστα
• Ας υποθέσουμε ότι αποθηκεύουμε δύο πεδία δεδομένων σε κάθε
κόμβο (ένα λεκτικό και έναν ακέραιο). Τότε η δομή δεδομένων
listnode θα είναι η ακόλουθη:
s

“abc”, 0
class listnode
{
previous next
public:
count
string s;
int count;
listnode *next;
listnode *prev;
listnode(): s(“”), count(0), next(NULL), prev(NULL) {};
listnode(const string &ss, const int &c): s(ss), count(c), next(NULL), prev(NULL) {};
};

Τα ιδιωτικά δεδομένα της διπλά


συνδεδεμένης λίστας
• Δεδομένα που προστατεύονται:
• head: δείκτης προς την κεφαλή της λίστας για τον οποίο ισχύει ότι
head->prev == NULL
• tail: δείκτης προς το τελευταίο στοιχείο της λίστας για το οποίο ισχύει ότι
tail->next == NULL
• size: αριθμός των κόμβων της λίστας
head
class mylist {
… NULL “abc”, 0 “white”, 0 “black”, 0 NULL
private:
listnode *head; tail
listnode *tail;
int size;
};

8
Η δημόσια διεπαφή (public interface) της
mylist (1/2)
• mylist();
• mylist(const mylist &l);
• ~mylist();
• mylist &operator=(const mylist &l);
• void print() const;
• void insertfront(const string &s, const int &c);
• void insertback(const string &s, const int &c);
• void insertbefore(listnode *ptr, const string &s, const int &c);
• void insertafter(listnode *ptr, const string &s, const int &c);
• void insertpos(const int &pos, const string &s, const int &c);

https://github.com/chgogos/oop/blob/master/various/COP3330/lect17/mylist.h
9

Η δημόσια διεπαφή (public interface) της


mylist (2/2)
• void removefront();
• void removeback();
• void remove(listnode *ptr);
• void removepos(const int &pos);
• listnode front() const;
• listnode back() const;
• int length() const;
• listnode *search(const string &s) const;
• listnode *findmaxcount() const;
• void removemaxcount();
• bool searchandinc(const string &s);
https://github.com/chgogos/oop/blob/master/various/COP3330/lect17/mylist.h
10
Υλοποίηση της mylist
• Κατασκευαστές
• Προκαθορισμένος κατασκευαστής: Δημιουργία μιας άδειας λίστας.
• Κατασκευαστής αντιγραφής: Πρέπει να διανύει την υπάρχουσα λίστα και να εισάγει έναν
νέο κόμβο με τις ίδιες τιμές στη λίστα (με τη χρήση του insertback).
• Καταστροφέας
• Πρέπει να διανύει τη λίστα και να διαγράφει όλους τους κόμβους της (που έχουν
δημιουργηθεί με τη χρήση του τελεστή new).
• Τελεστής ανάθεσης, operator=
• Παρόμοια λογική με τον κατασκευαστή αντιγραφής.
• Η συνάρτηση print
• Διανύει τη λίστα και εμφανίζει ένα προς ένα τους κόμβους της.
• Οι υπόλοιπες συναρτήσεις αποτελούν διαφορετικές εκδόσεις της εισαγωγής
(insert), διαγραφής (remove) και αναζήτησης (search) που θα παρουσιαστούν
στη συνέχεια.

https://github.com/chgogos/oop/blob/master/various/COP3330/lect17/mylist.cpp 11

Εισαγωγή (insert)
• insertback
• Δύο περιπτώσεις:
• Εισαγωγή σε άδεια λίστα.
• Εισαγωγή σε λίστα με στοιχεία.
• Εισαγωγή σε άδεια λίστα
• Δημιουργία ενός νέου κόμβου (prev=NULL, next=NULL), και τόσο ο
δείκτης head όσο και δείκτης tail πρέπει να δείχνουν στο νέο κόμβο.
listnode *t = new listnode(s, c);
if (head == NULL) {
head = t;
tail = t;
size++;
}
https://github.com/chgogos/oop/blob/master/various/COP3330/lect17/mylist.cpp 12
insertback (1/4)
• insertback σε μια λίστα με στοιχεία
• Βήμα 1: Δημιουργία του νέου κόμβου
listnode *t = new listnode(s,c);

head
t

NULL “abc”, 0 “black”, 0 “white”, 0 NULL

tail NULL “xxx”, 0 NULL

https://github.com/chgogos/oop/blob/master/various/COP3330/lect17/mylist.cpp 13

insertback (2/4)
• insertback σε μια λίστα με στοιχεία
• Βήμα 2: σύνδεση του νέου κόμβου με την ουρά της λίστας (δείκτης next)
tail->next = t;

head
t

NULL “abc”, 0 “black”, 0 “white”, 0

tail NULL “xxx”, 0 NULL

https://github.com/chgogos/oop/blob/master/various/COP3330/lect17/mylist.cpp 14
insertback (3/4)
• insertback σε μια λίστα με στοιχεία
• Βήμα 3: σύνδεση του νέου κόμβου με την λίστα (δείκτης prev)
t->prev = tail;

head
t

NULL “abc”, 0 “black”, 0 “white”, 0

tail
“xxx”, 0 NULL

https://github.com/chgogos/oop/blob/master/various/COP3330/lect17/mylist.cpp 15

insertback (4/4)
• insertback σε μια λίστα με στοιχεία
• Βήμα 4: αλλαγή του δείκτη tail έτσι ώστε να δείχνει στο νέο κόμβο
tail = t;

head
t

NULL “abc”, 0 “black”, 0 “white”, 0

“xxx”, 0 NULL

tail
https://github.com/chgogos/oop/blob/master/various/COP3330/lect17/mylist.cpp 16
insertbefore (1/5)
• Η εισαγωγή στην αρχή (insertfront) είναι παρόμοια με την
insertback.
• Η εισαγωγή πριν τη κεφαλή είναι ισοδύναμη με την insertfront.
• H εισαγωγή στο ενδιάμεσο της λίστας πριν το δείκτη ptr είναι
διαφορετική: ένας νέος κόμβος πρόκειται να προστεθεί μεταξύ του
ptr->prev και του ptr.
head

NULL “abc”, 0 “black”, 0 “white”, 0 “xxx”, 0 NULL

tail
ptr 17

insertbefore (2/5)
• H εισαγωγή στο ενδιάμεσο της λίστας πριν το δείκτη ptr.
• Ένας νέος κόμβος πρόκειται να προστεθεί μεταξύ του ptr->prev και του ptr.
• Βήμα 1: δημιουργία ενός νέου κόμβου:
listnode *t; t
t = new listnode(s,c);
NULL “yyy”, 0 NULL

head

NULL “abc”, 0 “black”, 0 “white”, 0 “xxx”, 0 NULL

tail
ptr 18
insertbefore (3/5)
• H εισαγωγή στο ενδιάμεσο της λίστας πριν το δείκτη ptr.
• Ένας νέος κόμβος πρόκειται να προστεθεί μεταξύ του ptr->prev και του ptr.
• Βήμα 2: σύνδεση του νέου κόμβου με τη λίστα:
t->next = ptr; t
t->prev = ptr->prev;
“yyy”, 0
head

NULL “abc”, 0 “black”, 0 “white”, 0 “xxx”, 0 NULL

tail
ptr 19

insertbefore (4/5)
• H εισαγωγή στο ενδιάμεσο της λίστας πριν το δείκτη ptr.
• Ένας νέος κόμβος πρόκειται να προστεθεί μεταξύ του ptr->prev και του ptr.
• Βήμα 3: αλλαγή του next για το ptr->prev:
ptr->prev->next = t; t

“yyy”, 0
head

NULL “abc”, 0 “black”, 0 “white”, 0 “xxx”, 0 NULL

tail
ptr 20
insertbefore (5/5)
• H εισαγωγή στο ενδιάμεσο της λίστας πριν το δείκτη ptr.
• Ένας νέος κόμβος πρόκειται να προστεθεί μεταξύ του ptr->prev και του ptr.
• Βήμα 4: αλλαγή του ptr->prev:
ptr->prev = t; t

“yyy”, 0
head

NULL “abc”, 0 “black”, 0 “white”, 0 “xxx”, 0 NULL

tail
ptr 21

Διαγραφή κόμβου (remove)


• removefront
• Δύο περιπτώσεις:
1. Η λίστα έχει ένα μόνο στοιχείο και συνεπώς μετά τη διαγραφή πρέπει να προκύψει η
κενή λίστα.
if (head == tail) { // one item
delete head;
head = tail = NULL;
size = 0;
return;
}

2. Η λίστα έχει περισσότερα από ένα στοιχεία.

https://github.com/chgogos/oop/blob/master/various/COP3330/lect17/mylist.cpp 22
removefront (1/4)
• removefront
• Η λίστα έχει περισσότερα από ένα στοιχεία
• Βήμα 1: listnode *t = head;
head

NULL “abc”, 0 “black”, 0 “white”, 0 “xxx”, 0 NULL

tail

https://github.com/chgogos/oop/blob/master/various/COP3330/lect17/mylist.cpp 23

removefront (2/4)
• removefront
• Η λίστα έχει περισσότερα από ένα στοιχεία
• Βήμα 2: ο δείκτης head προχωρά έτσι ώστε να δείξει το αμέσως επόμενο στοιχείο της
λίστας σε σχέση με αυτό που ήδη έδειχνε: head = head->next;
head

NULL “abc”, 0 “black”, 0 “white”, 0 “xxx”, 0 NULL

tail

https://github.com/chgogos/oop/blob/master/various/COP3330/lect17/mylist.cpp 24
removefront (3/4)
• removefront
• Η λίστα έχει περισσότερα από ένα στοιχεία
• Βήμα 3: αποσύνδεση του δείκτη prev του head: head->prev = NULL;
head

NULL “abc”, 0 NULL “black”, 0 “white”, 0 “xxx”, 0 NULL

tail

https://github.com/chgogos/oop/blob/master/various/COP3330/lect17/mylist.cpp 25

removefront (4/4)
• removefront
• Η λίστα έχει περισσότερα από ένα στοιχεία
• Βήμα 4: delete t;
head

NULL “black”, 0 “white”, 0 “xxx”, 0 NULL

tail

https://github.com/chgogos/oop/blob/master/various/COP3330/lect17/mylist.cpp 26
remove (1/3)
• Διαγραφή του στοιχείου στο οποίο δείχνει ο δείκτης ptr.
• Βήμα 1: αλλαγή του next για το δείκτη ptr->prev
ptr->prev->next = ptr->next;
head

NULL “abc”, 0 “black”, 0 “white”, 0 “xxx”, 0 NULL

tail

ptr

https://github.com/chgogos/oop/blob/master/various/COP3330/lect17/mylist.cpp 27

remove (2/3)
• Διαγραφή του στοιχείου στο οποίο δείχνει ο δείκτης ptr.
• Βήμα 2: αλλαγή του prev για το δείκτη ptr->prev
ptr->next->prev = ptr->prev;
head

NULL “abc”, 0 “black”, 0 “white”, 0 “xxx”, 0 NULL

tail

ptr

https://github.com/chgogos/oop/blob/master/various/COP3330/lect17/mylist.cpp 28
remove (3/3)
• Διαγραφή του στοιχείου στο οποίο δείχνει ο δείκτης ptr.
• Βήμα 3: delete ptr;

head

NULL “abc”, 0 “black”, 0 “xxx”, 0 NULL

tail

ptr

https://github.com/chgogos/oop/blob/master/various/COP3330/lect17/mylist.cpp 29

Αναζήτηση (search)
• Χρησιμοποιώντας μια επανάληψη με while επιτυγχάνεται διάσχιση
όλων των κόμβων της λίστας.
listnode *mylist::search(const string &s)
{
listnode *t = head;

while ((t!=NULL) && (t->s != s)) t = t->next;


return t;
}

https://github.com/chgogos/oop/blob/master/various/COP3330/lect17/mylist.cpp 30
Ερωτήσεις σύνοψης
• Γιατί οι λειτουργίες εισαγωγής και διαγραφής έχουν αυξημένο
υπολογιστικό κόστος στους στατικούς πίνακες και στους δυναμικούς
πίνακες;
• Ποια είναι τα πλεονεκτήματα και ποια τα μειονεκτήματα των
συνδεδεμένων λιστών;
• Τι πλεονεκτήματα και τι μειονεκτήματα έχουν οι διπλά συνδεδεμένες
λίστες έναντι των απλά συνδεδεμένων λιστών;

31

Εξαιρέσεις
#18
Τμήμα Πληροφορικής και Τηλεπικοινωνιών (Άρτα)
Πανεπιστήμιο Ιωαννίνων
Γκόγκος Χρήστος

V 1.0
Χειρισμός εξαιρέσεων στη C++
• Εξαίρεση (exception): ένα λάθος ή μια προβληματική κατάσταση
• π.χ. διαίρεση με το μηδέν, πρόσβαση σε NULL δείκτη, …
• Χειρισμός εξαιρέσεων (exception handling): αφορά την αντιμετώπιση
του λάθους ή της προβληματικής κατάστασης.
• H C++ έχει ενσωματωμένους μηχανισμούς για χειρισμό εξαιρέσεων.
• Αν δεν χρησιμοποιηθεί ο ενσωματωμένος στη γλώσσα μηχανισμός χειρισμού
εξαιρέσεων, τα λάθη και οι προβληματικές καταστάσεις μπορούν να αντιμετωπιστούν
με προσθήκη ελέγχων (ifs) μέσα στον κώδικα.

https://github.com/chgogos/oop/blob/master/various/COP3330/lect18/sample1.cpp
https://github.com/chgogos/oop/blob/master/various/COP3330/lect18/sample2.cpp

Γιατί να χρησιμοποιήσει κανείς χειρισμό


εξαιρέσεων;
• Ο έλεγχος λαθών με χρήση εντολών if αναμιγνύει το χειρισμό λαθών με
τον κώδικα που επιτελεί το κύριο έργο του προγράμματος.
• Πολλά από τα πιθανά λάθη συμβαίνουν πολύ σπάνια.
• Ο κώδικας χειρισμού αυτών των σπάνιων περιπτώσεων δεν θα πρέπει να
αναμιγνύεται με την κύρια λογική του προγράμματος καθώς η κατανόηση και η
αποσφαλμάτωση του κώδικα καθίστανται δύσκολες.
• Με το χειρισμό εξαιρέσεων ο κώδικας που χειρίζεται τις εξαιρέσεις
τοποθετείται ξεχωριστά από την κύρια λογική του προγράμματος και έτσι
βελτιώνεται η αναγνωσιμότητα του κώδικα.
• Ο χειρισμός εξαιρέσεων, συχνά βελτιώνει και την ανοχή σε σφάλματα
(fault tolerance) του προγράμματος καθώς αποτελεί ένα συστηματικότερο
τρόπο για το χειρισμό λαθών.

3
Πότε πρέπει να χρησιμοποιείται ο χειρισμός
εξαιρέσεων;
• Ο χειρισμός εξαιρέσεων δεν αποτελεί πάντα τον πλέον κατάλληλο
τρόπο για το χειρισμό λαθών.
• Π.χ. ο παραδοσιακός τρόπος (με χρήση ifs) είναι καλύτερος για έλεγχο
εισόδου.

• Πότε ο χειρισμός εξαιρέσεων αποτελεί καλύτερη λύση;


• Χειρισμός προβλημάτων που συμβαίνουν σπάνια.
• Χειρισμός προβλημάτων που ο χειρισμός τους δεν αφορά ένα μόνο μπλοκ
κώδικα.
• Όταν πρέπει να χρησιμοποιηθεί μια ομοιόμορφη τεχνική χειρισμού λαθών
από πολλούς προγραμματιστές που ο καθένας εργάζεται στο δικό του τμήμα
κώδικα.
4

Ο χειρισμός εξαιρέσεων στη C++


• Τα μπλοκς try-throw-catch

try
{
… κώδικας που πρέπει να ελεγχθεί για εξαιρέσεις
… πιθανές προκλήσεις εξαιρέσεων (throw exceptions)
}
catch (type1 catch_parameter_1)
{… κώδικας χειρισμού εξαίρεσης τύπου type1}
catch (type2 catch_parameter_2)
{… κώδικας χειρισμού εξαίρεσης τύπου type2}

5
Το μπλοκ try
• Συντακτικό:
try
{
… η κύρια λογική, πιθανά προκαλεί εξαιρέσεις (throw exceptions)
}
• Περιέχει τον κώδικα που πρόκειται να εκτελεστεί όταν η εκτέλεση θα
πραγματοποιηθεί χωρίς προβλήματα
• Ωστόσο, ο κώδικας μπορεί να προκαλέσει εξαιρέσεις, άρα θα πρέπει να
πραγματοποιηθεί «δοκιμαστική» λειτουργία (try).
• Αν κάτι μη συνηθισμένο συμβεί, αυτό υποδηλώνεται με την πρόκληση μιας
εξαίρεσης (throw exception).

Η εντολή throw
• Σύνταξη εντολής throw:
• throw exception_for_value_to_be_thrown;
• Σημασία:
• Υποδηλώνει ότι συνέβη μια εξαίρεση.
• Αν μια εντολή throw εκτελεστεί, το μπλοκ try αυτόματα τερματίζει.
• Το πρόγραμμα προσπαθεί να ταιριάξει την εξαίρεση με κάποιο από τα catch μπλοκς που
ακολουθούν το try μπλοκ (που περιέχουν τον κώδικα χειρισμού της κάθε εξαίρεσης).
• Το ταίριασμα γίνεται με βάση τον τύπο της τιμής που γίνεται throw.
• Αν βρεθεί κάποιο ταίριασμα, εκτελείται ο κώδικας στο catch μπλοκ του.
• Μόνο ένα catch μπλοκ μπορεί να ταιριάξει το πολύ.
• Το πρόγραμμα συνεχίζει την εκτέλεσή του με τον κώδικα μετά το τελευταίο catch μπλοκ.
• Τόσο η τιμή της εξαίρεσης όσο και η ροή ελέγχου μεταβιβάζονται (γίνονται
throw) στο catch μπλοκ που αποτελεί το χειριστή της εξαίρεσης.

7
To catch μπλοκς
• Ένα ή περισσότερα catch μπλοκς ακολουθούν το try μπλοκ.
• Κάθε catch μπλοκ έχει έναν τύπο παραμέτρου.
• Κάθε catch μπλοκ είναι ένας χειριστής εξαιρέσεων (που χειρίζεται
εξαιρέσεις ενός τύπου).
• catch (type catch_block_parameter)
{ … κώδικας χειρισμού εξαίρεσης }
• Η catch_block_parameter συλλαμβάνει (catches) την τιμή της εξαίρεσης
που προκαλείται και η οποία μπορεί να χρησιμοποιηθεί στον κώδικα
χειρισμού της εξαίρεσης.
• Η εξαίρεση που προκαλείται ταιριάζει με μια από τις παραμέτρους των
catch μπλοκς.
• Αν αυτό δεν συμβεί τότε έχουμε την κατάσταση στην οποία δεν γίνεται catch μια
εξαίρεση (un-caught exception).

Σύνοψη του try-throw-catch


• Σε κανονικές συνθήκες (δηλαδή στο συνηθέστερο σενάριο) εκτελείται το
try μπλοκ και στη συνέχεια ο κώδικας μετά το τελευταίο catch μπλοκ.
• Αν το try μπλοκ προκαλέσει μια εξαίρεση (δηλαδή εκτελεστεί μια εντολή
throw) τότε:
• Το try μπλοκ σταματά αμέσως μετά την εντολή throw.
• Ο κώδικας στο catch μπλοκ ξεκινά να εκτελείται με τη throw τιμή να περνά ως
παράμετρος του κατάλληλου catch μπλοκ.
• Όταν ολοκληρώσει την εκτέλεση του το catch μπλοκ, εκτελείται ο κώδικας μετά το
catch μπλοκ.
• Αν προκληθεί μια εξαίρεση (γίνει throw) αλλά δεν ταιριάξει με κάποιο
από τα διαθέσιμα catch μπλοκς τότε η συνάρτηση τερματίζει.
https://github.com/chgogos/oop/blob/master/various/COP3330/lect18/sample3.cpp
https://github.com/chgogos/oop/blob/master/various/COP3330/lect18/sample4.cpp
9
Πολλαπλά throws και catches
• Κάθε catch μπλοκ χειρίζεται έναν τύπο εξαίρεσης.
• Μπορεί να χρειαστούμε πολλά catch μπλοκς.
• Όταν η τιμή που γίνεται throw δεν είναι σημαντική για το χειρισμό
της εξαίρεσης, τότε το όνομα της παραμέτρου στο catch μπλοκ
μπορεί να παραληφθεί.
• Π.χ. catch(int) {…}
• To catch(…) συλλαμβάνει οποιαδήποτε εξαίρεση, και μπορεί να
χρησιμοποιηθεί ως ο προκαθορισμός χειριστής εξαιρέσεων.
• To catch(…) τοποθετείται ως τελευταίο catch μπλοκ.
https://github.com/chgogos/oop/blob/master/various/COP3330/lect18/sample5.cpp
10

Κλάσεις εξαιρέσεων
• Συχνά ορίζονται κλάσεις με σκοπό το χειρισμό εξαιρέσεων.
• Μπορούν να χρησιμοποιηθούν έτσι ώστε για κάθε κλάση εξαίρεσης
να υπάρχει διαφορετική κλάση.
• Οι κλάσεις εξαιρέσεων είναι κανονικές κλάσεις που απλά
χρησιμοποιούνται με σκοπό το χειρισμό εξαιρέσεων.

https://github.com/chgogos/oop/blob/master/various/COP3330/lect18/sample6.cpp

11
Οι ενσωματωμένες κλάσεις εξαιρέσεων του
προτύπου της C++
• Στην C++ υπάρχει η standard βιβλιοθήκη που περιλαμβάνει
ορισμένες προκαθορισμένες κλάσεις εξαιρέσεων.
• Η βασική κλάση είναι η exception και βρίσκεται στο αρχείο
επικεφαλίδας exception:
• #include <exception>
using std::exception;
• Ο προγραμματιστής μπορεί να ορίσει δικές του κλάσεις εξαιρέσεων
που να κληρονομούν από την κλάση std::exception.
https://github.com/chgogos/oop/blob/master/various/COP3330/lect18/sample7.cpp

12

Παράγωγες κλάσεις εξαιρέσεων της


std::exception
• Ορισμένες παράγωγες κλάσεις της std::exception είναι:
• bad_alloc ➔ προκαλείται από το new όταν δημιουργείται πρόβλημα κατά
τη δέσμευση μνήμης.
• bad_cast ➔ προκαλείται σε περίπτωση αποτυχίας του dynamic_cast.
• logic_error ➔ σφάλμα σχετιζόμενο με την εσωτερική λογική του
κώδικα.
• out_of_range ➔ σφάλμα εκτός περιοχής.
• invalid_argument ➔ μη έγκυρο όρισμα.
• runtime_error ➔ σφάλμα κατά το χρόνο εκτέλεσης.
• domain_error ➔ σφάλμα που υποδηλώνει ότι χρησιμοποιήθηκε μια τιμή
εκτός του εύρους αποδεκτών τιμών.
• #include <stdexcept>
13
Πρόκληση μιας εξαίρεσης μέσα από μια
συνάρτηση
• Μέχρι τώρα, οι εξαιρέσεις προκαλούνται (γίνονται throw) και
συλλαμβάνονται (γίνονται catch) στο ίδιο επίπεδο κώδικα.
• Στην πράξη, τα προγράμματα χωρίζονται σε τμήματα με τη χρήση
συναρτήσεων.
• Οι συναρτήσεις μπορεί να χρειάζεται να προκαλέσουν εξαιρέσεις
που θα συλλαμβάνονται σε άλλες συναρτήσεις.

https://github.com/chgogos/oop/blob/master/various/COP3330/lect18/sample8.cpp

14

noexcept και throw()


• Στη δήλωση μιας συνάρτησης μπορεί να προσδιοριστεί ότι ο
συγκεκριμένος κώδικας δεν προκαλεί εξαιρέσεις. Αυτό γίνεται με τη
δεσμευμένη λέξη noexcept.
• Αν μια συνάρτηση έχει δηλωθεί ως noexcept και προκαλεί εξαίρεση ή
καλεί άλλη συνάρτηση που μπορεί να προκαλέσει εξαίρεση τότε
δημιουργείται σφάλμα και καλείται η std::terminate.
• Το noexcept είναι ισοδύναμο με το noexcept(true) και με το
throw().
• Το noexcept(false) σηματοδοτεί ότι ο κώδικας μπορεί να προκαλέσει
εξαιρέσεις και η προκαθορισμένη συμπεριφορά του είναι να μεταδώσει
την εξαίρεση ένα επίπεδο παραπάνω (propagate), δηλαδή στη συνάρτηση
που κάλεσε τον κώδικα που προκάλεσε την εξαίρεση.
https://github.com/chgogos/oop/blob/master/various/COP3330/lect18/sample9.cpp
15
Κόστος εξαιρέσεων
• Οι εξαιρέσεις όταν συμβαίνουν είναι κατά πολύ περισσότερο
χρονοβόρες σε σχέση με τους απλούς ελέγχους if.
• Συνεπώς, ή χρήση τους συνίσταται όταν η συνάρτηση από μόνη της
δεν μπορεί να χειριστεί το συμβάν ή δεν είναι σχεδιαστικά σωστό να
το κάνει καθώς η συγκεκριμένη αρμοδιότητα μπορεί να ανήκει σε
κάποια άλλη κλάση.

16

Ερωτήσεις σύνοψης
• Τι είναι και πως λειτουργούν τα try-throw-catch μπλοκς;
• Ποια είναι τα πλεονεκτήματα και ποια τα μειονεκτήματα των εξαιρέσεων;
• Τι μπορεί να γίνει throw σε έναν κώδικα;
• Ποιος είναι ο ρόλος του catch(…), σε ποια θέση τοποθετείται και γιατί;
• Τι είναι η κλάση std::exception;
• Τι συμβαίνει αν μια εξαίρεση δεν συλλαμβάνεται (γίνεται catch);
• Ποιος είναι ο ρόλος της δεσμευμένης λέξης noexcept;
• Ποιος είναι ο ρόλος της εξαίρεσης logic_error ποιος της εξαίρεσης
runtime_error;

17
Χρήσιμες έννοιες στη C++ και
μερικές δομές δεδομένων
#19
Τμήμα Πληροφορικής και Τηλεπικοινωνιών (Άρτα)
Πανεπιστήμιο Ιωαννίνων
Γκόγκος Χρήστος

V 1.0

Προγράμματα C++ με ορίσματα γραμμής


εντολών
• int main(int argc, char* argv[]){…}
• argc είναι το πλήθος των ορισμάτων γραμμής εντολών, τα ορίσματα
διαχωρίζονται με κενά μεταξύ τους.
• argv είναι ένας πίνακας δεικτών προς λεκτικά που περιέχουν τα ορίσματα
γραμμής εντολών

https://github.com/chgogos/oop/blob/master/various/COP3330/lect19/sample1.cpp

2
Αρχεία κεφαλίδων (header files) C++
• Τα αρχεία πηγαίου κώδικα $ g++ sample2.cpp
In file included from myheader2.h:1,
χρησιμοποιούν την οδηγία from sample2.cpp:5:
#include προκειμένου να myheader1.h:1:7: error: redefinition of 'class
συμπεριλάβουν τα αρχεία abc'
κεφαλίδων. class abc {
^~~
• Μερικές φορές η χρήση αρχείων In file included from sample2.cpp:4:
κεφαλίδων μπορεί να myheader1.h:1:7: note: previous definition of
δημιουργήσει προβλήματα. 'class abc'
• Κάνοντας include το ίδιο header file class abc {
δύο ή περισσότερες φορές μπορεί να ^~~
δημιουργηθούν λάθη «διπλής
δήλωσης». https://github.com/chgogos/oop/blob/master/various/COP333
0/lect19/sample2.cpp
• Κάνοντας include δύο φορές το
stdio.h ωστόσο δεν δημιουργείται https://github.com/chgogos/oop/blob/master/various/COP333
αντίστοιχο πρόβλημα. Γιατί; 0/lect19/myheader1.h
https://github.com/chgogos/oop/blob/master/various/COP333
0/lect19/myheader2.h 3

Header files στη C++


• Ο ακόλουθος μηχανισμός αποτρέπει το να συμπεριληφθεί το σώμα
ενός header αρχείου πολλαπλές φορές.

#ifndef MYHEADER
#define MYHEADER

/* το σώμα του header ή #pragma once
αρχείου */
#endif

https://github.com/chgogos/oop/blob/master/various/COP3330/lect19/sample3.cpp

4
Μακροεντολές με παραμέτρους
• Οι μακροεντολές με παραμέτρους μοιάζουν και λειτουργούν
παρόμοια με τις συναρτήσεις
• #define max(a,b) (a>b)?a:b

• Οι μακροεντολές με παραμέτρους πρέπει να ορίζονται προσεκτικά


αλλιώς μπορεί να παράγονται μη αναμενόμενα αποτελέσματα.
• Ποιο είναι το πρόβλημα στην ακόλουθη μακροεντολή;
#define sum(a,b) a+b

cout << 10 * sum(2, 3) << endl; // αποτέλεσμα 23 και όχι 50 !

C++ λειτουργίες ανά bit (bitwise)


• 1 byte = 8 bits
• Ένα byte είναι η μικρότερη ποσότητα μνήμης η οποία μπορεί να αντιστοιχιστεί
σε μια μεταβλητή (char c).
• Ωστόσο, μπορούμε να χρησιμοποιούμε κάθε επιμέρους bit με τους bitwise
τελεστές:
• bitwise and (ΚΑΙ): &
• 0xFF & 0x00 == 0x00
• bitwise or (Ή): |
• 0xFF & 0x00 == 0xFF
• bitwise exclusive or (ΑΠΟΚΛΕΙΣΤΙΚΟ Ή): ^
• 0xFF & 0x00 == 0xFF
• left shift (ΑΡΙΣΤΕΡΗ ΟΛΙΣΘΗΣΗ): <<
• right shift (ΔΕΞΙΑ ΟΛΙΣΘΗΣΗ): >>
• bitwise complement (ΣΥΜΠΛΗΡΩΜΑ): ~
• ~0xF0 == 0x0F

6
Bitwise λειτουργίες
• unsigned char x = 8 (00001000)
• unsigned char x = 192 (11000000)

• x = 00001000 = 00001000
• y = 11000000 = 11000000
• x&y = 00000000 x|y = 11001000

• Έλεγχος αν το bit στη θέση 2 (τρίτο από δεξιά προς αριστερά) του x είναι 1:
• if (x & 0x04 !=0)
• Με αυτό τον τρόπο επιτυγχάνεται σύνταξη λογικών εκφράσεων με βάση τα bits
μιας μεταβλητής
7

Δομές δεδομένων
• Οι δομές δεδομένων επιτρέπουν την ευκολότερη συγγραφή
προγραμμάτων.
• Οι δομές δεδομένων εστιάζουν στην οργάνωση των δεδομένων με
κατάλληλο τρόπο έτσι ώστε να πραγματοποιούνται αποδοτικότερα
λειτουργίες που απαιτούνται.
• Η επιλογή των κατάλληλων δομών δεδομένων είναι εξαιρετικά
σημαντική για τη δημιουργία αποδοτικών προγραμμάτων.
• Πολλές κοινές δομές δεδομένων έχουν υλοποιηθεί στην STL
(Standard Template Library) της C++.

8
Δομές Δεδομένων – ένα παράδειγμα
• Έστω ένα πρόγραμμα στο οποίο ένα μεγάλο σύνολο λέξεων αποθηκεύεται με
κάποιο τρόπο.
• Δεδομένης μιας λέξης, θέλουμε να γνωρίζουμε το που έχει αποθηκευτεί.
• Αν οι λέξεις είναι αποθηκευμένες σε έναν πίνακα ή σε μια συνδεδεμένη λίστα ή
σε ένα διάνυσμα τότε θα πρέπει να περάσουμε από όλα τα δεδομένα (στη
χειρότερη περίπτωση) έτσι ώστε να βρεθεί η θέση του στοιχείου που ψάχνουμε,
δηλαδή η λειτουργία αυτή θα έχει πολυπλοκότητα χρόνου Ο(Ν).
• Μια καλύτερη δομή δεδομένων για το συγκεκριμένο πρόβλημα είναι ένας
πίνακας κατακερματισμού που εντοπίζει το στοιχείο προς αναζήτηση σε χρόνο
Ο(1).
• Σε μεγάλα σύνολα δεδομένων η χρήση πίνακα κατακερματισμού στη θέση
δομών δεδομένων όπως οι πίνακες, οι συνδεδεμένες λίστες και τα διανύσματα
επιτυγχάνουν επιτάχυνση του χρόνου εκτέλεσης της τάξης του 100.
9

Δομές Δεδομένων – ένα ακόμα παράδειγμα


• Έστω το πρόβλημα του χρονοπρογραμματισμού εργασιών που εκτελούνται σε
μια CPU.
• Καθώς συμβαίνει πολύ συχνά (~50 φορές ανά δευτερόλεπτο), πρέπει να υλοποιηθεί με τον
πλέον αποδοτικό τρόπο.
• Ένας πίνακας αποθηκεύει τις πληροφορίες της κάθε εργασίας που πρέπει να
εκτελεστεί μαζί με την προτεραιότητα της.
• Ζητείται η επιλογή της εργασίας με την υψηλότερη προτεραιότητα κάθε φορά προκειμένου
να εκτελεστεί.
• Οι λειτουργίες που θα πρέπει να υποστηρίζονται είναι:
• Εισαγωγή μιας νέας διεργασίας με δεδομένη προτεραιότητα στη δομή.
• Αφαίρεση της διεργασίας με την υψηλότερη προτεραιότητα από τη δομή.
• Πως μπορούν και οι δύο λειτουργίες να γίνουν αποδοτικά;
• Ο πίνακας (είτε είναι ταξινομημένος είτε όχι) δεν αποτελεί αποδοτική λύση.
• Η χρήση της δομής Ουρά Προτεραιότητας (priority queue) είναι ιδανική.
• Η ουρά προτεραιότητας (σωρός μεγίστων) διατηρεί το στοιχείο με τη μεγαλύτερη προτεραιότητα σε θέση που
μπορεί να προσπελάσει σε χρόνο Ο(1), ενώ η αφαίρεση του μέγιστου στοιχείου από την ουρά προτεραιότητας
όπως και η προσθήκη ενός νέου στοιχείου ολοκληρώνεται σε χρόνο Ο(logN).

10
Αφηρημένοι τύποι δεδομένων (ADT=Abstract
Data Types)
• Ένα αφηρημένος τύπος δεδομένων (ADT=Abstract Data Type) ορίζεται από τις
λειτουργίες τις οποίες πρέπει να υποστηρίζει και δεν περιέχει λεπτομέρειες
υλοποίησης
• Οι δομές δεδομένων αποτελούν συνήθως υλοποιήσεις αφηρημένων τύπων
δεδομένων: στοίβες, ουρές, διανύσματα, συνδεδεμένες λίστες, δένδρα
• Στοίβες (stacks)
• LIFO (Last In First Out). Οι εισαγωγές και οι διαγραφές επιτρέπονται μόνο από την κορυφή
της στοίβας.
• Μια στοίβα έχει δύο βασικές λειτουργίες:
• push: προσθέτει ένα στοιχείο στην κορυφή της στοίβας
• pop: αφαιρεί ένα στοιχείο από την κορυφή της στοίβας
• Τυπικές περιοχές εφαρμογών των στοιβών είναι οι μεταγλωττιστές, τα λειτουργικά
συστήματα, η διαχείριση της μνήμης του προγράμματος κατά την κλήση συναρτήσεων κ.α.

11

Ουρές (queues)
• FIFO (First In First Out): Οι εισαγωγές γίνονται στο πίσω άκρο της
ουράς, και οι διαγραφές γίνονται από το εμπρός άκρο της ουράς.
• Μια ουρά έχει δύο βασικές λειτουργίες:
• enqueue: προσθήκη ενός στοιχείου στην ουρά
• dequeue: αφαίρεση ενός στοιχείου από την ουρά
• Τυπικές περιοχές εφαρμογών στις οποίες χρησιμοποιούνται ουρές
είναι η ουρά αναμονής του εκτυπωτή, ο προγραμματισμός εργασιών
στα λειτουργικά συστήματα κ.α.

12
Διανύσματα (vectors)
• Τα διανύσματα είναι δομές δεδομένων που αποθηκεύουν δεδομένα του
ίδιου τύπου και βασίζονται στην αποθήκευσή τους σε κάποιο πίνακα.
• Ενθυλακώνοντας έναν πίνακα σε μια κλάση, μπορούμε:
• να χρησιμοποιήσουμε δυναμική δέσμευση μνήμης έτσι ώστε να αυξομειώνουμε το
μέγεθος του πίνακα εφόσον χρειάζεται.
• να χειριζόμαστε απόπειρες πρόσβασης εκτός των ορίων του πίνακα.
• Πλεονέκτημα: τυχαία πρόσβαση (πρόσβαση απευθείας σε κάποιο στοιχείο
με βάση το δείκτη).
• Μειονεκτήματα: Οι εισαγωγές και οι διαγραφές ενδιάμεσα στη δομή είναι
αργές καθώς μπορεί να απαιτούν την ολίσθηση πολλών στοιχείων που
καταλαμβάνουν διαδοχικές θέσεις στον πίνακα.

13

Συνδεδεμένες λίστες (linked lists)


• Οι συνδεδεμένες λίστες αποτελούν συλλογές δεδομένων που συνδέονται με τη χρήση δεικτών,
σε μια σειρά.
• Αποτελούνται από συλλογές «κόμβων», που δημιουργούνται από μια αναδρομική κλάση (ή
struct).
• Αναδρομική κλάση: μια κλάση που τα μέλη δεδομένων της περιέχουν τουλάχιστον ένα δείκτη που δείχνει
προς ένα αντικείμενο της ίδια κλάσης.
• Κάθε κόμβος περιέχει κάποια δεδομένα, και έναν δείκτη προς το επόμενο κόμβο (στην απλά συνδεδεμένη
λίστα)
• Οι κόμβοι μπορούν να βρίσκονται οπουδήποτε στη μνήμη και όχι υποχρεωτικά σε συνεχόμενες θέσεις
μνήμης όπως στους πίνακες.
• Οι κόμβοι συνήθως δεσμεύονται δυναμικά, συνεπώς μια συνδεδεμένη λίστα μπορεί να γίνει όσο μεγάλη
απαιτείται.
• Πλεονεκτήματα: Οι εισαγωγές και οι διαγραφές είναι συνήθως γρήγορες. Απαιτούν τη
δημιουργία ενός νέου κόμβου και την αλλαγή κάποιων δεικτών.
• Μειονεκτήματα: Δεν υποστηρίζουν τυχαία πρόσβαση. Ο εντοπισμός ενός στοιχείου της λίστας
απαιτεί τη διάσχιση της λίστας.
• Παρατηρήστε ότι τα πλεονεκτήματα των διανυσμάτων αποτελούν μειονεκτήματα των
συνδεδεμένων λιστών και αντίστροφα.
14
Δένδρα (trees)
• Τα δένδρα είναι μη-γραμμικές συλλογές δεδομένων, που συνδέονται με
δείκτες (όπως οι συνδεδεμένες λίστες).
• Αποτελούνται από κόμβους (με αναδρομικούς ορισμούς). Κάθε κόμβος
μπορεί να περιέχει δύο ή περισσότερους δείκτες προς άλλους κόμβους.
• Τυπική περίπτωση δένδρου είναι τα δυαδικά δένδρα (Binary Trees):
• Κάθε κόμβος περιέχει ένα στοιχείο δεδομένων, και δύο δείκτες που καθένας δείχνει
προς έναν άλλο κόμβο.
• Είναι ιδιαίτερα χρήσιμα για τη γρήγορη αναζήτηση καθώς και για τη διατήρηση
δεδομένων σε ταξινομημένη σειρά.
• Η αναζήτηση σε ένα δυαδικό δένδρο αναζήτησης (BST=Binary Search Tree)
συνίσταται στην εύρεση μιας διαδρομής στο δένδρο, που ξεκινά από τη ρίζα του
δένδρου και σε κάθε επιλογή μονοπατιού (επιλογή αριστερού ή δεξιού κόμβου)
απαλείφει τις μισές από τις εναπομείνασες αποθηκευμένες τιμές της δομής.

15

Ερωτήσεις σύνοψης
• Ποιος είναι ο ρόλος των argc και argv μεταβλητών στην main;
• Τι εξυπηρετεί η δήλωση #pragma once σε ένα αρχείο header;
• Πως αλλιώς μπορεί να γραφεί ισοδύναμος κώδικας με τη δήλωση
#pragma once ;
• Ποιο είναι το αποτέλεσμα της εντολής 56 << 1;
• Πότε η συνδεδεμένη λίστα αποτελεί καλύτερη λύση σε σχέση με το
διάνυσμα;
• Μπορεί να κατασκευαστεί τελεστής τυχαίας προσπέλασης [] για μια
συνδεδεμένη λίστα;

16
Pointers in C and C++
Βασικές γνώσεις για τους δείκτες στη C++
Νοέμβριος 2019
Τμήμα Πληροφορικής και Τηλεπικοινωνιών
Πανεπιστήμιο Ιωαννίνων
Γκόγκος Χρήστος

https://github.com/chgogos/ceteiep_dsa/tree/master/appendix_pointers

Απλό παράδειγμα με δείκτη (τελεστές &, *)

#include <cstdio>
using namespace std;

int main()
{
int x = 5; x=7 &x=61fe1c
int *px; // δήλωση χωρίς αρχικοποίηση δείκτη
px=61fe1c *px=7, &px=61fe10
px = &x; // αρχικοποίηση δείκτη
*px = 7; // αποαναφορά (dereference) δείκτη
printf("x=%i &x=%x\n", x, &x);
printf("px=%x *px=%i, &px=%x\n", px, *px, &px);
}

2
simple_pointer_example.cpp
Συνηθισμένο λάθος με δείκτη
#include <cstdio>
using namespace std;

int main()
{
int *p1;
int *p2 = NULL;
int *p3 = nullptr; // C++ guidelines
printf("p1=%x, p2=%x, p3=%x\n", p1, p2, p3); p1=6, p2=0, p3=0

*p1 = 7; // μη προβλέψιμη συμπεριφορά


printf("bye");
}

3
uninitialized_pointer.cpp

Δείκτης σε struct
#include <iostream>
using namespace std;

struct point {
int x;
int y;
};

int main() {
point a = {3, 2};
point *p = &a;
p->x = 4; // ή (*p).x = 4;
p->y = 5; // ή (*p).y = 5;
}

4
pointer_to_struct.cpp
Δείκτης σε union
#include <cstdio>
using namespace std;
union my_union {
int i;
double d;
char c;
}; 1 0.00
int main() {
1374389535 3.14
my_union mu; 1374389601 3.14 a
my_union *p = &mu;
printf("sizeof union is %d\n", sizeof(mu));

p->i = 1;
printf("%d %.2f %c\n", p->i, p->d, p->c);

p->d = 3.14;
printf("%d %.2f %c\n", p->i, p->d, p->c);

p->c = 'a';
printf("%d %.2f %c\n", p->i, p->d, p->c);
}

5
pointer_to_union.cpp

Κλήση με αναφορά (call by reference)


#include <iostream>
using namespace std;

// κλήση με αναφορά για την παράμετρο y (σε C και C++)


void fun1(int x, int *y) {
x++;
(*y)++;
}
56
// κλήση με αναφορά για την παράμετρο y (C++) 57
void fun2(int x, int &y) {
x++;
y++;
}

int main() {
int a=5,b=5;
fun1(a,&b);
cout << a << " " << b << endl;
fun2(a,b);
cout << a << " " << b << endl;
}

6
call_by_reference.cpp
Δείκτες και πίνακες (αριθμητική δεικτών)
#include <iostream>
using namespace std;

int main() {
int a[] = {1, 2, 3, 4, 5};
int *p;
p = a; // ή p=&a[0];
*p = 7; // {7, 2, 3, 4, 5};
4
p++;
*p = 7; // {7, 7, 3, 4, 5};
p += 2;
*p = 7; // {7, 7, 3, 7, 5};
int *p1 = &a[0];
int *p2 = &a[4];
cout << p2 - p1 << endl; // εμφανίζει 4
}

7
pointer_arithmetic.cpp

Διαφορά πίνακα και δείκτη


#include <iostream>
using namespace std;

int main() { 1-1-1


int a[] = {1, 2, 3, 4, 5};
2-2-2
int n = sizeof(a) / sizeof(a[0]);
int *p = a; 3-3-3
for(int i=0;i<n;i++) { 4-4-4
cout << a[i] << "-" << p[i] << "-" << *(p+i) << endl; 5-5-5
} size of a: 20
cout << "size of a: " << sizeof(a) << endl; size of p: 8
cout << "size of p: " << sizeof(p) << endl;
p++;
// a++; // error: lvalue required as increment operand
}

8
pointer_vs_array.cpp
Δυναμική δέσμευση μνήμης (C)
#include <cstdio>
#include <cstdlib>

int main() {
int *p = (int *)malloc(sizeof(int));
*p = 5;
printf("p=%x *p=%i\n", p, *p);
free(p);
p=cd1360 *p=5
p = (int *)calloc(sizeof(int), 1); p=cd1360 *p=0
printf("p=%x *p=%i\n", p, *p); p=cd1360 p[0]=1 p[1]=2
free(p); p=cd1360 p[0]=1 p[1]=2, p[2]=3
p = (int *)malloc(sizeof(int)*2);
p[0]=1;
p[1]=2;
printf("p=%x p[0]=%i p[1]=%i\n", p, p[0], p[1]);

p = (int*)realloc(p,3);
p[2]=3;
printf("p=%x p[0]=%i p[1]=%i, p[2]=%i\n", p, p[0], p[1], p[2]);
free (p);
}
9
dynamic_in_c.cpp

Δυναμική δέσμευση μνήμης για struct (C)


#include <cstdlib>
#include <cstdio>
using namespace std;

struct point {
int x;
int y;
}; 11

int main() {
point *p = (point *)malloc(sizeof(point));
p->x = 1;
p->y = 1;
printf("%i %i\n", p->x, p->y);
free(p);
}

10
dynamic_memory_allocation_struct1.cpp
Δυναμική δέσμευση μνήμης (C++)
#include <iostream>
#include <cstdio>
p=6d1690 *p=5
int main() { p=6d1690 *p[0]=7154192 *p[1]=0 *p[2]=7143760
int *p = new int;
p=6d1690 *p[0]=0 *p[1]=0 *p[2]=0
*p = 5;
printf("p=%x *p=%i\n", p, *p);
delete p;

p = new int[3];
printf("p=%x *p[0]=%i *p[1]=%i *p[2]=%i\n", p, p[0], p[1], p[2]);
delete[] p;

p = new int[3](); // αρχικοποίηση με 0


printf("p=%x *p[0]=%i *p[1]=%i *p[2]=%i\n", p, p[0], p[1], p[2]);
delete[] p;
}

11
dynamic_in_cpp.cpp

Δυναμική δέσμευση μνήμης για struct (C++)


#include <iostream>
using namespace std;

struct point {
int x;
int y;
}; 11

int main() {
point *p = new point;
p->x = 1;
p->y = 1;
cout << p->x << " " << p->y << endl;
delete p;
}

12
dynamic_memory_allocation_struct2.cpp
Η περίπτωση του *p++
#include <iostream>
using namespace std;

int main() {
int a[] = {10, 20, 30, 40, 50};
int *p = a; 10
int sum = 0;
20
for (int i = 0; i < 5; i++) {
cout << *p << endl; 30
sum += *p++; 40
} 50
cout << "sum=" << sum << endl; sum=150
}

13
pointer_plus_plus.cpp

Δείκτης προς δυναμικά δεσμευμένο πίνακα


εγγραφών
#include <iostream>
using namespace std;

struct point {
int x;
int y;
POINTER TO DYNAMIC ARRAY: 1 1
};

int main() {
point *p1 = new point[5];
p1[0].x = 1;
p1[0].y = 1;
cout << "POINTER TO DYNAMIC ARRAY: " << p1[0].x << " " << p1[0].y << endl;
delete[] p1;
}

14
pointer_to_dynamic_array.cpp
Αυτόματος πίνακας δεικτών προς εγγραφές
#include <iostream>
using namespace std;

struct point {
int x;
int y;
};

int main() { AUTOMATIC ARRAY OF POINTERS: 1 1


point *p2[5]; // automatic array of pointers
for (int i = 0; i < 5; i++) {
p2[i] = new point;
}
p2[0]->x = 1;
p2[0]->y = 1;
cout << “AUTOMATIC ARRAY OF POINTERS: " << p2[0]->x << " " << p2[0]->y << endl;
for (int i = 0; i < 5; i++) {
delete p2[i];
}
}
15
automatic_array_of_pointers.cpp

Δυναμικός πίνακας δεικτών προς εγγραφές


#include <iostream>
using namespace std;
struct point {
int x;
int y;
};
int main() {
point **p3 = new point *[5]; // dynamic array of pointers
for (int i = 0; i < 5; i++) {
p3[i] = new point; DYNAMIC ARRAY OF POINTERS: 1 1
}
p3[0]->x = 1;
p3[0]->y = 1;
cout << "DYNAMIC ARRAY OF POINTERS: " << p3[0]->x << " " << p3[0]->y << endl;
for (int i = 0; i < 5; i++) {
delete p3[i];
}
delete[] p3;
}
16
dynamic_array_of_pointers.cpp
Δείκτες σε void
#include <cstdio>
using namespace std;
void fun1(int *p) {
printf("fun(int*) >>> &p=%x p=%x *p=%i\n", &p, p, *p);
}
void fun2(double *p) { 1. ip=1b1690 dp=1b16b0 vp=0 *ip=7 *dp=7.70
printf("fun(double*) >>> &p=%x p=%x *p=%.2f\n", &p, p, *p); 2. ip=1b1690 dp=1b16b0 vp=1b1690 *ip=1 *dp=7.70
} fun(int*) >>> &p=61fdd0 p=1b1690 *p=1
int main() { 3. ip=1b1690 dp=1b16b0 vp=1b16b0 *ip=1 *dp=1.50
int *ip = new int(7); fun(double*) >>> &p=61fdd0 p=1b16b0 *p=1.50
double *dp = new double(7.7);
void *vp = nullptr;
printf("1. ip=%x dp=%x vp=%x *ip=%i *dp=%.2f\n", ip, dp, vp, *ip, *dp);

vp = ip;
*(int *)vp = 1;
printf("2. ip=%x dp=%x vp=%x *ip=%i *dp=%.2f\n", ip, dp, vp, *ip, *dp);
fun1((int *)vp);

vp = dp;
*(double *)vp = 1.5;
printf("3. ip=%x dp=%x vp=%x *ip=%i *dp=%.2f\n", ip, dp, vp, *ip, *dp);
fun2((double *)vp);
} 17
void_pointer.cpp

Δείκτες συναρτήσεων (function pointers)


#include <iostream>
using namespace std;

int fun1(int a, int b){


return a + b;
}

int fun2(int a, int b){ 9


return a*b; 20
}

int fun(int (*fn)(int, int), int a, int b){


return fn(a,b);
}

int main(){
cout << fun(fun1, 4,5) << endl;
cout << fun(fun2, 4,5) << endl;
}

18
function_pointers1.cpp
f(-3.1415)=-9.26536e-05
f(-2.5132)=-0.587845

Δείκτες συναρτήσεων f(-1.8849)=-0.951074


f(-1.2566)=-0.951045
f(-0.6283)=-0.58777
f(4.44089e-16)=4.44089e-16
#include <iostream> f(0.6283)=0.58777
f(1.2566)=0.951045
#include <iostream> f(1.8849)=0.951074
#include <cmath> f(2.5132)=0.587845
using namespace std; f(3.1415)=9.26536e-05
f(-3.1415)=-1
f(-2.5132)=-0.808973
void calc(double (*fn)(double), double from, double to, int points) { f(-1.8849)=-0.308964
double step = (to - from) / points; f(-1.2566)=0.309052
f(-0.6283)=0.809028
for (int i = 0; i <= points; i++) { f(4.44089e-16)=1
double x = from + i * step; f(0.6283)=0.809028
double y = fn(x); f(1.2566)=0.309052
f(1.8849)=-0.308964
cout << "f(" << x << ")=" << y << endl; f(2.5132)=-0.808973
} f(3.1415)=-1
} f(0)=0
f(10)=3.16228
f(20)=4.47214
int main() { f(30)=5.47723
double PI = 3.1415; f(40)=6.32456
f(50)=7.07107
calc(sin, -PI, PI, 10); f(60)=7.74597
calc(cos, -PI, PI, 10); f(70)=8.3666
calc(sqrt, 0, 100, 10); f(80)=8.94427
f(90)=9.48683
} f(100)=10
19
function_pointers2.cpp

Δείκτες συναρτήσεων
#include <iostream>
#include <vector> aristea chris maria nikos
#include <algorithm>
using namespace std;
chris maria nikos aristea

int compare_string_by_length(string s1, string s2) {


return s1.length() - s2.length();
} sort(v.begin(), v.end(), compare_string_by_length);
int main() {
vector<string> v = {"chris", "maria", "nikos", "aristea"}; ή
sort(v.begin(), v.end());
int (*cfs)(string, string) = compare_string_by_length;
for (string s : v) { sort(v.begin(), v.end(), cfs);
cout << s << " ";
} ή
cout << endl;
auto cfs2 = compare_string_by_length;
// ταξινόμηση με βάση το μήκος λεκτικών (αύξουσα) sort(v.begin(), v.end(), cfs2);
int (*cfs)(string, string) = compare_string_by_length;
sort(v.begin(), v.end(), cfs);
for (string s : v) {
cout << s << " ";
}
cout << endl;
}
20
function_pointers3.cpp
Δείκτης σε δείκτη: **
#include <iostream>
using namespace std;
struct node {
int value;
node *next;
};
void push_front(node **head, int x) {
node *new_node = new node;
new_node->value = x; 975
new_node->next = *head;
*head = new_node;
}
void print_list(node *head) {
node *cur = head;
while (cur != NULL) {
cout << cur->value << " "; cur = cur->next;
}
cout << endl;
}
int main() {
node **head = new node*;
push_front(head, 5); push_front(head, 7); push_front(head, 9);
print_list(*head);
}
21
linked_list1.cpp

Αναφορά σε δείκτη: *&


#include <iostream>
using namespace std;
struct node {
int value;
node *next;
};
void push_front(node *&head, int x) {
node *new_node = new node;
new_node->value = x;
975
new_node->next = head;
head = new_node;
}
void print_list(node *head) {
node *cur = head;
while (cur != NULL) {
cout << cur->value << " "; cur = cur->next;
}
cout << endl;
}
int main() {
node *head = NULL;
push_front(head, 5); push_front(head, 7); push_front(head, 9);
print_list(head);
}
22
linked_list2.cpp
Συνάρτηση που επιστρέφει δείκτη
#include <iostream>
using namespace std;
struct node {
int value;
node *next;
};
node *push_front(node *head, int x) {
node *new_node = new node;
new_node->value = x; 975
new_node->next = head;
return new_node;
}
void print_list(node *head) {
node *cur = head;
while (cur != NULL)
cout << cur->value << " "; cur = cur->next;
cout << endl;
}
int main() {
node *head = NULL;
head = push_front(head, 5); head = push_front(head, 7); head = push_front(head, 9);
print_list(head);
}
23
linked_list3.cpp

Οι iterators των vectors είναι δείκτες


#include <iostream>
#include <vector>

using namespace std;

int main() {
vector<int> v = {1, 2, 3, 4, 5};

vector<int>::iterator itr = v.begin();


12345
while (itr != v.end()) { 23456
cout << *itr << " ";
(*itr)++;
itr++;
}
cout << endl;

for (auto it = v.begin(); it != v.end(); it++) {


cout << *it << " ";
}
cout << endl;
}

24
vector_iterator_is_a_pointer.cpp
Οι iterators των maps είναι δείκτες
#include <iostream>
#include <map>
using namespace std;
int main() {
std::map<std::string, int> months = {
{"Jan", 31}, {"Feb", 28}, {"Mar", 31}, {"Apr", 30}
};
map<string, int>::iterator itr = months.begin(); Apr 30
while (itr != months.end()) { Feb 28
if (itr->second == 31)
itr = months.erase(itr);
else
++itr;
}
for (auto itr = months.cbegin(); itr != months.cend(); ++itr)
cout << itr->first << " " << itr->second << endl;
}

25
map_iterator_is_a_pointer.cpp

Απλός δείκτης σε int (ο ρόλος του const)


#include <cstdio>
using namespace std;

int main() {
int a = 5, b = 10;
const int c = 20; pointer to int: a=6 b=11 c=20 *p1=11
int *p1; // pointer to int

p1 = &a;
(*p1)++;
p1 = &b;
(*p1)++;
// p1 = &c; // error: assigning to 'int *' from incompatible type 'const int *'
printf("pointer to int: a=%i b=%i c=%i *p1=%i\n", a, b, c, *p1);

26
const_and_pointers.cpp
Δείκτης σε const int
#include <cstdio>
using namespace std;

int main()
{
int a = 5, b = 10;
const int c = 20;
int const *p2; // pointer to const int pointer to const int: a=5 b=10 c=20 *p2=20

p2 = &a;
p2 = &b;
p2 = &c;
printf("pointer to const int: a=%i b=%i c=%i *p2=%i\n", a, b, c, *p2);
// p2 = &a;
// (*p2)++; // error: read-only variable is not assignable
}

27
const_and_pointers.cpp

const δείκτης σε int


#include <cstdio>
using namespace std;

int main()
{
int a = 5, b = 10;
const int c = 20; const pointer to int: a=6 b=10 c=20 *p3=6
int *const p3 = &a; // const pointer to int

(*p3)++;
// p3 = &b; // error: cannot assign to variable 'p3' with const-qualified type 'int *const'
printf("const pointer to int: a=%i b=%i c=%i *p3=%i\n", a, b, c, *p3);
}

28
const_and_pointers.cpp
const δείκτης σε const int

#include <cstdio>
using namespace std;

int main()
{
int a = 5, b = 10; const pointer to const int: a=5 b=10 c=20 *p4=5
const int c = 20;
int const *const p4 = &a; // const pointer to const int

// (*p4)++; // error: read-only variable is not assignable


// p4 = &b; // error: cannot assign to variable 'p4' with const-qualified type 'const int *const'
// p4 = &c; // error: cannot assign to variable 'p4' with const-qualified type 'const int *const'
printf("const pointer to const int: a=%i b=%i c=%i *p4=%i\n", a, b, c, *p4);
}

29
const_and_pointers.cpp

Ο δείκτης this
#include <iostream>
using namespace std;
class point {
private:
int x, y;
public:
point() {}
void set_xy(int x, int y) {
this->x = x; 0x62fe18(x=1, y=2)
this->y = y;
}
void display() {
cout << this << "(x=" << x << ", y=" << y << ")" << endl;
}
};
int main() {
point a_point;
a_point.set_xy(1, 2);
a_point.display();
}
30
this_pointer.cpp
Δυναμικός δισδιάστατος πίνακας (δείκτης σε
δείκτη)
#include <iostream>
using namespace std;
void print(int **a, int m, int n) {
for (int i = 0; i < m; i++) {
for (int j = 0; j < n; j++)
cout << a[i][j] << " ";
cout << endl;
}
} 12
int main() { 34
int m = 3, n = 2, c = 0;
56
int **mat = new int *[m];
for (int i = 0; i < m; i++) {
mat[i] = new int[n];
for (int j = 0; j < n; j++)
mat[i][j] = ++c;
}
print(mat, m, n);
for (int i = 0; i < m; i++)
delete[] mat[i];
delete[] mat;
}
31
2d_array.cpp

STL
Standard Template Library
Τμήμα Πληροφορικής και Τηλεπικοινωνιών (Άρτα)
Πανεπιστήμιο Ιωαννίνων
Γκόγκος Χρήστος

V 1.0
Standard Template Library
• Η STL είναι μια βιβλιοθήκη επαναχρησιμοποιήσιμων στοιχείων που
βασίζεται στο γενερικό προγραμματισμό (προγραμματισμό με templates).
• Η STL αποτελείται από
• Containers (περιέκτες): Επιτρέπουν την οργάνωση μιας συλλογής αντικειμένων στη
μνήμη του Η/Υ. Πρόκειται για templated κλάσεις (π.χ. vector<int>, list<double>, …).
• Algorithms (αλγόριθμοι): Αλγόριθμοι που εφαρμόζονται σε containers (π.χ. sort,
find, …). Είναι γενικοί και μπορούν να χρησιμοποιηθούν σε διάφορους τύπους
containers.
• Iterators (επαναλήπτες): «Δείκτες» που επιτρέπουν τη διάσχιση και σε ορισμένες
περιπτώσεις την αλλαγή ενός container. Ένας iterator δείχνει σε κάποιο στοιχείο
ενός container.

Containers
• Σε ένα container μπορούν να αποθηκευτούν τιμές βασικών τύπων καθώς και αντικείμενα.
• Τα containers χωρίζονται σε δύο βασικές κατηγορίες:
• Ακολουθιακά containers (sequence containers) – κάθε αντικείμενο ακολουθείται από κάποιο άλλο
αντικείμενο, έχουν δηλαδή γραμμική διευθέτηση.
• vector (διάνυσμα)
• array (διάνυσμα σταθερού μεγέθους)
• list (διπλά συνδεδεμένη λίστα)
• forward_list (απλά συνδεδεμένη λίστα)
• deque (ουρά με δύο άκρα)
• container adaptors
• stack (στοίβα)
• queue (ουρά)
• priority_queue (ουρά προτεραιότητας)
• Containers αντιστοίχισης (associative containers) – γίνεται χρήση κλειδιών για την πρόσβαση στα στοιχεία
του container, επιτρέπουν τη γρήγορη πρόσβαση βάσει κλειδιών.
• set (σύνολο)
• multiset
• map (πίνακας αντιστοίχισης)
• multimap

3
std::vector
• Διάνυσμα με δυνατότητα δυναμικής αύξησης του μεγέθους του έτσι ώστε
να δεχθεί νέα στοιχεία.
• Constructors
• vector<T> v; // άδειο vector
• vector<T> v(n); // vector με n αντίγραφα της προκαθορισμένης τιμής του Τ
• vector<T> v(n, value); // vector με n αντίγραφα της τιμής value
• Προσθήκη νέων στοιχείων std::vector<int> v1{1, 2, 3, 4};
• push_back()
• Μέγεθος – χωρητικότητα 1 2 3 4
• size()
• capacity() v.begin() v.end()

Δεικτοδότηση σε vector
• Δεικτοδότηση με []
• Υψηλή απόδοση, η πρόσβαση γίνεται χωρίς έλεγχο ορίων του vector.
• Δεικτοδότηση με at()
• Πραγματοποιεί έλεγχο ορίων, προκαλεί την εξαίρεση out_of_range exception
αν επιχειρηθεί πρόσβαση εκτός των ορίων του vector.
https://chgogos.github.io/oop/cpp_playground/ex046/stl_vector1.cpp
https://chgogos.github.io/oop/cpp_playground/ex046/stl_vector2.cpp
https://chgogos.github.io/oop/cpp_playground/ex046/stl_vector3.cpp
https://chgogos.github.io/oop/cpp_playground/ex046/stl_vector4.cpp
https://chgogos.github.io/oop/cpp_playground/ex046/stl_vector5.cpp

5
std::array
• Διάνυσμα με προκαθορισμένο μέγεθος
• Βασικές λειτουργίες:
• size()
• τελεστής []
std::array<int, 5> a{1, 2, 3, 4, 5};

https://chgogos.github.io/oop/cpp_playground/ex046/stl_array.cpp

std::deque
• Διάνυσμα με δύο άκρα.
• Βασικές συναρτήσεις μέλη:
• operator[]
• at()
• front() https://chgogos.github.io/oop/cpp_playground/ex046/stl_deque.cpp
• push_front()
• back()
• push_back()
• …

7
std::forward_list
• Απλά συνδεδεμένη λίστα
• Βασικές συναρτήσεις μέλη:
• front()
• πρόσβαση στο πρώτο στοιχείο της λίστας
• push_front() https://chgogos.github.io/oop/cpp_playground/ex046/stl_forward_list.cpp

• εισαγωγή στοιχείου στην αρχή της λίστας


• pop_front()
• διαγραφή του πρώτου στοιχείου της λίστας
• sort()
• ταξινόμηση
• remove_if()
• διαγραφή στοιχείων που ικανοποιούν συγκεκριμένα κριτήρια
• …

std::list
• Διπλά συνδεδεμένη λίστα
• Βασικές συναρτήσεις μέλη:
• front()
• πρόσβαση στο πρώτο στοιχείο της λίστας
• push_front()
• εισαγωγή στοιχείου στην αρχή της λίστας https://chgogos.github.io/oop/cpp_playground/ex046/stl_list.cpp
• back()
• πρόσβαση στο πρώτο στοιχείο της λίστας
• push_back()
• εισαγωγή στοιχείου στο τέλος της λίστας
• sort()
• ταξινόμηση
• reverse()
• Αντιστροφή
• …

9
std::set
• Σε ένα set αποθηκεύονται αντικείμενα που καθένα από αυτά
αποτελεί το ίδιο ή περιέχει ένα κλειδί.
• Κάθε κλειδί μπορεί να υπάρχει μόνο μια φορά το πολύ.
• Τα κλειδιά είναι ταξινομημένα.
• To std::set συνήθως υλοποιείται ως ισορροπημένο δυαδικό δένδρο
αναζήτησης.
• Παραλλαγές του set
• multiset: επιτρέπει πολλές εμφανίσεις του ίδιου κλειδιού.
• unordered_set: δεν υπάρχει διάταξη με βάση τα κλειδιά, υλοποιείται ως
πίνακας κατακερματισμού.
https://chgogos.github.io/oop/cpp_playground/ex046/stl_set.cpp
https://chgogos.github.io/oop/cpp_playground/ex046/stl_set2.cpp 10

std::map
• Σε ένα map αποθηκεύονται ζεύγη (key, value), δηλαδή για κάθε
κλειδί υπάρχει μια σχετιζόμενη τιμή.
• Κάθε κλειδί μπορεί να υπάρχει μόνο μια φορά το πολύ.
• Τα ζεύγη είναι ταξινομημένα με βάση τα κλειδιά.
• Παραλλαγές του map:
• multimap: επιτρέπει πολλές εμφανίσεις του ίδιου κλειδιού.
• unordered_map: δεν υπάρχει διάταξη με βάση τα κλειδιά, υλοποιείται ως
πίνακας κατακερματισμού.
https://chgogos.github.io/oop/cpp_playground/ex046/stl_map.cpp
https://chgogos.github.io/oop/cpp_playground/ex046/stl_map2.cpp
https://chgogos.github.io/oop/cpp_playground/ex046/stl_multimap.cpp
11
Container Adaptor κλάσεις
• stack (στοίβα)
• queue (ουρά)
• priority_queue (ουρά προτεραιότητας)
https://chgogos.github.io/oop/cpp_playground/ex046/stl_stack.cpp
https://chgogos.github.io/oop/cpp_playground/ex046/stl_queue.cpp
https://chgogos.github.io/oop/cpp_playground/ex046/stl_priority_queue.cpp

12

Iterators
• Οι iterators είναι αντικείμενα που μοιάζουν με δείκτες και χρησιμοποιούνται για
την πρόσβαση στα περιεχόμενα ενός container.
• Επιτρέπουν τη «διάσχιση» ενός container από το ένα στοιχείο του στο επόμενο.
• Οι συναρτήσεις begin() και end() επιτρέπουν την αναφορά στο πρώτο
στοιχείο ενός container και στη θέση αμέσως μετά από το τελευταίο στοιχείο
ενός container, αντίστοιχα. Εναλλακτικά μπορούν να χρησιμοποιηθούν οι
συναρτήσεις μέλη begin() και end() για containers όπως το vector.
vector<int> v;

vector<int>::iterator iter=begin(v);
ή
auto iter = begin(v);
ή
auto iter = v.begin(); // συνάρτηση μέλος του vector

13
Iterators
• Οι iterators αποτελούν ένα μηχανισμό για τον καθορισμό μιας θέσης
μέσα σε ένα container.
• Υπάρχουν διάφοροι τύποι iterators, αλλά όλοι χρησιμοποιούν τον
ίδιο βασικό τρόπο διάσχισης και πρόσβασης στα στοιχεία ενός
container, δηλαδή:
• Τον τελεστή ++ για τη μετακίνηση στο επόμενο στοιχείο.
• Τον τελεστή * για την πρόσβαση στο τρέχον στοιχείο.
• Σύγκριση ενός iterator με έναν άλλο iterator.

14

Τύποι iterators
• Υπάρχουν συγκεκριμένοι random
access
iterators που μπορούν να
χρησιμοποιηθούν με κάθε
bidirectional
container.
• Υπάρχουν 5 βασικοί τύποι
forward
iterators: random access,
bidirectional, forward, input,
output input

output

15
Τύπος iterator ανά container
Container Iterator • O vector<int>::iterator είναι random
vector random access access iterator για πρόσβαση σε ακεραίους.
deque random access • O list<float>::iterator είναι
bidirectional iterator για πρόσβαση σε
list bidirectional πραγματικούς.
set bidirectional
• O forward_list<student>::iterator
multiset bidirectional είναι forward iterator για πρόσβαση σε
map bidirectional αντικείμενα student.
multimap bidirectional • Οι container adaptors (stack, queue,
forward_list forward
priority_queue) δεν έχουν iterators.
unordered_set forward
https://chgogos.github.io/oop/cpp_playground/ex046/stl_random_iterator.cpp
https://chgogos.github.io/oop/cpp_playground/ex046/stl_bidirectional_iterator.cpp
https://chgogos.github.io/oop/cpp_playground/ex046/stl_forward_iterator.cpp
16

Iterator adaptors για εισαγωγή (inserters –


insert iterators)
• Οι insert iterators είναι adaptors • Υπάρχουν 3 είδη inserters,
που επιτρέπουν σε αλγορίθμους ανάλογα με το container που θα
να λειτουργούν εισάγοντας δεχθεί τις τιμές:
επιπλέον στοιχεία στα • back_inserter(), αν το container
containers αντί να ενημερώνουν υποστηρίζει push_back()
τα ήδη υπάρχοντα στοιχεία. • front_inserter(), αν το container
υποστηρίζει push_front()
• Έτσι οι αλγόριθμοι μπορούν να • inserter(), αν το container
προσθέτουν στοιχεία σε υποστηρίζει insert()
container προορισμούς που δεν
είναι ήδη επαρκώς μεγάλα για
να δεχθούν τις νέες τιμές.
https://chgogos.github.io/oop/cpp_playground/ex046/insert_iterators.cpp

17
stream iterator
• vector<int> v{1,2,3,4,5};
copy(v.begin(), v.end(), ostream_iterator<int>(cout, “ “));

https://github.com/chgogos/oop/blob/master/cpp_playground/ex046/stream_iterator1.cpp

• vector<int> v;
copy(istream_iterator<int>(cin), istream_iterator<int>(),
back_inserter(v))

https://github.com/chgogos/oop/blob/master/cpp_playground/ex046/stream_iterator2.cpp

18

Χρήσιμες συναρτήσεις για iterators


• Μόνο οι random access iterators επιτρέπουν την πρόσθεση ή την
αφαίρεση μιας ακέραιας τιμής καθώς και την αφαίρεση ενός iterator
από έναν άλλο. Ωστόσο, η ίδια λειτουργικότητα μπορεί να επιτευχθεί
σε άλλους iterators με τις συναρτήσεις:
• advance(iter, n) που μετακινεί τον iterator προς τα μπροστά ή προς τα
πίσω ανάλογα με το αν η παράμετρος n λάβει θετική ή αρνητική τιμή.
• distance(iter1, iter2) που επιστρέφει το πλήθος θέσεων ανάμεσα
στους δύο iterators (η τιμή που επιστρέφεται πρέπει να γίνει cast σε int).

https://chgogos.github.io/oop/cpp_playground/ex046/advance.cpp
https://chgogos.github.io/oop/cpp_playground/ex046/distance.cpp

19
Δείκτες συναρτήσεων (function pointers)
• Οι δείκτες συναρτήσεων είναι μεταβλητές που κρατούν τη διεύθυνση
μιας συνάρτησης.
• «Περίεργη» σύνταξη
• int (*fp)(int, int); // δήλωση function pointer με όνομα fp προς μια
συνάρτηση που δέχεται 2 ακεραίους και επιστρέφει έναν ακέραιο.
• Αν υπάρχει μια συνάρτηση της μορφής int fun(int, int) τότε μπορεί ο
function pointer να δείχνει σε αυτή τη συνάρτηση:
• fp = fun;
• Η δε κλήση της συνάρτησης μέσω του function pointer γίνεται ως εξής:
• x = fp(2,3); ή ως x = (*fp)(2,3);
https://chgogos.github.io/oop/cpp_playground/ex001/function_pointers1.cpp
https://chgogos.github.io/oop/cpp_playground/ex001/function_pointers2.cpp
https://chgogos.github.io/oop/cpp_playground/ex001/function_pointers3.cpp 20

Δείκτες συναρτήσεων (function pointers)


• Στη C++ 11 υπάρχει η δυνατότητα δήλωσης ενός function pointer με
το std::function που ορίζεται στο header <functional>.
• std::function<int(int,int)> fp; // δήλωση function pointer με όνομα fp προς
μια συνάρτηση που δέχεται 2 ακεραίους και επιστρέφει έναν ακέραιο.

https://chgogos.github.io/oop/cpp_playground/ex001/function_pointers4.cpp
https://chgogos.github.io/oop/cpp_playground/ex001/function_pointers5.cpp
https://chgogos.github.io/oop/cpp_playground/ex001/function_pointers6.cpp

21
Αντικείμενα συναρτήσεων (functors =
function objects)
• Functor είναι οποιοδήποτε αντικείμενο στο οποίο έχει
υπερφορτωθεί ο τελεστής () και συνεπώς μπορεί να
χρησιμοποιηθεί με παρενθέσεις σαν να είναι συνάρτηση.
• Ένα functor μπορεί να διατηρεί κατάσταση καθώς ως αντικείμενο
μπορεί να διαθέτει μέλη δεδομένων.
• Συνήθως ένα functor είναι ταχύτερο από ένα function pointer.

22

Functors που ορίζονται από το χρήστη


class FunctionObjectType
{
public:
τύπος_επιστροφής operator()(παράμετροι)
{
}
};

https://chgogos.github.io/oop/cpp_playground/ex046/functor1.cpp
https://chgogos.github.io/oop/cpp_playground/ex046/functor2.cpp
https://chgogos.github.io/oop/cpp_playground/ex046/functor3.cpp

23
Τύποι functors – η περίπτωση των predicates
• Στην STL υπάρχουν τα ακόλουθα functors:
• generators, πρόκειται για functors που καλούνται χωρίς παραμέτρους.
• unary functions, πρόκειται για functors που καλούνται με μια παράμετρο.
• binary functions , πρόκειται για functors που καλούνται με δύο
παραμέτρους.
• Τα κατηγορήματα (predicates) είναι functors που επιστρέφουν μια
λογική τιμή.
• Μια unary function που επιστρέφει μια λογική τιμή είναι ένα predicate.
• Μια binary function που επιστρέφει μια λογική τιμή είναι ένα binary
predicate.
• Παράδειγμα χρήσης functor με τη remove_if
https://chgogos.github.io/oop/cpp_playground/ex046/remove_if_functor.cpp
24

Functors της STL (1/2)


• Αριθμητικά δυαδικά • Σχεσιακά δυαδικά κατηγορήματα
κατηγορήματα • equal_to<T> f;
• plus<T> f; • f(x,y) επιστρέφει x == y.
• f(x,y) επιστρέφει x + y. • not_equal_to<T> f;
• minus<T> f; • f(x,y) επιστρέφει x != y.
• f(x,y) επιστρέφει x - y. • less<T> f;
• multiplies<T> f; • f(x,y) επιστρέφει x < y.
• f(x,y) επιστρέφει x - y. • less_equal<T> f;
• divides<T> f; • f(x,y) επιστρέφει x <= y.
• f(x,y) επιστρέφει x / y. • greater<T> f;
• modulus<T> f; • f(x,y) επιστρέφει x > y.
• f(x,y) επιστρέφει x % y. • greater_equal<T> f;
• f(x,y) επιστρέφει x >= y.

25
Functors της STL (2/2)
• Λογικά δυαδικά κατηγορήματα • Αριθμητικά μοναδιαία functors
• logical_and<T> f; • negate<T> f;
• f(x,y) επιστρέφει x && y. • f(x) επιστρέφει -x.
• logical_or<T> f; • logical_or<T> f;
• f(x,y) επιστρέφει x || y. • f(x) επιστρέφει !x.

https://chgogos.github.io/oop/cpp_playground/ex046/functor4.cpp
https://chgogos.github.io/oop/cpp_playground/ex046/functor5.cpp

26

Λάμδας (lambdas)
• Λάμδα (lambda ή closure) είναι μια ανώνυμη συνάρτηση που μπορεί
να γραφεί απευθείας ως παράμετρος ή γενικότερα να
χρησιμοποιηθεί σε μια έκφραση.
auto my_lambda = [](int x)->int {return x*2;};
cout << my_lambda(5); // εμφανίζει την τιμή 10

27
Σύνταξη λάμδα συναρτήσεων: [](){}
• [captures](parameters)->return_type
{statements;}
• [captures]
• Ποιες μεταβλητές έξω από τη λάμδα θα είναι
διαθέσιμες στις εντολές της λάμδα και αν αυτές
οι μεταβλητές θα περνούν με τιμή ή με
αναφορά.
• (parameters)
https://chgogos.github.io/oop/cpp_playground/ex071/lambda1.cpp

• παράμετροι που θα χρειάζονται για την κλήση https://chgogos.github.io/oop/cpp_playground/ex071/lambda2.cpp


της λάμδα (μπορεί η λίστα παραμέτρων να είναι
κενή). https://chgogos.github.io/oop/cpp_playground/ex071/lambda3.cpp
• -> ret
• προαιρετικό, αν παραληφθεί ο μεταγλωττιστής https://chgogos.github.io/oop/cpp_playground/ex071/lambda4.cpp
«διαπιστώνει» τον τύπο επιστροφής από τις
εντολές return που περιέχονται στο σώμα της
λάμδα.
• {statements;}
• Το σώμα της λάμδα.

28

function pointers vs function objects


(functors) vs lambdas
• Στο παράδειγμα fp_functor_lambda.cpp όλα τα στοιχεία ενός
std::vector<int> διπλασιάζονται. Η εργασία γίνεται με τη
χρήση της συνάρτηση transform() που δέχεται ως 4η παράμετρο
έναν function pointer ή έναν functor ή ένα lambda.

https://chgogos.github.io/oop/cpp_playground/ex087/fp_functor_lambda.cpp

29
Αλγόριθμοι: sort(), merge()
• Η sort() ταξινομεί μια περιοχή τιμών.
• H stable_sort() ταξινομεί διατηρώντας τη μεταξύ τους σειρά για
στοιχεία που έχουν το ίδιο κλειδί.
• Η merge() συγχωνεύει δύο ταξινομημένες περιοχές τιμών σε μια.

https://github.com/chgogos/oop/blob/master/cpp_playground/ex086/stl_sort.cpp
https://github.com/chgogos/oop/blob/master/cpp_playground/ex086/stl_stable_sort.cpp
https://github.com/chgogos/oop/blob/master/cpp_playground/ex086/stl_merge.cpp

30

Αλγόριθμοι: find(), find_if(), find_if_not()


• H find() επιστρέφει έναν iterator στο πρώτο στοιχείο μιας περιοχής
τιμών που ισούται με την παράμετρο που δέχεται.
• H find_if() επιστρέφει έναν iterator στο πρώτο στοιχείο μιας περιοχής
τιμών για την οποία το predicate που δέχεται ως παράμετρο είναι
αληθές.
• H find_if_not() επιστρέφει έναν iterator στο πρώτο στοιχείο μιας
περιοχής τιμών για την οποία το predicate που δέχεται ως
παράμετρο είναι ψευδές.
https://github.com/chgogos/oop/blob/master/cpp_playground/ex086/stl_find.cpp

https://github.com/chgogos/oop/blob/master/cpp_playground/ex086/stl_set_find.cpp

31
Αλγόριθμοι: count(), count_if()
• H count() καταμετρά τις εμφανίσεις μιας τιμής σε μια περιοχή τιμών.
• Η count_if() καταμετρά τα στοιχεία σε μια περιοχή τιμών για τα
οποία το predicate που δέχεται ως παράμετρο είναι αληθές.

https://github.com/chgogos/oop/blob/master/cpp_playground/ex086/stl_count.cpp

32

Αλγόριθμοι: for_each()
• Εφαρμόζει ένα lambda σε κάθε στοιχείο μιας περιοχής.

vector<int> v{4, 3, 6, 1, 2, 8, 7};


for_each(v.begin(), v.end(), [](int &x) { x++; });

https://github.com/chgogos/oop/blob/master/cpp_playground/ex086/stl_for_each.cpp

33
erase remove idiom
• Η διαγραφή ενός συνόλου τιμών από ένα vector με βάση κάποιο
predicate γίνεται ως εξής:

vector<int> v{7,13,2,8,9,1,8,4,5};
// διαγραφή περιττών τιμών από το v
v.erase(remove_if(v.begin(), v.end(), [](int x){return x%2==1;}), v.end());

2884

34

map – filter – reduce


• Μια συχνά χρησιμοποιούμενη αλληλουχία ενεργειών είναι η map –
filter – reduce:
• map: αντιστοίχιση κάθε τιμής σε κάποια άλλη τιμή.
• filter: φιλτράρισμα τιμών έτσι ώστε να διατηρηθεί ένα υποσύνολο από
αυτές.
• reduce: συνάθροιση όλων των τιμών σε μια.
• H αλληλουχία ενεργειών map – filter – reduce στη C++ υλοποιείται
ως εξής:
• map: transform()
• filter: remove_if() + erase()
• reduce: accumulate()
https://chgogos.github.io/oop/cpp_playground/ex071/map_filter_reduce.cpp
35
Άλλοι αλγόριθμοι της STL
• Υπάρχει πληθώρα επιπλέον αλγορίθμων στην STL (> 100)

36

C++ vs Java
Τμήμα Πληροφορικής και Τηλεπικοινωνιών (Άρτα)
Πανεπιστήμιο Ιωαννίνων
Γκόγκος Χρήστος

V 1.0
Δημοφιλείς γλώσσες προγραμματισμού
(2020)

https://www.tiobe.com/tiobe-index/

https://redmonk.com/sogrady/2020/07/27/language-rankings-6-20/
https://pypl.github.io/PYPL.html 2

Αντικειμενοστρέφεια
• Τόσο η C++ όσο και η Java υποστηρίζουν τον αντικειμενοστραφή
προγραμματισμό
• Στην Java τα πάντα(;) είναι αντικείμενα και δεν επιτρέπει σε δεδομένα ή
συναρτήσεις να υπάρχουν εκτός κλάσεων
• Η C++ υποστηρίζει και το μοντέλο διαδικασιακού προγραμματισμού (procedural
programming)
• Η Java αντιμετωπίζει διαφορετικά τους πρωτογενείς τύπους (π.χ. int,
double, char κλπ) από τα αντικείμενα
• H Java είναι απλούστερη από τη C++ και περισσότερο ασφαλής
• H Java υποστηρίζει πολυμορφισμό αυτόματα ενώ στη C++ χρειάζεται να
δηλώνονται οι συναρτήσεις μέλη ως virtual
• Η Java υποστηρίζει διεπαφές (interfaces) έναν τρόπο επιβολής
υλοποίησης συγκεκριμένων συναρτήσεων από κλάσεις

3
Σύγκριση χαρακτηριστικών C++ και Java (1/2)

C++ Java
• Υποστηρίζει δείκτες, πολλαπλή • Δεν υποστηρίζει δείκτες, πολλαπλή
κληρονομικότητα, υπερφόρτωση κληρονομικότητα και υπερφόρτωση
τελεστών τελεστών
• O προγραμματιστής είναι υπεύθυνος • Εμπεριέχει ελέγχους για όρια πινάκων
έτσι ώστε να μην βγαίνει εκτός ορίων • Διαθέτει αυτόματο garbage collection
πινάκων
• Η διαχείριση μνήμης είναι ευθύνη του • Είναι αρκετά γρήγορη
προγραμματιστή • Είναι cross-platform (o κώδικας αρχικά
• Είναι γρήγορη μεταγλωττίζεται σε bytecode και στη
συνέχεια διερμηνεύεται από το JVM) -
• Ο κώδικας μεταγλωττίζεται (το WORA(Write Once and Run Everywhere)
εκτελέσιμο δεν εκτελείται σε άλλη
πλατφόρμα)

Σύγκριση χαρακτηριστικών C++ και Java (2/2)

C++ Java
• Υποστηρίζει πέρασμα με τιμή (call • Υποστηρίζει μόνο πέρασμα με τιμή
by value) και πέρασμα με (call by value)
αναφορά (call by reference) • Χρησιμοποιεί generics
• Χρησιμοποιεί templates • Διαθέτει βιβλιοθήκες για μεγάλο
• Στην ίδια τη γλώσσα υπάρχει εύρος λειτουργιών (π.χ. GUIs,
μικρότερος αριθμός βιβλιοθηκών Network programming, JDBC κ.α.)
• Είναι system level • Βρίσκεται υψηλότερα από το
• Χρησιμοποιείται όπου απαιτείται system level
ταχύτατη εκτέλεση κώδικα (π.χ. • Χρησιμοποιείται για ανάπτυξη
επιστημονικές εφαρμογές, εφαρμογών σε κινητές συσκευές
ανάπτυξη παιχνιδιών) (Android)
5
JVM (Java Virtual Machine) - μεταφερσιμότητα κώδικα

JVM Windows

Java bytecode
.java JVM Linux
compiler .class

JVM OSX
bytecode: ενδιάμεση μορφή κώδικα που μπορεί
να εκτελεστεί διερμηνευόμενος από το JVM

Java – επιτάχυνση εκτέλεσης με τον JIT


compiler
Ο JIT compiler μεταγλωττίζει ακολουθίες bytecode σε κώδικα
μηχανής και βελτιστοποιήσεις (π.χ. ελάττωση των
προσπελάσεων στη μνήμη, διατηρώντας δεδομένα που Φάση εκτέλεσης
προσπελαύνονται συχνά σε καταχωρητές)

.java
πηγαίος .class εκτελέσιμος
java compiler JIT compiler
κώδικας σε bytecode κώδικας
java

Φάση μεταγλώττισης

7
Java: Garbage collection (1/2)
• Η συλλογή απορριμμάτων στη Java είναι η διαδικασία της αυτόματης
διαχείρισης μνήμης (δέσμευσης/αποδέσμευσης μνήμης)
• Ο GC (Garbage Collector) εντοπίζει τα αντικείμενα που δεν
χρησιμοποιούνται, τα διαγράφει και ελευθερώνει τη μνήμη που
κατελάμβαναν
• Η δυναμική δέσμευση μνήμης γίνεται (στο heap) με τον τελεστή new
και η μνήμη παραμένει δεσμευμένη έως ότου να πάψουν να
υπάρχουν αναφορές προς τη μνήμη αυτή
• Η συλλογή απορριμμάτων γίνεται αυτόματα καθώς εκτελείται το
πρόγραμμα
8

Java: Garbage collection (2/2)


• To JVM της Oracle, HotSpot, διαθέτει 4 Garbage Collectors (Serial,
Parallel, Concurrent Mask Sweep, Garbage First) με διαφορετικά
χαρακτηριστικά και επιδόσεις
• Η βασική ιδέα λειτουργίας των Garbage Collectors είναι ότι:
• Σε πρώτη φάση εντοπίζουν τα αντικείμενα για τα οποία δεν υπάρχει
αναφορά και τα σημειώνουν ως «έτοιμα προς αποκομιδή»
• Σε δεύτερη φάση διαγράφει τα «έτοιμα προς αποκομιδή» αντικείμενα
• Προαιρετικά, η μνήμη αναδιατάσσεται μετά τη διαγραφή έτσι ώστε τα
εναπομείναντα αντικείμενα να βρίσκονται σε συνεχόμενες θέσεις στην αρχή
του heap

9
Παραδείγματα κώδικα
Υλοποιήσεις σε C++ και σε Java

10

Hello World σε C++ και σε Java

C++ Java
hello.cpp Hello.java
#include <iostream> public class Hello {
using namespace std; public static void main(String[] args) {
int main() System.out.println("Hello World");
{ }
cout << "Hello World" << endl;
} }
$ g++ hello.cpp
$ ./a.out
$ javac Hello.java
Hello World
$ java Hello
https://github.com/chgogos/oop/blob/master/various/CPP_VS_JAVA/helloworld_cpp/hello.cpp
Hello World
https://github.com/chgogos/oop/blob/master/various/CPP_VS_JAVA/helloworld_java/Hello.java

11
Έλεγχος ορίων (bounds check)

C++ bounds.cpp Java Bounds.java

#include <iostream> public class Bounds {


using namespace std; public static void main(String[] args) {
int main() int a[] = new int[] { 0, 1, 2, 3, 4, 5 };
{ for (int i = 0; i < 10; i++) {
int a[] = {0, 1, 2, 3, 4, 5}; System.out.print(a[i] + " ");
for (int i = 0; i < 10; i++) }
{ }
cout << a[i] << " "; }
} $ javac Bounds.java
$ java Bounds
} 0 1 2 3 4 5 Exception in thread "main" java.lang.ArrayIndexOutOfBoundsException: Index
6 out of bounds for length 6
$ g++ bounds.cpp at Bounds.main(Bounds.java:5)
$ ./a.out
0 1 2 3 4 5 16 7 0 0 https://github.com/chgogos/oop/blob/master/various/CPP_VS_JAVA/bounds_check_java/Bounds.java

https://github.com/chgogos/oop/blob/master/various/CPP_VS_JAVA/bounds_check_cpp/bounds.cpp
12

Κλάσεις και αντικείμενα

C++ Java
dogs.cpp public class Dog {
Dog.java
#include <iostream>
using namespace std; private String name;
class dog private String species;
{ public Dog(String n, String s) {
private: name = n;
string name; species = s;
string species; }
public: public void makeSound() {
dog(string n, string s) : name(n), species(s) {} System.out.println("Dog " + name + " of species "
void make_sound() + species + " barks");
{ }
cout << "Dog " << name << " of species " << species public static void main(String[] args) {
<< " barks" << endl; Dog obj = new Dog("Adelle", "Maltese");
} obj.makeSound();
}; }
int main() }
{
dog obj("Adelle", "Maltese"); $ javac Dog.java
obj.make_sound(); $ java Dog
}
Dog Adelle of species Maltese barks
$ g++ dogs.cpp
https://github.com/chgogos/oop/blob/master/various/CPP_VS_JAVA/simple_java/Dog.java
$ ./a.out
Dog Adelle of species Maltese barks
13
https://github.com/chgogos/oop/blob/master/various/CPP_VS_JAVA/simple_cpp/dog.cpp
Παραδείγματα κώδικα σε C++ και Java

Κώδικας σε C++ Κώδικας σε Java


• https://chgogos.github.io/oop/preparation/p • https://chgogos.github.io/oop/lab2020-
repare01.cpp 2021/lab06/prepare01a/
• https://chgogos.github.io/oop/preparation/p • https://chgogos.github.io/oop/lab2020-
repare02.cpp 2021/lab06/prepare01b/
• https://chgogos.github.io/oop/preparation/p • https://chgogos.github.io/oop/lab2020-
repare03.cpp 2021/lab06/prepare01c/

Εκφωνήσεις: https://chgogos.github.io/oop/preparation/proodos.pdf

14

UML
διαγράμματα κλάσεων
Τμήμα Πληροφορικής και Τηλεπικοινωνιών (Άρτα)
Πανεπιστήμιο Ιωαννίνων
Γκόγκος Χρήστος

V 1.0
Τι είναι η UML;
• H UML (Unified Modeling Language) είναι μια γλώσσα
μοντελοποίησης, βασισμένη σε διαγράμματα, που υποστηρίζει την
ανάπτυξη λογισμικού σε όλα τα στάδιά του. Ορίζει ένα κοινό
λεξιλόγιο και κοινά αποδεκτούς τύπους διαγραμμάτων για τη
περιγραφή του λογισμικού καθώς αναπτύσσεται εξετάζοντάς το υπό
διάφορες οπτικές γωνίες
• Η πρώτη έκδοση της UML ανακοινώθηκε το 1997 και η τρέχουσα
έκδοσή της είναι η UML 2.5.1 (Δεκέμβριος 2017)
• https://www.omg.org/spec/UML/About-UML/

Σημαντικοί τύποι διαγραμμάτων στην UML

Διαγράμματα δομής Διαγράμματα συμπεριφοράς


• Διάγραμμα κλάσεων (Class Diagram) • Διάγραμμα περίπτωσης χρήσης (Use
• Διάγραμμα συστατικών (Component Case Diagram)
Diagram) • Διάγραμμα δραστηριότητας (Activity
• Διάγραμμα σύνθετης δομής (Composite Diagram)
Structure Diagram) • Διάγραμμα μηχανών καταστάσεων (State
• Διάγραμμα αντικειμένων (Object Machine Diagram)
Diagram) • Διάγραμμα ακολουθίας (Sequence
• Διάγραμμα πακέτων (Package Diagram) Diagram)
• Διάγραμμα ανάπτυξης/εγκατάστασης • Διάγραμμα επικοινωνίας
(Deployment Diagram) (Communication Diagram)
• Διάγραμμα χρονισμού (Timing Diagram)

3
Διαγράμματα κλάσεων
• Τα διαγράμματα κλάσεων είναι τα πλέον κοινά διαγράμματα UML
• Τα διαγράμματα κλάσεων περιγράφουν:
• τους τύπους των αντικειμένων
• τις στατικές σχέσεις μεταξύ των αντικειμένων
• τις ιδιότητες των κλάσεων
• τις λειτουργίες των κλάσεων
• τους περιορισμούς στον τρόπο με τον οποίο τα αντικείμενα συνδέονται
μεταξύ τους
• Στην UML οι ιδιότητες (μέλη δεδομένων στη C++) και οι λειτουργίες
(συναρτήσεις μέλη στη C++) μιας κλάσης αναφέρονται από κοινού
ως χαρακτηριστικά (features)

Απεικόνιση μιας κλάσης σε διάγραμμα


κλάσεων
• Μια κλάση σε ένα διάγραμμα κλάσεων αναπαρίσταται με ένα
τετράγωνο που χωρίζεται σε 3 τμήματα (το πολύ)
• Στο 1ο τμήμα βρίσκεται το όνομα της κλάσης
• Στο 2ο τμήμα βρίσκονται οι ιδιότητες
• Στο 3ο τμήμα βρίσκονται οι λειτουργίες
• Κάθε χαρακτηριστικό της κλάσης επισημαίνεται με ειδικά
σύμβολα(+,-,#) προκειμένου να προσδιοριστεί το επίπεδο
προστασίας του
• + για public
• - για private
• # για protected
• Για τα χαρακτηριστικά της κλάσης χρησιμοποιείται στυλ δήλωσης
Algol
• <αναγνωριστικό>:<τύπος> για ιδιότητες
• <αναγνωριστικό>(<όνομα παραμέτρου>:<τύπος>):<τύπος επιστροφής>
για λειτουργίες
• Επιπλέον, μπορούν να καταγράφονται μεταδεδομένα (metadata)
με τη μορφή στερεότυπων που περιλαμβάνονται σε διπλά
εισαγωγικά (guillemets)
• π.χ. «constructor», «NotNull», «abstract», …
5
Κώδικας για απεικόνιση κλάσης σε
διάγραμμα κλάσεων
class Example
{
private:
int x;
void foo(int x){…}

protected:
int y;
int bar(int y, int z){…}

public:
Example(){…}
https://github.com/chgogos/oop/blob/master/uml/example1.cpp
int z;
};

Σχέσεις (relationships)
• Τύποι σχέσεων
• Εξάρτηση (dependency)
• Συσχέτιση (association)
• Συνάθροιση (aggregation)
• Σύνθεση (composition)
• Γενίκευση (generalization)
• Υλοποίηση (realization)
• Για κάθε τύπο σχέσης
χρησιμοποιείται
διαφορετικός οπτικά τύπος
σύνδεσης
7
Σχέση εξάρτησης (dependency)
• Το A έχει εξάρτηση από το Β
• Αν ένα αντικείμενο της κλάσης Β
περνά ως παράμετρος σε
συνάρτηση της κλάσης Α
ή
• Αν ένα αντικείμενο της κλάσης Β
εκχωρείται σε τοπική μεταβλητή
μιας συνάρτησης της κλάσης A
• Η σχέση εξάρτησης είναι γνωστή
και ως σχέση “uses a”
(χρησιμοποιεί ένα)

Παράδειγμα με σχέση εξάρτησης (κώδικας)


class B
{
public:
void foo(){}
};

class A
{
public:
void bar(B b){b.foo();}
};

int main() https://github.com/chgogos/oop/blob/master/uml/dependency1.cpp


{ https://github.com/chgogos/oop/blob/master/uml/dependency2.cpp
B obj1; A obj2;
obj2.bar(obj1);
}
9
Σχέση συσχέτισης (association)
• Ένα Α έχει πρόσβαση σε ένα Β
• Η σχέση συσχέτισης είναι
γνωστή και ως σχέση “has-a”
(έχει ένα) ή “knows-a” (γνωρίζει
ένα)

10

Παράδειγμα με σχέση συσχέτισης (κώδικας)


class B
{
public:
void foo(){}
};
class A
{
private:
B *b_ptr;
public:
A(B *b) : b_ptr(b){}
void bar(){ b_ptr->foo();} https://github.com/chgogos/oop/blob/master/uml/association1.cpp
};
int main() https://github.com/chgogos/oop/blob/master/uml/association2.cpp
{
B obj1; A obj2(&obj1);
obj2.bar();
} 11
Σχέση συνάθροισης (aggregation)
• Το A έχει ως μέρος του το Β, αν και
η διάρκεια ζωής τους μπορεί να
διαφοροποιείται καθώς το Β
μπορεί να μοιράζεται σε πολλά Α
• Η σχέση συνάθροισης είναι
παρόμοια με τη σχέση συσχέτισης,
αλλά υποδηλώνει σαφώς ότι το Α
δεν έχει έλεγχο πάνω στη διάρκεια
ζωής του Β
• Το σύμβολο της συνάθροισης (ο
ρόμβος) τοποθετείται από την
πλευρά της κλάσης που
αναπαριστά το όλο στη σχέση όλο-
μέρος
12

Παράδειγμα με σχέση συνάθροισης


(κώδικας)
class B
{ https://github.com/chgogos/oop/blob/master/uml/aggregation1.cpp
private:
int data[100]; https://github.com/chgogos/oop/blob/master/uml/aggregation2.cpp
public:
B(){}
};
class A
{
private:
int x;
B &b; // aggregation (weak containment)
public:
A(B &rb) : b(rb){}
};
int main() {
B b_obj; A obj1(b_obj); A obj2(b_obj);
} 13
Σχέση σύνθεσης (composition)
• Το A έχει ως μέρος του το Β, και
η διάρκεια ζωής του Β ελέγχεται
από το Α (καθώς το Β περιέχεται
στο Α)
• Αν καταστραφεί το Α τότε
καταστρέφεται και το Β
• Το σύμβολο της σύνθεσης (ο
μαύρος ρόμβος) τοποθετείται
από την πλευρά της κλάσης που
αναπαριστά το όλο στη σχέση
όλο-μέρος

14

Παράδειγμα με σχέση σύνθεσης (κώδικας)


class B
{
private:
int data[100];
public:
B(){}
};
class A
{
private:
int x;
B b; // composition (strong containment)
public:
A(){} https://github.com/chgogos/oop/blob/master/uml/composition1.cpp
}; https://github.com/chgogos/oop/blob/master/uml/composition2.cpp
int main()
{
A obj1, obj2;
} 15
Σχέση γενίκευσης (generalization)
• H κλάση Base κληρονομεί στην κλάση Derived
• H κλάση Derived κληρονομεί από την κλάση
Base
• Η κλάση Base αποτελεί μια γενίκευση της
κλάσης Derived
• Η κλάση Derived αποτελεί μια εξειδίκευση
(specialization) της κλάσης Base
• Τα αντικείμενα της κλάσης Derived
θεωρούνται αντικείμενα και της κλάσης Base
• Τα αντικείμενα της κλάσης Base δεν
θεωρούνται αντικείμενα της κλάσης Derived
• Η σχέση γενίκευσης είναι γνωστή ως σχέση
“IS-A”

16

Παράδειγμα με σχέση γενίκευσης (κώδικας)


class Base
{
protected:
int x;
};

class Derived : public Base


{
private:
int y;
public:
void foo() {…}
};

int main()
{ https://github.com/chgogos/oop/blob/master/uml/generalization1.cpp
Derived obj;
obj.foo();
} 17
Γενίκευση με αφηρημένη κλάση (κώδικας)
class Shape Τα italics
υποδηλώνουν ότι η
{
κλάση Shape είναι
public: αφηρημένη (abstract)
virtual ~Shape() {}
virtual void draw() = 0; // pure virtual function
};
class Rectangle : public Shape
{
public:
void draw(){…}
};
class Triangle : public Shape
{…};
class Circle : public Shape
{…}; https://github.com/chgogos/oop/blob/master/cpp_playground/ex022/shapes.cpp
int main()
{
Shape *a[] = {new Rectangle, new Triangle, new Circle};
for (int i = 0; i < 3; i++) a[i]->draw();
for (int i = 0; i < 3; i++) delete a[i];
} 18

Σχέση υλοποίησης (realization)


• H κλάση Β υλοποιεί τη διεπαφή
που ορίζεται στο Α
• Ως διεπαφή ορίζεται ένα σύνολο
από λειτουργίες
• Χρησιμοποιείται όταν θέλουμε
να δείξουμε ότι μια κλάση
υλοποιεί λειτουργίες που
ορίζονται σε μια διεπαφή
(συχνά χρησιμοποιείται η
σήμανση «interface»)

19
Παράδειγμα με σχέση υλοποίησης (κώδικας)
class Person
{
protected:
https://github.com/chgogos/oop/blob/master/uml/realization1.cpp
string name;
public:
Person(string n) : name(n) {}
};
class Musician
{
public:
virtual void play() = 0;
};
class Pianist : public Person, public Musician
{
private:
string piano;
public:
Pianist(string name, string piano) : Person(name), piano(piano) {}
void play()
{
cout << "Musician: " << name << " Instrument: " << piano << endl;
}
}; Στη C++ μια διεπαφή ορίζεται ως αφηρημένη
κλάση (στο παράδειγμα η κλάση Musician)
int main(){
Pianist p("Nikolaos", "Steinway & Sons");
p.play();
} 20

Πλοηγησιμότητα συσχετίσεων (navigability)


• Τοποθετώντας βελάκια στα • Το Β μπορεί να προσπελαστεί
άκρα των συσχετίσεων από το Α
μπορούμε να διευκρινίσουμε τη • Το διάγραμμα δεν υποδηλώνει
δυνατότητα μετάβασης από ένα ότι το Α μπορεί να
αντικείμενο μιας κλάσης σε προσπελαστεί από το Β
αντικείμενα της συσχετιζόμενης
κλάσης

21
Πολλαπλότητα συσχετίσεων (multiplicity)
• * (οποιοσδήποτε αριθμός) • Κάθε αντικείμενο Α σχετίζεται
• 1 (ακριβώς ένα) με οποιοδήποτε αριθμό
αντικειμένων Β
• n (ακριβώς n)
• Κάθε αντικείμενο B σχετίζεται
• 0..1 (μηδέν ή ένα) με ένα αντικείμενο A
• 1..* (ένα ή περισσότερα)
• n..m (από n μέχρι m)

22

Παράδειγμα διαγράμματος κλάσεων


• Παρατηρείστε το διαφορετικό επίπεδο
συμπλήρωσης λεπτομέρειας στα επιμέρους
συστατικά του διαγράμματος (είναι φυσιολογικό)
• Εξετάζοντας τις συσχετίσεις προς την κατεύθυνση
της πλοηγησιμότητας:
• Μια παραγγελία αφορά έναν πελάτη
• Ο πελάτης μπορεί να είναι εταιρικός ή απλός πελάτης
• Σε περίπτωση που είναι εταιρικός πελάτης μπορεί να
απευθύνεται σε κανένα ή ένα εμπορικό αντιπρόσωπο
• Κάθε παραγγελία έχει καμία, μια ή περισσότερες
γραμμές παραγγελίας
• Κάθε γραμμή παραγγελίας αφορά ένα προϊόν
• Επιπλέον:
• Ένας εμπορικός αντιπρόσωπος μπορεί να έχει πολλούς
εταιρικούς πελάτες
• Ένας πελάτης μπορεί να έχει πραγματοποιήσει καμία,
μια ή περισσότερες παραγγελίες
• Μια γραμμή παραγγελίας ανήκει σε μια παραγγελία
• Ένα προϊόν μπορεί να έχει πολλές γραμμές παραγγελίας
που το αφορούν

23

You might also like