Créer des "classes" en C, sur la pile vs le tas?

chaque fois que je vois Une Classe C (n'importe quelle structure qui est destinée à être utilisée en accédant à des fonctions qui prennent un pointeur vers elle comme premier argument) je les vois implémentés 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 retourne un pointeur vers ce.

chaque fois que je vois new apparaître en C++ inutilement, il semble généralement conduire les programmeurs C++ fou, mais cette pratique semble acceptable en C. Ce qui donne? Y a-t-il une raison expliquant pourquoi les "classes" de struct attribuées en tas sont si courantes?

47
demandé sur Therhang 2015-07-28 13:00:58

12 réponses

Il y a plusieurs raisons à cela.

  1. utilisant des pointeurs" opaques
  2. manque de destructeurs
  3. systèmes Embarqués (débordement de pile problème)
  4. Conteneurs
  5. inertie
  6. "paresse"

discutons-en brièvement.

pour pointes opaques , il 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 changements et permettant d'autres choses intéressantes, comme implémenter la classe différemment pour différentes plateformes.

bien sûr, cela interdit d'utiliser les variables de pile de CClass . Mais, OTOH, on peut voir que cela n'interdit pas l'allocation CClass objets statiquement ( à partir d'un pool) - retourné par CClass_create ou peut-être une autre fonction comme CClass_create_static .

manque de destructeurs - comme le compilateur C ne détruira pas automatiquement vos objets de la pile CClass , vous devez le faire vous-même (en appelant manuellement la fonction de destructeur). Ainsi, le seul avantage qui reste est le fait que la répartition des piles est, en général, plus rapide que la répartition des tas. OTOH, vous ne devez pas utiliser le tas - vous pouvez allouer à partir d'une piscine, ou une arène, ou un tel chose, et c'est peut-être presque aussi rapide que l'allocation de pile, sans les problèmes potentiels de l'allocation de pile discuté ci-dessous.

Embedded systems - la pile n'est pas une ressource "infinie", vous savez. Bien sûr, pour la plupart des applications sur les os "réguliers" d'aujourd'hui (POSIX, Windows...), elle est presque. Mais, sur les systèmes embarqués, la pile peut être aussi faible que quelques KBs. C'est extrême, mais même les "grands" systèmes embarqués ont une pile qui est dans MBs. Ainsi, il sera exécuté si sur-utilisé. Quand C'est le cas, la plupart du temps il n'y a aucune garantie de ce qui va se passer - AFAIK, en C et c++ c'est un "comportement non défini". OTOH, CClass_create() peut retourner le pointeur NULL quand vous êtes hors de mémoire, et vous pouvez gérer cela.

Containers - les utilisateurs de C++ aiment l'attribution de pile, mais, si vous créez un std::vector sur pile, son contenu sera attribué tas. Vous pouvez modifier cela, bien sûr, mais c'est le comportement par défaut, et il fait sa vie il est beaucoup plus facile de dire "tous les membres d'un conteneur sont entassés" plutôt que d'essayer de comprendre comment les manipuler s'ils ne le sont pas.

inertie - Eh bien, L'OO vient de SmallTalk. Tout y est dynamique, donc, la traduction " naturelle "en C est la façon de" tout mettre sur le tas". Ainsi, les premiers exemples étaient comme cela et ils ont inspiré d'autres pendant de nombreuses années.

paresse " - si vous savez vous voulez seulement des objets de pile, vous avez besoin de quelque chose comme:

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

mais, si vous voulez permettre à la fois stack et heap, vous devez ajouter:

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

C'est plus de travail à faire pour l'exécuteur, mais c'est aussi déroutant pour l'utilisateur. On peut faire des interfaces légèrement différentes, mais cela ne change pas le fait que vous avez besoin de deux ensembles de fonctions.

bien sûr, la raison "conteneurs" est aussi en partie une " paresse "" raison.

50
répondu srdjan.veljkovic 2015-07-28 10:51:02

en supposant, comme dans votre question, CClass_create et CClass_destroy utilisez malloc/free , alors pour moi faire ce qui suit 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 avons aussi plutôt faire ceci:

void Myfunc()
{
  CClass myinstance;
  ...

}

que ceci:

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

  delete myinstance;
}

afin d'éviter une new / delete .

14
répondu Jabberwocky 2015-07-29 05:51:10

en C, lorsqu'un composant fournit une fonction "create", le responsable de l'implémentation contrôle également la façon dont le composant est initialisé. Ainsi ,non seulement émule C++' operator new mais aussi le constructeur de classe.

renoncer à ce contrôle sur l'initialisation signifie beaucoup plus de vérification des erreurs sur les entrées, donc garder le contrôle rend plus facile de fournir un comportement cohérent et prévisible.

je prends aussi exception à malloc toujours utilisé pour allouer la mémoire. Cela peut souvent être le cas, mais pas toujours. Par exemple, dans certains systèmes embarqués, vous trouverez que malloc / free n'est pas utilisé du tout. Les fonctions X_create peuvent être affectées d'autres façons, par exemple à partir d'un tableau dont la taille est fixe au moment de la compilation.

