Quelles techniques / stratégies les gens utilisent-ils pour construire des objects en C (pas C ++)?

Je m’intéresse particulièrement aux objects destinés à être utilisés à partir de C, par opposition aux implémentations d’objects qui constituent le kernel de langages interprétés tels que python.

J’ai tendance à faire quelque chose comme ça:

struct foo_ops { void (*blah)(struct foo *, ...); void (*plugh)(struct foo *, ...); }; struct foo { struct foo_ops *ops; /* data fields for foo go here */ }; 

Avec ces définitions de structure, le code implémentant foo ressemble à ceci:

 static void plugh(struct foo *, ...) { ... } static void blah(struct foo *, ...) { ... } static struct foo_ops foo_ops = { blah, plugh }; struct foo *new_foo(...) { struct foo *foop = malloc(sizeof(*foop)); foop->ops = &foo_ops; /* fill in rest of *foop */ return foop; } 

Ensuite, dans le code qui utilise foo:

 struct foo *foop = new_foo(...); foop->ops->blah(foop, ...); foop->ops->plugh(foop, ...); 

Ce code peut être nettoyé avec des macros ou des fonctions en ligne afin qu’il ressemble plus à C

 foo_blah(foop, ...); foo_plugh(foop, ...); 

bien que si vous vous en tenez à un nom raisonnablement court pour le champ “ops”, écrire simplement le code affiché à l’origine n’est pas particulièrement prolixe.

Cette technique est tout à fait adaptée à la mise en œuvre de conceptions C relativement simples basées sur des objects, mais elle ne gère pas les exigences plus avancées, telles que la représentation explicite des classes et l’inheritance de méthodes. Pour ceux-là, vous pourriez avoir besoin de quelque chose comme GObject (comme EFraim l’a mentionné), mais je vous conseillerais de vous assurer que vous avez réellement besoin des fonctionnalités supplémentaires des frameworks plus complexes.

Votre utilisation du terme “objects” est un peu vague, donc je vais supposer que vous demandez comment utiliser le langage C pour réaliser certains aspects de la programmation orientée object (n’hésitez pas à me corriger sur cette hypothèse.)

Méthode Polymorphisme:

Le polymorphism de méthode est généralement émulé en C à l’aide de pointeurs de fonction. Par exemple, si j’avais une structure qui représentait une image_scaler (quelque chose qui prend une image et la redimensionne à de nouvelles dimensions), je pourrais faire quelque chose comme ça:

 struct image_scaler { //member variables int (*scale)(int, int, int*); } 

Ensuite, je pourrais faire plusieurs scalers d’image en tant que tels:

 struct image_scaler nn, bilinear; nn->scale = &nearest_neighbor_scale; bilinear->scale = &bilinear_scale; 

Cela me permet d’obtenir un comportement polymorphe pour toute fonction qui prend un image_scaler et utilise sa méthode d’échelle en lui passant simplement un autre image_scaler.

Héritage

L’inheritance est généralement obtenu en tant que tel:

 struct base{ int x; int y; } struct derived{ struct base; int z; } 

Maintenant, je suis libre d’utiliser les champs supplémentaires dérivés, en plus d’obtenir tous les champs de base «hérités». De plus, si vous avez une fonction qui ne prend qu’une base de structure. vous pouvez simplement convertir votre pointeur struct dervied en un pointeur struct base sans conséquence

Bibliothèques telles que GObject .

En gros, GObject fournit un moyen courant de décrire des valeurs opaques (entiers, chaînes) et des objects (en décrivant manuellement l’interface – en tant que structure de pointeurs de fonction, correspondant à une table VTable en C ++) – vous trouverez plus d’informations sur la structure dans sa référence.

Vous utiliseriez souvent aussi des tables vtables comme dans “COM en clair C”

Comme vous pouvez le constater en parcourant toutes les réponses, il existe des bibliothèques, des pointeurs de fonction, des moyens d’inheritance, une encapsulation, etc., tous disponibles (le C ++ était à l’origine un frontal pour le C).

Cependant, j’ai trouvé qu’un aspect TRÈS important du logiciel était la lisibilité. Avez-vous essayé de lire le code d’il y a 10 ans? En conséquence, j’ai tendance à adopter l’approche la plus simple lorsque je fais des choses comme des objects en C.

Posez les questions suivantes:

  1. S’agit-il d’un client avec une date limite (si oui, considérez la POO)?
  2. Puis-je utiliser une POO (souvent moins de code, plus rapide à développer, plus lisible)?
  3. Puis-je utiliser une bibliothèque (code existant, modèles existants)?
  4. Suis-je contraint par la mémoire ou le processeur (par exemple, Arduino)?
  5. Existe-t-il une autre raison technique d’utiliser C?
  6. Puis-je garder mon C très simple et lisible?
  7. De quelles fonctionnalités POO ai-je vraiment besoin pour mon projet?

Je reviens généralement à quelque chose comme l’API GLIB qui me permet d’encapsuler mon code et fournit une interface très lisible. S’il en faut plus, j’ajoute des pointeurs de fonction pour le polymorphism.

 class_A.h: typedef struct _class_A {...} Class_A; Class_A* Class_A_new(); void Class_A_empty(); ... #include "class_A.h" Class_A* my_instance; my_instance = Class_A_new(); my_instance->Class_A_empty(); // can override using function pointers 

Regardez l’ implémentation d’IJG . Ils utilisent non seulement setjmp / longjmp pour la gestion des exceptions, ils ont vtables et tout. C’est une bibliothèque assez bien écrite et suffisamment petite pour que vous puissiez avoir un très bon exemple.

Semblable à l’approche de Dale, PostgreSQL ™ représente un peu plus en profondeur la façon dont PostgreSQL représente les nœuds d’parsing d’arborescence, les types d’expression, etc., de manière interne. Il existe des structures Node et Expr par défaut, le long des lignes de

 typedef struct { NodeTag n; } Node; 

NodeTag est un typedef pour unsigned int, et il existe un fichier d’en-tête avec un groupe de constantes décrivant tous les types de nœuds possibles. Les nœuds eux-mêmes ressemblent à ceci:

 typedef struct { NodeTag n = FOO_NODE; /* other members go here */ } FooNode; 

et un FooNode peut être FooNode en un Node en toute impunité, à cause d’une bizarrerie de structures C: si deux structures ont des premiers membres identiques, elles peuvent être converties l’une vers l’autre.

Oui, cela signifie qu’un FooNode peut être BarNode un BarNode , ce que vous ne voulez probablement pas faire. Si vous voulez une vérification correcte du type à l’exécution, GObject est la voie à suivre, mais préparez-vous à détester la vie pendant que vous maîsortingsez la vie.

(note: des exemples de mémoire, je n’ai pas encore piraté les composants internes de Postgres. La FAQ des développeurs contient plus d’informations.)