Pourquoi ne pas utiliser des pointeurs pour tout en C++?

supposons que je définisse une classe:

class Pixel {
    public:
      Pixel(){ x=0; y=0;};
      int x;
      int y;
}

alors écrivez du code en l'utilisant. Pourquoi je ferais la suite?

Pixel p;
p.x = 2;
p.y = 5;

venant d'un monde Java j'écris toujours:

Pixel* p = new Pixel();
p->x = 2;
p->y = 5;

ils font la même chose, non? On est sur la pile, tandis que l'autre est sur le tas, donc je vais devoir le supprimer plus tard. Est-il une différence fondamentale entre les deux? Pourquoi devrais-je préférer l'un à l'autre?

73
demandé sur Peter Mortensen 2009-06-30 19:26:02

23 réponses

Oui, on est sur la pile, l'autre sur le tas. Il y a deux différences importantes:

  • tout d'abord, l'évidence, et moins important: les allocations tas sont lents. Les attributions par pile sont rapides.
  • Second, et beaucoup plus important est RAII . Parce que la version stack-attribuée est automatiquement nettoyé, il est utile . Son destructeur est automatiquement appelé, ce qui permet de garantir que les ressources allouées par la classe en débarrasser. C'est essentiellement comme ça qu'on évite les fuites de mémoire en C++. Vous les Évitez en n'appelant jamais vous-même delete , mais en l'enveloppant dans des objets empilés qui appellent delete en interne, typiquement dans leur destructeur. Si vous tentez de suivre manuellement toutes les attributions, et appelez delete au bon moment, je vous garantis que vous aurez au moins une fuite de mémoire par 100 lignes de code.

comme petit exemple, considérez ce code:

class Pixel {
public:
  Pixel(){ x=0; y=0;};
  int x;
  int y;
};

void foo() {
  Pixel* p = new Pixel();
  p->x = 2;
  p->y = 5;

  bar();

  delete p;
}

c'est un code innocent, Non? Nous créons un pixel, puis nous appelons une fonction sans rapport, puis nous supprimons le pixel. Est-il une fuite de mémoire?

Et la réponse est "peut-être". Que se passe-t-il si bar fait exception? delete n'est jamais appelé, le pixel n'est jamais effacé, et nous perdons de la mémoire. Maintenant, considérez ceci:

void foo() {
  Pixel p;
  p.x = 2;
  p.y = 5;

  bar();
}

ce ne sera pas une fuite de mémoire. Bien sûr, dans ce cas simple, tout est sur la pile, donc il est nettoyé automatiquement, mais même si la classe Pixel avait fait une allocation dynamique interne, cela ne fuirait pas non plus. La classe Pixel serait simplement dotée d'un destructeur qui la supprime, et ce destructeur serait appelé peu importe comment nous quittons la fonction foo . Même si on la laisse parce que bar a fait une exception. Ce qui suit, légèrement artificiel exemple:

class Pixel {
public:
  Pixel(){ x=new int(0); y=new int(0);};
  int* x;
  int* y;

  ~Pixel() {
    delete x;
    delete y;
  }
};

void foo() {
  Pixel p;
  *p.x = 2;
  *p.y = 5;

  bar();
}

