Con­trai­re­ment aux langages C++ et Objective-C, axés sur l’OOP, C ne propose aucune fonc­tion­na­lité orientée objet de manière native. Ce langage est très répandu et la pro­gram­ma­tion orientée objet est ex­trê­me­ment populaire ; il existe donc des solutions pour utiliser l’OOP in C.

OOP in C : est-ce vraiment possible ?

Le langage de pro­gram­ma­tion C en tant que tel ne prend pas en charge la pro­gram­ma­tion orientée objet. Ce langage constitue un excellent exemple du style de pro­gram­ma­tion structuré propre à la pro­gram­ma­tion im­pé­ra­tive Il reste toutefois possible d’intégrer à C des approches orientées objet; en effet, le langage dispose déjà de tous les éléments né­ces­saires. Le langage C a donc notamment servi de base à la pro­gram­ma­tion orientée objet avec Python.

Avec l’OOP, vous pouvez définir vos propres « types de données abstraits » (ADT). Un ADT peut prendre la forme d’un ensemble de valeurs possibles et de fonctions opérant sur ces dernières. Il est essentiel de dissocier l’interface visible en externe de la mise en œuvre (im­plé­men­ta­tion) interne. En tant qu’uti­li­sa­teur, vous pouvez donc être sûr que le com­por­te­ment des objets de ce type est conforme à la des­crip­tion.

Les langages orientés objet comme Python, Java et C++ font appel au concept de « classe » pour modéliser des types de données abstraits. Ces classes agissent comme des modèles per­met­tant de créer des objets si­mi­laires ; cette solution porte également le nom d’ins­tan­cia­tion. Aucune classe n’est intégrée par défaut au langage C, et il n’est pas possible de les y re­pro­duire. Il existe toutefois dif­fé­rentes solutions pour im­plé­men­ter des fonc­tion­na­li­tés d’OOP avec C.

Comment fonc­tionne l’OOP avec C ?

Pour com­prendre le fonc­tion­ne­ment de l’OOP avec C, il convient tout d’abord de répondre à cette question : qu’est-ce pré­ci­sé­ment que l’OOP] ? La pro­gram­ma­tion orientée objet est un style de pro­gram­ma­tion courant qui cor­res­pond à une sorte de paradigme de pro­gram­ma­tion impératif. L’OOP va donc à l’encontre de la pro­gram­ma­tion dé­cla­ra­tive et de sa forme spé­cia­li­sée, la pro­gram­ma­tion fonc­tion­nelle.

Le fait de modéliser des objets et de les faire interagir les uns avec les autres constitue le fondement de la pro­gram­ma­tion orientée objet. Le dé­rou­le­ment du programme dépend des in­te­rac­tions des objets ; il n’est donc connu qu’au moment de l’exécution du programme. En substance, l’OOP ne se ca­rac­té­rise que par trois éléments :

  1. Les objets en­cap­su­lent leur état interne.
  2. Les objets reçoivent des messages par l’in­ter­mé­diaire de leurs méthodes.
  3. L’at­tri­bu­tion de ces méthodes est effectuée de manière dynamique lors de l’exécution.

Dans les langages purement axés sur l’OOP comme Java, un objet cor­res­pond à une unité to­ta­le­ment autonome. Elle comprend une structure de données, quel que soit son niveau de com­plexité, ainsi que des méthodes (fonctions) opérant sur celle-ci. L’état interne de l’objet est re­pré­senté par les données qu’il contient. Il ne peut être lu et modifié que par l’in­ter­mé­diaire des méthodes. Une fonction du langage portant le nom de « Garbage Collector » est gé­né­ra­le­ment utilisée pour gérer la mémoire des objets.

Le langage C ne permet pas de relier fa­ci­le­ment une structure et des fonctions à des objets. Il est toutefois possible d’élaborer un système clair avec des struc­tures de données, des dé­fi­ni­tions de types, des pointeurs et des fonctions. Comme il est d’usage avec le langage C, le pro­gram­meur est res­pon­sable de l’at­tri­bu­tion et de la li­bé­ra­tion de la mémoire.

