Anders als die OOP-Sprachen C++ und Objective-C bringt C von Hause aus keine ob­jekt­ori­en­tier­ten Features mit. Aufgrund der großen Ver­brei­tung der Sprache und der Be­liebt­heit ob­jekt­ori­en­tier­ter Pro­gram­mie­rung exis­tie­ren Ansätze, OOP in C ein­zu­set­zen.

OOP in C – geht das ei­gent­lich?

Die C-Pro­gram­mier­spra­che ist an sich nicht für die ob­jekt­ori­en­tier­te Pro­gram­mie­rung gedacht. Die Sprache ist ein Pa­ra­de­bei­spiel des struk­tu­rier­ten Pro­gram­mier­stils innerhalb der im­pe­ra­ti­ven Pro­gram­mie­rung. Dennoch ist es möglich, in C ob­jekt­ori­en­tier­te Ansätze nach­zu­bil­den. Die Sprache hat alle dafür be­nö­tig­ten Kom­po­nen­ten an Bord. So diente C u. a. als Grundlage für die ob­jekt­ori­en­tier­te Pro­gram­mie­rung in Python.

Mit OOP lassen sich eigene „abstrakte Da­ten­ty­pen“ (ADT) de­fi­nie­ren. Einen ADT kann man sich als Menge möglicher Werte und darauf ope­rie­ren­der Funk­tio­nen vor­stel­len. Wichtig ist, dass die nach außen hin sichtbare Schnitt­stel­le und die interne Umsetzung (Im­ple­men­ta­ti­on) von­ein­an­der ent­kop­pelt sind. So können Sie als Nutzer bzw. Nutzerin darauf vertrauen, dass Objekte des Typs sich der Be­schrei­bung ent­spre­chend verhalten.

Ob­jekt­ori­en­tier­te Sprachen wie Python, Java und C++ nutzen das Konzept der „Klasse“, um abstrakte Da­ten­ty­pen zu mo­del­lie­ren. Klassen dienen als Vorlage zum Erzeugen gleich­ar­ti­ger Objekte; man spricht dabei auch von In­stan­zi­ie­rung. Von Hause aus kennt C keine Klassen, und diese lassen sich innerhalb der Sprache auch nicht nach­bil­den. Statt­des­sen exis­tie­ren ver­schie­de­ne Ansätze, OOP-Features in C zu im­ple­men­tie­ren.

Wie funk­tio­niert OOP in C?

Um zu verstehen, wie OOP in C funk­tio­niert, stellen wir uns zunächst die Frage: Was ist ob­jekt­ori­en­tier­te Pro­gram­mie­rung (OOP)? OOP ist ein ver­brei­te­ter Pro­gram­mier­stil, der eine Aus­prä­gung des im­pe­ra­ti­ven Pro­gram­mier­pa­ra­dig­mas darstellt. Damit steht OOP im Gegensatz zur de­kla­ra­ti­ven Pro­gram­mie­rung und deren Spe­zia­li­sie­rung, der funk­tio­na­len Pro­gram­mie­rung.

Die grund­le­gen­de Idee der ob­jekt­ori­en­tier­ten Pro­gram­mie­rung besteht darin, Objekte zu mo­del­lie­ren und un­ter­ein­an­der in­ter­agie­ren zu lassen. Der Pro­gramm­fluss ergibt sich aus den In­ter­ak­tio­nen der Objekte und steht somit erst zur Laufzeit fest. Im Kern umfasst OOP lediglich drei Ei­gen­schaf­ten:

  1. Objekte kapseln ihren internen Zustand.
  2. Objekte empfangen Nach­rich­ten über ihre Methoden.
  3. Die Zuordnung der Methoden erfolgt dynamisch zur Laufzeit.

Ein Objekt in einer reinen OOP-Sprache wie Java ist eine in sich ge­schlos­se­ne Einheit. Diese umfasst eine beliebig komplexe Da­ten­struk­tur sowie Methoden (Funk­tio­nen), die darauf operieren. Der interne Zustand des Objekts, ab­ge­bil­det in den ent­hal­te­nen Daten, lässt sich nur über die Methoden auslesen und verändern. Für die Spei­cher­ver­wal­tung von Objekten kommt in der Regel ein „Garbage Collector“ genanntes Sprach-Feature zum Einsatz.

Eine Ver­bin­dung von Da­ten­struk­tur und Funk­tio­nen zu Objekten ist in C nicht ohne Weiteres möglich. Statt­des­sen strickt man sich ein über­schau­ba­res System aus Da­ten­struk­tu­ren, Typ­de­fi­ni­tio­nen, Zeigern und Funk­tio­nen. Wie in C üblich, ist der Pro­gram­mie­rer bzw. die Pro­gram­mie­re­rin für die ord­nungs­ge­mä­ße Zuteilung und Freigabe von Speicher zuständig.

