web-dev-qa-db-fra.com

Comment implémentez-vous une classe en C?

En supposant que je dois utiliser C (pas de compilateurs C++ ou orientés objet) et que je n’ai pas d’allocation de mémoire dynamique, quelles techniques puis-je utiliser pour implémenter une classe ou une bonne approximation d’une classe? Est-ce toujours une bonne idée d'isoler la "classe" dans un fichier séparé? Supposons que nous puissions préallouer la mémoire en supposant un nombre fixe d'instances, ou même en définissant la référence à chaque objet comme constante avant la compilation. N'hésitez pas à faire des suppositions sur le concept OOP que je devrai mettre en œuvre (cela variera) et suggérer la meilleure méthode pour chacune.

Restrictions

  • Je dois utiliser C et pas un OOP parce que j'écris du code pour un système intégré, et que le compilateur et la base de code préexistant sont en C.
  • Il n'y a pas d'allocation de mémoire dynamique car nous n'avons pas assez de mémoire pour supposer que nous ne nous épuiserons pas si nous commençons à l'allouer de manière dynamique.
  • Les compilateurs avec lesquels nous travaillons n'ont aucun problème avec les pointeurs de fonction
130
Ben Gartner

Cela dépend du jeu de fonctionnalités "orienté objet" exact que vous souhaitez avoir. Si vous avez besoin de choses telles que la surcharge et/ou les méthodes virtuelles, vous devez probablement inclure des pointeurs de fonction dans les structures:

typedef struct {
  float (*computeArea)(const ShapeClass *shape);
} ShapeClass;

float shape_computeArea(const ShapeClass *shape)
{
  return shape->computeArea(shape);
}

Cela vous permettrait d'implémenter une classe en "héritant" de la classe de base et en implémentant une fonction appropriée:

typedef struct {
  ShapeClass shape;
  float width, height;
} RectangleClass;

static float rectangle_computeArea(const ShapeClass *shape)
{
  const RectangleClass *rect = (const RectangleClass *) shape;
  return rect->width * rect->height;
}

Bien sûr, cela nécessite que vous implémentiez également un constructeur, qui s'assure que le pointeur de la fonction est correctement configuré. Normalement, vous alloueriez dynamiquement de la mémoire pour l'instance, mais vous pouvez également laisser l'appelant le faire également:

void rectangle_new(RectangleClass *rect)
{
  rect->width = rect->height = 0.f;
  rect->shape.computeArea = rectangle_computeArea;
}

Si vous voulez plusieurs constructeurs différents, vous devrez "décorer" les noms de fonctions, vous ne pouvez pas avoir plus d'une fonction rectangle_new():

void rectangle_new_with_lengths(RectangleClass *rect, float width, float height)
{
  rectangle_new(rect);
  rect->width = width;
  rect->height = height;
}

Voici un exemple de base montrant l'utilisation:

int main(void)
{
  RectangleClass r1;

  rectangle_new_with_lengths(&r1, 4.f, 5.f);
  printf("rectangle r1's area is %f units square\n", shape_computeArea(&r1));
  return 0;
}

J'espère que cela vous donne quelques idées, au moins. Pour un framework C performant et riche en objets, regardez dans la bibliothèque GObject de glib.

Notez également qu'aucune "classe" explicite n'est modélisée ci-dessus, chaque objet possède ses propres pointeurs de méthode, ce qui est un peu plus flexible que ce que vous trouverez généralement en C++. En outre, cela coûte de la mémoire. Vous pourriez vous en sortir en introduisant les pointeurs de méthode dans une structure class et en inventant un moyen de faire référence à une classe pour chaque instance d'objet.

81
unwind

Je devais le faire une fois aussi pour faire mes devoirs. J'ai suivi cette approche:

  1. Définissez vos données membres dans une structure.
  2. Définissez les membres de votre fonction qui prennent un pointeur sur votre structure comme premier argument.
  3. Faites-les dans un en-tête et un c. En-tête pour la définition de la structure et les déclarations de fonction, c pour les implémentations.

