2009-03-28 9 views
5

Je travaille actuellement sur une application basée sur C et je suis un peu bloquée sur la libération de la mémoire d'une manière non-anti-pattern. Je suis un amateur de gestion de mémoire.Motifs pour libérer de la mémoire en C?

Mon problème principal est que je déclare des structures de mémoire dans différentes étendues, et que ces structures sont transmises par référence à d'autres fonctions. Certaines de ces fonctions peuvent lancer des erreurs et exit().

Comment puis-je libérer mes structures si je quitte() dans une portée, mais toutes mes structures de données ne sont pas dans cette portée? J'ai l'impression que j'ai besoin de tout emballer dans un gestionnaire d'exception Pseudo et que le gestionnaire s'occupe de la libération, mais cela semble encore moche car il devrait savoir tout ce que je peux ou ne pas avoir besoin de libérer.

Répondre

3

Envisager des enveloppes à malloc et de les utiliser de manière disciplinée. Suivez la mémoire que vous allouez (dans une liste liée peut-être) et utilisez un wrapper pour sortir afin d'énumérer votre mémoire pour la libérer. Vous pouvez également nommer la mémoire avec un paramètre supplémentaire et membre de votre structure de liste liée. Dans les applications où la mémoire allouée est très dépendante de la portée, vous trouverez une fuite de mémoire et cela peut être une bonne méthode pour vider la mémoire et l'analyser.

MISE À JOUR: L'enfilage dans votre application rendra cela très complexe. Voir d'autres réponses concernant les problèmes de threading.

+0

Vous pouvez même utiliser la fonction atexit() et écrire une fonction pour libérer toute la mémoire allouée sur la liste chaînée (qui devrait être une variable globale dans ce cas) sur un simple appel à exit() - comme vous le feriez Il ne faut pas oublier de ne pas utiliser la sortie normale(). –

+0

Mais pourquoi? Le système d'exploitation maintient déjà une liste de mémoire allouée. La duplication de cette fonctionnalité va simplement ralentir le processus d'arrêt sans autre bénéfice qu'un peu d'inflation de l'ego. Sauf si vous envisagez de porter votre application sur MSDOS, laissez l'os faire son travail. – Eclipse

+0

Le pourquoi est parce que finalement la discipline et la fonctionnalité vous aideront à traquer les fuites de mémoire. Si vous le codez correctement, il peut être désactivé via une définition DEFINE ou MACRO. – ojblass

3

Vous n'avez pas besoin de vous soucier de libérer de la mémoire lorsque exit() est appelé. Lorsque le processus se termine, le système d'exploitation libère toute la mémoire associée.

+0

Ce n'était pas toujours vrai. Il est considéré comme une bonne pratique sanitaire de libérer les structures mallocées avant de sortir. Il aide également lorsque vous avez besoin de trouver des fuites de mémoire réelles. –

+0

Ceci est seulement pour les systèmes d'exploitation agréables. Il est bon de libérer() de la mémoire quand vous en avez fini, ne serait-ce que pour prendre l'habitude. –

+0

Bon, je présume qu'il discute des échecs critiques; c'est-à-dire que le processus est en train de mourir et de mourir rapidement. – Michael

1

Vous pouvez créer un gestionnaire de mémoire simple pour la mémoire malloc qui est partagée entre des étendues/fonctions.

Enregistrez-le lorsque vous le faites, désactivez-le lorsque vous le libérez. Avoir une fonction qui libère toute la mémoire enregistrée avant d'appeler exit.

Il ajoute un peu de surcharge, mais il permet de garder une trace de la mémoire. Il peut également vous aider à traquer les fuites de mémoire embêtantes.

3

Je pense que pour répondre à cette question de façon appropriée, nous aurions besoin de connaître l'architecture de l'ensemble de votre programme (ou système, ou quel que soit le cas).

La réponse est: ça dépend. Il existe un certain nombre de stratégies que vous pouvez utiliser.

Comme d'autres l'ont souligné, sur un système d'exploitation de bureau ou de serveur moderne, vous pouvez exit() et ne pas vous soucier de la mémoire allouée par votre programme. Cette stratégie change, par exemple, si vous développez sur un système d'exploitation intégré dans lequel exit() peut ne pas tout nettoyer. Généralement ce que je vois est quand les fonctions individuelles retournent en raison d'une erreur, ils s'assurent de nettoyer tout ce qu'ils ont eux-mêmes attribué. Vous ne verriez aucun appel exit() après avoir appelé, disons, 10 fonctions. Chaque fonction à son tour indiquerait une erreur quand elle revient, et chaque fonction nettoierait après elle-même. La fonction main() originale (si vous voulez - elle pourrait ne pas être appelée main()) détecterait l'erreur, nettoiera toute la mémoire allouée et prendra les mesures appropriées.

Lorsque vous n'avez que des portées-limites, ce n'est pas sorcier.Là où cela devient difficile, c'est si vous avez plusieurs threads d'exécution et des structures de données partagées. Ensuite, vous aurez peut-être besoin d'un ramasse-miettes ou d'un moyen de compter les références et de libérer de la mémoire lorsque le dernier utilisateur de la structure aura terminé. Par exemple, si vous regardez la source de la pile réseau BSD, vous verrez qu'elle utilise une valeur refcnt (nombre de références) dans certaines structures qui doivent rester "actives" pendant une longue période et partagées entre différentes utilisateurs. (C'est essentiellement ce que font aussi les éboueurs.)

0