Il en résulte un code C basé sur les objets qui ne ressemble en rien aux résultats ha­bi­tuel­le­ment obtenus avec les langages d’OOP, mais qui a le mérite de fonc­tion­ner. Découvrez avec nous une pré­sen­ta­tion rapide des prin­ci­paux concepts de l’OOP et de leur équi­valent avec le langage C :

Concept d’OOP Équi­valent avec le langage C
Classe Type « Struct »
Instance de classe Instance « Struct »
Méthode d’instance Fonction qui accepte les pointeurs vers les variables « Struct »
Variable « this » / « self » Pointeur vers la variable « Struct »
Ins­tan­cia­tion At­tri­bu­tion et référence par l’in­ter­mé­diaire d’un pointeur
Mot-clé « new » Appel de la fonction « malloc »

Modéliser des objets en tant que struc­tures de données

Com­men­çons par étudier la manière dont la structure de données d’un objet peut être modélisée avec C, dans le style des langages d’OOP. C’est un langage compact qui ne requiert que peu de cons­truc­tions lin­guis­tiques. Pour créer des struc­tures de données, quel que soit leur niveau de com­plexité, il convient d’utiliser des « Structs » ; ce nom vient du terme « Data Structure » (lit­té­ra­le­ment « structure de données »).

Une « C Struct » définit donc une structure de données com­pre­nant des champs appelés « Members » (lit­té­ra­le­ment « membres »). Dans d’autres langages, cette cons­truc­tion peut également être nommée « record ». Il est possible de se re­pré­sen­ter une « Struct » comme une ligne dans une table de base de données : elle cor­res­pond à l’as­so­cia­tion de plusieurs champs, qui peuvent être de types dif­fé­rents.

La syntaxe d’une dé­cla­ra­tion « Struct » avec C est ex­trê­me­ment simple :

struct struct_name;
C

Il est éven­tuel­le­ment possible de définir la « Struct » en précisant le nom et le type des « Members ». Prenons un exemple standard : un point dans un espace en deux di­men­sions, avec des coor­don­nées « x » et « y ». Vous trouverez ci-dessous la dé­fi­ni­tion de cette « Struct » :

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

En code C tra­di­tion­nel, il convient ensuite de procéder à l’ins­tan­cia­tion d’une variable « Struct ». Nous créons donc cette variable et nous ini­tia­li­sons les deux champs avec la valeur « 0 » :

struct point origin = {0, 0};
C

Les valeurs de ces champs peuvent alors être extraites et re­dé­fi­nies. Pour accéder aux « Members », il faut passer par la syntaxe « origin.x » et « origin.y », qui vous est peut-être déjà familière si vous tra­vail­lez avec d’autres langages de pro­gram­ma­tion :

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

Ici, nous ne res­pec­tons toutefois pas l’exigence relative à l’en­cap­su­la­tion : pour accéder à l’état interne d’un objet, il faut avoir recours à des méthodes définies à cet effet. Il sem­ble­rait donc qu’un élément fasse encore défaut à notre exemple.

Définir des types pour la création d’objets

Comme nous l’avons déjà expliqué, le concept de classe n’existe pas dans le langage C. Il est cependant possible de définir des types à l’aide de l’ins­truc­tion « typedef ». Celle-ci nous permet de donner un nouveau nom à un type de données :

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

Il est possible de définir un type « Point » cor­res­pon­dant à notre « Struct “point” » en procédant comme suit :

typedef struct point Point;
C

La com­bi­nai­son de « typedef » avec une dé­fi­ni­tion « Struct » cor­res­pond à peu près à une dé­fi­ni­tion de classe avec Java :

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

Dans notre exemple, « point » cor­res­pond au nom de la « Struct », tandis que « Point » cor­res­pond au nom du type défini.