Voici un exemple simple:

/// Queue.h
struct Queue
{
    /// members
}
typedef struct Queue Queue;

void Push(Queue* q, int element);
void pop(Queue* q);
// etc.
/// 
23
erelender

Si vous ne voulez qu'une classe, utilisez un tableau de structs en tant que données "objets" et transmettez-leur des pointeurs aux fonctions "membre". Vous pouvez utiliser typedef struct _whatever Whatever avant de déclarer struct _whatever pour masquer l'implémentation du code client. Il n'y a pas de différence entre un tel "objet" et la bibliothèque standard C FILE objet.

Si vous voulez plus d'une classe avec héritage et fonctions virtuelles, il est courant d'avoir des pointeurs sur les fonctions en tant que membres de la structure, ou un pointeur partagé sur une table de fonctions virtuelles. La bibliothèque GObject utilise à la fois cette astuce et celle de typedef, et est largement utilisée.

Il existe également un livre sur les techniques disponibles en ligne - Programmation orientée objet avec ANSI C .

12
Pete Kirkham

Interfaces et implémentations en C: Techniques pour créer un logiciel réutilisable, David R. Hanson

http://www.informit.com/store/product.aspx?isbn=020149841

Ce livre fait un excellent travail pour répondre à votre question. Il fait partie de la série Addison Wesley Professional Computing.

Le paradigme de base ressemble à ceci:

/* for data structure foo */

FOO *myfoo;
myfoo = foo_create(...);
foo_something(myfoo, ...);
myfoo = foo_append(myfoo, ...);
foo_delete(myfoo);
7
Mark Harrison

vous pouvez jeter un oeil à GOBject. c'est une bibliothèque de système d'exploitation qui vous donne un moyen détaillé de créer un objet.

http://library.gnome.org/devel/gobject/stable/

7
Alex

Utilisez un struct pour simuler les données membres d'une classe. En termes de portée de méthode, vous pouvez simuler des méthodes privées en plaçant les prototypes de fonction private dans le fichier .c et les fonctions public dans le fichier .h.

4
Taylor Leese

Je vais donner un exemple simple de la façon dont OOP devrait être fait en C. Je réalise que cette thead date de 2009, mais je voudrais l’ajouter quand même.

/// Object.h
typedef struct Object {
    uuid_t uuid;
} Object;

int Object_init(Object *self);
uuid_t Object_get_uuid(Object *self);
int Object_clean(Object *self);

/// Person.h
typedef struct Person {
    Object obj;
    char *name;
} Person;

int Person_init(Person *self, char *name);
int Person_greet(Person *self);
int Person_clean(Person *self);

/// Object.c
#include "object.h"

int Object_init(Object *self)
{
    self->uuid = uuid_new();

    return 0;
}
uuid_t Object_get_uuid(Object *self)
{ // Don't actually create getters in C...
    return self->uuid;
}
int Object_clean(Object *self)
{
    uuid_free(self->uuid);

    return 0;
}

/// Person.c
#include "person.h"

int Person_init(Person *self, char *name)
{
    Object_init(&self->obj); // Or just Object_init(&self);
    self->name = strdup(name);

    return 0;
}
int Person_greet(Person *self)
{
    printf("Hello, %s", self->name);

    return 0;
}
int Person_clean(Person *self)
{
    free(self->name);
    Object_clean(self);

    return 0;
}

/// main.c
int main(void)
{
    Person p;

    Person_init(&p, "John");
    Person_greet(&p);
    Object_get_uuid(&p); // Inherited function
    Person_clean(&p);

    return 0;
}

Le concept de base implique de placer la 'classe héritée' en haut de la structure. De cette façon, l'accès aux 4 premiers octets de la structure accède également aux 4 premiers octets de la 'classe héritée' (optimisations Asuming non-folles). Désormais, lorsque le pointeur de la structure est converti en "classe héritée", la "classe héritée" peut accéder aux "valeurs héritées" de la même manière qu'elle aurait normalement accès à ses membres.

