Object-georienteerd programmeren in C komt best veel voor; er zijn eigenlijk drie manieren waarop je het kunt aanpakken.
1. Het simpelste idioom is wat .oisyn voorstelt: je definieert een structure met je data members, en een aantal functies die daarop werken. Eventueel kun je de inhoud van je structure helemaal ondoorzichtig maken, door een aparte functie te maken die 'm alloceert.
C:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
| // header file:
struct Foo;
struct Foo *Foo_create();
struct void Foo_destroy();
int Foo_method(struct Foo *this);
// source:
struct Foo {
int data;
};
struct Foo *Foo_create() {
struct Foo *this = malloc(sizeof(struct Foo));
this->data = 0;
return this;
}
int Foo_method(struct Foo *this) {
return this->data;
}
// etc. |
Dit model wordt gebruikt door een heleboel C libraries. Voordeel is dat je de structure niet eens in je header file hoeft te declareren, en je dus een stricte scheiding hebt tussen je interface en je implementatie. Als Foo onderdeel van een shared library is, kun je de inhoud van je struct prima wijzigen (door extra velden toe te voegen bijvoorbeeld) zonder programma's die tegen je library linken kapot gaan.
Een belangrijk nadeel is dat je op deze manier niet makkelijk dingen als class inheritance of polymorphie kunt implementeren. Het is dus een heel beperkte vorm van OOP.
2. Een variant is wat RoadRunner84 voorstelt: de methoden die op een object werken zijn onderdeel van de struct. In hetzelfde voorbeeld als hierboven:
C:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
| struct Foo {
int data;
int (method*) (struct Foo* this);
};
struct Foo *Foo_construct() {
struct Foo *this = malloc(sizeof(struct Foo));
this->data = 0;
this->method = &Foo_method;
return this;
}
// gebruik:
{
struct Foo *foo = Foo_construct();
return foo->method(foo);
} |
Echt elegant is de C code niet; je moet nog steeds de instantie ook als argument aan z'n methode meegeven. Voordeel is wel dat je dingen als inheritance en polymorfie kunt implementeren (polymorfie is triviaal), maar een nadeel is dat je struct wel in de headers gedeclareert moet worden en dat als je wijzigingen maakt in je struct, je een incompatible library krijgt.
Sleepycat's
BerkeleyDB (tegenwoordig van Oracle) gebruikt precies deze aanpak, en daarom zijn verschillende versies ook incompatible (en zie je op veel systemen veel verschillende versies van BerkeleyDB naast elkaar geïnstalleerd).
Het is dus niet zo vergezocht als .oisyn zegt; het is een prima methode voor relatief heavy-weight objecten, en als je niet van plan bent je ABI al te vaak te veranderen.
3. Een derde mogelijkheid is om objecten min of meer te implementeren zoals dat in C++ gedaan wordt. Dat wil zeggen, dat elke instantie een pointer heeft naar een beschrijving van de klasse, waarin ook de methoden van die klasse beschreven worden (in C++ is dat een vtable pointer).
Dit geeft je in principe volledige OOP mogelijkheden in C, met als nadeel dat de C compiler een heleboel dingen niet voor je doet, zoals bijvoorbeeld impliciete upcasts toestaan. Het komt er op neer dat je dan als programmeur zelf een heleboel conventies moet aanhouden om de boel correct en overzichtelijk te houden.
Deze aanpak wordt in GLib gebruikt voor OOP, en wordt voor een heleboel object-georienteerde C applicaties gebruikt (denk aan dingen als GTK, GStreamer, etc.) Als je zoiets wil implementeren, zou ik je aanraden om gewoon direct GLib te gebruiken, want dat scheelt ten eerste een heleboel werk, en ten tweede zorgt het ervoor dat je een min-of-meer standaard coding conventie gebruikt in plaats van je eigen, nieuwe conventie te introduceren. GLib gebruikt ook veel macro's om een heleboel implementatiedetails voor de programmeur te verbergen.
Voor details van GLib verwijs ik je graag door naar de
GLib Reference Manual; ook erg interessant om door te lezen als je geen GLib gebruikt maar eens wil zien hoe je een goed OO-systeem voor C ontwerpt/implementeert.