la classe Pixel alloue maintenant en interne de la mémoire en tas, mais son destructeur s'occupe de la nettoyer, donc quand utilise la classe, nous n'avons pas à nous en inquiéter. (Je devrais probablement mentionner que le dernier exemple est simplifié beaucoup de choses, afin de montrer le principe général. Si nous étions réellement utiliser cette classe, il contient plusieurs erreurs possibles. Si l'allocation de y échoue, x n'obtient jamais libéré, et si le Pixel est copié, nous finissons avec les deux instances essayant de supprimer les mêmes données. Prenez donc le dernier exemple ici avec un grain de sel. Dans le monde réel de code est un peu plus compliqué, mais il montre que l'idée générale)

bien sûr, la même technique peut être étendue à d'autres ressources que les allocations de mémoire. Par exemple, il peut être utilisé pour garantir que les fichiers ou les connexions de base de données sont fermés après utilisation, ou que les serrures de synchronisation pour votre code de threading sont publier.

185
répondu jalf 2012-05-01 07:37:16

ils ne sont pas les mêmes jusqu'à ce que vous ajoutiez la suppression.

Votre exemple est trop trivial, mais le destructeur peut en fait contenir du code du travail. Ceci est désigné sous le RAII.

ajouter la suppression. Assurez-vous que cela se produit même lorsque les exceptions se propagent.

Pixel* p = NULL; // Must do this. Otherwise new may throw and then
                 // you would be attempting to delete an invalid pointer.
try
{
    p = new Pixel(); 
    p->x = 2;
    p->y = 5;

    // Do Work
    delete p;
}
catch(...)
{
    delete p;
    throw;
}

Si vous aviez choisi quelque chose de plus intéressant, comme un fichier (qui est une ressource qui doit être fermé). Puis le faire correctement Java avec les conseils dont vous avez besoin pour ce faire.

File file;
try
{
    file = new File("Plop");
    // Do work with file.
}
finally
{
    try
    {
        file.close();     // Make sure the file handle is closed.
                          // Oherwise the resource will be leaked until
                          // eventual Garbage collection.
    }
    catch(Exception e) {};// Need the extra try catch to catch and discard
                          // Irrelevant exceptions. 

    // Note it is bad practice to allow exceptions to escape a finally block.
    // If they do and there is already an exception propagating you loose the
    // the original exception, which probably has more relevant information
    // about the problem.
}

le même code en C++

std::fstream  file("Plop");
// Do work with file.

// Destructor automatically closes file and discards irrelevant exceptions.

bien que les gens mentionnent la vitesse (en raison de trouver/allouer la mémoire sur le tas). Personnellement, ce n'est pas un facteur décisif pour moi (les allocateurs sont très rapides et ont été optimisés pour l'utilisation C++ de petits objets qui sont constamment créés/détruits).

la raison principale pour moi est la durée de vie de l'objet. Un objet défini localement a une durée de vie très spécifique et bien définie et le destructeur est garanti d'être appelé à la fin (et peut donc avoir des effets secondaires spécifiques). Par contre, un pointeur contrôle une ressource avec une durée de vie dynamique.

la principale différence entre C++ et Java est:

Le concept qui est propriétaire du pointeur. Il incombe au propriétaire de supprimer l'objet au moment opportun. C'est pourquoi vous voyez très rarement cru pointeurs comme celui des programmes réels (puisqu'il n'y a pas d'information de propriété associée à un pointeur raw ). Au lieu de cela les pointeurs sont habituellement enveloppés dans des pointeurs intelligents. Le pointeur intelligent définit la sémantique de qui est propriétaire de la mémoire et donc qui est responsable du nettoyage.

exemples:

 std::auto_ptr<Pixel>   p(new Pixel);
 // An auto_ptr has move semantics.
 // When you pass an auto_ptr to a method you are saying here take this. You own it.
 // Delete it when you are finished. If the receiver takes ownership it usually saves
 // it in another auto_ptr and the destructor does the actual dirty work of the delete.
 // If the receiver does not take ownership it is usually deleted.

 std::tr1::shared_ptr<Pixel> p(new Pixel); // aka boost::shared_ptr
 // A shared ptr has shared ownership.
 // This means it can have multiple owners each using the object simultaneously.
 // As each owner finished with it the shared_ptr decrements the ref count and 
 // when it reaches zero the objects is destroyed.

 boost::scoped_ptr<Pixel>  p(new Pixel);
 // Makes it act like a normal stack variable.
 // Ownership is not transferable.

il y en a d'autres.

30
répondu Martin York 2009-07-01 00:38:08

Logiquement qu'ils font la même chose, sauf pour le nettoyage. Juste le code d'exemple que vous avez écrit a une fuite de mémoire dans le cas de pointeur parce que cette mémoire n'est pas libérée.

venant d'un fond Java, il se peut que vous ne soyez pas tout à fait prêt pour la façon dont c++ tourne autour du fait de garder une trace de ce qui a été alloué et qui est responsable de le libérer.

en utilisant des variables de pile quand approprié, vous n'avez pas à vous soucier de en libérant cette variable, elle disparaît avec le cadre de la pile.

évidemment, si vous êtes super prudent, vous pouvez toujours allouer sur le tas et libre manuellement, mais une partie de la bonne ingénierie logicielle est de construire les choses d'une telle manière qu'ils ne peuvent pas casser, plutôt que de faire confiance à votre programmeur-fu super-humain de ne jamais faire une erreur.

25
répondu Clyde 2010-08-11 19:08:39

je préfère utiliser la première méthode chaque fois que j'en ai l'occasion parce que:

  • c'est plus rapide
  • je n'ai pas à vous soucier de la désallocation de la mémoire
  • p sera un objet valide pour l'ensemble de la portée actuelle
24
répondu rpg 2009-06-30 15:32:53

"Pourquoi ne pas utiliser des pointeurs pour le tout en C++"

une réponse simple - parce que cela devient un énorme problème gérer la mémoire - allouer et supprimer/libérer.

Automatique/objets de pile de supprimer certains de le travail.

c'est juste la première chose que je dirais à propos de la question.

14
répondu Tim 2009-06-30 15:33:03

une bonne règle générale de base est de ne jamais utiliser de nouveaux à moins que vous ne devez absolument. Vos programmes seront plus faciles à maintenir et moins sujets aux erreurs si vous n'utilisez pas new car vous n'avez pas à vous soucier de l'endroit où le nettoyer.

11
répondu Steve 2009-06-30 15:35:25

le code:

Pixel p;
p.x = 2;
p.y = 5;

ne fait pas d'allocation dynamique de mémoire - il n'y a pas de recherche de mémoire libre, pas de mise à jour de l'utilisation de la mémoire, rien. Il est totalement gratuit. Le compilateur réserve de l'espace sur la pile pour la variable au moment de la compilation - il s'avère avoir beaucoup d'espace à réserver et crée un opcode unique pour déplacer le pointeur de pile la quantité requise.

utiliser new nécessite tout ce que la gestion de la mémoire aérienne.

la question devient alors - Voulez-vous utiliser l'espace de pile ou l'espace de tas pour vos données. Les variables de pile (ou locales) comme " p " ne nécessitent pas de déréférencement, tandis que l'utilisation de new ajoute une couche d'indirection.

11
répondu Skizz 2012-07-03 13:57:00

Oui, au premier abord, qui fait sens, en provenance de Java ou C# arrière-plan. Il ne semble pas comme un gros problème pour oublier de libérer la mémoire allouée. Mais quand tu auras ta première fuite de mémoire, tu te gratteras la tête, parce que tu as juré que tu avais tout libéré. Puis la deuxième fois, ça arrive et la troisième fois, tu seras encore plus frustré. Enfin, après six mois de maux de tête dus à des problèmes de mémoire, vous allez commencer à vous en lasser et que la mémoire stack-alloed commencer à regarder de plus en plus attrayant. Comment agréable et propre-juste le mettre sur la pile et de l'oublier. Bientôt, vous utiliserez la pile chaque fois que vous pourrez vous en tirer.

mais ... rien ne remplace cette expérience. Mon conseil? Essayez-le à votre façon, pour l'instant. Vous le verrez.

10
répondu eeeeaaii 2009-06-30 15:45:35

ma réaction intestinale est juste pour vous dire que cela pourrait conduire à de sérieuses fuites de mémoire. Certaines situations dans lesquelles vous pourriez utiliser des pointeurs pourraient entraîner une confusion quant à savoir qui devrait être responsable de les supprimer. Dans les cas simples tels que votre exemple, il est assez facile de voir quand et où vous devriez appeler supprimer, mais quand vous commencez à passer des pointeurs entre les classes, les choses peuvent devenir un peu plus difficile.

je recommande de regarder dans le boost bibliothèque smart pointers pour vos pointeurs.

6
répondu Eric 2009-06-30 15:31:43

la meilleure raison de ne pas tout changer est que vous pouvez très déterministe nettoyage lorsque les choses sont sur la pile. Dans le cas des pixels, ce n'est pas si évident, mais dans le cas d'un fichier, par exemple, cela devient avantageux:

  {   // block of code that uses file
      File aFile("file.txt");
      ...
  }    // File destructor fires when file goes out of scope, closing the file
  aFile // can't access outside of scope (compiler error)

dans le cas de la création d'un fichier, vous devez vous rappeler de le supprimer pour obtenir le même comportement. Cela semble être un problème simple dans le cas ci-dessus. Considérez le code plus complexe, Cependant, tel que le stockage des pointeurs dans une donnée structure. Et si tu passais cette structure de données à un autre morceau de code? Qui est responsable du nettoyage. Qui fermerait tous vos dossiers?

quand on ne fait pas tout neuf, les ressources sont simplement nettoyées par le destructeur quand la variable sort de la portée. Vous pouvez donc avoir une plus grande confiance dans le fait que les ressources sont nettoyées avec succès.

ce concept est connu sous le nom de RAII -- L'Allocation des ressources est une initialisation et elle peut considérablement améliorer votre capacité de gérer l'acquisition et l'aliénation des ressources.

6
répondu Doug T. 2009-06-30 15:32:59

le premier cas n'est pas toujours une pile attribuée. S'il fait partie d'un objet, il sera attribué où qu'il se trouve. Par exemple:

class Rectangle {
    Pixel top_left;
    Pixel bottom_right;
}

Rectangle r1; // Pixel is allocated on the stack
Rectangle *r2 = new Rectangle(); // Pixel is allocated on the heap

les principaux avantages des variables de pile sont:

  • vous pouvez utiliser le RAII pattern pour gérer les objets. Dès que l'objet sort de sa portée, son destructeur est appelé. Un peu comme le motif" utiliser " en C#, mais automatique.
  • Il n'y a aucune possibilité de référence nulle.
  • vous n'avez pas à vous soucier de gérer manuellement la mémoire de l'objet.
  • il provoque moins d'allocations de mémoire. Les attributions de mémoire, en particulier les plus petites, seront probablement plus lentes en C++ qu'en Java.

une fois que l'objet a été créé, il n'y a aucune différence de performance entre un objet alloué sur le tas, et un autre alloué sur la pile (ou ailleurs).

cependant, vous ne pouvez pas utiliser n'importe quel type de polymorphisme à moins que vous n'utilisiez un pointeur - l'objet a un type complètement statique, qui est déterminé au moment de la compilation.

6
répondu BlackAura 2009-06-30 15:38:07

durée de vie des Objets. Lorsque vous voulez que la durée de vie de votre objet dépasse la durée de vie de la portée actuelle, vous devez utiliser le tas.

si d'un autre côté, vous n'avez pas besoin de la variable au-delà de la portée actuelle, déclarez-la sur la pile. Il sera automatiquement détruit quand il sera hors de portée. Faites attention en faisant circuler son adresse.

4
répondu Matt Brunell 2009-06-30 15:33:57

c'est une question de goût. Si vous créez une interface permettant aux méthodes de prendre des pointeurs au lieu de références, vous permettez à l'appelant de passer dans nil. Puisque vous permettez à l'utilisateur de passer dans Zéro, l'utilisateur passera dans Zéro.

Puisque vous posez la question "Qu'advient-il si ce paramètre est nul?", vous devez coder plus défensivement, en prenant soin de vérifications null tout le temps. Cela parle de l'aide de références.

cependant, parfois, vous voulez vraiment être en mesure de passer dans nil et puis les références sont hors de question :) les pointeurs vous donnent une plus grande flexibilité et vous permettent d'être plus paresseux, ce qui est vraiment bon. Ne jamais allouer jusqu'à savoir que vous devez allouer!