La dé­fi­ni­tion de classe cor­res­pon­dante avec Java est la suivante :

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

Le fait d’utiliser « typedef » nous permet de créer une variable « Point » sans avoir à utiliser le mot-clé « Struct » :

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

Il nous manque encore l’en­cap­su­la­tion de l’état interne.

En­cap­su­la­tion de l’état interne

Les objets re­pré­sen­tent leur état interne dans leur structure de données. Dans les langages d’OOP, comme Java, les mots-clés « private », « protected », etc., sont utilisés pour limiter l’accès aux données des objets. Ainsi, aucun accès direct n’est possible depuis l’extérieur, et l’interface et l’im­plé­men­ta­tion sont bien séparées l’une de l’autre.

Pour l’OOP in C, il convient d’utiliser un autre mécanisme. Nous allons utiliser une dé­cla­ra­tion « forward » en tant qu’interface dans le fichier d’en-tête, de manière à créer un « In­com­plete type » :

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

L’im­plé­men­ta­tion de la « Struct “point” » est ensuite effectuée dans un fichier distinct de code source C, qui intègre l’en-tête par la macro « include ». Cette stratégie empêche le type « Point » de créer des variables statiques, mais il est encore possible d’utiliser des pointeurs pour le type. Les objets étant des struc­tures de données créées de manière dynamique, ils sont de toute façon ré­fé­ren­cés à l’aide de pointeurs. Sur les instances « Struct », les pointeurs cor­res­pon­dent plus ou moins aux ré­fé­rences d’objets qui ont cours avec Java.

Remplacer des méthodes par des fonctions

Dans les langages d’OOP comme Java et Python, les objets englobent, en plus de leurs données, les fonctions qui opèrent sur ces dernières. Ces fonctions sont plus connues sous le nom de « méthodes » ou de « méthodes d’instance ». Pour écrire du code avec C dans le cadre de l’OOP, nous n’allons pas utiliser de méthodes, mais plutôt des fonctions. Celles-ci acceptent un pointeur vers une instance « Struct » :

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

Comme les classes n’existent pas dans le langage C, il n’est pas possible de regrouper toutes les fonctions d’un même type sous un seul et même nom. Nous allons plutôt ajouter au nom des fonctions un préfixe précisant le nom du type. Les sig­na­tures des fonctions cor­res­pon­dantes sont d’abord déclarées dans le fichier d’en-tête du langage C :

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

Nous pouvons main­te­nant im­plé­men­ter la fonction dans le fichier de code source C :

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

Cette stratégie peut être rap­pro­chée des méthodes utilisées avec Python, qui cor­res­pon­dent à des fonctions normales avec « self » en tant que premier paramètre. De plus, le pointeur vers une instance « Struct » est presque similaire à la variable « this » exploitée par Java ou Ja­vaS­cript. Ce qui est différent ici, c’est que le pointeur est transmis de façon explicite lors de l’appel de la fonction C :

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

Pour l’appel de fonction équi­valent avec Java, l’objet « point » est quant à lui dis­po­nible dans la méthode sous la forme d’une variable « this » :

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

En Python, il est possible d’appeler des méthodes en tant que fonctions avec un argument « self » explicite :

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

Ins­tan­cier des objets

La gestion manuelle de la mémoire constitue l’une des par­ti­cu­la­ri­tés les plus ca­rac­té­ris­tiques du langage C : il revient aux pro­gram­meurs d’attribuer de la mémoire aux struc­tures de données. Or, les langages dy­na­miques et orientés objet tels que Java ou Python ef­fec­tuent cette tâche à leur place. Avec Java, le mot-clé « new » est utilisé pour ins­tan­cier un objet. En coulisses, cela permet d’attribuer au­to­ma­ti­que­ment de la mémoire :

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

