Γενικά γνωρίζω και δουλεύω κυρίως τη Java. Παρ όλα αυτά έχω κατά καιρούς ασχοληθεί με native γλώσσες, κατά κύριο λόγο με την assembly. C ξέρω λίγα πράγματα, θεωρώ τον εαυτό μου αρχάριο.
Τελευταία έψαχνα μία native και αντικειμενοστραφή γλώσσα να μάθω να δουλεύω.
Ψάχνοντας κατάλαβα ότι ακόμα και σε απλή C, μπορείς να υλοποιήσεις σε πολύ καλό βαθμό τη λογική του αντικειμενοστραφούς προγραμματισμού!
Φυσικά υπάρχουν οι γλώσσες όπως C++, Objective-C κτλ. Αλλά αν κάποιος για οποιονδήποτε λόγο θέλει να μείνει στη C, δε χρειάζεται να στερείται της δύναμης του oop.
Προσπαθώντας να βρω τον τρόπο να κάνω oop στην C, κρατούσα κάποιες σημειώσεις (και ταυτόχρονα έφτιαχνα προγραμματάκια για να δω πως λειτουργεί).
Σκέφτηκα λοιπόν να μοιραστώ τις σημειώσεις μου αυτές μαζί σας, να μου πείτε τη γνώμη σας! Βέβαια ακόμα ψάχνω βελτιώσεις οπότε κάθε παρατήρηση είναι ευπρόσδεκτη!
Η παρακάτω διαδικασία επιτρέπει και τον πολυμορφισμό!
Παραθέτω:
Η C μπορεί κάλλιστα να λειτουργήσει με αντικειμενοστραφή λογική.
Η σύνταξή της ίσως είναι λίγο πιο "άβολη" από μία γνήσια αντικειμενοστραφή γλώσσα, μιας και θα
πρέπει να κάνουμε χειροκίνητα κάποια πράγματα που κάνει ο compiler της αντικειμενοστραφούς γλώσσας.
Θα μας λείπει επίσης η πολυτέλεια της υπερφόρτωσης συναρτήσεων.
Πέρα όμως από τη σύνταξη, μπορούμε να "χτίσουμε" όλη τη λογική του αντικειμενοστραφούς προγραμματισμού
στη C, και να χρησιμοποιήσουμε τα οφέλη του. Αυτό μπορεί να γίνεται ως εξής:
1. Η κάθε "κλάση" περιέχει δύο structs. Θα τα πούμε Struct1 και Struct1$. Επίσης θα έχει ένα static πεδίο:
- Μορφοποιημένος Κώδικας: Επιλογή όλων
-
static Struct1$ stat;
Η Struct1$ περιέχει πληροφορίες που αφορούν όλη την κλάση, δηλαδή static fields, και pointers
των συναρτήσεων (και static και non-static). Θα υπάρχει ΜΟΝΟ ένα instance αυτής της δομής που θα αφορά
όλη την κλάση.
Οι non-static συναρτήσεις θα παίρνουν σαν όρισμα ένα επιπλέον pointer για το struct που
τις "καλεί". Ο void pointer δε μας πειράζει, αρκεί να προσέχουμε όταν θα καλούμε τις συναρτήσεις.
H Struct1 περιγράφει το instance του αντικειμένου. Δηλαδή περιέχει τα non-static fields, ΚΑΙ ένα pointer για
Struct1$. Αυτή είναι η "σύνδεση" του αντικειμένου μας με τα static στοιχεία της κλάσης του.
Να πως θα μοιάζουν τα δύο struct:
- Μορφοποιημένος Κώδικας: Επιλογή όλων
-
typedef struct
{
SuperStruct$* super; //1. Το πρώτο στοιχείο είναι pointer στην "$" struct της "κλάσης" που επεκτείνουμε.
// αν δεν επεκτείνουμε καμία κλάση, παραλείπουμε
int some_static_field; //2. Νέα fields και συναρτήσεις. ΟΧΙ αυτά που θέλουμε να κάνουμε Override, ΜΟΝΟ νέα!
void* (*setValue)(void*, int);
int (*getValue)(void*);
}
Struct1$;
typedef struct
{
SuperStruct* super; //1. pointer στην υπερ-κλάση. Αν δεν επεκτείνουμε κάποια "κλάση" παραλείπουμε...
int value; //2. Νέα πεδία...
}
Struct1;
2. Η κλάση πρέπει να έχει έναν "constructor" για την δομή Struct1$, και ένα (ή περισσότερους)
για την Struct1.
Ο πρώτος χρειάζεται να εκτελεστεί μόνο μία φορά από την κλάση μας, και μία
φορά από κάθε κλάση που επεκτείνει τη δική μας. Η "κλάση" Struct1 (και κάθε "κλάση"
που την επεκτείνει) χρειαζεται μόνο ΕΝΑ αντικείμενο Struct1$. Ο constructor θα μοιάζει κάπως έτσι:
- Μορφοποιημένος Κώδικας: Επιλογή όλων
-
void newStruct1$(Struct1$* ptrs)
{
newSuperStruct$((SuperStruct$*)ptrs); //1. Το πρώτο πράγμα που κάνει ένας constructor είναι να καλεί τον constructor
// της μητρικής του "κλάσης". Αν δεν επεκτείνουμε κάποια "κλάση", παραλείπουμε
((SuperStruct$*)ptrs)->old_field = 5; //2. Κάνουμε Override όποια fields ή functions θέλουμε από την μητρική κλάση...
((SuperStruct$*)ptrs)->oldFunction = &newFunction; //Το όνομα της συνάρτησης θα συνεχίζει να είναι oldFunction.
//newFunction είναι απλά η διεύθυνση του νέου κώδικα που θα εκτελείται!
ptrs->some_static_field=8; //3. Δίνουμε τιμές στα νέα πεδία... Για functions δίνουμε τις διευθύνσεις
ptrs->getValue = &getValue; // του κώδικα, βάζοντας απλά το όνομά του με το σύμβολο &.
ptrs->setValue = &setValue;
}
3. Ο "constructor" της κλάσης Struct1 θα δέχεται ένα pointer Struct1, ένα char, συν ό,τι άλλο χρειάζεται. Θα
επιστρέφει pointer με το αντικείμενο που δημιούργησε.
Η λογική των δύο ορισμάτων είναι η εξής:
Πρώτο όρισμα: Αν θέλουμε ο constructor να δημιουργήσει χώρο στη μνήμη για το νέο struct,
δίνουμε NULL, αλλιώς δίνουμε την διεύθυνση του Struct1 που ήδη έχουμε.
Δεύτερο όρισμα: Πάντα 0 όταν δημιουργούμε δημιουργούμε ένα νέο αντικείμενο. Πάντα 1 όταν καλούμε τον constructor
μέσα από έναν άλλο constructor.
Ο pointer επιστροφής μπορεί να είναι void για να αποφύγουμε το typecasting.
Ο constructor θα μοιάζει κάπως έτσι:
- Μορφοποιημένος Κώδικας: Επιλογή όλων
-
void* newStruct1(Struct1* ptr, char old)
{
if (ptr==NULL) ptr = malloc(sizeof(Struct1)); //Βήμα 1: ΤΥΦΛΟΣΟΥΡΤΗΣ, κάθε constructor θα αρχίζει έτσι, αντικαθιστώντας
if (!old) // όμως τα Struct1 και Struct1$ με τα αντίστοιχα ονόματα.
{
if (*(char*)(&stat)==0) newStruct1$(&stat);
*(Struct1$**)ptr = &stat;
}
newSuperStruct((SuperStruct*)ptr, 1); //Βήμα 2: καλούμε τον constructor της μητρικής κλάσης
//με ορίσματα ptr και 1. Αν δεν επεκτείνουμε κάποια κλάση παραλείπουμε...
ptr->value = 5; //Βήμα 3. Τακτοποιούμε τα fields και ό,τι άλλο χρειάζεται.
//Για field της υπερκλάσης γράφουμε ((SuperStruct*)ptr)->old_field=...;
return ptr; //Βήμα 4. Επιστρέφουμε το pointer...
}
Σημείωσε ότι τα 2 πρώτα βήματα του constructor είναι τυφλοσούρτης. "Ελεύθερη βούληση" υπάρχει μόνο στο βήμα 3.
4. Χρησιμοποιούμε τα αντικείμενα που φτιάξαμε:
- Μορφοποιημένος Κώδικας: Επιλογή όλων
-
void main()
{
Struct1* s = newStruct1(NULL,0);
printf("Value: %d\n", s->st->getValue(s));
s->st->setValue(s, 10);
printf("New value: %d\n", s->st->getValue(s));
free(s);
}
Μπορεί να θεωρείς ότι το s->st είναι περιττό, και θα μπορούσαμε να καλούμε κατ ευθείαν το
&stat. Αλλά δεν είναι έτσι! Αν το Struct1 μας... δεν είναι πραγματικά Struct1, αλλά κάτι που επεκτείνει
το Struct1, τότε το s->st ΔΕΝ είναι το &stat της "κλάσης" μας, αλλά το &stat της "κλάσης"
που επεκτείνει τη "δική μας".