Comment implémentez-vous une classe en C? [fermé]


139

En supposant que je doive utiliser C (pas de C ++ ou de compilateurs orientés objet) et que je n'ai pas d'allocation de mémoire dynamique, quelles sont les techniques que je peux 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 une constante avant la compilation. N'hésitez pas à faire des hypothèses sur le concept de POO que je devrai implémenter (cela variera) et à suggérer la meilleure méthode pour chacun.

Restrictions:

  • Je dois utiliser C et non une POO car j'écris du code pour un système embarqué, et le compilateur et la base de code préexistante sont en C.
  • Il n'y a pas d'allocation de mémoire dynamique parce que nous n'avons pas assez de mémoire pour supposer raisonnablement que nous ne manquerons pas si nous commençons à l'allouer dynamiquement.
  • Les compilateurs avec lesquels nous travaillons n'ont aucun problème avec les pointeurs de fonction

26
Question obligatoire: devez-vous écrire du code orienté objet? Si vous le faites pour une raison quelconque, c'est bien, mais vous mènerez une bataille plutôt difficile. C'est probablement pour le mieux si vous évitez d'essayer d'écrire du code orienté objet en C. C'est certainement possible - voyez l'excellente réponse de dérouler - mais ce n'est pas exactement "facile", et si vous travaillez sur un système embarqué avec une mémoire limitée, cela peut ne pas être faisable. Je me trompe peut-être, cependant - je n'essaie pas de vous en dissuader, je présente simplement quelques contrepoints qui n'ont peut-être pas été présentés.
Chris Lutz

1
À proprement parler, nous n'avons pas à le faire. Cependant, la complexité du système a rendu le code impossible à maintenir. Mon sentiment est que la meilleure façon de réduire la complexité est de mettre en œuvre certains concepts de POO. Merci à tous ceux qui ont répondu dans les 3 minutes. Vous êtes fous et rapides!
Ben Gartner

8
Ce n'est que mon humble avis, mais la POO ne rend pas le code instantanément maintenable. Cela peut faciliter la gestion, mais pas nécessairement plus facile à maintenir. Vous pouvez avoir des "espaces de noms" en C (Apache Portable Runtime préfixe tous les symboles globaux avec apr_et GLib les préfixe avec g_pour créer un espace de noms) et d'autres facteurs d'organisation sans POO. Si vous restructurez de toute façon l'application, j'envisagerais de passer un peu de temps à essayer de créer une structure procédurale plus durable.
Chris Lutz

cela a été discuté sans fin auparavant - avez-vous regardé l'une des réponses précédentes?
Larry Watanabe le

Cette source, qui était dans une de mes réponses supprimées, peut également être utile: planetpdf.com/codecuts/pdfs/ooc.pdf Elle décrit une approche complète pour faire OO en C.
Ruben Steins

Réponses:


86

Cela dépend de l'ensemble de fonctionnalités "orienté objet" exact que vous souhaitez avoir. Si vous avez besoin de choses comme la surcharge et / ou des 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;
}

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

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 des fonctions, vous ne pouvez pas avoir plus d'une rectangle_new()fonction:

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 au moins quelques idées. Pour un framework orienté objet réussi et riche en C, consultez la bibliothèque GObject de glib .

Notez également qu'il n'y a pas de "classe" explicite modélisée ci-dessus, chaque objet a ses propres pointeurs de méthode qui sont un peu plus flexibles que ce que vous trouverez généralement en C ++. En outre, cela coûte de la mémoire. Vous pouvez vous en éloigner en insérant les pointeurs de méthode dans une classstructure et en inventant un moyen pour chaque instance d'objet de référencer une classe.


N'ayant pas eu à essayer d'écrire du C orienté objet, est-il généralement préférable de faire des fonctions qui prennent const ShapeClass *ou const void *comme arguments? Il semblerait que ce dernier soit un peu plus gentil en héritage, mais je peux voir les arguments dans les deux sens ...
Chris Lutz

1
@Chris: Ouais, c'est une question difficile. : | GTK + (qui utilise GObject) utilise la classe appropriée, c'est-à-dire RectangleClass *. Cela signifie que vous devez souvent faire des transtypages, mais ils fournissent des macros pratiques pour vous aider, vous pouvez donc toujours convertir BASECLASS * p en SUBCLASS * en utilisant simplement SUBCLASS (p).
détendre le

1
Mon compilateur échoue sur la deuxième ligne de code: float (*computeArea)(const ShapeClass *shape);dire que ShapeClassc'est un type inconnu.
DanielSank

