Developpez.com - C
X

Choisissez d'abord la catégorieensuite la rubrique :



Programmation orientée objets en C ?

Par CGi

Le 3 août 2005




Introduction :

Le langage C n'est pas un langage orienté objets, mais nous allons voir dans ce document que si l'on structure notre code selon des règles stricts que l'on va établir et que l'on n'en déroge pas, on peut s'approcher de la programmation orientée objets.
Ce document sera accompagné d'un exemple. Une librairie constituant une liste chaînée écrite sous forme de classe (j'emploierais le terme de classe quand-il n'y aura pas d'ambiguïtés). Cette liste chaînée sera une pile, fort semblable à celle vu dans l'article "La liste chaînée simple". Dans cet article nous allons élaborer son code en l'imaginant comme un objet.
Les langages orientés objets possèdent des classes pour construire les objets. Les classes sont des modèles utilisés pour construire les objets. Au sens de la programmation ce sont des types servant à instancier des objets.
Le langage C ne connaît pas les classes, nous nous orienterons donc vers un type proche : les structures (struct).
Dans les langages objets les classes possèdent des fonctions membres ou méthodes. Le C ne connaissant pas les méthodes nous utiliserons donc les fonctions.
Le code interne des fonctions de l'exemple étant très proche de l'article "La liste chaînée simple" et n'ayant pas de rapport avec le sujet, il ne sera donc pas commenté dans ce document.

Règles d'écritures :

On va tout d'abord énumérer les règles d'écriture que l'on va s'imposer :

- Toutes les fonctions publiques devrons faire référence à une structure du langage C (struct). Concrètement elles recevront toujours un pointeur sur la structure comme paramètre. Pour uniformiser les fonctions, ce pointeur sera toujours son premier paramètre. Cette structure représentera l'objet.

- Toutes les fonctions publiques devront avoir un pointeur de fonction associé parmi les membres de la structure. Ces pointeurs de fonctions seront l'équivalent des fonctions membres du C++.

- A l'utilisation de la classe on n'appellera jamais une fonction publique directement. On le fera à l'aide du pointeur de fonction que l'on a mis dans la structure.

- A l'utilisation de la classe on n'accédera jamais aux membres (données) de la structure. On le fera toujours à l'aide des fonctions (par l'intermédiaire de leurs pointeurs de fonctions).

- Pour éviter les redondances de nom de fonction on préfixera leurs noms du nom de la structure. Par exemple TPile_Push() avec le préfixe TPile car elle fait référence au type stuct TPile.

- Les pointeurs de fonctions membre de la structure auront par contre un nom court. Par exemple Push. Ils sont membres d'une structure, il n'y a plus de risque de redondance de nom.

- Les variables crée sur la base de cette structure le serons avec une fonction spéciale que l'on écrira toujours de la même façon Préfixe_Create. Ce qui fera TPile_Create pour notre exemple. Cette fonction n'aura pas de pointeur correspondant dans la structure. Ceci est normal, quand on l'appelle, l'instance de la structure n'est pas encore créée. Ce sera donc la seule fonction de la classe appelée directement. C'est elle qui crée l'objet, elle devra donc être appelée avant toute utilisation d'un objet. Cette fonction est l'équivalent du constructeur des langages orientés objets.

- La mémoire allouée en interne par la classe sera libérée par une fonction spéciale quand l'objet ne sera plus utile. On écrira toujours cette fonction sous la forme Préfixe_Free soit TPile_Free pour l'exemple. Cette fonction est l'équivalent du destructeur des langages orientés objets.

- Chaque classe sera mise dans un fichier séparé de même que leurs déclarations dans un fichier entête séparé

Ces conventions sont celle que je me suis donné pour écrire cet article, mais il est tout à fait possible qu'il en existe d'autres.



Mise en oeuvre :

Les données membres ou attributs de l'objet seront donc constitués par les membres d'une structure :

typedef struct Tpile
        {
                int Nombre;
                struct Titem *Top;
        } Tpile ;

Dans l'exemple nous en avons deux : un entier qui contiendra le nombre d'élément de la pile et un pointeur sur le sommet de la pile.
Les fonctions (membres) doivent accéder à différentes instances d'objets (structures dans notre cas). Elles recevront un pointeur sur ces structures comme paramètre. Ce pointeur est l'équivalent du pointeur this des objets en C++. Comme nous l'avons dit, nous mettrons ce pointeur (This) en leur premier paramètre. Voici si dessous un exemple de fonction (membres) TPile_Push qui reçoit donc le pointeur This comme premier paramètre.