9
répondu Sigve Kolbeinson 2015-07-28 10:17:30

Cela engendre beaucoup de réponses, car il est un peu opinion . Je veux tout de même expliquer pourquoi je préfère personnellement que mes "objets C" soient placés sur le tas. La raison est d'avoir tous mes champs cachés (parler: privé ) du code de consommation. C'est ce qu'on appelle un pointeur opaque . En pratique, cela signifie que votre fichier d'en-tête ne définit pas le struct utilisé, il le déclare seulement. Direct en conséquence, le code de consommation ne peut pas connaître la taille du struct et donc l'attribution de pile devient impossible.

l'avantage est: le code de consommation peut jamais jamais jamais dépendent de la définition du struct , ce qui signifie qu'il est impossible que vous d'une façon ou d'une autre rendre le contenu du struct incompatible avec l'extérieur et vous évitez la recompilation inutile de code de consommation lorsque le struct changement.

la première question est abordée dans en déclarant que les champs sont private . Mais la définition de votre class est toujours importée dans toutes les unités de compilation qui l'utilisent, ce qui rend nécessaire de les recompiler, même lorsque seuls vos membres private changent. La solution souvent utilisée dans est le modèle pimpl : avoir tous les membres privés dans un second struct (ou class ) qui est défini dans le fichier d'implémentation. Bien sûr, cela nécessite que votre pimpl soit alloué sur le tas.