@DanielSank qui est dû à l'absence de déclaration forward requise par le 'typedef struct` (non montré dans l'exemple donné). Parce que les structréférences elles-mêmes, il nécessite une déclaration avant d' être défini. Ceci est expliqué avec un exemple ici dans la réponse de Lundin . La modification de l'exemple pour inclure la déclaration de transfert devrait résoudre votre problème; typedef struct ShapeClass ShapeClass; struct ShapeClass { float (*computeArea)(const ShapeClass *shape); };
S.Whittaker

Que se passe-t-il lorsque Rectangle a une fonction que toutes les formes ne font pas? Par exemple, get_corners (). Un cercle n'implémenterait pas cela, mais un rectangle le pourrait. Comment accéder à une fonction qui ne fait pas partie de la classe parent dont vous avez hérité?
Otus

24

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

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

Un exemple simple serait celui-ci:

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

void push(Queue* q, int element);
void pop(Queue* q);
// etc.
/// 

C'est ce que j'ai fait dans le passé, mais avec l'ajout de la fausse portée en plaçant des prototypes de fonction dans le fichier .c ou .h selon les besoins (comme je l'ai mentionné dans ma réponse).
Taylor Leese

J'aime ça, la déclaration struct alloue toute la mémoire. Pour une raison quelconque, j'ai oublié que cela fonctionnerait bien.
Ben Gartner

Je pense que vous en avez besoin typedef struct Queue Queue;.
Craig McQueen

3
Ou simplement typedef struct {/ * members * /} Queue;
Brooks Moses

#Craig: Merci pour le rappel, mis à jour.
erelender le

12

Si vous ne voulez qu'une seule classe, utilisez un tableau de structs comme données "objets" et passez-leur des pointeurs vers les fonctions "membres". Vous pouvez utiliser typedef struct _whatever Whateveravant de déclarer struct _whateverpour masquer l'implémentation du code client. Il n'y a aucune différence entre un tel "objet" et l' FILEobjet de bibliothèque standard C.

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

Il y a aussi un livre sur les techniques de ce disponible en ligne - Programmation orientée objet avec C ANSI .


1
Cool! D'autres recommandations pour les livres sur la POO en C? Ou toute autre technique de conception moderne en C? (ou systèmes embarqués?)
Ben Gartner

7

vous pouvez jeter un oeil à GOBject. c'est une bibliothèque OS qui vous donne une manière verbeuse de faire un objet.

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


1
Très intéressé. Quelqu'un est-il au courant des licences? Pour mes besoins au travail, déposer une bibliothèque open source dans un projet ne fonctionnera probablement pas d'un point de vue juridique.
Ben Gartner

GTK +, et toutes les bibliothèques qui font partie de ce projet (y compris GObject), sont sous licence GNU LGPL, ce qui signifie que vous pouvez vous y connecter à partir d'un logiciel propriétaire. Je ne sais pas si cela sera faisable pour le travail intégré, cependant.
Chris Lutz

7

Interfaces et implémentations C: techniques de création de logiciels réutilisables , David R. Hanson

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

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

Le paradigme de base est quelque chose comme ceci:

/* for data structure foo */

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

5

Je vais donner un exemple simple de la façon dont la POO doit être faite en C. Je me rends compte que cette thead date de 2009 mais j'aimerais quand même l'ajouter.

/// 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 consiste à placer la «classe héritée» en haut de la structure. De cette façon, accéder aux 4 premiers octets de la structure accède également aux 4 premiers octets de la 'classe héritée' (en supposant des optimisations 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 accède normalement à ses membres.

Ceci et quelques conventions de dénomination pour les constructeurs, les destructeurs, les fonctions d'allocation et de désallocation (je recommande init, clean, new, free) vous feront un long chemin.

Comme pour les fonctions virtuelles, utilisez des pointeurs de fonction dans la structure, éventuellement avec Class_func (...); wrapper aussi. Comme pour les modèles (simples), ajoutez un paramètre size_t pour déterminer la taille, exigez un pointeur void *, ou exigez un type «classe» avec juste la fonctionnalité qui vous intéresse. (par exemple int GetUUID (Object * self); GetUUID (& p);)


Avertissement: tout le code écrit sur le smartphone. Ajoutez des contrôles d'erreur si nécessaire. Vérifiez les bogues.
yyny

4

Utilisez a structpour 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 privée dans le fichier .c et les fonctions publiques dans le fichier .h.


4
#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;
};

3

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


1
Quelqu'un peut-il donner une brève différence entre un type de données abstrait et une classe? J'ai toujours des deux concepts aussi étroitement liés.
Ben Gartner

Ils sont en effet étroitement liés. Une classe peut être considérée comme une implémentation d'un ADT, puisqu'elle pourrait (supposément) être remplacée par une autre implémentation satisfaisant la même interface. Je pense qu'il est cependant difficile de donner une différence exacte, car les concepts ne sont pas clairement définis.
Jørgen Fogh

3

Ma stratégie est:

  • Définissez tout le code de la classe dans un fichier séparé
  • Définissez toutes les interfaces de 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, ...) OU ClassHandle ClassInit (int x, int y, ...) selon la stratégie d'allocation de mémoire
  • 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 poignées prédéfinies (visibles dans l'interface) ou une limite fixe d'objets pouvant être instanciés
  • Si cela est 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 trous, des pièges potentiels ou des avantages / inconvénients cachés à 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'en indiquer le nom?


Par souci de style, si vous avez des informations à ajouter à votre question, vous devez modifier votre question pour inclure ces informations.
Chris Lutz

Vous semblez être passé de l'allocation dynamique de malloc d'un grand tas à ClassInit () en sélectionnant dynamiquement à partir d'un pool de taille fixe, plutôt que de faire quoi que ce soit à propos de ce qui se passera lorsque vous demandez un autre objet et que vous n'avez pas les ressources pour en fournir un .
Pete Kirkham

Oui, la charge de gestion de la mémoire est déplacée sur le code appelant ClassInit () pour vérifier que le handle retourné est valide. Nous avons essentiellement créé notre propre tas dédié pour la classe. Je ne suis pas sûr de voir un moyen d'éviter cela si nous voulons faire une allocation dynamique, à moins que nous n'implémentions un tas à usage général. Je préférerais isoler le risque hérité du tas à une classe.
Ben Gartner

3

Voir aussi cette réponse et celle-ci

C'est possible. Cela semble toujours être une bonne idée à l'époque, mais après cela, cela devient un cauchemar de maintenance. Votre code est jonché de morceaux de code liant tout ensemble. Un nouveau programmeur aura beaucoup de problèmes à lire et à comprendre le code si vous utilisez des pointeurs de fonction car il ne sera pas évident de savoir quelles fonctions sont appelées.

Le masquage des données avec les fonctions get / set est facile à implémenter en C mais arrêtez-vous là. J'ai vu plusieurs tentatives 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 de maintenance, j'éviterais.


2

Mon approche serait de déplacer les fonctions structet toutes les fonctions principalement associées vers un (des) fichier (s) source (s) séparé (s) afin qu'il puisse être utilisé "de manière portative".

En fonction de votre compilateur, vous pourrez peut- être inclure des fonctions dans le struct, mais c'est une extension très spécifique au compilateur, et n'a rien à voir avec la dernière version du standard que j'utilise régulièrement :)


2
Les pointeurs de fonction sont tous bons. Nous avons tendance à les utiliser pour remplacer les grandes instructions switch par une table de recherche.
Ben Gartner

2

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 pourriez essayer de déterrer un ancien préprocesseur C ++ et voir quel genre de solutions il crée.


Ce serait cfront; il a rencontré des problèmes lorsque des exceptions ont été ajoutées à C ++ - la gestion des exceptions n'est pas triviale.
Jonathan Leffler

2

GTK est entièrement construit sur C et utilise de nombreux concepts de POO. J'ai lu le code source de GTK et il est assez impressionnant et certainement plus facile à lire. Le concept de base est que chaque «classe» est simplement une structure et des fonctions statiques associées. Les fonctions statiques acceptent toutes la structure "instance" comme paramètre, font tout ce dont elles ont besoin et renvoient des résultats si nécessaire. Par exemple, vous pouvez avoir une fonction "GetPosition (CircleStruct obj)". La fonction creuserait simplement dans la structure, extrairait les numéros de position, construirait probablement un nouvel objet PositionStruct, collerait le x et y dans le nouveau PositionStruct et le retournerait. GTK implémente même l'héritage de cette manière en incorporant des structures dans des structures. assez intelligent.


1

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 affectez tous les pointeurs de fonction à des fonctions C standard, vous pourrez appeler des fonctions à partir de C dans une syntaxe très similaire à celle que vous feriez sous C ++.

Si vous voulez avoir des méthodes virtuelles, cela devient plus compliqué. Fondamentalement, vous devrez implémenter votre propre VTable à 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 le VTable. C'est essentiellement ce que fait C ++.

TBH cependant ... si vous voulez ce dernier, il vaut probablement mieux trouver un compilateur C ++ que vous pouvez utiliser et recompiler le projet. Je n'ai jamais compris l'obsession du C ++ non utilisable en 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 être un peu plus prudent dans ce que vous faites, mais ce n'est vraiment pas si compliqué.


Je l'ai déjà dit et je le répète, mais je le répète: vous n'avez pas besoin de pointeurs de fonction ou de la possibilité d'appeler des fonctions à partir de structures de style C ++ pour créer la POO en C, la POO concerne principalement l'héritage des fonctionnalités et des variables (contenu) qui peuvent tous deux être réalisés en C sans pointeurs de fonction ni code de duplication.
yyny

0

C n'est pas un langage POO, comme vous le faites remarquer à juste titre, il n'y a donc pas de moyen intégré d'écrire une vraie classe. Le mieux est de regarder les structures et les pointeurs de fonction , ceux-ci 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).

De plus, si vous pouvez utiliser C, vous pouvez probablement utiliser C ++ et obtenir des classes.


4
Je ne voterai pas contre, mais pour info, les pointeurs de fonction ou la possibilité d'appeler des fonctions à partir de structures (ce qui, je suppose, est votre intention) n'a rien à voir avec la POO. La POO concerne principalement l'héritage de fonctionnalités et de variables, qui peuvent tous deux être réalisés en C sans pointeurs de fonction ni duplications.
yyny
En utilisant notre site, vous reconnaissez avoir lu et compris notre politique liée aux cookies et notre politique de confidentialité.
Licensed under cc by-sa 3.0 with attribution required.