Αντικειμενοστρεφής προγραμματισμός στη C

Πολυμορφισμός στην C

Ο πολυμορφισμός είναι μία από τις πιο σημαντικές έννοιες στον αντικειμενοστρεφή προγραμματισμό. Αν κοιτάξουμε όλα τα αρχικά πρότυπα σχεδίασης, σχεδόν όλα τους, χρησιμοποιούν τον πολυμορφισμό και τις ιδιότητές του.

Μέθοδοι Constructor και Destructor

Στην C++, οι δύο αυτές μέθοδοι είναι κάποιες ειδικές μέθοδοι που καλούνται όταν ένα αντικείμενο δημιουργείται ή καταστρέφεται αντιστοίχως. Ο χειριστής new κατανέμει την μνήμη για την κλάση και μετά καλεί τον κατασκευαστή (Constructor) της κλάσης αυτής. Αντίστοιχα, ο χειριστής delete καλεί πρώτα τον Destructor της κλάσης και μετά αποδεσμεύει την μνήμη που χρησιμοποιούσε η κλάση.

Διάταξη μνήμης ενός αντικειμένου

Στην C πρέπει να δημιουργήσουμε δύο μεθόδους για κάθε κλάση. Μία για τον Constructor και μία για τον Destructor. Το όνομα της μεθόδου του Constructor θα είναι το όνομα της κλάσης μαζί με το _Ctor και το όνομα της μεθόδου του Destructor θα είναι το όνομα της κλάσης μαζί με το _Dtor (π.χ. για την κλάση X, θα έχουμε: X_Ctor και X_Dtor).
Στην C++, όταν δημιουργείται ένα αντικείμενο, κατανέμεται μνήμη μόνο για τα δεδομένα της κλάσης. Υπάρχει μόνο ένα αντίγραφο των μεθόδων μελών και διαμοιράζονται σε όλα τα στιγμιότυπα της κλάσης.
Στην C, για να υλοποιήσουμε αυτή την συμπεριφορά, δημιουργούμε όλες τις μεθόδους όλων των κλάσεων ως καθολικές (global )μεθόδους (αυτό συμπεριλαμβάνει και τις μεθόδους Constructor και Destructor). Τα ονόματα των μεθόδων θα έχουν στην αρχή το όνομα της κλάσης στην οποία ανήκουν και μετά underscore και το όνομά τους (π.χ. για την μέθοδο Two της κλάσης X, θα είναι X_Two). Επιπλέον είμαστε υποχρεωμένοι να βάλουμε τα δεδομένα της κλάσης σε μία δομή (structure) της C καθότι η λέξη class δεν είναι διαθέσιμη στην C.
Στην C++, μόνο ένα αντίγραφο των μεθόδων μελών υπάρχει στην μνήμη και για να διακρίνουμε τα στιγμιότυπα των κλάσεων μεταξύ τους, χρησιμοποιούμε τον δείκτη this, ο οποίος δείχνει κάθε στιγμή στο στιγμιότυπο που δημιουργήθηκε. Οι μέθοδοι χρησιμοποιούν τον δείκτη this για να αποκτούν πρόσβαση στα δεδομένα. Εσωτερικά, η διεύθυνση στον δείκτη this δίνεται στις μεθόδους μέλη μέσα από τον καταχωρητή ecx του μικροεπεξεργαστή.
Στην C, για να υλοποιήσουμε τον δείκτη this, δηλώνουμε μία μεταβλητή global integer που ονομάζουμε ECX. Πριν καλέσουμε οποιαδήποτε μέθοδο, η μεταβλητή ECX θα τεθεί ώστε να δείχνει στην διεύθυνση μνήμης που χρησιμοποιεί η δομή δεδομένων μας. Όλες οι μέθοδοι χρησιμοποιούν, έτσι, την μεταβλητή ECX ώστε να αποκτήσουν πρόσβαση στα μέλη της δομής.

Κληρονομικότητα