Der dabei ent­ste­hen­de ob­jekt­ba­sier­te C-Code sieht nicht ganz so aus wie von OOP-Sprachen gewohnt, funk­tio­niert aber. Wir geben einen Überblick über die zentralen OOP-Konzepte samt ihrer Ent­spre­chung in C:

OOP-Konzept Ent­spre­chung in C
Klasse Struct-Typ
Klassen-Instanz Struct-Instanz
Instanz-Methode Funktion, die Zeiger auf Struct-Variable ent­ge­gen­nimmt
this-/self-Variable Zeiger auf Struct-Variable
In­stan­zi­ie­rung Al­lo­zie­rung und Referenz via Zeiger
new-Schlüs­sel­wort Aufruf von malloc

Objekte als Da­ten­struk­tu­ren mo­del­lie­ren

Schauen wir uns zunächst an, wie sich die Da­ten­struk­tur eines Objekts im Stil von OOP-Sprachen in C mo­del­lie­ren lässt. C ist eine kompakte Sprache, die mit wenigen Sprach­kon­struk­ten auskommt. Zum Erzeugen beliebig komplexer Da­ten­struk­tu­ren kommen die so­ge­nann­ten „Structs“ zum Einsatz, deren Name sich vom Begriff „Data Structure“ herleitet.

Eine C-Struct definiert eine Da­ten­struk­tur, die „Members“ genannte Felder umfasst. In anderen Sprachen wird ein der­ar­ti­ges Konstrukt auch als „Record“ be­zeich­net, und so kann man sich eine Struct gut wie die Zeile einer Da­ten­bank­ta­bel­le vor­stel­len: ein Verbund mehrerer Felder ggf. un­ter­schied­li­chen Typs.

Die Syntax einer Struct-De­kla­ra­ti­on in C ist denkbar einfach:

struct struct_name;

Optional de­fi­nie­ren wir die Struct auch gleich, indem wir die Members mit Namen und Typ angeben. Be­trach­ten wir als Stan­dard­bei­spiel einen Punkt im zwei­di­men­sio­na­len Raum mit x- und y-Ko­or­di­na­te. Wir zeigen die Struct-De­fi­ni­ti­on:

struct point {
    /* X-coordinate */
    int x;
    /* Y-coordinate */
    int y;
};

In her­kömm­li­chem C-Code folgt an­schlie­ßend die In­stan­zi­ie­rung einer Struct-Variable. Wir erzeugen die Variable und in­itia­li­sie­ren beide Felder mit 0:

struct point origin = {0, 0};

Im Anschluss lassen sich die Werte der Felder auslesen und neu setzen. Der Member-Zugriff erfolgt über die aus anderen Sprachen vertraute Syntax origin.x und origin.y:

/* Read struct member */
origin.x == 0
/* Assign struct member */
origin.y = 42

Damit verletzen wir jedoch die An­for­de­rung der Kapselung: Auf den internen Zustand eines Objekts darf nur über dafür de­fi­nier­te Methoden zu­ge­grif­fen werden. Unserem Ansatz fehlt also noch etwas.

Typen zur Erzeugung von Objekten de­fi­nie­ren

Wie erwähnt kennt C kein Konzept der Klasse. Statt­des­sen lassen sich Typen mit der typedef-Anweisung de­fi­nie­ren. Mit typedef geben wir einem Datentyp einen neuen Namen:

typedef <old-type-name> <new-type-name>

So lässt sich für unsere point-Struct ein ent­spre­chen­der Point-Typ de­fi­nie­ren:

typedef struct point Point;

Die Kom­bi­na­ti­on von typedef mit einer struct-De­fi­ni­ti­on ent­spricht in etwa einer Klas­sen­de­fi­ni­ti­on in Java:

typedef struct point {
    /* X-coordinate */
    int x;
    /* Y-coordinate */
    int y;
} Point;
Hinweis

Im Beispiel ist „point“ der Name der Struct, wo­hin­ge­gen „Point“ der Name des de­fi­nier­ten Typs ist.

Hier die ent­spre­chen­de Klas­sen­de­fi­ni­ti­on in Java:

class Point {
    private int x;
    private int y;
};

Die Nutzung von typedef erlaubt uns, eine Point-Variable ohne Nutzung des struct-Schlüs­sel­worts zu erzeugen:

Point origin = {0, 0}
/* Instead of */
struct point origin = {0, 0}

Was weiterhin fehlt, ist die Kapselung des internen Zustands.

Kapselung des internen Zustands