Ceci, ainsi que certaines conventions de dénomination pour les constructeurs, les destructeurs, les fonctions d'allocation et de désalkylion (je recommande init, clean, new, free) vous mènera loin.

Comme pour les fonctions virtuelles, utilisez les pointeurs de fonction dans la structure, éventuellement avec Class_func (...); emballage aussi. En ce qui concerne les modèles (simples), ajoutez un paramètre size_t pour déterminer la taille, utilisez un pointeur *, ou un type 'classe' contenant uniquement les fonctionnalités qui vous intéressent. (par exemple, int GetUUID (Object * self); GetUUID (& p);)

4
YoYoYonnY
#include <stdio.h>
#include <math.h>
#include <string.h>
#include <uchar.h>

/**
 * Define Shape class
 */
typedef struct Shape Shape;
struct Shape {
    /**
     * Variables header...
     */
    double width, height;

    /**
     * Functions header...
     */
    double (*area)(Shape *shape);
};

/**
 * Functions
 */
double calc(Shape *shape) {
        return shape->width * shape->height;
}

/**
 * Constructor
 */
Shape _Shape() {
    Shape s;

    s.width = 1;
    s.height = 1;

    s.area = calc;

    return s;
}

/********************************************/

int main() {
    Shape s1 = _Shape();
    s1.width = 5.35;
    s1.height = 12.5462;

    printf("Hello World\n\n");

    printf("User.width = %f\n", s1.width);
    printf("User.height = %f\n", s1.height);
    printf("User.area = %f\n\n", s1.area(&s1));

    printf("Made with \xe2\x99\xa5 \n");

    return 0;
};
4
Pierozi

Dans votre cas, la bonne approximation de la classe pourrait être le an ADT . Mais ce ne sera toujours pas la même chose.

3
Artem Barger

Peut-être ce livre en ligne pourrait vous aider à résoudre votre problème.

3