ajoutant à cela: moderne les langues OOP (comme par exemple ou ) ont des moyens pour allouer des objets (et généralement décider si c'est stack ou heap en interne) sans que le code d'appel connaisse leur définition.

8
répondu 2015-07-28 11:56:42

en général, le fait que vous voyez un * ne signifie pas qu'il a été malloc 'D. Vous pourriez avoir un pointeur vers la 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à certaines informations sur l'objet en cours de destruction.

de plus, les pointeurs, qu'ils soient ou non malloc 'd, Sont le seul moyen qui vous permet de modifier l'objet.

je n'ai pas voir les raisons particulières pour utiliser le tas au lieu de la pile: vous ne recevez pas moins de mémoire utilisée. Ce qui est nécessaire, Cependant, pour initialiser de telles "classes" sont des fonctions init/destroy parce que la structure de données sous-jacente peut effectivement avoir besoin de contenir des données dynamiques, donc l'utilisation de pointeurs.

3
répondu edmz 2015-07-28 10:11:24

je changerais le" constructeur " en void CClass_create(CClass*);

il ne retournera pas une instance/référence de la struct, mais sera appelé sur une.

Qu'il soit attribué sur la" pile " ou dynamiquement, il dépend entièrement de vos exigences de scénario d'utilisation. Quelle que soit la façon dont vous l'attribuez, vous appelez simplement CClass_create() en passant la structure attribuée comme 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 attention de ne pas renvoyer une référence à un local (attribué sur la pile), parce que C'est UB.

peu importe comment vous l'attribuez, vous devrez appeler void CClass_destroy(CClass*); au bon endroit (la fin de la vie de cet objet qui est), et si alloué dynamiquement, aussi libérer cette mémoire.

distinguer entre allocation/désallocation et construction / destruction, ceux ne sont pas les mêmes (même si dans C++ ils pourraient être automatiquement couplés ensemble).

3
répondu dtech 2015-07-29 07:07:31

parce qu'une fonction ne peut retourner une pile assignée struct que si elle ne contient pas de pointeurs vers d'autres structures attribuées. Si elle ne contient que des objets simples (int, bool, floats, chars et tableaux d'eux mais aucun pointeur ) vous pouvez l'allouer sur la pile. Mais vous devez savoir que si vous le retourner, il sera copié. Si vous voulez autoriser des pointeurs vers d'autres structures, ou qui veulent éviter la copie puis utilisez tas.

Mais si vous pouvez créer la structure dans un Unité de niveau supérieur et ne l'utiliser que dans les fonctions appelées et ne jamais le retourner, alors la pile est appropriée

2
répondu Serge Ballesta 2015-07-28 10:28:37

si le nombre maximum d'objets d'un type quelconque qui devront exister simultanément est fixe, le système devra être capable de faire quelque chose avec chaque instance "live", et les articles en question ne consomment pas trop d'argent, la meilleure approche est généralement ni l'allocation de tas ni l'allocation de pile, mais plutôt un tableau statiquement alloué, avec des méthodes de "créer" et de "détruire". L'utilisation d'un tableau évitera le besoin de maintenir une liste d'objets Liés, et le rendra possibilité de gérer le cas où un objet ne peut pas être détruit immédiatement parce qu'il est "occupé" [par exemple, si des données arrivent sur un canal via une interruption ou DMA quand le code utilisateur décide qu'il n'est plus intéressé par le canal et en dispose, le code utilisateur peut définir un drapeau "disposer quand c'est fait" et revenir sans avoir à se soucier d'une interruption en attente ou d'un stockage DMA overwrite qui ne lui est plus attribué].

L'utilisation D'un pool d'objets de taille fixe rend l'allocation et la dé-allocation sont beaucoup plus prévisibles que le stockage d'un tas de taille mixte. L'approche n'est pas bonne dans les cas où la demande est variable et où les objets occupent beaucoup d'espace (individuellement ou collectivement), mais lorsque la demande est généralement cohérente (par exemple, une application a besoin de 12 objets en tout temps, et parfois jusqu'à 3 de plus), elle peut fonctionner beaucoup mieux que d'autres approches. La seule faiblesse est que toute configuration doit soit être effectuée à l'endroit où la le buffer est déclaré, ou doit être exécuté par le code exécutable dans les clients. Il n'y a aucun moyen d'utiliser la syntaxe d'initialisation variable sur un site client.

D'ailleurs, en utilisant cette approche, il n'est pas nécessaire que le code client reçoive des pointeurs vers quoi que ce soit. Au lieu de cela, on peut identifier les ressources en utilisant n'importe quelle taille d'entier est commode. En outre, si le nombre de ressources n'aurez jamais à dépasser le nombre de bits dans un int , il peut être utile d'avoir certaines variables d'état utilisent un bit par ressource. Par exemple, on pourrait avoir les variables timer_notifications (écrit seulement via le gestionnaire d'interruption) et timer_acks (écrit seulement via le code mainline) et spécifier que le bit N de (timer_notifications ^ timer_acks) sera défini chaque fois que le timer N veut du service. En utilisant une telle approche, le code n'a besoin de lire que deux variables pour déterminer si un minuteur a besoin de service, plutôt que d'avoir à lire une variable pour chaque minuteur.

2
répondu supercat 2015-07-28 22:23:03

c manque de certaines choses que les programmeurs C++ tiennent pour acquises, à savoir:

  1. prescripteurs publics et privés
  2. constructeurs et destructeurs

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

Si vous exposez la structure dans votre .h fichier, ce sera signifie que les utilisateurs peuvent accéder directement aux membres qui rompt l'encapsulation. De plus, ne pas forcer la création permet une construction incorrecte de votre objet.

2
répondu doron 2015-08-13 12:22:24

est votre question "pourquoi en C il est normal d'allouer la mémoire dynamiquement 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.

1
répondu Serve Laurijssen 2015-07-28 10:09:18

est en fait un retour de flamme vers C++ rendant" Nouveau " trop facile.

en théorie, l'utilisation de ce modèle de construction de classe en C est identique à l'utilisation de" new " en C++, donc il ne devrait pas y avoir de différence. Cependant, la façon dont les gens ont tendance à penser les 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 exactes que l'ordinateur devra faire pour accomplir vos buts. Ce n'est pas universelle, mais c'est un très commun de la mentalité. On présume que vous avez pris le temps de faire l'analyse coûts-avantages du malloc/gratuit.

en C++, il est devenu beaucoup plus facile d'écrire des lignes de code qui font beaucoup pour vous, sans que vous vous en rendiez compte. Il est assez commun pour quelqu'un d'écrire une ligne de code, et même pas réaliser qu'il s'est produit pour appeler pour 100 ou 200 nouveaux/supprime! Cela a provoqué un retour de flamme, où le développeur C++ va fanatiquement et efface, de peur qu'ils soient appelés accidentellement partout dans la place.

ce sont, bien sûr, des généralisations. En aucun cas les communautés C et C++ ne s'adaptent à ces moules. Cependant, si vous êtes flack sur l'utilisation de nouveaux au lieu de mettre des choses sur le tas, cela peut être la cause profonde.

1
répondu Cort Ammon 2015-07-29 04:31:07

il est assez étrange que vous le voyiez si souvent. Tu devais avoir l'air d'un code" paresseux".

en C la technique que vous décrivez est typiquement réservée aux types de bibliothèques "opaques", c'est-à-dire aux types de structures dont les définitions sont intentionnellement rendues invisibles au code du client. Puisque le client ne peut pas déclarer de tels objets, l'idiome doit vraiment sur l'allocation dynamique dans le code de bibliothèque "caché".

pour dissimuler la définition de la structure n'est pas nécessaire, un langage typique ressemble habituellement comme suit

typedef struct CClass
{
    int member_a;
    float member_b;
} CClass;

CClass* CClass_init(CClass* cclass);
void CClass_release(CClass* cclass);

Function CClass_init initialise l'objet *cclass et renvoie le même pointeur que le résultat. I. e. la charge de l'allocation de mémoire pour l'objet est placé sur l'appelant et l'appelant peut les répartir comme il l'entend

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, l'utilisation de la première technique pour les types non opaques (comme dans votre code d'origine) est généralement une pratique discutable. C'est exactement discutable comme utilisation gratuite de la mémoire dynamique en C++. Cela fonctionne, mais encore une fois, l'utilisation de la mémoire dynamique quand elle n'est pas requise est aussi mal vue en C qu'en C++.

0
répondu AnT 2015-07-28 22:33:07