Créer des “classes” en C, sur la stack vs le tas?

Chaque fois que je vois une “classe” C (toute structure destinée à être utilisée en accédant à des fonctions qui lui pointent le premier argument), je les vois comme ceci:

typedef struct { int member_a; float member_b; } CClass; CClass* CClass_create(); void CClass_destroy(CClass *self); void CClass_someFunction(CClass *self, ...); ... 

Et dans ce cas, CClass_create toujours malloc s, c’est de la mémoire et renvoie un pointeur vers celui-ci.

Chaque fois que je vois de new versions en C ++ inutilement, cela semble généralement rendre les programmeurs C ++ complètement fous, mais cette pratique semble acceptable en C. Qu’est-ce qui donne? Y a-t-il une raison pour laquelle les classes “class” allouées au tas sont si courantes?

Il y a plusieurs raisons à cela.

  1. Utiliser des pointeurs “opaques”
  2. Manque de destructeurs
  3. Systèmes embarqués (problème de dépassement de stack)
  4. Conteneurs
  5. Inertie
  6. “Paresse”

Parlons-en brièvement.

Pour les pointeurs opaques , cela vous permet de faire quelque chose comme:

 struct CClass_; typedef struct CClass_ CClass; // the rest as in your example 

Ainsi, l’utilisateur ne voit pas la définition de struct CClass_ , l’isolant des modifications apscopes et activant d’autres éléments intéressants, comme l’implémentation différente de la classe pour différentes plates-formes.

Bien entendu, cela interdit l’utilisation de variables de stack de CClass . Mais, OTOH, on peut voir que cela n’empêche pas d’allouer des objects CClass manière statique (à partir d’un pool) – renvoyés par CClass_create ou peut-être une autre fonction comme CClass_create_static .

Manque de destructeurs – le compilateur C ne CClass pas automatiquement vos objects de stack CClass , vous devez le faire vous-même (en appelant manuellement la fonction destructeur). Ainsi, le seul avantage qui rest est le fait que l’allocation de la stack est en général plus rapide que l’allocation de tas. OTOH, vous n’avez pas besoin d’utiliser le tas – vous pouvez allouer à partir d’un pool, ou d’une arène, ou quelque chose du genre, et cela peut être presque aussi rapide que l’allocation de stack, sans les problèmes potentiels d’allocation de stack discutés ci-dessous.

Systèmes embarqués – Stack n’est pas une ressource “infinie”, vous savez. Bien sûr, pour la plupart des applications sur les systèmes d’exploitation “classiques” actuels (POSIX, Windows …), c’est presque le cas. Mais, sur les systèmes embarqués, la stack peut atteindre quelques Ko seulement. C’est extrême, mais même les «gros» systèmes embarqués ont une stack en Mo. Donc, il sera épuisé s’il est surutilisé. Quand c’est le cas, la plupart du temps, rien ne garantit ce qui va se passer – AFAIK, à la fois en C et en C ++, c’est-à-dire “Comportement non défini”. CClass_create() , CClass_create() peut renvoyer le pointeur NULL lorsque vous n’avez plus de mémoire et vous pouvez gérer cela.

Conteneurs – Les utilisateurs de C ++ aiment l’allocation de stack, mais si vous créez un std::vector sur la stack, son contenu sera alloué au tas. Vous pouvez bien sûr modifier cela, mais c’est le comportement par défaut, et il est beaucoup plus facile de dire “tous les membres d’un conteneur sont affectés au tas” plutôt que d’essayer de savoir comment les gérer.

Inertie – eh bien, l’OO venait de SmallTalk. Tout y est dynamic, donc la traduction “naturelle” en C est la méthode “mettre tout sur le tas”. Donc, les premiers exemples étaient comme ça et ils ont inspiré les autres pendant de nombreuses années.

Paresse ” – si vous savez que vous ne voulez que des objects de stack, vous avez besoin de quelque chose comme:

 CClass CClass_make(); void CClass_deinit(CClass *me); 

Mais si vous voulez autoriser la stack et le tas, vous devez append:

 CClass *CClass_create(); void CClass_destroy(CClass *me); 

C’est plus un travail à faire pour l’implémenteur, mais c’est aussi déroutant pour l’utilisateur. On peut créer des interfaces légèrement différentes, mais cela ne change rien au fait que vous avez besoin de deux ensembles de fonctions.