Très simplement, pourquoi ne pas avoir une implémentation comptée par référence, alors quand vous créez un objet et le faites circuler, vous augmentez et décrémentez le nombre compté de référence (n'oubliez pas d'être atomique si vous avez plus d'un thread). Ainsi, lorsqu'un objet n'est plus utilisé (références zéro), vous pouvez le supprimer en toute sécurité ou le supprimer automatiquement dans l'appel de décrément du nombre de références.

1

Le conseil de Michael est le son - si vous partez, vous n'avez pas besoin de vous soucier de libérer de la mémoire puisque le système le récupèrera quand même. Une exception à cela est les segments de mémoire partagée - au moins dans la mémoire partagée System V. Ces segments peuvent persister plus longtemps que le programme qui les crée.

Une option non mentionnée jusqu'à présent consiste à utiliser un schéma d'allocation de mémoire basé sur l'arène, construit au-dessus de la norme malloc(). Si l'application entière utilise une seule arène, votre code de nettoyage peut libérer cette arène, et tout est libéré à la fois. (APR - Apache Portable Runtime - fournit une fonctionnalité de pools que je crois être similaire: "C Interfaces and Implementations" de David Hanson fournit un système d'allocation de mémoire basé sur l'arène, j'en ai écrit un que vous pourriez utiliser si vous le vouliez.) peut penser à cela comme "la collecte des ordures du pauvre". En tant que discipline générale de la mémoire, chaque fois que vous allouez dynamiquement de la mémoire, vous devez comprendre quel code va le libérer et quand il peut être libéré. Il y a quelques modèles standards. Le plus simple est "alloué dans cette fonction, libéré avant que cette fonction ne retourne". Cela permet de garder une grande partie de la mémoire sous contrôle (si vous n'effectuez pas trop d'itérations sur la boucle contenant l'allocation de mémoire) et de l'étendre à la fonction en cours et aux fonctions qu'elle appelle. Évidemment, vous devez être raisonnablement sûr que les fonctions que vous appelez ne vont pas écarter (pointer) les pointeurs vers les données et essayer de les réutiliser plus tard après avoir libéré et réutilisé la mémoire. Le motif standard suivant est illustré par fopen() et fclose(); il y a une fonction qui alloue un pointeur à de la mémoire, qui peut être utilisée par le code appelant, puis relâchée quand le programme a fini avec. Cependant, cela devient souvent très similaire au premier cas - il est généralement une bonne idée d'appeler fclose() dans la fonction qui a également appelé fopen().

La plupart des 'modèles' restants sont quelque peu ad hoc.

1

Les gens ont déjà fait remarquer que vous n'avez probablement pas besoin de vous soucier de libérer de la mémoire si vous quittez (ou annulez) votre code en cas d'erreur. Mais juste au cas où, voici un modèle que j'ai développé et que j'utilise beaucoup pour créer et détruire des ressources en cas d'erreur.NOTE: Je montre un motif ici pour faire un point, ne pas écrire de vrai code!

int foo_create(foo_t *foo_out) { 
    int res; 
    foo_t foo; 
    bar_t bar; 
    baz_t baz; 
    res = bar_create(&bar); 
    if (res != 0) 
     goto fail_bar; 
    res = baz_create(&baz); 
    if (res != 0) 
     goto fail_baz; 
    foo = malloc(sizeof(foo_s)); 
    if (foo == NULL) 
     goto fail_alloc; 
    foo->bar = bar; 
    foo->baz = baz; 
    etc. etc. you get the idea 
    *foo_out = foo; 
    return 0; /* meaning OK */ 

    /* tear down stuff */ 
fail_alloc: 
    baz_destroy(baz); 
fail_baz: 
    bar_destroy(bar); 
fail_bar: 
    return res; /* propagate error code */ 
} 

Je peux parier que je vais obtenir des commentaires disant "c'est mauvais parce que vous utilisez goto". Mais c'est une utilisation disciplinée et structurée de goto qui rend le code plus clair, plus simple et plus facile à maintenir s'il est appliqué de manière cohérente. Vous ne pouvez pas obtenir un chemin de démontage simple et documenté à travers le code sans cela.

Si vous voulez voir ceci dans le vrai code commercial en service, jetez un oeil, par exemple, arena.c from the MPS (qui est par hasard un système de gestion de mémoire).

C'est une sorte d'essai d'un homme pauvre ... gestionnaire d'arrivée, et vous donne quelque chose qui ressemble un peu à un destructeur. Je vais maintenant avoir l'air d'un barbare, mais dans mes nombreuses années de travail sur le code C d'autres personnes, l'absence de chemins d'erreur clairs est souvent un problème très sérieux, en particulier dans le code réseau et d'autres situations peu fiables. Les présenter m'a occasionnellement fait un peu de revenus de consultance.

Il y a beaucoup d'autres choses à dire à propos de votre question - je vais juste laisser ce motif dans le cas où c'est utile.

+0

C'est mauvais parce que vous utilisez goto. Désolé, n'a pas pu résister: D –

+0

Si vous avez initialisé votre 'baz' et' bar' avec 'NULL' et fait votre fonction de destruction de sorte qu'ils gèrent un paramètre' NULL' avec élégance (comme 'free'), alors vous auriez être capable d'utiliser un seul label 'fail:', le rendant un peu moins encombré. –

+0

Merci pour la suggestion! C'est une règle de codage dans mes projets à haute fiabilité pour éviter d'utiliser NULL pour à peu près n'importe quoi. Avoir des valeurs nulles autour conduit à toutes sortes d'erreurs. Nous évitons surtout d'utiliser NULL pour indiquer une action spéciale que vous devriez prendre. Mais il y a un tout autre essai que je pourrais écrire sur NULL: P – rptb1

Questions connexes