75 votes

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

Supposons que je définir une classe:

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

Puis écrire le code de l'utiliser. Pourquoi je ferais la suite?

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

Venant d'un monde Java j'ai toujours écrire:

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

En gros, 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 un plutôt que l'autre?

188voto

jalf Points 142628

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

  • La première, la plus évidente, et la moins importante: les allocations de Tas sont lents. Pile allocations sont rapides.
  • Deuxième, et beaucoup plus important, c'est RAII. Parce que la pile alloué version est automatiquement nettoyé, il est utile. Le destructeur est appelé automatiquement, ce qui permet de garantir que les ressources allouées par la classe en débarrasser. C'est essentiellement la façon dont vous éviter les fuites de mémoire en C++. Vous les éviter en ne l'appelant delete vous-même, mais en l'enveloppant dans de la pile des objets alloué qui appellent delete en interne, typiquement dans leur destructeur. Si vous tentez d'manuellement garder une trace de toutes les attributions, et appelez - delete , au bon moment, je vous garantie que vous aurez au moins une fuite de mémoire pour 100 lignes de code.

Un petit exemple, considérer 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;
}

Assez innocent code, non? Nous avons créer un pixel, puis nous appeler sans rapport avec la fonction, puis nous supprimer le pixel. Est-il une fuite de mémoire?

Et la réponse est "peut-être". Ce qui se passe si bar déclenche une exception? delete n'est jamais appelée, le pixel n'est jamais supprimé, et nous avons une fuite de 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 s'est nettoyé automatiquement, mais même si l' Pixel classe avait fait une allocation dynamique interne, qui ne serait pas de fuite non plus. L' Pixel classe serait tout simplement donné un destructeur que le supprime, et ce destructeur sera appelé n'importe comment, nous laisser l' foo fonction. Même si on le laisse parce qu' bar a déclenché une exception. Le suivant, un peu artificiel exemple montre ceci:

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();
}

Le Pixel classe maintenant en interne alloue la mémoire du tas, mais son destructeur prend soin de la nettoyer, de sorte que lors de l' utilisation de la classe, nous n'avons pas à s'en soucier. (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 la répartition de y échoue, x n'est jamais libéré, et si le Pixel est copié, nous nous retrouvons avec les deux cas, essayez de supprimer les mêmes données. Afin de prendre 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 de verrous de synchronisation pour votre enfilage code sont libérés.

30voto

Loki Astari Points 116129

Ils ne sont pas les mêmes jusqu'à ce que vous ajoutez le supprimer.
Votre exemple est trop trivial, mais le destructeur peut en fait contenir du code du travail. Ceci est désigné sous le RAII.

Ajoutez donc la supprimer. Assurez-vous qu'il arrive, même lorsque les exceptions sont de multiplication.

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 en 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.

Si les gens parler de la vitesse (parce que de trouver/allocation de 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 le C++ utilisation de petits objets qui sont constamment créés ou détruits).

La raison principale pour moi est l'objet de temps de la vie. Local à objet défini est très spécifique et bien défini de durée de vie et le destructeur est garanti d'être appelé à la fin (et donc peuvent avoir des effets secondaires spécifiques). Un pointeur sur l'autre main le contrôle d'une ressource avec une dynamique de durée de vie.

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

Le concept qui est propriétaire du pointeur. Il est de la responsabilité du propriétaire de la suppression de l'objet au moment opportun. C'est pourquoi vous très rarement voir raw pointeurs comme ça dans la vraie programmes (comme il n'y a pas d'appropriation de l'information associée à un raw pointeur). Au lieu de cela les pointeurs sont généralement emballé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.

Des exemples sont:

 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 en existe d'autres.

25voto

Clyde Points 3881

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

Venant de Java arrière-plan, vous ne pouvez être complètement préparé pour combien de C++ tourne autour de garder une trace de ce qui a été alloué et qui est responsable de la libération.

En utilisant des variables de pile lorsque approprié, vous n'avez pas à vous soucier de libérer cette variable, il s'en va avec le cadre de la pile.

Évidemment, si vous êtes un super prudent, vous pouvez toujours allouer sur le tas et gratuit manuellement, mais une partie de bon génie logiciel est de construire les choses de telle manière qu'ils ne peuvent pas briser, plutôt que de faire confiance à votre super-programmeur humain-fu de ne jamais faire une erreur.

24voto

rpg Points 5305

Je préfère utiliser la première méthode, chaque fois que j'ai de la chance 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

14voto

Tim Points 13334

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

Une réponse simple - parce qu'il devient un énorme problème avec la gestion de la mémoire - l'allocation et la suppression/libération.

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

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

Prograide.com

Prograide est une communauté de développeurs qui cherche à élargir la connaissance de la programmation au-delà de l'anglais.
Pour cela nous avons les plus grands doutes résolus en français et vous pouvez aussi poser vos propres questions ou résoudre celles des autres.

Powered by:

X