Lorsque nous écrivons du code lié à l’OOP avec C, nous dé­fi­nis­sons une fonction de cons­truc­teur spéciale aux fins de l’ins­tan­cia­tion. Celle-ci permet d’attribuer de la mémoire à notre instance « Struct », de l’ini­tia­li­ser et de mettre en place un pointeur s’y référant :

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;
};
C

Dans cet exemple, nous avons choisi de dissocier l’ini­tia­li­sa­tion des « Members » de la « Struct » de l’ins­tan­cia­tion. Nous faisons à nouveau appel à une fonction avec le préfixe « Point » :

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

Comment réécrire un projet C pour qu’il soit orienté objet ?

Sauf cir­cons­tances ex­cep­tion­nelles, nous ne vous con­seil­lons pas de réécrire un projet existant avec C en utilisant les tech­niques d’OOP pré­cé­dem­ment dé­ve­lop­pées. Il nous semble en effet plus judicieux d’adopter l’une des approches suivantes :

  1. Réécrire le projet dans un langage de type C proposant des fonc­tion­na­li­tés d’OOP, en utilisant la base de code C déjà existante en tant que spé­ci­fi­ca­tion.
  2. Réécrire certaines parties du projet avec un langage prenant en charge l’OOP et conserver des com­po­sants spé­ci­fiques en langage C.

Si la base de code C a été écrite cor­rec­te­ment, cette deuxième stratégie devrait vous permettre d’obtenir des résultats sa­tis­fai­sants. Il n’est pas rare d’im­plé­men­ter en C des parties es­sen­tielles du programme, puis d’y accéder à partir d’autres langages. Aucun autre ne se prête d’ailleurs mieux que C à cet exercice. Mais quels langages peuvent vous permettre de recréer un projet C déjà existant en faisant appel aux principes de l’OOP ?

Langages orientés objet de type C

Nombreux sont les langages de type C qui proposent une pro­gram­ma­tion orientée objet intégrée. C++ est pro­ba­ble­ment le plus connu d’entre tous, mais ce langage est aussi réputé pour sa com­plexité ; beaucoup de pro­gram­meurs l’ont d’ailleurs abandonné au cours de ces dernières années. Comme leurs cons­truc­tions lin­guis­tiques de base se res­semblent énor­mé­ment, il est re­la­ti­ve­ment aisé d’intégrer du code C à C++.

Objective-C est toutefois beaucoup plus facile à prendre en main. Ce « dialecte » C s’inspire du langage original d’OOP utilisé par Smalltalk et a d’abord été utilisé prin­ci­pa­le­ment pour pro­gram­mer des ap­pli­ca­tions sur les systèmes d’ex­ploi­ta­tion Mac et iOS. Il a ensuite servi de base à la mise au point du langage Swift, développé par Apple. Les fonctions écrites avec C peuvent donc être appelées à partir de ces deux langages.

Langages orientés objet basés sur C

Vous pouvez également utiliser d’autres langages d’OOP dont la syntaxe n’est pas similaire à celle du langage C pour réécrire un projet créé avec C. Python, Rust et Java proposent des solutions standard relatives à l’in­té­gra­tion de code C .

C’est notamment le cas des « Python Bindings ». Les types de données Python doivent alors être traduits en « ctypes » équi­va­lents. En outre, la C Foreign Function Interface (CFFI, lit­té­ra­le­ment « interface de fonction étrangère de C ») au­to­ma­tise dans une certaine mesure la tra­duc­tion des types.

Avec Rust, la prise en charge de l’appel de fonctions C ne demande que peu d’efforts. Pour ce faire, une Foreign Function Interface (FFI) peut également être définie à l’aide du mot-clé « extern ». Les fonctions Rust qui accèdent à des fonctions externes doivent être déclarées « unsafe » (non sé­cu­ri­sées) :

extern "C" {
    fn abs(input: i32) -> i32;
}
fn main() {
    unsafe {
        println!("Absolute value of -3 according to C: {}", abs(-3));
    }
}
Rust
Aller au menu principal