Κληρονομικότητα στον αντικειμενοστρεφή προγραμματισμό αναφέρεται στη δυνατότητα επαναχρησιμοποίησης κώδικα από ήδη υπάρχοντα αντικείμενα, στη δημιουργία νέων αντικειμένων τα οποία μοιράζονται ιδιότητες με ήδη υπάρχοντα ή και τα δύο ανάλογα τη γλώσσα προγραμματισμού.  Ένα αντικείμενο αντικείμενο αποτελεί ένα στιγμιότυπο μιας κλάσης, οι κλάσεις μπορούν να κληρονομούν από άλλες κλάσεις τόσο τα γνωρίσματα όσο και της μεθόδους. Η κύρια κλάση ονομάζεται βάση (base class), ενώ οι κλάσεις οι οποίες κληρονομούν από αυτή ονομάζονται παραγόμενες (derived). Η "base class" επιτρέπει τη δημιουργία μίας γενικής διεπαφής που παρέχει πιο εξειδικευμένες λειτουργίες με τη χρήση εικονικών μεθόδων (virtual functions).

Από τη C++ στη C

Πριν παρουσιάσουμε την κληρονομικότητα στη C πρέπει να δούμε πως αναπαριστούμε μία κλάση.
Ορίζουμε λοιπόν την κλάση "Person" στη γλώσσα C++, η οποία αποτελείται από τα πεδία pFirstName, pLastName; καθώς και κάποιες μεθόδους οι οποίες επιδρούν στα δεδομένα αυτά.

class Person{private:
    char* pFirstName;
    char* pLastName;

public:
    Person(const char* pFirstName, const char* pLastName);
    ~Person();

    void displayInfo();
    void writeToFile(const char* pFileName);};

Για να αναπαραστήσουμε την κλάση "Person" στη γλώσσα C θα χρησιμοποιήσουμε δομές, και συναρτήσεις που επιδρούν πάνω τους. Για παράδειγμα η κλάση "Person" θα μπορούσε να γραφεί στη γλώσσα C ως εξής:

typedef struct _Person Person;typedef void    (*fptrDisplayInfo)(Person*);typedef void    (*fptrWriteToFile)( Person*, const char*);typedef void    (*fptrDelete)( Person *) ;typedef struct _Person 
{
    char* pFName;
    char* pLName;    
    fptrDisplayInfo   Display;
    fptrWriteToFile   WriteToFile;
    fptrDelete      Delete;} Person;person* new_Person(const char* const pFirstName, const char* const pLastName);void delete_Person(Person* const pPersonObj);void Person_DisplayInfo(Person* const pPersonObj);void Person_WriteToFile(Person* const pPersonObj, const char* pFileName);

Η χρήση δεικτών σε συναρτήσεις γίνεται ώστε να επιτευχθεί η ενθυλάκωση, η σύνδεση δηλαδή των δεδομένων με τις μεθόδους. Οι αντικειμενοστρεφείς γλώσσες προγραμματισμού παρέχουν τη δυνατότητα άμεσης πρόσβασης των δεδομένων μέσα από τις μεθόδους της κλάσης με τη χρήση του δείκτη "this". Καθώς ο δείκτης αυτός δεν υπάρχει στη γλώσσα C θα πρέπει με κάποιον τρόπο οι μέθοδοι να γνωρίζουν από ποιο αντικείμενο να αντλήσουν και να τροποποιήσουν τα δεδομένα. Για να γίνει αυτό θα πρέπει να υπάρχει μια αναφορά στο αντικείμενο το οποίο θα τροποποιήσουν, η οποία δίνεται σαν όρισμα.

Κληρονομικότητα C

Για να επιτύχουμε την κληρονομικότητα στη C θα πρέπει με κάποιον τρόπο η παραγόμενη κλάση να γνωρίζει ποια είναι η κλάση βάση, να έχει δηλαδή μία αναφορά προς την κλάση βάση. Αυτό επιτυγχάνεται με την προσθήκη ενός στιγμιότυπου, της κλάσης βάση, στην παραγόμενη κλάση. Μπορούμε λοιπόν να μετατρέψουμε (casting) την παραγόμενη κλάση στην κλάση βάση (πολυμορφισμός), αλλά και να χρησιμοποιήσουμε της μεθόδους της κλάσης βάσης (κληρονομικότητα).