Bien entendu, la raison des “conteneurs” est aussi en partie une raison de “paresse”.

En supposant, comme dans votre question, CClass_create et CClass_destroy utilisent malloc/free , alors pour moi, suivre est une mauvaise pratique:

 void Myfunc() { CClass* myinstance = CClass_create(); ... CClass_destroy(myinstance); } 

parce que nous pourrions éviter un malloc et un libre facilement:

 void Myfunc() { CClass myinstance; // no malloc needed here, myinstance is on the stack CClass_Initialize(&myinstance); ... CClass_Uninitialize(&myinstance); // no free needed here because myinstance is on the stack } 

avec

 CClass* CClass_create() { CClass *self= malloc(sizeof(CClass)); CClass_Initialize(self); return self; } void CClass_destroy(CClass *self); { CClass_Uninitialize(self); free(self); } void CClass_Initialize(CClass *self) { // initialize stuff ... } void CClass_Uninitialize(CClass *self); { // uninitialize stuff ... } 

En C ++, nous préférerions également faire ceci:

 void Myfunc() { CClass myinstance; ... } 

que ceci:

 void Myfunc() { CClass* myinstance = new CCLass; ... delete myinstance; } 

Afin d’éviter une new / delete inutile.

Dans C, lorsqu’un composant fournit une fonction “create”, l’implémenteur du composant contrôle également la manière dont le composant est initialisé. Ainsi, non seulement l’ émulateur C ++ ‘ operator new mais aussi le constructeur de la classe.

Renoncer à ce contrôle sur l’initialisation signifie beaucoup plus de vérification des erreurs sur les entrées, alors garder le contrôle facilite la fourniture d’un comportement cohérent et prévisible.

Je malloc aussi à ce que malloc toujours utilisé pour allouer de la mémoire. Cela peut souvent être le cas, mais pas toujours. Par exemple, dans certains systèmes intégrés, vous constaterez que malloc / free n’est pas utilisé du tout. Les fonctions X_create peuvent allouer d’autres manières, par exemple à partir d’un tableau dont la taille est fixée au moment de la compilation.

Cela engendre beaucoup de réponses car il est quelque peu basé sur l’opinion . Je veux quand même expliquer pourquoi je préfère personnellement que mes «objects C» soient alloués sur le tas. La raison est que mes champs sont tous cachés (parle: privé ) du code consommateur. Cela s’appelle un pointeur opaque . En pratique, cela signifie que votre fichier d’en-tête ne définit pas la struct en cours d’utilisation, il le déclare uniquement. Conséquence directe, la consommation de code ne permet pas de connaître la taille de la struct et l’allocation de la stack devient donc impossible.

L’avantage est que: consumr du code ne peut jamais dépendre de la définition de la struct , cela signifie qu’il est impossible de rendre le contenu de la struct incohérent de l’extérieur et d’éviter une recompilation inutile du code consommé lorsque la struct change.

Le premier problème est résolu en c ++ en déclarant les champs private . Mais la définition de votre class est toujours imscope dans toutes les unités de compilation qui l’utilisent, ce qui nécessite de les recomstackr, même lorsque seuls vos membres private changent. La solution souvent utilisée dans c ++ est le modèle pimpl : tous les membres privés d’une deuxième struct (ou: class ) ne sont définis que dans le fichier d’implémentation. Bien sûr, cela nécessite que votre pimpl soit alloué sur le tas.

Ajoutons à cela: les langages OOP modernes (comme par exemple java ou c # ) ont des moyens d’allouer des objects (et décident généralement s’il s’agit d’une stack ou d’un tas en interne) sans que le code appelant connaisse leur définition.

En général, le fait de voir un * ne signifie pas qu’il a été malloc ‘d. Vous pourriez avoir un pointeur sur static variable globale static , par exemple; dans votre cas, en effet, CClass_destroy() ne prend aucun paramètre qui suppose qu’il connaît déjà des informations sur l’object détruit.

De plus, les pointeurs, qu’ils soient ou non malloc ‘d sont le seul moyen de modifier l’object.

Je ne vois pas de raisons particulières d’utiliser le tas au lieu de la stack: vous n’utilisez pas moins de mémoire. Ce qui est nécessaire, cependant, pour initialiser de telles “classes” sont des fonctions init / destroy car la structure de données sous-jacente peut avoir besoin de contenir des données dynamics, donc des pointeurs.

Je changerais le “constructeur” en un void CClass_create(CClass*);

Il ne renverra pas une instance / référence de la structure, mais sera appelé sur une.

Qu’elle soit allouée sur la “stack” ou de manière dynamic, elle dépend entièrement des exigences de votre scénario d’utilisation. CClass_create() que soit son CClass_create() , vous appelez simplement CClass_create() passant la structure allouée en tant que paramètre.

 { CClass stk; CClass_create(&stk); CClass *dyn = malloc(sizeof(CClass)); CClass_create(dyn); CClass_destroy(&stk); // the local object lifetime ends here, dyn lives on } // and later, assuming you kept track of dyn CClass_destroy(dyn); // destructed free(dyn); // deleted 

Faites juste attention à ne pas renvoyer une référence à un local (alloué sur la stack), car c’est UB.

void CClass_destroy(CClass*); que soit l’affectation, vous devez appeler void CClass_destroy(CClass*); au bon endroit (la fin de la durée de vie de cet object) et, si elle est allouée dynamicment, libère également cette mémoire.

Distinguer l’allocation / la désallocation et la construction / destruction, ce ne sont pas les mêmes (même si en C ++, ils pourraient être couplés automatiquement).

Parce qu’une fonction ne peut renvoyer qu’une structure allouée par stack si elle ne contient aucun pointeur vers d’autres structures allouées. S’il ne contient que des objects simples (int, bool, floats, chars et tableaux mais pas de pointeur ), vous pouvez l’allouer sur la stack. Mais vous devez savoir que si vous le retournez, il sera copié. Si vous souhaitez autoriser des pointeurs vers d’autres structures ou si vous souhaitez éviter la copie, utilisez heap.

Mais si vous pouvez créer la structure dans une unité de niveau supérieur et l’utiliser uniquement dans les fonctions appelées et ne jamais la renvoyer, la stack est appropriée

Si le nombre maximum d’objects d’un type quelconque devant exister simultanément est fixé, le système devra pouvoir faire quelque chose avec chaque instance “en direct”, et les éléments en question ne consumnt pas trop d’argent, le meilleur. L’approche n’est généralement ni l’allocation de tas ni l’allocation de stack, mais plutôt un tableau alloué statiquement, ainsi que des méthodes “create” et “destroy”. L’utilisation d’un tableau évite d’avoir à gérer une liste d’objects liés et permet de gérer le cas où un object ne peut pas être détruit immédiatement car il est “occupé” [par exemple si des données arrivent sur un canal via une interruption ou DMA lorsque le code utilisateur décide qu’il n’est plus intéressé par le canal et qu’il le supprime, le code utilisateur peut définir un indicateur “disposer dès lors qu’il est terminé” et retourner sans avoir à craindre une interruption en attente ou un stockage par écrasement DMA il].

L’utilisation d’un pool d’objects de taille fixe de taille fixe rend l’allocation et la désallocation beaucoup plus prévisibles que le stockage d’un segment de taille mixte. L’approche n’est pas géniale dans les cas où la demande est variable et que les objects prennent beaucoup d’espace (individuellement ou collectivement), mais lorsque la demande est la plupart du temps cohérente (par exemple, une application nécessite 12 objects en permanence et parfois 3 plus) cela peut s’avérer bien meilleur que des approches alternatives. La seule faiblesse est que toute installation doit être effectuée à l’endroit où le tampon statique est déclaré ou doit être effectuée par le code exécutable dans les clients. Il n’y a aucun moyen d’utiliser la syntaxe d’initialisation des variables sur un site client.

Incidemment, lorsque vous utilisez cette approche, il n’est pas nécessaire que le code client reçoive des pointeurs sur quoi que ce soit. Au lieu de cela, on peut identifier les ressources en utilisant la taille de nombre entier qui convient le mieux. De plus, si le nombre de ressources ne doit jamais dépasser le nombre de bits d’un int , il peut être utile de faire en sorte que certaines variables d’état utilisent un bit par ressource. Par exemple, on pourrait avoir des variables timer_notifications (écrites uniquement via un gestionnaire d’interruption) et des timer_acks (écrits uniquement via le code principal) et spécifier que le bit N de (timer_notifications ^ timer_acks) sera défini chaque fois que le timer N veut être (timer_notifications ^ timer_acks) . En utilisant une telle approche, le code n’a besoin que de lire deux variables pour déterminer si un temporisateur a besoin de service, plutôt que de devoir lire une seule variable pour chaque temporisateur.

C manque certaines choses que les programmeurs C ++ considèrent comme acquises.

  1. spécificateurs publics et privés
  2. constructeurs et destructeurs

Le grand avantage de cette approche est que vous pouvez masquer la structure dans votre fichier C et forcer la construction et la destruction correctes avec vos fonctions de création et de destruction.

Si vous exposez la structure dans votre fichier .h, cela signifie que les utilisateurs peuvent accéder directement aux membres, ce qui interrompt l’encapsulation. De même, ne pas forcer la création permet une construction incorrecte de votre object.

Votre question est la suivante: pourquoi, en C, il est normal d’allouer dynamicment de la mémoire et en C ++ ce n’est pas le cas?

C ++ a beaucoup de constructions en place qui rendent les nouvelles redondantes. copier, déplacer et les constructeurs normaux, les destructeurs, la bibliothèque standard, les allocateurs.

Mais en C, vous ne pouvez pas le contourner.

C’est en fait une réaction en C ++ qui rend “nouvelle” trop facile.

En théorie, l’utilisation de ce modèle de construction de classe en C est identique à l’utilisation de “nouveau” en C ++, il ne devrait donc y avoir aucune différence. Cependant, la façon dont les gens ont tendance à penser aux langues est différente, de sorte que la façon dont les gens réagissent au code est différente.

En C, il est très courant de penser aux opérations que l’ordinateur devra effectuer pour atteindre ses objectives. Ce n’est pas universel, mais c’est un état d’esprit très commun. On suppose que vous avez pris le temps de faire l’parsing coût / bénéfice du malloc / free.

En C ++, il est devenu beaucoup plus facile d’écrire des lignes de code qui font beaucoup pour vous, sans même vous en rendre compte. Il est assez courant que quelqu’un écrive une ligne de code et ne se rende même pas compte qu’il a fallu 100 ou 200 nouvelles suppressions! Cela a provoqué un retour de bâton, où les développeurs de C ++ s’attaquent aux nouvelles et les suppriment de manière fanatique, par crainte d’être appelés accidentellement partout.

Ce sont bien sûr des généralisations. Les communautés C et C ++ ne sont en aucun cas adaptées à ces moules. Cependant, si vous obtenez des flacks avec new au lieu de les placer sur le tas, cela peut être la cause principale.

C’est assez étrange que vous le voyiez si souvent. Vous devez avoir regardé comme un code “paresseux”.

En C, la technique que vous décrivez est généralement réservée aux types de bibliothèque “opaques”, c’est-à-dire aux types de structure dont les définitions sont intentionnellement rendues invisibles par le code du client. Étant donné que le client ne peut pas déclarer de tels objects, le langage doit être vraiment sur l’allocation dynamic dans le code de bibliothèque “caché”.

Lorsque la dissimulation de la définition de la structure n’est pas requirejse, un idiome C typique se présente généralement comme suit

 typedef struct CClass { int member_a; float member_b; } CClass; CClass* CClass_init(CClass* cclass); void CClass_release(CClass* cclass); 

Fonction CClass_init initialise l’object *cclass et renvoie le même pointeur que résultat. C’est-à-dire que la charge de l’allocation de mémoire pour l’object est placée sur l’appelant et que l’appelant peut l’atsortingbuer comme bon lui semble

 CClass cclass; CClass_init(&cclass); ... CClass_release(&cclass); 

Un exemple classique de cet idiome serait pthread_mutex_t avec pthread_mutex_init et pthread_mutex_destroy .

En attendant, utiliser l’ancienne technique pour les types non opaques (comme dans votre code d’origine) est généralement une pratique discutable. Il est tout à fait discutable que l’utilisation gratuite de la mémoire dynamic en C ++. Cela fonctionne, mais encore une fois, l’utilisation de la mémoire dynamic lorsque ce n’est pas nécessaire est aussi mal vue en C qu’en C ++.