4
répondu ralphtheninja 2009-06-30 15:39:32

The issue isn't pointers per se (mis à part l'introduction de NULL pointeurs), mais faire la gestion de la mémoire à la main.

la partie drôle, bien sûr, est que chaque tutoriel Java que j'ai vu a mentionné le collecteur d'ordures est tellement cool hotness parce que vous n'avez pas à vous rappeler d'appeler delete , alors que dans la pratique C++ ne nécessite delete quand vous appelez new (et delete[] quand vous appelez new[] ).

4
répondu Max Lybbert 2009-12-23 09:05:22

N'utilisez des pointeurs et des objets alloués dynamiquement que lorsque vous le devez. Dans la mesure du possible, utilisez des objets alloués de façon statique (global ou stack).

  • les objets statiques sont plus rapides (pas de nouveaux/Supprimer, pas d'accès indirect)
  • Pas de durée de vie des objets à vous inquiéter au sujet de
  • moins de frappes plus lisibles
  • beaucoup plus robuste. Chaque "- > "est un accès potentiel à une mémoire nulle ou non valide

pour clarifier, par "statique" dans ce contexte, j'entends Non-dynamiquement alloué. OIE, ce qui n'est PAS sur le tas. Oui, ils peuvent avoir des problèmes de vie d'objet aussi - en termes d'ordre de destruction de singleton - mais les coller sur le tas ne résout généralement rien.

2
répondu Roddy 2009-06-30 16:03:07

Pourquoi ne pas utiliser des pointeurs pour tout?

ils sont plus lents.

les optimisations du compilateur ne seront pas aussi efficaces avec la sémantique d'accès au pointeur, vous pouvez le lire dans n'importe quel nombre de sites web, Mais voici un PDF décent de Intel.

Vérification des pages, 13,14,17,28,32,36;

détection de mémoire inutile références dans la notation de boucle:

for (i = j + 1; i <= *n; ++i) { 
X(i) -= temp * AP(k); } 

la notation pour les limites de boucle contient le pointeur ou la mémoire référence. Le compilateur n'a pas aucun moyen de prédire si la valeur référencé par le pointeur n est en cours changé avec des itérations de boucle par certains autre affectation. Cela utilise la boucle pour recharger la valeur référencée par n pour chaque itération. Le générateur de code le moteur peut également refuser de programmer un logiciel de pipeline de la boucle lorsque le potentiel pointeur l'aliasing est trouvé. Depuis le valeur référencée par le pointeur n n'est pas anging dans la boucle et il est invariant à l'index de boucle, le chargement de *n s à effectuer en dehors de la boucle limites pour simplification de l'ordonnancement et du pointeur la désambiguïsation.

... un certain nombre de variations sur ce thème....

Complexe de références de mémoire. Ou dans d'autres des mots, l'analyse des références telles que complexe pointeur calculs, déformations la capacité des compilateurs à générer code efficace. Endroits dans le code où le compilateur ou le matériel est effectuer un calcul complexe dans afin de déterminer où les données réside, doit être au centre de attention. Aliasing du pointeur et code simplification aider le compilateur à reconnaître les modes d'accès à la mémoire, permettant au compilateur de chevauchement accès à la mémoire avec manipulation de données. Réduire les références de mémoire inutiles peut exposer pour le compilateur l' capacité de pipeline logiciel. Beaucoup d'autres propriétés de localisation de données, telles que comme aliasing ou alignement, peut être facilement reconnaissable si référence de mémoire les calculs sont simples. L'utilisation de réduction de la résistance ou à induction méthodes pour simplifier les références de mémoire est crucial pour aider le compilateur.

2
répondu RandomNickName42 2017-08-10 15:21:47

regarder la question sous un angle différent...

en C++, vous pouvez référencer des objets en utilisant des pointeurs ( Foo * ) et des références ( Foo & ). Dans la mesure du possible, j'utilise une référence au lieu d'un pointeur. Par exemple, en passant par référence à une fonction/méthode, l'utilisation de références permet au code de (espérons) faire les hypothèses suivantes:

  • l'objet référencé n'est pas la propriété de la fonction/méthode, donc ne devrait pas delete l'objet. C'est comme dire "Tiens, utilise ces données, mais rends-les quand tu auras fini".
  • les références à des pointeurs Nuls sont moins probables. Il est possible de passer une référence nulle, mais au moins ce ne sera pas la faute de la fonction/méthode. Une référence ne peut pas être réassignée à une nouvelle adresse de pointeur, de sorte que votre code ne pourrait pas avoir accidentellement réassigné à NULL ou une autre adresse de pointeur invalide, causant une page défectueuse.
1
répondu spoulson 2009-06-30 16:02:40

la question est: pourquoi utiliseriez-vous des pointeurs pour tout? Empiler les objets alloués sont non seulement plus sûr et plus rapide à créer, mais il ya encore moins de Dactylographie et le code semble mieux.

1
répondu Nemanja Trifunovic 2009-06-30 17:20:32

quelque chose que je n'ai pas vu mentionné est l'utilisation accrue de la mémoire. En supposant que 4 Octets entiers et des pointeurs

Pixel p;

utilisera 8 octets, et

Pixel* p = new Pixel();

utilisera 12 octets, soit une augmentation de 50%. Il ne semble pas beaucoup jusqu'à ce que vous allouez assez pour une image 512x512. Alors vous parlez 2MB au lieu de 3MB. C'est ignorer la gestion du tas avec tous ces objets sur eux.

0
répondu KeithB 2009-06-30 18:37:22

les Objets créés sur la pile sont créés plus rapidement que les objets alloués.

pourquoi?

parce que l'allocation de la mémoire (avec gestionnaire de mémoire par défaut) prend un certain temps (pour trouver un bloc vide ou même allouer ce bloc).

vous n'avez pas non plus de problèmes de gestion de mémoire puisque l'objet stack se détruit automatiquement lorsqu'il est hors de portée.

le code est plus simple quand vous n'utilisez pas de pointeurs. Si votre conception vous permet d'utiliser des objets stack, je vous recommande de le faire.

moi-même, Je ne compliquerais pas le problème en utilisant des pointeurs intelligents.

OTOH j'ai travaillé un peu dans le champ embedded et créer des objets sur la pile n'est pas très intelligent (comme la pile allouée pour chaque tâche/thread n'est pas très grande - vous devez être prudent).

donc c'est une question de choix et de restrictions, il n'y a pas de réponse pour tous les adapter.

Et, comme toujours, n'oubliez pas de keep it simple , autant que possible.

0
répondu INS 2009-06-30 20:46:03

en gros, quand vous utilisez des pointeurs bruts, vous n'avez pas de RAII.

0
répondu 2009-07-01 14:36:49

qui m'a beaucoup troublé quand j'étais un nouveau programmeur C++ (et c'était mon premier langage). Il y a beaucoup de très mauvais tutoriels C++ qui semblent généralement tomber dans l'une des deux catégories: les tutoriels "C / C++", ce qui signifie vraiment que c'est un tutoriel C (peut-être avec des classes), et les tutoriels C++ qui pensent que C++ est Java avec delete.

je pense qu'il m'a fallu environ 1 à 1,5 ans (au moins) pour taper" nouveau " n'importe où dans mon code. J'ai souvent utilisé des conteneurs STL comme vector., qui a pris soin de cela pour moi.

je pense que beaucoup de réponses semblent ignorer ou simplement éviter de dire directement comment éviter cela. Vous n'avez généralement pas besoin d'allouer à nouveau dans le constructeur et nettoyer avec delete dans le destructeur. Au lieu de cela, vous pouvez directement le bâton de l'objet lui-même dans la classe (plutôt qu'un pointeur) et d'initialiser l'objet lui-même dans le constructeur. Alors le constructeur par défaut fait tout ce dont vous avez besoin dans la plupart des cas.

pour presque toutes les situations où cela ne fonctionnera pas (par exemple, si vous risquez de manquer d'espace de stack), vous devriez probablement utiliser l'un des conteneurs standard de toute façon: std::string, std::vector, et std::map sont les trois que j'utilise le plus souvent, mais std::deque et std::list sont également assez fréquents. Les autres (des choses comme std::set et le non-standard corde ) ne sont pas utilisés autant mais se comportent de la même manière. Ils se répartissent tous à partir du free store (langage C++ pour "le tas" dans quelques autres langues), voir: C++ STL question: allocateurs

0
répondu David Stone 2017-05-23 12:09:02

le premier cas est préférable à moins que plus de membres soient ajoutés à la classe de Pixel. Comme de plus en plus de membre est ajouté, il y a une possibilité d'exception de débordement de pile

-2
répondu Uday 2009-07-03 08:23:31