struct base{
    /* base class members */};struct derived{
    struct base super;
    /* derived class members */};struct derived d;struct base *base_ptr = (struct base *)&d;  // upcaststruct derived derived_ptr = (struct derived *)base_ptr;  // downcast

Παράδειγμα

Θα δημιουργήσουμε μία κλάση "Point" η οποία αντιπροσωπεύει ένα σημείο στο δισδιάστατο χώρο. Η κλάση αυτή θα περιέχει δύο ακέραιες μεταβλητές, οι οποίες αντιπροσωπεύουν τις συντεταγμένες του σημείου. Θα παρέχουμε επίσης τη δυνατότητα δημιουργίας και καταστροφής του αντικειμένου καθώς επίσης και τη δυνατότητα μετακίνησής του. Θέλουμε επίσης και την κλάση "Circle" η οποία θα αντιπροσωπεύει ένα κύκλο. Η κλάση αυτή θα αποτελείται απ ο ένα σημείο καθώς επίσης και μία επιπλέον μεταβλητή η οποία θα αποτελεί την ακτίνα του κύκλου και η κλάση αυτή θα παρέχει μία μέθοδο δημιουργίας και διαγραφής καθώς επίσης και μία μέθοδο μετακίνησης.

Η κλάση "Class"

Θέλουμε να κατασκευάσουμε την κλάση "Point" με τέτοιο τρόπο ώστε η κλάση "Circle" να μπορεί να κληρονομήσει απ ο αυτή. Κάθε κλάση στον αντικειμενοστρεφή προγραμματισμό έχει κάποια κοινά χαρακτηριστικά με τις υπόλοιπες κλάσεις (κάθε κλάση έχει ένα κατασκευαστή), για να διατηρήσουμε μία συνοχή με τις αντικειμενοστρεφείς γλώσσες θα πρέπει να παρέχουμε μία παρόμοια διεπαφή. Καθώς το να παρέχουμε ένα γενικό τρόπο κατασκευής και δημιουργίας αντικειμένων προσθέτει αρκετά προβλήματα, κάνει τον κώδικα επιρρεπή σε λάθη και περιορίζει τις δυνατότητες θα πρέπει το κάθε αντικείμενο να γνωρίζει τη πόρους χρειάζεται καθώς και πώς να τους ελευθερώσει. Έτσι μπορούμε να χρησιμοποιούμε τη γενική συνάρτηση "new()" η οποία θα αναλαμβάνει να δημιουργεί τα αντικείμενα και η συνάρτηση "delete()" η οποία θα καταστρέφει το αντικείμενο.

struct Class {
    size_t size;
    void * (* ctor) (void * self, va_list * app);
    void * (* dtor) (void * self);
};
Η μεταβλητή "size" περιέχει το μέγεθος το οποίο θα δεσμεύσει η "new()" για το αντικείμενο, "ctor" είναι η συνάρτηση δημιουργίας του αντικειμένου η οποία λαμβάνει σαν είσοδο τη μνήμη που έχει δεσμεύσει η "new()" καθώς και επιπλέον ορίσματα για την αρχικοποίηση των μεταβλητών, "dtor" είναι η συνάρτηση καταστροφής ενός αντικειμένου οι οποία δέχεται το αντικείμενο προς διαγραφή.
void * new (const void * _class, ...){ 
    const struct Class * class = _class;
    void * p = calloc(1, class -> size);
    assert(p);
    * (const struct Class **) p = class;
    if (class -> ctor){ 
        va_list ap;
        va_start(ap, _class);
        p = class -> ctor(p, & ap);
        va_end(ap);
    }
    return p;}