int TPile_Push(TPile *This, int Val)
{
        Titem *new_item = malloc(sizeof(Titem));
        if(!new_item) return ITEM_ALLOC_ERROR;
        new_item->Value = Val;
        new_item->prec = This->Top;
        This->Top = new_item;
        This->Nombre++;
        return 0;
}

Nous nous étions donné comme règle d'appeler ces fonctions par l'intermédiaire de pointeurs de fonctions membres de la structure. Ajoutons ses pointeurs de fonctions à la structure :

typedef struct TPile
        {
                int(*Push)(struct TPile*, int);
                int(*Pop)(struct TPile*);
                void(*Clear)(struct TPile*);
                void(*Free)(struct TPile*);
                int(*Length)(struct TPile*);
                void(*View)(struct TPile*);

                int Nombre;
                struct Titem *Top;
        } TPile ;

Mais avant d'utiliser ces fonctions (membres), il est impératif de créer et d'initialiser l'objet. Ce que nous ferons en une seule opération en utilisant une fonction qui créera l'objet (structure), initialisera ses membres et retournera l'objet.
Dans la pratique on va en créer deux, une pour l'initialisation d'un objet (variable) auto et l'autre pour la création d'un objet (variable) dynamique. On les appellera des constructeurs.
Le premier retourne la copie d'un objet :

TPile TPile_Create()
{
       TPile This;
       TPile_Init(&This);
       This.Free = TPile_Free;
       return This;
}

Le second retourne un pointeur sur un objet créé dynamiquement :

TPile* TPile_New_Create()
{
       TPile *This = malloc(sizeof(TPile));
       if(!This) return NULL;
       TPile_Init(This);
       This->Free = TPile_New_Free;
       return This;
}

Nous lui mettrons le mot New_ entre le préfixe et Create pour le différencier.
Les membres de la structure sont initialisés dans une fonction commune TPile_Init :

static void TPile_Init(TPile *This)
{
       This->Push = TPile_Push;
       This->Pop = TPile_Pop;
       This->Clear = TPile_Clear;
       This->Length = TPile_Length;
       This->View = TPile_View;
       This->Nombre = 0;
       This->Top = NULL;       
}

Cette fonction contient les initialisations communes au deux constructeurs. Elles est donc appelée dans chaque constructeur. Les pointeurs de fonctions sont affectés avec l'adresse des fonctions qui leurs sont associées et les membres avec leurs valeurs. Le pointeur de fonction sur le destructeur sera initialisé dans les constructeurs car il sera différent selon que l'objet est créé dynamiquement ou non. Nous aurons donc deux destructeurs.

void TPile_Free(TPile *This)
{
        TPile_Clear(This);
        puts("Destruction de la pile static.\n");
}
/******************************************************************************/

void TPile_New_Free(TPile *This)
{
        if(This) TPile_Clear(This);
        free(This);        
        puts("Destruction de la pile dynamique.\n");
}

Nous n'aurons pas le souci de savoir lequel on doit appeler car le pointeur de fonction lui étant destiné pointera sur le bon destructeur. Il est initialisé dans le constructeur.
Vous avez du remarquer que j'ai mis la fonction TPile_Init en static. La raison est que je ne veux pas que l'on y accède de l'extérieur. C'est un moyen de la rendre privé. Ce n'est malheureusement pas possible pour les données membres. En fait on pourrait mettre toutes les fonctions en static car on ne les appelle jamais directement de l'extérieur. Nous ne le ferons pas, car nous ne pourrions plus les dériver ultérieurement. Mais ceci fera l'objet d'un futur chapitre.

Utilisation de la classe :

L'utilisation en fait extrêmement simple avec une syntaxe proche du C++. Voici une création d'un objet de type TPile en tant que variable local :

        TPile MaPile = TPile_Create();

        MaPile.Push(&MaPile, 10);
        MaPile.Push(&MaPile, 25);

        MaPile.View(&MaPile);

        MaPile.Free(&MaPile);

Syntaxe proche du C++ mise à par que pour une variable locale (auto), il faut appeler le constructeur et le destructeur implicitement.
Et si dessous la création d'un objet de type TPile en que variable dynamique :

        TPile *MaPile = TPile_New_Create();

        MaPile->Push(MaPile, 10);
        MaPile->Push(MaPile, 25);

        MaPile->View(MaPile);

        MaPile->Free(MaPile);

Autre différence avec le C++ le pointeur (this) sur la structure doit être passé implicitement comme paramètre. En C++ il est caché.
Voilà pour cette première partie qui est un bon exercice pour la manipulation des pointeurs et des pointeurs de fonctions.

