Qu'advient-il des ordures en C ++?

51

Java a un GC automatique qui stoppe parfois le monde de temps en temps, mais prend en charge les déchets sur un tas. Maintenant, les applications C / C ++ ne gèlent pas STW, leur utilisation de la mémoire ne se développe pas non plus à l'infini. Comment ce comportement est-il atteint? Comment sont pris en charge les objets morts?

Ju Shua
la source
38
Remarque: stop-the-world est le choix d'implémentation de certains éboueurs, mais certainement pas de tous. Par exemple, il y a des GC simultanés qui s'exécutent en même temps que le mutateur (c'est ce que les développeurs du GC appellent le programme réel). Je pense que vous pouvez acheter une version commerciale de la JVM J9 open source d’IBM dotée d’un collecteur sans pause. Azul Zing a un collecteur "sans pause" qui n'est pas réellement, mais extrêmement rapide, de sorte qu'il n'y ait pas de pauses visibles (ses pauses GC sont dans le même ordre qu'un commutateur de contexte de thread de système d'exploitation, ce qui n'est généralement pas perçu comme une pause). .
Jörg W Mittag
14
La plupart des (long) en cours d' exécution des programmes C ++ que j'utilise n'ont utilisation de la mémoire qui pousse unboundedly temps sur. Est-il possible que vous n'ayez pas l'habitude de laisser les programmes ouverts plus de quelques jours à la fois?
Jonathan Cast
12
Tenez compte du fait qu'avec le C ++ moderne et ses constructions, vous n'avez plus besoin de supprimer manuellement la mémoire (à moins que vous ne souhaitiez une optimisation particulière), car vous pouvez gérer la mémoire dynamique à l'aide de pointeurs intelligents. Évidemment, cela ajoute une charge supplémentaire au développement en C ++ et vous devez être un peu plus prudent, mais ce n'est pas tout à fait différent, vous devez simplement vous rappeler d'utiliser la construction du pointeur intelligent au lieu d'appeler simplement manuel new.
Andy
9
Notez qu'il est toujours possible d'avoir des fuites de mémoire dans un langage mal rempli. Je ne suis pas familier avec Java, mais les fuites de mémoire sont malheureusement assez courantes dans le monde géré de GC .NET. Les objets qui sont indirectement référencés par un champ statique ne sont pas automatiquement collectés, les gestionnaires d'événements sont une source très courante de fuites, et la nature non déterministe du garbage collection ne permet pas d'éliminer complètement la nécessité de libérer manuellement des ressources (ce qui conduit à la propriété IDisposable). modèle). Cela dit, le modèle de gestion de la mémoire C ++ utilisé correctement est de loin supérieur au garbage collection.
Cody Grey
26
What happens to garbage in C++? N'est-ce pas habituellement compilé dans un exécutable?
BJ Myers

Réponses:

100

Le programmeur est responsable de la newsuppression des objets par lesquels il a créé delete. Si un objet est créé, mais qu'il n'est pas détruit avant que le dernier pointeur ou la référence ne soit périmé, il tombe dans les fissures et devient une fuite de mémoire .

Malheureusement pour C, C ++ et d'autres langages qui n'incluent pas de GC, cela ne fait que s'accumuler avec le temps. Une application ou le système risque de manquer de mémoire et d’allouer de nouveaux blocs de mémoire. À ce stade, l'utilisateur doit recourir à la fin de l'application pour que le système d'exploitation puisse récupérer cette mémoire utilisée.

En ce qui concerne la résolution de ce problème, plusieurs choses rendent la vie du programmeur beaucoup plus facile. Ceux-ci sont principalement pris en charge par la nature de la portée .

int main()
{
    int* variableThatIsAPointer = new int;
    int variableInt = 0;

    delete variableThatIsAPointer;
}

Ici, nous avons créé deux variables. Ils existent dans Block Scope , comme défini par les {}accolades. Lorsque l'exécution sort de cette portée, ces objets sont automatiquement supprimés. Dans ce cas, variableThatIsAPointercomme son nom l'indique, est un pointeur sur un objet en mémoire. Lorsqu'il est hors de portée, le pointeur est supprimé, mais l'objet vers lequel il pointe reste. Ici, nous, deletecet objet avant qu'il ne sorte du champ d'application, afin de nous assurer qu'il n'y a pas de fuite de mémoire. Cependant, nous aurions pu également passer ce pointeur ailleurs et nous espérer qu'il sera supprimé ultérieurement.

Cette nature de la portée s'étend aux classes:

class Foo
{
public:
    int bar; // Will be deleted when Foo is deleted
    int* otherBar; // Still need to call delete
}

Ici, le même principe s'applique. Nous n'avons pas à nous soucier de barquand Fooest supprimé. Cependant otherBar, seul le pointeur est supprimé. Si otherBarest le seul pointeur valide sur l’objet qu’il pointe, nous devrions probablement le deleteplacer dans Foole destructeur de. C'est le concept moteur de la RAII

L'allocation de ressources (acquisition) est effectuée lors de la création de l'objet (en particulier l'initialisation) par le constructeur, tandis que la désaffectation des ressources (libération) est effectuée lors de la destruction de l'objet (en particulier la finalisation) par le destructeur. Ainsi, la ressource est garantie d'être conservée entre la fin de l'initialisation et le début de la finalisation (le fait de conserver les ressources est un invariant de classe) et de ne la conserver que lorsque l'objet est en vie. Ainsi, s'il n'y a pas de fuite d'objet, il n'y a pas de fuite de ressource.

RAII est également le moteur typique des pointeurs intelligents . Dans la bibliothèque standard sont ces C ++, std::shared_ptr, std::unique_ptret std::weak_ptr; Bien que j'ai vu et utilisé d'autres shared_ptr/ weak_ptrmises en œuvre qui suivent les mêmes concepts. Pour ceux-ci, un compteur de références suit le nombre de pointeurs vers un objet donné, et est automatiquement deleteaffiché dès qu’il n’y a plus de référence.

Au-delà de cela, tout se résume à des pratiques et à une discipline appropriées permettant au programmeur de s’assurer que son code traite correctement les objets.

Thebluefish
la source
4
supprimé via delete- c'est ce que je cherchais. Impressionnant.
Ju Shua
3
Vous voudrez peut-être ajouter quelque chose sur les mécanismes de cadrage fournis dans c ++ qui permettent de rendre la plupart des nouveautés et des suppressions presque entièrement automatiques.
Whatsisname
9
@whatsisname ce n'est pas que nouveau et supprimer sont rendus automatiques, c'est qu'ils ne se produisent pas du tout dans de nombreux cas
Caleth
10
Le deleteest automatiquement appelé pour vous par les pointeurs intelligents si vous les utilisez. Vous devez donc envisager de les utiliser chaque fois qu’un stockage automatique ne peut pas être utilisé.
Marian Spanik
11
@JuShua Notez qu'en écrivant en C ++ moderne, vous ne devriez jamais avoir besoin d'avoir réellement deletedans votre code d'application (et à partir de C ++ 14, la même chose avec new), mais utilisez plutôt des pointeurs intelligents et RAII pour supprimer des objets de tas. std::unique_ptrLe type et la std::make_uniquefonction constituent le remplacement direct et le plus simple du code de l'application newet deleteau niveau de celui-ci.
Hyde
82

C ++ n'a pas de récupération de place.

Les applications C ++ doivent se débarrasser de leurs propres déchets.

Les programmeurs d'applications C ++ doivent comprendre cela.

Quand ils oublient, le résultat s'appelle une "fuite de mémoire".

John R. Strohm
la source
22
Vous avez certainement veillé à ce que votre réponse ne contienne pas non plus de déchets, ni de médailles ...
partir du
15
@leftaroundabout: Merci. Je considère que c'est un compliment.
John R. Strohm
1
OK cette réponse sans ordures a un mot clé à rechercher: fuite mémoire. Il serait également agréable de mentionner newet delete.
Ruslan
4
@Ruslan de même aussi mallocet free, ou new[]et delete[]ou tout autre allocateurs (comme Windows de GlobalAlloc, LocalAlloc, SHAlloc, CoTaskMemAlloc, VirtualAlloc, HeapAlloc, ...), et la mémoire allouée pour vous (par exemple via fopen).
user253751
43

En C, C ++ et autres systèmes sans Garbage Collector, le développeur et ses bibliothèques offrent des fonctionnalités pour indiquer quand la mémoire peut être récupérée.

L'installation la plus élémentaire est le stockage automatique . Plusieurs fois, le langage lui-même garantit que les articles sont éliminés:

int global = 0; // automatic storage

int foo(int a, int b) {
    static int local = 1; // automatic storage

    int c = a + b; // automatic storage

    return c;
}

Dans ce cas, le compilateur est chargé de savoir quand ces valeurs sont inutilisées et de récupérer le stockage qui leur est associé.

Lorsque vous utilisez le stockage dynamique , en C, la mémoire est traditionnellement allouée avec mallocet récupérée free. En C ++, la mémoire est traditionnellement allouée avec newet récupérée delete.

C n'a pas beaucoup changé au fil des ans, mais C ++ moderne évite newet deletecomplètement et repose plutôt sur des installations bibliothèque (qui utilisent eux - mêmes newet de façon deleteappropriée):

  • les pointeurs intelligents sont les plus célèbres: std::unique_ptretstd::shared_ptr
  • mais les conteneurs sont en réalité beaucoup plus répandue: std::string, std::vector, std::map, ... tout gérer en interne de manière transparente la mémoire allouée dynamiquement

En parlant de shared_ptr, il y a un risque: si un cycle de références est formé, et non rompu, il peut y avoir une fuite de mémoire. Il appartient au développeur d'éviter cette situation, le plus simple étant d'éviter shared_ptrcomplètement et le second, d'éviter les cycles au niveau du type.

En conséquence, les fuites de mémoire ne sont pas un problème en C ++ , même pour les nouveaux utilisateurs, à condition qu'ils s'abstiennent d'utiliser new, deleteou std::shared_ptr. Ceci est différent de C où une discipline ferme est nécessaire et généralement insuffisante.


Cependant, cette réponse ne serait pas complète sans mentionner la jumelle des fuites de mémoire: les pointeurs en suspens .

Un pointeur en suspens (ou une référence en suspens) est un danger créé en gardant un pointeur ou une référence à un objet mort. Par exemple:

int main() {
    std::vector<int> vec;
    vec.push_back(1);     // vec: [1]

    int& a = vec.back();

    vec.pop_back();       // vec: [], "a" is now dangling

    std::cout << a << "\n";
}

L'utilisation d'un pointeur suspendu, ou référence, est un comportement indéfini . En général, heureusement, il s’agit d’un crash immédiat; malheureusement, cela provoque souvent une corruption de la mémoire en premier lieu ... et de temps en temps, un comportement étrange se présente parce que le compilateur émet un code vraiment étrange.

Le comportement non défini est le problème le plus important avec C et C ++ à ce jour, en termes de sécurité / exactitude des programmes. Vous voudrez peut-être consulter Rust pour une langue dépourvue de collecteur de place et de comportement non défini.

Matthieu M.
la source
17
Re: "L’utilisation d’un pointeur ou référence référencé est un comportement indéfini . En général, heureusement, il s’agit d’un crash immédiat": Vraiment? Cela ne correspond pas du tout à mon expérience. Au contraire, selon mon expérience, l’utilisation d’un pointeur suspendu ne provoque presque jamais un crash immédiat. . .
Ruakh
9
Ouais, car pour être "suspendu", un pointeur doit avoir ciblé la mémoire allouée précédemment à un moment donné, et il est généralement peu probable que cette mémoire ait été complètement désaffectée du processus, de sorte qu'elle n'est plus accessible du tout, car ce sera un bon candidat pour une réutilisation immédiate ... en pratique, les pointeurs en suspension ne causent pas de crash, ils causent le chaos.
Leushenko
2
"En conséquence, les fuites de mémoire ne sont pas un problème en C ++", bien sûr, il y a toujours des liaisons en C avec les bibliothèques à bousiller, ainsi que les fichiers shared_ptr récursifs ou même les fichiers_un_ptrs récursifs, entre autres.
Mooing Duck
3
“Ce n'est pas un problème en C ++, même pour les nouveaux utilisateurs” - je qualifierais cela de “nouveaux utilisateurs qui ne viennent pas d'un langage similaire à Java ou C ”.
gauche du
3
@leftaroundabout: c'est qualifié "tant qu'ils s'abstiennent d'utiliser new, deleteet shared_ptr"; sans newet shared_ptrvous avez la propriété directe, donc pas de fuites. Bien sûr, vous risquez d'avoir des pointeurs en suspens, etc., mais j'ai bien peur que vous deviez quitter le C ++ pour vous en débarrasser.
Matthieu M.
27

C ++ a cette chose appelée RAII . En gros, cela signifie que les ordures sont nettoyées au fur et à mesure plutôt que de les laisser en tas et de laisser le nettoyeur ranger après vous. (Imaginez-moi dans ma chambre en train de regarder le ballon de foot. Alors que je bois des canettes de bière et que j'ai besoin de nouvelles canettes, la méthode C ++ consiste à amener la canette vide à la corbeille en direction du réfrigérateur, la méthode C # consiste à la jeter par terre. et attendez que la femme de ménage vienne les chercher quand elle vient faire le ménage).

Il est maintenant possible de perdre de la mémoire en C ++, mais pour ce faire, vous devez quitter les constructions habituelles et revenir à la méthode C: allouer un bloc de mémoire et garder la trace de ce bloc sans assistance linguistique. Certaines personnes oublient ce pointeur et ne peuvent donc pas supprimer le bloc.

gbjbaanb
la source
9
Les pointeurs partagés (qui utilisent RAII) constituent un moyen moderne de créer des fuites. Supposons que les objets A et B se référencent via des pointeurs partagés et que rien d'autre ne fasse référence à l'objet A ou à l'objet B. Le résultat est une fuite. Ce référencement mutuel n’est pas un problème dans les langues avec garbage collection.
David Hammen
@DavidHammen bien sûr, mais au prix de traverser presque tous les objets pour en être sûr. Votre exemple de pointeurs intelligents ignore le fait que le pointeur intelligent lui-même sera hors de portée et que les objets seront alors libérés. Vous supposez qu'un pointeur intelligent est comme un pointeur, ce n'est pas un objet qui est passé sur la pile, comme la plupart des paramètres. Ce n'est pas très différent des fuites de mémoire causées dans les langues du GC. par exemple, le fameux où supprimer un gestionnaire d’événements d’une classe d’UI laisse celle-ci référencée en silence et donc fuit.
gbjbaanb
1
@gbjbaanb dans l'exemple avec les pointeurs intelligents, ni pointeur intelligent jamais est hors de portée, qui est la raison pour laquelle il y a une fuite. Etant donné que les deux objets pointeurs intelligents sont alloués dans une étendue dynamique , et non pas lexicale, ils essaient d'attendre l'autre avant de se détruire. Le fait que les pointeurs intelligents soient des objets réels en C ++ et pas seulement des pointeurs est exactement ce qui cause la fuite ici. Les objets pointeur intelligent supplémentaires dans les portées de pile qui pointaient également vers les objets conteneur ne peuvent pas les désallouer lorsqu'ils se détruisent eux-mêmes, car refcount non nul.
Leushenko
2
La façon. NET est de ne pas le jeter par terre. Il le garde juste où il était jusqu'à ce que la femme de ménage vienne. Et du fait de la manière dont .NET alloue de la mémoire dans la pratique (non contractuel), le tas ressemble plus à une pile à accès aléatoire. C'est un peu comme avoir une pile de contrats et de papiers et l'examiner de temps en temps pour éliminer ceux qui ne sont plus valables. Et pour rendre cela plus facile, ceux qui survivent à chaque rejet sont promus dans une pile différente, de sorte que vous puissiez éviter de traverser toutes les piles la plupart du temps - à moins que la première pile ne devienne assez grande, la femme de ménage ne touche pas les autres.
Luaan
@Luaan c'était une analogie ... Je suppose que vous seriez plus heureux si je disais qu'il reste des canettes sur la table jusqu'à ce que la femme de ménage vienne nettoyer.
gbjbaanb
26

Il convient de noter que, dans le cas de C ++, il est souvent mal compris que "vous devez gérer manuellement la mémoire". En fait, votre code ne gère généralement pas la mémoire.

Objets de taille fixe (avec durée de vie)

Dans la grande majorité des cas, lorsque vous avez besoin d'un objet, celui-ci aura une durée de vie définie dans votre programme et sera créé sur la pile. Cela fonctionne pour tous les types de données primitifs intégrés, mais aussi pour les instances de classes et de structures:

class MyObject {
    public: int x;
};

int objTest()
{
    MyObject obj;
    obj.x = 5;
    return obj.x;
}

Les objets empilés sont automatiquement supprimés à la fin de la fonction. En Java, les objets sont toujours créés sur le tas et doivent donc être supprimés par un mécanisme tel que le garbage collection. Ceci n'est pas un problème pour les objets de pile.

Objets gérant des données dynamiques (avec durée de vie)

L'utilisation de l'espace sur la pile fonctionne pour les objets de taille fixe. Lorsque vous avez besoin d'une quantité variable d'espace, telle qu'un tableau, une autre approche est utilisée: la liste est encapsulée dans un objet de taille fixe qui gère la mémoire dynamique à votre place. Cela fonctionne car les objets peuvent avoir une fonction de nettoyage spéciale, le destructeur. Il est garanti d'être appelé lorsque l'objet sort du domaine et fait l'inverse du constructeur:

class MyList {        
public:
    // a fixed-size pointer to the actual memory.
    int* listOfInts; 
    // constructor: get memory
    MyList(size_t numElements) { listOfInts = new int[numElements]; }
    // destructor: free memory
    ~MyList() { delete[] listOfInts; }
};

int listTest()
{
    MyList list(1024);
    list.listOfInts[200] = 5;
    return list.listOfInts[200];
    // When MyList goes off stack here, its destructor is called and frees the memory.
}

Il n'y a pas du tout de gestion de mémoire dans le code où la mémoire est utilisée. La seule chose dont nous devons nous assurer est que l'objet que nous avons écrit possède un destructeur approprié. Peu importe la façon dont nous laissons les choses en place listTest, que ce soit via une exception ou simplement en y retournant, le destructeur ~MyList()sera appelé et nous n’aurons pas besoin de gérer de mémoire.

(Je pense que c'est une décision de conception amusante d'utiliser l' opérateur binaire NOT~ , pour indiquer le destructeur. Lorsqu'il est utilisé sur des nombres, il inverse les bits; par analogie, cela indique que ce que le constructeur a fait est inversé.)

Fondamentalement, tous les objets C ++ qui ont besoin de mémoire dynamique utilisent cette encapsulation. Cela a été appelé RAII ("l’acquisition des ressources est une initialisation"), ce qui est assez étrange pour exprimer la simple idée que les objets se soucient de leurs propres contenus; ce qu'ils acquièrent, c'est à eux de les nettoyer.

Objets polymorphes et durée de vie au-delà de la portée

Ces deux cas concernaient une mémoire dont la durée de vie est clairement définie: la durée de vie est identique à la portée. Si nous ne voulons pas qu'un objet expire lorsque nous quittons la portée, il existe un troisième mécanisme qui peut gérer la mémoire pour nous: un pointeur intelligent. Les pointeurs intelligents sont également utilisés lorsque vous avez des instances d'objets dont le type varie au moment de l'exécution, mais qui ont une interface ou une classe de base commune:

class MyDerivedObject : public MyObject {
    public: int y;
};
std::unique_ptr<MyObject> createObject()
{
    // actually creates an object of a derived class,
    // but the user doesn't need to know this.
    return std::make_unique<MyDerivedObject>();
}

int dynamicObjTest()
{
    std::unique_ptr<MyObject> obj = createObject();
    obj->x = 5;
    return obj->x;
    // At scope end, the unique_ptr automatically removes the object it contains,
    // calling its destructor if it has one.
}

Il existe un autre type de pointeur intelligent std::shared_ptrpermettant de partager des objets entre plusieurs clients. Ils ne suppriment leur objet contenu que lorsque le dernier client est hors de portée. Ils peuvent donc être utilisés dans des situations dans lesquelles on ignore totalement le nombre de clients et la durée d'utilisation de l'objet.

En résumé, nous voyons que vous ne faites pas vraiment de gestion de mémoire manuelle. Tout est encapsulé et est ensuite pris en charge au moyen d'une gestion de la mémoire entièrement automatique et basée sur la portée. Dans les cas où cela ne suffit pas, des pointeurs intelligents sont utilisés pour encapsuler la mémoire brute.

Il est considéré comme une très mauvaise pratique d’utiliser des pointeurs bruts en tant que propriétaires de ressources n’importe où dans le code C ++, des allocations brutes en dehors des constructeurs et des deleteappels bruts en dehors des destructeurs, car ils sont presque impossibles à gérer en cas d’exception et généralement difficiles à utiliser en toute sécurité.

Le meilleur: cela fonctionne pour tous les types de ressources

L’un des principaux avantages de RAII est qu’il ne se limite pas à la mémoire. Il fournit en fait un moyen très naturel de gérer des ressources telles que des fichiers et des sockets (ouverture / fermeture) et des mécanismes de synchronisation tels que des mutex (verrouillage / déverrouillage). Fondamentalement, toutes les ressources pouvant être acquises et devant être libérées sont gérées exactement de la même manière en C ++, et aucune partie de cette gestion n'est laissée à l'utilisateur. Tout cela est encapsulé dans des classes qui acquièrent dans le constructeur et libèrent dans le destructeur.

Par exemple, une fonction verrouillant un mutex est généralement écrite comme ceci en C ++:

void criticalSection() {
    std::scoped_lock lock(myMutex); // scoped_lock locks the mutex
    doSynchronizedStuff();
} // myMutex is released here automatically

D'autres langues compliquent beaucoup les choses, en vous demandant de le faire manuellement (dans une finallyclause, par exemple ) ou en générant des mécanismes spécialisés qui résolvent ce problème, mais pas de manière particulièrement élégante (généralement plus tard dans la vie, lorsque suffisamment de souffert de la lacune). Ces mécanismes sont try-with-resources en Java et l' instruction using en C #, qui sont tous deux des approximations de la norme RAII de C ++.

Donc, pour résumer, tout ceci était un compte-rendu très superficiel de RAII en C ++, mais j'espère que cela aidera les lecteurs à comprendre que la gestion de la mémoire et même des ressources en C ++ n'est généralement pas "manuelle", mais en réalité essentiellement automatique.

Felix Dombek
la source
7
C'est la seule réponse qui ne désinforme pas les gens et ne décrit pas le C ++ plus difficile ou dangereux qu'il ne l'est réellement.
Alexander Revo
6
En passant, il est considéré comme une mauvaise pratique d’utiliser le pointeur brut en tant que propriétaires de ressources. Il n'y a rien de mal à les utiliser s'ils signalent quelque chose dont la survie au pointeur est garantie.
Alexander Revo
8
Je seconde Alexandre. Je suis perplexe de voir que "le C ++ n'a pas de gestion de mémoire automatisée, oubliez deleteça et vous êtes mort", les réponses dépassent les 30 points et sont acceptées, alors que celui-ci en compte cinq. Est-ce que quelqu'un utilise réellement le C ++ ici?
Quentin
8

En ce qui concerne plus particulièrement le langage C, le langage ne vous fournit aucun outil pour gérer la mémoire allouée de manière dynamique. Vous êtes absolument responsable de vous assurer que chaque personne *alloca un correspondant freequelque part.

Là où les choses se compliquent vraiment, c’est quand une allocation de ressources échoue à mi-parcours; essayez-vous à nouveau, annulez-vous et recommencez-vous depuis le début, annulez-vous et sortez-vous avec une erreur, laissez-vous vous échapper et laissez-vous le système d'exploitation s'en charger?

Par exemple, voici une fonction pour allouer un tableau 2D non contigu. Le comportement ici est que si une défaillance d'allocation se produit au milieu du processus, nous annulons tout et renvoyons une indication d'erreur à l'aide d'un pointeur NULL:

/**
 * Allocate space for an array of arrays; returns NULL
 * on error.
 */
int **newArr( size_t rows, size_t cols )
{
  int **arr = malloc( sizeof *arr * rows );
  size_t i;

  if ( arr ) // malloc returns NULL on failure
  {
    for ( i = 0; i < rows; i++ )
    {
      arr[i] = malloc( sizeof *arr[i] * cols );
      if ( !arr[i] )
      {
        /**
         * Whoopsie; we can't allocate any more memory for some reason.
         * We can't just return NULL at this point since we'll lose access
         * to the previously allocated memory, so we branch to some cleanup
         * code to undo the allocations made so far.  
         */
        goto cleanup;
      }
    }
  }
  goto done;

/**
 * We encountered a failure midway through memory allocation,
 * so we roll back all previous allocations and return NULL.
 */
cleanup:
  while ( i )         // this is why we didn't limit the scope of i to the for loop
    free( arr[--i] ); // delete previously allocated rows
  free( arr );        // delete arr object
  arr = NULL;

done:
  return arr;
}

Ce code est assez moche avec ceux-là goto, mais, en l'absence de tout mécanisme structuré de gestion des exceptions, c'est quasiment le seul moyen de traiter le problème sans tout renverser, en particulier si votre code d'allocation de ressources est imbriqué. d'une boucle de profondeur. C’est l’une des rares fois où gotoc’est une option attrayante; sinon, vous utilisez un groupe de drapeaux et d’ ifénoncés supplémentaires .

Vous pouvez vous simplifier la vie en écrivant des fonctions d'allocateur / de désallocateur dédiées pour chaque ressource, par exemple:

Foo *newFoo( void )
{
  Foo *foo = malloc( sizeof *foo );
  if ( foo )
  {
    foo->bar = newBar();
    if ( !foo->bar ) goto cleanupBar;
    foo->bletch = newBletch(); 
    if ( !foo->bletch ) goto cleanupBletch;
    ...
  }
  goto done;

cleanupBletch:
  deleteBar( foo->bar );
  // fall through to clean up the rest

cleanupBar:
  free( foo );
  foo = NULL;

done:
  return foo;
}

void deleteFoo( Foo *f )
{
  deleteBar( f->bar );
  deleteBletch( f->bletch );
  free( f );
}
John Bode
la source
1
C'est une bonne réponse, même avec les gotodéclarations. Ceci est une pratique recommandée dans certaines régions. C'est un schéma couramment utilisé pour se protéger contre l'équivalent d'exceptions de C. Observez le code du noyau Linux, qui regorge d' gotoinstructions - et qui ne fuit pas.
David Hammen
"sans juste renflouer complètement" -> en toute justice, si vous voulez parler de C, c'est probablement une bonne pratique. C est un langage à utiliser soit pour manipuler des blocs de mémoire provenant d’autre chose, soit pour fragmenter de petits morceaux de mémoire en d’autres procédures, mais il est préférable de ne pas faire les deux en même temps de manière entrelacée. Si vous utilisez des "objets" classiques en C, vous n'utiliserez probablement pas le langage de manière optimale.
Leushenko
La seconde gotoest étrangère. Ce serait plus lisible si vous changiez goto done;en return arr;et arr=NULL;done:return arr;en return NULL;. Bien que dans des cas plus complexes, il se peut qu’il y ait plusieurs gotos commençant à se dérouler à des niveaux de disponibilité différents (ce qui serait fait par un déroulement de pile d’exceptions en C ++).
Ruslan
2

J'ai appris à classer les problèmes de mémoire dans un certain nombre de catégories différentes.

  • Une fois gouttes. Supposons qu'un programme perd 100 octets au démarrage, mais ne fuira plus jamais. Poursuivre et éliminer ces fuites non récurrentes est bien (j'aime bien avoir un rapport vierge par une fonction de détection de fuites) mais n'est pas essentiel. Parfois, il faut s'attaquer à des problèmes plus importants.

  • Fuites répétées. Une fonction appelée de manière répétitive au cours de la durée de vie d’un programme qui perd régulièrement de la mémoire est un gros problème. Ces gouttes vont torturer à mort le programme, et éventuellement le système d'exploitation.

  • Références mutuelles. Si les objets A et B se référencent via des pointeurs partagés, vous devez faire quelque chose de spécial, que ce soit dans la conception de ces classes ou dans le code qui implémente / utilise ces classes pour rompre la circularité. (Ce n'est pas un problème pour les langages collectés.)

  • Se souvenir trop. C'est le cousin maléfique des fuites d'ordures / mémoire. RAII ne va pas aider ici, pas plus que la collecte des ordures. C'est un problème dans n'importe quelle langue. Si une variable active a un chemin qui la connecte à un bloc de mémoire aléatoire, ce bloc de mémoire aléatoire n'est pas gâché. Faire en sorte qu'un programme devienne oublieux et qu'il puisse durer plusieurs jours est délicat. Créer un programme pouvant durer plusieurs mois (par exemple, jusqu'à ce que le disque tombe en panne) est très, très délicat.

Je n'ai pas eu de problème sérieux avec les fuites pendant très longtemps. L'utilisation de RAII en C ++ aide beaucoup à remédier à ces écoulements et fuites. (Il faut toutefois être prudent avec les pointeurs partagés.) Plus important encore, j'ai eu des problèmes avec des applications dont l'utilisation de la mémoire ne cesse de croître, en raison de connexions non exploitées à la mémoire qui ne sont plus d'aucune utilité.

David Hammen
la source
-6

Il appartient au programmeur C ++ d'implémenter sa propre forme de récupération de place si nécessaire. Sinon, vous obtiendrez ce que l'on appelle une «fuite de mémoire». Il est assez courant que les langages «de haut niveau» (tels que Java) aient un garbage collection intégré, mais pas les langages «de bas niveau» tels que C et C ++.

xDr_Johnx
la source