Objekte bilden ihren internen Zustand in ihrer Da­ten­struk­tur ab. In OOP-Sprachen wie Java kommen die Schlüs­sel­wör­ter „private“, „protected“ etc. zum Einsatz, um den Zugriff auf Ob­jekt­da­ten zu be­schrän­ken. So wird der direkte Zugriff von außen ver­hin­dert und die Trennung von Schnitt­stel­le und Im­ple­men­ta­ti­on si­cher­ge­stellt.

Um OOP in C zu rea­li­sie­ren, kommt ein anderer Me­cha­nis­mus zum Einsatz. Wir nutzen als Schnitt­stel­le eine so­ge­nann­te Forward-De­kla­ra­ti­on in der Header-Datei und erzeugen damit einen „In­com­ple­te type“:

/* In C header file */
struct point;
/* Incomplete type */
typedef struct point Point;

Die Im­ple­men­ta­ti­on der point-Struct folgt in einer separaten C-Quelltext-Datei, die den Header per include-Macro einbindet. Dieser Ansatz ver­hin­dert die Erzeugung sta­ti­scher Variablen des Point-Typs. Weiterhin möglich ist die Ver­wen­dung von Zeigern des Typs. Da es sich bei Objekten um dynamisch erzeugte Da­ten­struk­tu­ren handelt, werden sie ohnehin mit Zeigern re­fe­ren­ziert. Zeiger auf Struct-Instanzen ent­spre­chen in etwa den in Java zum Einsatz kommenden Objekt-Re­fe­ren­zen.

Methoden durch Funk­tio­nen ersetzen

In OOP-Sprachen wie Java und Python umfassen Objekte neben ihren Daten die darauf ope­rie­ren­den Funk­tio­nen. Diese werden als Methoden bzw. Instanz-Methoden be­zeich­net. Wenn wir OOP-Code in C schreiben, nutzen wir statt Methoden Funk­tio­nen, die einen Zeiger auf eine Struct-Instanz ent­ge­gen­neh­men:

/* Pointer to `Point` struct */
Point * point;

Da C keine Klassen kennt, ist es nicht möglich, die zu einem Typ ge­hö­ren­den Funk­tio­nen unter einem ge­mein­sa­men Namen zu grup­pie­ren. Statt­des­sen versehen wir die Funk­ti­ons­na­men mit einem Präfix, der den Namen des Typs enthält. Die ent­spre­chen­den Funktions-Si­gna­tu­ren werden zunächst in der C-Header-Datei de­kla­riert:

/* In C header file */
/* Function to move update a point's coordinates */
void Point_move(Point * point, int new_x, int new_y);

Im Anschluss im­ple­men­tie­ren wir die Funktion in der C-Quelltext-Datei:

/* In C source file */
void Point_move(Point * point, int new_x, int new_y) {
    point->x = new_x;
    point->y = new_y;
};

Der Ansatz erinnert an Python-Methoden, die normale Funk­tio­nen sind, die self als ersten Parameter ent­ge­gen­neh­men. Ferner ent­spricht der Zeiger auf eine Struct-Instanz in etwa der this-Variable in Java oder Ja­va­Script. Der Un­ter­schied liegt darin, dass beim Aufruf der C-Funktion der Zeiger explizit übergeben wird:

/* Call function with pointer argument */
Point_move(point, 42, 51);

Beim äqui­va­len­ten Funk­ti­ons­auf­ruf in Java steht das point-Objekt innerhalb der Methode als this-Variable zur Verfügung:

// Call instance method from outside of class
point.move(42, 51)
// Call instance method from within class
this.move(42, 51)

Python erlaubt, Methoden als Funk­tio­nen mit ex­pli­zi­tem Self-Argument auf­zu­ru­fen:

# Call instance method from outside or from within class
self.move(42, 51)
# Function call from within class
move(self, 42, 51)

Objekte in­stan­zi­ie­ren

Eine prägende Ei­gen­schaft von C ist die manuelle Spei­cher­ver­wal­tung: Pro­gram­mie­rer und Pro­gram­mie­re­rin­nen sind dafür zuständig, Speicher für Da­ten­struk­tu­ren zu al­lo­zie­ren. Ob­jekt­ori­en­tier­te, dy­na­mi­sche Sprachen wie Java und Python nehmen ihnen die Arbeit ab. In Java kommt zur In­stan­zi­ie­rung eines Objekts das new-Schlüs­sel­wort zum Einsatz. Unter der Haube wird dabei au­to­ma­tisch Speicher alloziert:

// Create new Point instance
Point point = new Point();