Codes sources de l'exemple :

Pile.h :

#ifndef CGI_TPILE_H
#define CGI_TPILE_H

#define ITEM_ALLOC_ERROR  1
#define PILE_EMPTY       -1

#ifdef __cplusplus
  extern "C" {
#endif

/*  Structure représantant un élément de la pile. */
typedef struct Titem
        {
                int Value;
                struct Titem *prec;
        } Titem ;

/*  Structure représantant l'objet pile. */
typedef struct TPile
        {
            /*  Les pointeurs sur fonctions (membres) :                       */
            /*  Push empile une valeur sur la pile.
                retourne ITEM_ALLOC_ERROR si l'allocation a échouée sinon 0
                int Push(TPile, int)                                          */
                int(*Push)(struct TPile*, int);

            /*  Pop retire la dernière valeur empilé sur la pile.
                retourne PILE_EMPTY si la pile est vide.
                int Pop(TPile)                                                */
                int(*Pop)(struct TPile*);

            /*  Clear vide la pile.
                void Clear(TPile)                                             */
                void(*Clear)(struct TPile*);

            /*  Free détruit la pile.
                void Free(TPile)                                              */
                void(*Free)(struct TPile*);

            /*  Lenght retourne le nombre d'élément de la pile.
                int Length(TPile)                                             */
                int(*Length)(struct TPile*);

            /*  View affiche la totalité de la pile en commençant par le sommet.
                void View(TPile)                                              */
                void(*View)(struct TPile*);

            /*  Les données membres :                                         */
                int Nombre;
                struct Titem *Top;

        } TPile ;


/*  Pile_Create crée une pile. */
TPile TPile_Create(void);

TPile* TPile_New_Create(void);

int TPile_Push(TPile*, int);

int TPile_Pop(TPile*);

void TPile_Clear(TPile*);

int TPile_Length(TPile*);

void TPile_View(TPile*);

void TPile_Free(TPile*);

void TPile_New_Free(TPile*);

#ifdef __cplusplus
}
#endif

#endif
Pile.c :
#include<stdlib.h>
#include<stdio.h>

#include "Pile.h"

static TPile* TPile_Init(TPile*);

TPile TPile_Create()
{
       TPile This;
       TPile_Init(&This);
       This.Free = TPile_Free;
       return This;
}
/******************************************************************************/

TPile* TPile_New_Create()
{
       TPile *This = malloc(sizeof(TPile));
       if(!This) return NULL;
       TPile_Init(This);
       This->Free = TPile_New_Free;
       return This;
}
/******************************************************************************/

static void TPile_Init(TPile *This)
{
       This->Push = TPile_Push;
       This->Pop = TPile_Pop;
       This->Clear = TPile_Clear;
       This->Length = TPile_Length;
       This->View = TPile_View;
       This->Nombre = 0;
       This->Top = NULL;
}
/******************************************************************************/

int TPile_Push(TPile *This, int Val)
{
        Titem *new_item = malloc(sizeof(Titem));
        if(!new_item) return ITEM_ALLOC_ERROR;
        new_item->Value = Val;
        new_item->prec = This->Top;
        This->Top = new_item;
        This->Nombre++;
        return 0;
}
/******************************************************************************/

int TPile_Pop(TPile *This)
{
        int Val;
        Titem *tmp;
        if(!This->Top) return PILE_EMPTY;
        tmp = This->Top->prec;
        Val = This->Top->Value;
        free(This->Top);
        This->Top = tmp;
        This->Nombre--;
        return Val;
}
/******************************************************************************/

void TPile_Clear(TPile *This)
{
        Titem *tmp;
        while(This->Top)
          {
             tmp = This->Top->prec;
             free(This->Top);
             This->Top = tmp;
          }
        This->Nombre = 0;
}
/******************************************************************************/

int TPile_Length(TPile *This)
{
        return This->Nombre;
}
/******************************************************************************/

void TPile_View(TPile *This)
{
       Titem *tmp = This->Top;
       while(tmp)
          {
             printf("%d\n",tmp->Value);
             tmp = (*tmp).prec;
          }
}
/******************************************************************************/

void TPile_Free(TPile *This)
{
        TPile_Clear(This);
        puts("Destruction de la pile static.\n");
}
/******************************************************************************/

void TPile_New_Free(TPile *This)
{
        if(This) TPile_Clear(This);
        free(This);        
        puts("Destruction de la pile dynamique.\n");
}