Ma stratégie est la suivante:

  • Définir tout le code de la classe dans un fichier séparé
  • Définir toutes les interfaces pour la classe dans un fichier d'en-tête séparé
  • Toutes les fonctions membres prennent un "ClassHandle" qui remplace le nom de l'instance (au lieu de o.foo (), appelez foo (oHandle)
  • Le constructeur est remplacé par une fonction void ClassInit (ClassHandle h, int x, int y, ...) OR ClassHandle ClassInit (int x, int y, ...) en fonction de la mémoire stratégie d'allocation
  • Toutes les variables membres sont stockées en tant que membre d'une structure statique dans le fichier de classe, l'encapsulant dans le fichier, empêchant les fichiers extérieurs d'y accéder.
  • Les objets sont stockés dans un tableau de la structure statique ci-dessus, avec des descripteurs prédéfinis (visibles dans l'interface) ou une limite fixe d'objets pouvant être instanciés.
  • Si utile, la classe peut contenir des fonctions publiques qui vont parcourir le tableau et appeler les fonctions de tous les objets instanciés (RunAll () appelle chaque Run (oHandle)
  • Une fonction Deinit (ClassHandle h) libère la mémoire allouée (index de tableau) dans la stratégie d'allocation dynamique

Quelqu'un voit-il des problèmes, des failles, des pièges potentiels ou des avantages/inconvénients cachés de l'une ou l'autre variante de cette approche? Si je réinvente une méthode de conception (et je suppose que je dois l'être), pouvez-vous m'indiquer le nom de celle-ci?

3
Ben Gartner

Voir aussi cette réponse et celui-ci

C'est possible. Cela semble toujours être une bonne idée sur le moment, mais après cela devient un cauchemar d’entretien. Votre code est encombré de morceaux de code liant le tout. Un nouveau programmeur aura beaucoup de difficultés à lire et à comprendre le code si vous utilisez des pointeurs de fonction, car le nom des fonctions n’est pas évident.

Le masquage de données avec les fonctions get/set est facile à implémenter en C mais arrêtez-vous là. J'ai vu de nombreuses tentatives en ce sens dans l'environnement embarqué et à la fin, c'est toujours un problème de maintenance.

Puisque vous êtes tous prêts à avoir des problèmes d’entretien, j’éviterai cela.

3
Gerhard

Il existe un livre très complet sur le sujet, qui mérite peut-être d'être lu:

Programmation orientée objet en ANSI-C

3
Ruben Steins

Mon approche serait de déplacer les fonctions struct et toutes principalement associées vers un ou plusieurs fichiers source distincts, de manière à ce que utilisé "portable".

Selon votre compilateur, vous pourriez pouvoir inclure des fonctions dans le struct, mais c'est un très extension spécifique au compilateur, et n’a rien à voir avec la dernière version de la norme que j’utilise couramment :)

2
warren

Le premier compilateur c ++ était en fait un préprocesseur qui traduisait le code C++ en C.

Il est donc très possible d'avoir des classes en C. Vous pouvez essayer de digérer un ancien préprocesseur C++ et voir quel type de solutions il crée.

2
Toad

GTK est entièrement construit sur C et utilise de nombreux concepts OOP. J'ai lu le code source de GTK et il est assez impressionnant et nettement plus facile à lire. Le concept de base est que " class "est simplement une structure et des fonctions statiques associées. Les fonctions statiques acceptent toutes la" instance "comme paramètre, font ce qui est alors nécessaire et renvoient des résultats si nécessaire. Par exemple, vous pouvez avoir une fonction" GetPosition (CircleStruct obj ) ". La fonction se contenterait de creuser dans la structure, d’extraire les numéros de position, de construire probablement un nouvel objet PositionStruct, de coller les valeurs x et y dans la nouvelle PositionStruct et de la renvoyer. GTK implémente même l’héritage de cette manière en incorporant des structures dans des structures. assez intelligent.

2
rocketsarefast

Voulez-vous des méthodes virtuelles?

Sinon, vous définissez simplement un ensemble de pointeurs de fonction dans la structure elle-même. Si vous assignez tous les pointeurs de fonction aux fonctions C standard, vous pourrez appeler des fonctions à partir de C avec une syntaxe très similaire à celle que vous utiliseriez sous C++.

Si vous voulez avoir des méthodes virtuelles, cela devient plus compliqué. Fondamentalement, vous devrez implémenter votre propre VTable pour chaque structure et affecter des pointeurs de fonction à la VTable en fonction de la fonction appelée. Vous auriez alors besoin d'un ensemble de pointeurs de fonction dans la structure elle-même, qui à son tour appelle le pointeur de fonction dans la VTable. C’est essentiellement ce que fait C++.

TBH cependant ... si vous voulez ce dernier, vous feriez probablement mieux de chercher un compilateur C++ que vous pouvez utiliser et de recompiler le projet. Je n’ai jamais compris l’obsession du C++ de ne pas pouvoir être utilisé en mode embarqué. Je l'ai utilisé plusieurs fois et cela fonctionne est rapide et n'a pas de problèmes de mémoire. Bien sûr, vous devez faire un peu plus attention à ce que vous faites, mais ce n'est vraiment pas si compliqué.

1
Goz

C n’est pas une langue OOP, comme vous le signalez à juste titre, il n’existe donc pas de méthode intégrée pour écrire une vraie classe. Votre meilleur pari est de regarder structs , et pointeurs de fonction , ils vous permettront de construire une approximation d'une classe. Cependant, comme C est procédural, vous voudrez peut-être envisager d'écrire plus de code de type C (c'est-à-dire sans essayer d'utiliser des classes ).

En outre, si vous pouvez utiliser C, vous pouvez probablement utiliser C++ et obtenir des classes.

0
user130076