Wenn wir OOP-Code in C schreiben, de­fi­nie­ren wir für die In­stan­zi­ie­rung eine spezielle Kon­struk­tor-Funktion. Diese alloziert Speicher für unsere Struct-Instanz, in­itia­li­siert diese und gibt einen Zeiger darauf zurück:

Point * Point_new(int x, int y) {
    /* Allocate memory and cast to pointer type */
    Point * point = (Point *) malloc(sizeof(Point));
    /* Initialize members */
    Point_init(point, x, y);
    // return pointer
    return point;
};

In unserem Beispiel ent­kop­peln wir die In­itia­li­sie­rung der Struct-Members von der In­stan­zi­ie­rung. Wiederum kommt eine Funktion mit dem Point-Präfix zum Einsatz:

void Point_init(Point * point, int x, int y) {
    point->x = x;
    point->y = y;
};

Wie lässt sich ein C-Projekt ob­jekt­ori­en­tiert neu aufsetzen?

Ein be­stehen­des Projekt mit den be­schrie­be­nen OOP-Techniken in C neu zu schreiben, ist nur in Aus­nah­me­fäl­len zu empfehlen. Sinn­vol­ler sind die folgenden Ansätze:

  1. Projekt in einer C-artigen Sprache mit OOP-Features neu schreiben und die exis­tie­ren­de C-Codebasis als Spe­zi­fi­ka­ti­on nutzen
  2. Teile des Projekts in einer OOP-Sprache neu schreiben und spe­zi­fi­sche C-Kom­po­nen­ten bei­be­hal­ten

Sofern die C-Codebasis sauber ge­schrie­ben ist, sollte der zweite Ansatz gute Er­geb­nis­se liefern. Es ist gang und gäbe, per­for­manz­kri­ti­sche Pro­gramm­tei­le in C zu im­ple­men­tie­ren und aus anderen Sprachen darauf zu­zu­grei­fen. Wohl keine andere Sprache eignet sich dafür besser als C. Doch welche Sprachen sind geeignet, um ein be­stehen­des C-Projekt unter Nutzung von OOP-Prin­zi­pi­en neu auf­zu­set­zen?

C-artige ob­jekt­ori­en­tier­te Sprachen

Es gibt eine reich­hal­ti­ge Auswahl C-ähnlicher Sprachen mit ein­ge­bau­ter Ob­jekt­ori­en­tie­rung. Die wohl be­kann­tes­te ist C++; jedoch ist die Sprache be­rüch­tigt für ihre Kom­ple­xi­tät, was in den ver­gan­ge­nen Jahren zu einer Abkehr von C++ führte. Auf Grund der weit­ge­hen­den Über­ein­stim­mung der grund­le­gen­den Sprach­kon­struk­te lässt sich C-Code relativ einfach in C++ einbinden.

Sehr viel leicht­ge­wich­ti­ger als C++ ist Objective-C. Der an die Original-OOP-Sprache Smalltalk an­ge­lehn­te C-Dialekt kam vor allem zur Pro­gram­mie­rung von Ap­pli­ka­tio­nen auf Mac- und frühen iOS-Be­triebs­sys­te­men zum Einsatz. Später folgte darauf aufbauend die Ent­wick­lung der Apple-eigenen Sprache Swift. Aus beiden Sprachen heraus lassen sich in C ge­schrie­be­ne Funk­tio­nen aufrufen.

Auf C auf­bau­en­de ob­jekt­ori­en­tier­te Sprachen

Andere OOP-Pro­gram­mier­spra­chen, die der Syntax nach nicht mit C verwandt sind, eignen sich ebenfalls zum Neu­schrei­ben eines C-Projekts. Für Python, Rust und Java exis­tie­ren Stan­dardan­sät­ze zum Einbinden von C-Code.

In Python erlauben die so­ge­nann­ten Python Bindings das Einbinden von C-Code. Dabei müssen ggf. Python-Da­ten­ty­pen in die ent­spre­chen­den ctypes übersetzt werden. Ferner gibt es das C Foreign Function Interface (CFFI), das das Über­set­zen der Typen in gewissem Rahmen au­to­ma­ti­siert.

Auch Rust un­ter­stützt das Aufrufen von C-Funk­tio­nen mit geringem Aufwand. Mithilfe des extern-Schlüs­sel­worts lässt sich ein da­hin­ge­hen­des Foreign Function Interface (FFI) de­fi­nie­ren. Rust-Funk­tio­nen, die auf externe Funk­tio­nen zugreifen, müssen als unsafe de­kla­riert werden:

extern "C" {
    fn abs(input: i32) -> i32;
}
fn main() {
    unsafe {
        println!("Absolute value of -3 according to C: {}", abs(-3));
    }
}
Zum Hauptmenü