Voici un exemple d'utilisation de la pile que nous venons de construire.

main.c :

#include <stdlib.h>
#include <stdio.h>

#include "Pile.h"

int main()
{
        TPile MaPile = TPile_Create();

        MaPile.Push(&MaPile, 10);
        MaPile.Push(&MaPile, 25);
        MaPile.Push(&MaPile, 33);
        MaPile.Push(&MaPile, 12);

        puts("Affichage de la pile :");
        MaPile.View(&MaPile);
        puts("------");

        printf("Nb d'elements : %d\n",MaPile.Length(&MaPile));
        puts("------");

        puts("Deux valeurs soutirees de la pile :");
        printf("%d\n",MaPile.Pop(&MaPile));
        printf("%d\n",MaPile.Pop(&MaPile));
        puts("------");

        puts("Affichage de la pile :");
        MaPile.View(&MaPile);
        puts("------");

        MaPile.Clear(&MaPile);
        MaPile.Push(&MaPile, 18);

        puts("Affichage de la pile apres vidage et ajout d'une valeur :");
        MaPile.View(&MaPile);
        puts("------\n");

        MaPile.Free(&MaPile);

#ifdef __WIN32__
        system("PAUSE");
#endif
        return 0;
}

Voici un exemple d'utilisation de la pile que nous venons de construire. Avec une création dynamique de l'objet :

main.c :

#include <stdlib.h>
#include <stdio.h>

#include "Pile.h"

int main()
{
        TPile *MaPile = TPile_New_Create();

        MaPile->Push(MaPile, 10);
        MaPile->Push(MaPile, 25);
        MaPile->Push(MaPile, 33);
        MaPile->Push(MaPile, 12);

        puts("Affichage de la pile :");
        MaPile->View(MaPile);
        puts("------");

        printf("Nb d'elements : %d\n",MaPile->Length(MaPile));
        puts("------");

        puts("Deux valeurs soutirees de la pile :");
        printf("%d\n",MaPile->Pop(MaPile));
        printf("%d\n",MaPile->Pop(MaPile));
        puts("------");

        puts("Affichage de la pile :");
        MaPile->View(MaPile);
        puts("------");

        MaPile->Clear(MaPile);
        MaPile->Push(MaPile, 18);

        puts("Affichage de la pile apres vidage et ajout d'une valeur :");
        MaPile->View(MaPile);
        puts("------\n");

        MaPile->Free(MaPile);

#ifdef __WIN32__
        system("PAUSE");
#endif
        return 0;
}




Bonne lecture,
CGi.





C/C++
  Les pointeurs du C/C++.   Les listes chaînées.             Liste simple.             Liste triée.             Liste double.   Les arbres.   Les tas.   Le C orienté objets ?

  1 - La fenêtre principale.   2 - Contrôles et messages.   3 - Les commandes.   4 - Dialogue std.   5 - Contexte de périph.   6 - Dessiner.   7 - Les ressources.   8 - Dialogue perso.   9 - Dialogue comm.   10 - Les accélérateurs.

Assembleur
  Assembleur sous Visual C++.

C++ BUILDER
  Trucs et astuces.   Composant.   TRichEdit.   TDrawGrid.   Application MDI.   TThread.   wxWidgets.   Style Win XP.

  Première application.   Construire un menu.   Dessiner.   Sisers, Timers...   Dialogues standards.   Dialogues perso.

DotNet
  Composant C# Builder.   Contrôle WinForm.   Application MDI.

Java
  Applet java.





Copyright 2002-2016 CGi - Tous droits réservés CGi. Toutes reproduction, utilisation ou diffusion de ce document par quelque moyen que ce soit autre que pour un usage personnel doit faire l'objet d'une autorisation écrite de la part de l'auteur, propriétaire des droits intellectuels.
Les codes sources de ce document sont fournis en l'état. L'utilisateur les utilise à ses risques et périls, sans garantie d'aucune sorte de la part de l'auteur. L'auteur n'est responsable d'aucun dommage subi par l'utilisateur pouvant résulter de l'utilisation ou de la distribution des codes sources de ce document.
De la même façon, l'auteur n'est en aucun cas responsable d'une quelconque perte de revenus ou de profits, ou de données, ou de tous dommages directs ou indirects, susceptibles de survenir du fait de l'utilisation des codes sources de ce document, quand bien même l'auteur aurait été averti de la possibilité de tels dommages. L'utilisation des codes sources de ce document vaut acceptation par l'utilisateur des termes de la licence ci-dessus.

Contacter le responsable de la rubrique C