Η συνάρτηση "new()" δημιουργεί το νέο αντικείμενο και επιστρέφει ένα δείκτη σε αυτό. Μετά τη δημιουργία του αντικειμένου η μεταβλητή "p" δείχνει στο νέο αντικείμενο και ο δείκτης "class" του αντικειμένου "δείχνει" στην αρχή του αντικειμένου. Εάν υπάρχει κατασκευαστής για το αντικείμενο τότε καλούμε τον κατασκευαστή και επιστρέφουμε το αποτέλεσμά του, δηλαδή το νέο αντικείμενο.
Η συνάρτηση "delete()" διαγράφει το αντικείμενο καλώντας τη συνάρτηση καταστροφής του αντικειμένου, η οποία είναι υπεύθυνη για την απελευθέρωση τον πόρων που έχει δεσμεύσει.

void delete (void * self){ 
    const struct Class ** cp = self;
    if (self && * cp && (* cp) -> dtor)
        self = (* cp) -> dtor(self);
    free(self);}

Η κλάση "Point"

Μπορούμε να χρησιμοποιήσουμε την κλάση "Class" προσθέτοντας ένα δείκτη προς τη μέθοδο void (* draw) (const void * self) η οποία θα σχεδιάζει το σημείο.
struct Point {
    const void * class;
    int x, y; /* coordinates */};

static void * Point_ctor (void * _self, va_list * app){ 
    struct Point * self = _self;
    self -> x = va_arg(* app, int);
    self -> y = va_arg(* app, int);
    return self;}

static void Point_draw (const void * _self){ 
    const struct Point * self = _self;
    printf("Draw point at (%d,%d)", self -> x, self -> y);}
Η συνάρτηση "move()" δεν χρειάζεται να "συνδεθεί" με την κλάση καθώς η μεταφορά του σημείου και του κύκλου είναι ίδια καθώς ο κύκλος αποτελείται από ένα σημείο και την ακτίνα.

Η κλάση "Circle"

Η κλάση "Circle" αποτελείται απ ο ένα σημείο και την ακτίνα άρα η κλάση "Circle" γίνεται:
stuct Circle{
    const struct Point _p;
    int rad;};static void * Circle_ctor (void * _self, va_list * app){ 
    struct Point * self = _self;
    self -> x = va_arg(* app, int);
    self -> y = va_arg(* app, int);
    self -> rad = va_arg(* app, int);
    return self;}

static void Point_draw (const void * _self){ 
    printf("Draw circle at (%d,%d) with radius %d\n",
           self -> x, self -> y, self -> rad);}

Πρότυπα (Templates)

Τα πρότυπα είναι μία αρκετά εύχρηστη λειτουργία των αντικειμενοστρεφών γλωσσών με την οποία μπορούν να δημιουργηθούν αλγόριθμοι και αφηρημένες μέθοδοι για να προσδιοριστούν έπειτα αναλόγως με τους τύπους δεδομένων που θα συσχετιστούν.
Εν γένει, τα πρότυπα είναι λειτουργία μεταπρογραμματισμού (metaprogramming), χρησιμοποιούμενη από αρκετές γλώσσες μεταξύ των οποίων και η C++, με την οποία επιτρέπεται η δημιουργία ενός γενικού αλγόριθμου ο οποίος μπορεί να χρησιμοποιηθεί όταν συσχετιστεί με κάποιον τύπο δεδομένων και για οποιονδήποτε τύπο δεδομένων. 
Η πιο συνηθισμένη χρήση των templates είναι για την υλοποίηση δομών δεδομένων(Containers) οι οποίες μπορούν να χρησιμοποιηθούν για οποιονδήποτε τύπο δεδομένων. Έτσι, ο προγραμματιστής μπορεί να χρησιμοποιήσει έτοιμο αλγόριθμο, όπως η λειτουργία μίας λίστας, με διαφορετικούς τύπους δεδομένων αρχικοποιώντας κάθε φοράτην δομή αυτή με τον τύπο δεδομένων που επιθυμεί. Ένα παράδειγμα είναι η κλάση CAtlArray, της βιβλιοθήκης atlcoll.h, που ανήκει στη βιβλιοθήκη ATL (Active Template Library). Ενδεικτικά, για τη λειτουργία της αρχικοποίησης:
CAtlArray<int> myTestArray;int theElement;int maxSizeOfArray = 100;

for (theElement = 0; theElement < maxSizeOfArray; theElement++){
    myTestArray.add(theElement);}