Pourquoi devrais-je utiliser un pointeur plutôt que l'objet lui-même?


1603

Je viens d'un arrière-plan Java et j'ai commencé à travailler avec des objets en C ++. Mais une chose qui m'est venue à l'esprit est que les gens utilisent souvent des pointeurs vers des objets plutôt que les objets eux-mêmes, par exemple cette déclaration:

Object *myObject = new Object;

plutôt que:

Object myObject;

Ou au lieu d'utiliser une fonction, disons testFunc(), comme ceci:

myObject.testFunc();

nous devons écrire:

myObject->testFunc();

Mais je ne peux pas comprendre pourquoi devrions-nous procéder de cette façon. Je suppose que cela a à voir avec l'efficacité et la vitesse car nous avons un accès direct à l'adresse mémoire. Ai-je raison?


406
Bravo à vous d'avoir remis en question cette pratique plutôt que de simplement la suivre. La plupart du temps, les pointeurs sont sur-utilisés.
Luchian Grigore

120
Si vous ne voyez pas de raison d'utiliser des pointeurs, ne le faites pas. Préférez les objets. Préférez les objets avant unique_ptr avant shared_ptr avant les pointeurs bruts.
Stefan

113
note: en java, tout (sauf les types basiques) est un pointeur. vous devriez donc plutôt demander le contraire: pourquoi ai-je besoin d'objets simples?
Karoly Horvath

119
Notez qu'en Java, les pointeurs sont masqués par la syntaxe. En C ++, la différence entre un pointeur et un non-pointeur est rendue explicite dans le code. Java utilise des pointeurs partout.
Daniel Martín

216
Fermer comme trop large ? Sérieusement? S'il vous plaît, les gens, notez que cette méthode de programmation Java ++ est très courante et l'un des problèmes les plus importants de la communauté C ++ . Elle doit être traitée sérieusement.
Manu343726

Réponses:


1576

Il est très regrettable que vous voyiez l'allocation dynamique si souvent. Cela montre simplement le nombre de mauvais programmeurs C ++.

Dans un sens, vous avez deux questions regroupées en une seule. La première est quand devrions-nous utiliser l'allocation dynamique (en utilisantnew )? La seconde est quand devrions-nous utiliser des pointeurs?

Le message important à retenir est que vous devez toujours utiliser l'outil approprié pour le travail . Dans presque toutes les situations, il y a quelque chose de plus approprié et de plus sûr que d'effectuer l'allocation dynamique manuelle et / ou d'utiliser des pointeurs bruts.

Allocation dynamique

Dans votre question, vous avez montré deux façons de créer un objet. La principale différence est la durée de stockage de l'objet. Lors d'une opération Object myObject;dans un bloc, l'objet est créé avec une durée de stockage automatique, ce qui signifie qu'il sera détruit automatiquement lorsqu'il sortira du cadre. Lorsque vous le faites new Object(), l'objet a une durée de stockage dynamique, ce qui signifie qu'il reste en vie jusqu'à ce que vous le explicitiez delete. Vous ne devez utiliser la durée de stockage dynamique que lorsque vous en avez besoin. Autrement dit, vous devriez toujours préférer créer des objets avec une durée de stockage automatique lorsque vous le pouvez .

Les deux principales situations dans lesquelles vous pourriez avoir besoin d'une allocation dynamique:

  1. Vous avez besoin de l'objet pour survivre à la portée actuelle - cet objet spécifique à cet emplacement de mémoire spécifique, pas une copie de celui-ci. Si vous êtes d'accord pour copier / déplacer l'objet (la plupart du temps vous devriez l'être), vous devriez préférer un objet automatique.
  2. Vous devez allouer beaucoup de mémoire , ce qui peut facilement remplir la pile. Ce serait bien si nous n'avions pas à nous préoccuper de cela (la plupart du temps, vous ne devriez pas avoir à le faire), car c'est vraiment en dehors du domaine de C ++, mais malheureusement, nous devons faire face à la réalité des systèmes nous développons pour.

Lorsque vous avez absolument besoin d'une allocation dynamique, vous devez l'encapsuler dans un pointeur intelligent ou un autre type qui exécute RAII (comme les conteneurs standard). Les pointeurs intelligents fournissent une sémantique de propriété des objets alloués dynamiquement. Jetez un œil à std::unique_ptret std::shared_ptr, par exemple. Si vous les utilisez correctement, vous pouvez presque entièrement éviter d'effectuer votre propre gestion de la mémoire (voir la règle du zéro ).

Pointeurs

Cependant, il existe d'autres utilisations plus générales des pointeurs bruts au-delà de l'allocation dynamique, mais la plupart ont des alternatives que vous devriez préférer. Comme précédemment, préférez toujours les alternatives, sauf si vous avez vraiment besoin de pointeurs .

  1. Vous avez besoin d'une sémantique de référence . Parfois, vous voulez passer un objet à l'aide d'un pointeur (quelle que soit la façon dont il a été alloué) parce que vous voulez que la fonction à laquelle vous le passez ait accès à cet objet spécifique (pas à une copie). Cependant, dans la plupart des situations, vous devez préférer les types de référence aux pointeurs, car c'est précisément pour cela qu'ils sont conçus. Notez qu'il ne s'agit pas nécessairement d'étendre la durée de vie de l'objet au-delà de la portée actuelle, comme dans la situation 1 ci-dessus. Comme précédemment, si vous êtes d'accord pour transmettre une copie de l'objet, vous n'avez pas besoin de sémantique de référence.

  2. Vous avez besoin de polymorphisme . Vous ne pouvez appeler des fonctions que de manière polymorphe (c'est-à-dire selon le type dynamique d'un objet) via un pointeur ou une référence à l'objet. Si c'est le comportement dont vous avez besoin, vous devez utiliser des pointeurs ou des références. Encore une fois, les références devraient être préférées.

  3. Vous voulez représenter qu'un objet est facultatif en permettant nullptrà a d'être passé lorsque l'objet est omis. S'il s'agit d'un argument, vous devriez préférer utiliser des arguments par défaut ou des surcharges de fonctions. Sinon, vous devez de préférence utiliser un type qui encapsule ce comportement, tel que std::optional(introduit dans C ++ 17 - avec les normes C ++ antérieures, utilisez boost::optional).

  4. Vous souhaitez découpler les unités de compilation pour améliorer le temps de compilation . La propriété utile d'un pointeur est que vous n'avez besoin que d'une déclaration directe du type pointé (pour utiliser réellement l'objet, vous aurez besoin d'une définition). Cela vous permet de découpler des parties de votre processus de compilation, ce qui peut considérablement améliorer le temps de compilation. Voir l' idiome Pimpl .

  5. Vous devez vous interfacer avec une bibliothèque C ou une bibliothèque de style C. À ce stade, vous êtes obligé d'utiliser des pointeurs bruts. La meilleure chose que vous puissiez faire est de ne laisser vos pointeurs bruts se détacher qu'au dernier moment possible. Vous pouvez obtenir un pointeur brut à partir d'un pointeur intelligent, par exemple, en utilisant sa getfonction membre. Si une bibliothèque effectue une allocation pour vous qu'elle s'attend à ce que vous désallouiez via une poignée, vous pouvez souvent envelopper la poignée dans un pointeur intelligent avec un suppresseur personnalisé qui désallouera l'objet de manière appropriée.


83
"Vous avez besoin de l'objet pour survivre à la portée actuelle." - Une remarque supplémentaire à ce sujet: il y a des cas où il semble que vous ayez besoin de l'objet pour survivre à la portée actuelle, mais ce n'est vraiment pas le cas. Si vous placez votre objet à l'intérieur d'un vecteur, par exemple, l'objet sera copié (ou déplacé) dans le vecteur, et l'objet d'origine peut être détruit en toute sécurité à la fin de sa portée.

25
N'oubliez pas que s / copier / déplacer / dans de nombreux endroits maintenant. Le retour d'un objet n'implique certainement pas un mouvement. Vous devez également noter que l'accès à un objet via un pointeur est orthogonal à la façon dont il a été créé.
Puppy

15
Je manque une référence explicite au RAII sur cette réponse. C ++ est tout (presque tout) sur la gestion des ressources, et RAII est le moyen de le faire sur C ++ (Et le principal problème généré par les pointeurs bruts: Breaking RAII)
Manu343726

11
Les pointeurs intelligents existaient avant C ++ 11, par exemple boost :: shared_ptr et boost :: scoped_ptr. D'autres projets ont leur propre équivalent. Vous ne pouvez pas obtenir la sémantique des mouvements, et l'assignation de std :: auto_ptr est défectueuse, donc C ++ 11 améliore les choses, mais les conseils sont toujours bons. (Et un triste pinceau, il ne suffit pas d'avoir accès à un compilateur C ++ 11, il est nécessaire que tous les compilateurs que vous souhaitiez que votre code fonctionne avec le support C ++ 11. Oui, Oracle Solaris Studio, je suis vous regarde.)
armb

7
@ MDMoore313 Vous pouvez écrireObject myObject(param1, etc...)
user000001

174

Il existe de nombreux cas d'utilisation pour les pointeurs.

Comportement polymorphe . Pour les types polymorphes, des pointeurs (ou références) sont utilisés pour éviter le découpage:

class Base { ... };
class Derived : public Base { ... };

void fun(Base b) { ... }
void gun(Base* b) { ... }
void hun(Base& b) { ... }

Derived d;
fun(d);    // oops, all Derived parts silently "sliced" off
gun(&d);   // OK, a Derived object IS-A Base object
hun(d);    // also OK, reference also doesn't slice

Sémantique de référence et éviter la copie . Pour les types non polymorphes, un pointeur (ou une référence) évitera de copier un objet potentiellement cher

Base b;
fun(b);  // copies b, potentially expensive 
gun(&b); // takes a pointer to b, no copying
hun(b);  // regular syntax, behaves as a pointer

Notez que C ++ 11 a une sémantique de déplacement qui peut éviter de nombreuses copies d'objets coûteux en argument de fonction et en tant que valeurs de retour. Mais l'utilisation d'un pointeur les évitera certainement et permettra plusieurs pointeurs sur le même objet (alors qu'un objet ne peut être déplacé qu'une seule fois).

Acquisition de ressources . La création d'un pointeur vers une ressource à l'aide de l' newopérateur est un anti-modèle en C ++ moderne. Utilisez une classe de ressources spéciale (l'un des conteneurs Standard) ou un pointeur intelligent ( std::unique_ptr<>ou std::shared_ptr<>). Considérer:

{
    auto b = new Base;
    ...       // oops, if an exception is thrown, destructor not called!
    delete b;
}

contre.

{
    auto b = std::make_unique<Base>();
    ...       // OK, now exception safe
}

Un pointeur brut ne doit être utilisé que comme une "vue" et ne doit en aucun cas être impliqué dans la propriété, que ce soit par la création directe ou implicitement par le biais de valeurs de retour. Voir aussi ce Q&A de la FAQ C ++ .

Contrôle plus précis de la durée de vie Chaque fois qu'un pointeur partagé est copié (par exemple en tant qu'argument de fonction), la ressource vers laquelle il pointe est maintenue en vie. Les objets normaux (non créés par new, directement par vous ou à l'intérieur d'une classe de ressources) sont détruits lorsqu'ils sortent de la portée.


17
"La création d'un pointeur vers une ressource en utilisant le nouvel opérateur est un anti-modèle" Je pense que vous pourriez même améliorer le fait d' avoir un pointeur brut propre à quelque chose est un anti-modèle . Non seulement la création, mais le passage de pointeurs bruts en tant qu'arguments ou valeurs de retour impliquant un transfert de propriété à unique_ptr
mon humble avis

1
@dyp tnx, mis à jour et référence à la FAQ C ++ FAQ sur ce sujet.
TemplateRex

4
L'utilisation de pointeurs intelligents partout est un anti-modèle. Il y a quelques cas particuliers où elle est applicable, mais la plupart du temps, la même raison qui plaide pour l'allocation dynamique (durée de vie arbitraire) est également contre tout pointeur intelligent habituel.
James Kanze

2
@JamesKanze Je ne voulais pas impliquer que les pointeurs intelligents devraient être utilisés partout, juste pour la propriété, et aussi que les pointeurs bruts ne devraient pas être utilisés pour la propriété, mais uniquement pour les vues.
TemplateRex

2
@TemplateRex Cela semble un peu idiot étant donné que cela hun(b)nécessite également la connaissance de la signature, sauf si vous êtes d'accord pour ne pas savoir que vous avez fourni le mauvais type jusqu'à la compilation. Bien que le problème de référence ne soit généralement pas détecté au moment de la compilation et demande plus d'efforts pour le débogage, si vous vérifiez la signature pour vous assurer que les arguments sont corrects, vous pourrez également voir si l'un des arguments est une référence. de sorte que le bit de référence devient quelque chose d'un non-problème (surtout lorsque vous utilisez des IDE ou des éditeurs de texte qui affichent la signature d'une fonction sélectionnée). Aussi const&.
JAB

130

Il existe de nombreuses excellentes réponses à cette question, notamment les cas d'utilisation importants des déclarations avancées, du polymorphisme, etc.

Examinons la situation en comparant les deux langues:

Java:

Object object1 = new Object(); //A new object is allocated by Java
Object object2 = new Object(); //Another new object is allocated by Java

object1 = object2; 
//object1 now points to the object originally allocated for object2
//The object originally allocated for object1 is now "dead" - nothing points to it, so it
//will be reclaimed by the Garbage Collector.
//If either object1 or object2 is changed, the change will be reflected to the other

L'équivalent le plus proche de ceci est:

C ++:

Object * object1 = new Object(); //A new object is allocated on the heap
Object * object2 = new Object(); //Another new object is allocated on the heap
delete object1;
//Since C++ does not have a garbage collector, if we don't do that, the next line would 
//cause a "memory leak", i.e. a piece of claimed memory that the app cannot use 
//and that we have no way to reclaim...

object1 = object2; //Same as Java, object1 points to object2.

Voyons la méthode alternative C ++:

Object object1; //A new object is allocated on the STACK
Object object2; //Another new object is allocated on the STACK
object1 = object2;//!!!! This is different! The CONTENTS of object2 are COPIED onto object1,
//using the "copy assignment operator", the definition of operator =.
//But, the two objects are still different. Change one, the other remains unchanged.
//Also, the objects get automatically destroyed once the function returns...

La meilleure façon d'y penser est que - plus ou moins - Java (implicitement) gère les pointeurs vers les objets, tandis que C ++ peut gérer soit les pointeurs vers les objets, soit les objets eux-mêmes. Il existe des exceptions à cela - par exemple, si vous déclarez des types Java "primitifs", ce sont des valeurs réelles qui sont copiées, et non des pointeurs. Donc,

Java:

int object1; //An integer is allocated on the stack.
int object2; //Another integer is allocated on the stack.
object1 = object2; //The value of object2 is copied to object1.

Cela dit, l'utilisation de pointeurs n'est PAS nécessairement la bonne ou la mauvaise façon de gérer les choses; cependant, d'autres réponses ont couvert cela de manière satisfaisante. L'idée générale est cependant qu'en C ++, vous avez beaucoup plus de contrôle sur la durée de vie des objets et sur leur emplacement.

À retenir - la Object * object = new Object()construction est en fait ce qui se rapproche le plus de la sémantique Java (ou C # d'ailleurs) typique.


7
Object2 is now "dead": Je pense que vous voulez dire myObject1ou plus précisément the object pointed to by myObject1.
Clément

2
En effet! Reformulé un peu.
Gerasimos R

2
Object object1 = new Object(); Object object2 = new Object();est un très mauvais code. Le deuxième nouveau ou le deuxième constructeur Object peut lancer, et maintenant object1 est divulgué. Si vous utilisez des news bruts , vous devez envelopper les newobjets ed dans des wrappers RAII dès que possible.
PSkocik

8
En effet, ce serait le cas s'il s'agissait d'un programme, et que rien d'autre ne se passait autour de lui. Heureusement, ceci est juste un extrait d'explication montrant comment un pointeur en C ++ se comporte - et l'un des rares endroits où un objet RAII ne peut pas être remplacé par un pointeur brut, étudie et apprend des pointeurs bruts ...
Gerasimos R

80

Une autre bonne raison d'utiliser des pointeurs serait pour les déclarations à terme . Dans un projet assez grand, ils peuvent vraiment accélérer le temps de compilation.


7
cela ajoute vraiment au mélange d'informations utiles, donc heureux d'avoir répondu!
TemplateRex

3
std :: shared_ptr <T> fonctionne également avec les déclarations avancées de T. (std :: unique_ptr <T> ne fonctionne pas )
berkus

13
@berkus: std::unique_ptr<T>fonctionne avec les déclarations avancées de T. Vous avez juste besoin de vous assurer que lorsque le destructeur du std::unique_ptr<T>est appelé, Tc'est un type complet. Cela signifie généralement que votre classe qui contient le std::unique_ptr<T>déclare son destructeur dans le fichier d'en-tête et l'implémente dans le fichier cpp (même si l'implémentation est vide).
David Stone

Les modules vont-ils résoudre ce problème?
Trevor Hickey

@TrevorHickey Vieux commentaire je sais, mais pour y répondre quand même. Les modules ne supprimeront pas la dépendance, mais devraient rendre l'inclusion de la dépendance très bon marché, presque gratuite en termes de coût de performance. De plus, si l'accélération générale des modules était suffisante pour obtenir vos temps de compilation dans une plage acceptable, ce n'est plus un problème non plus.
Aidiakapi

79

Préface

Java n'a rien à voir avec C ++, contrairement au battage médiatique. La machine hype Java voudrait que vous croyiez que parce que Java a une syntaxe similaire à C ++, les langages sont similaires. Rien ne peut être plus éloigné de la vérité. Cette désinformation fait partie des raisons pour lesquelles les programmeurs Java se tournent vers C ++ et utilisent une syntaxe de type Java sans comprendre les implications de leur code.

Nous continuons

Mais je ne peux pas comprendre pourquoi devrions-nous procéder de cette façon. Je suppose que cela a à voir avec l'efficacité et la vitesse car nous avons un accès direct à l'adresse mémoire. Ai-je raison?

Au contraire, en fait. Le tas est beaucoup plus lent que la pile, car la pile est très simple par rapport au tas. Les variables de stockage automatique (ou variables de pile) ont leurs destructeurs appelés une fois qu'ils sont hors de portée. Par exemple:

{
    std::string s;
}
// s is destroyed here

En revanche, si vous utilisez un pointeur alloué dynamiquement, son destructeur doit être appelé manuellement. deleteappelle ce destructeur pour vous.

{
    std::string* s = new std::string;
}
delete s; // destructor called

Cela n'a rien à voir avec le new syntaxe répandue en C # et Java. Ils sont utilisés à des fins complètement différentes.

Avantages de l'allocation dynamique

1. Vous n'avez pas besoin de connaître à l'avance la taille du tableau

L'un des premiers problèmes rencontrés par de nombreux programmeurs C ++ est que lorsqu'ils acceptent des entrées arbitraires d'utilisateurs, vous ne pouvez allouer qu'une taille fixe pour une variable de pile. Vous ne pouvez pas non plus modifier la taille des tableaux. Par exemple:

char buffer[100];
std::cin >> buffer;
// bad input = buffer overflow

Bien sûr, si vous avez utilisé un std::string place, il se std::stringredimensionne en interne de sorte que cela ne devrait pas être un problème. Mais essentiellement, la solution à ce problème est l'allocation dynamique. Vous pouvez allouer de la mémoire dynamique en fonction de l'entrée de l'utilisateur, par exemple:

int * pointer;
std::cout << "How many items do you need?";
std::cin >> n;
pointer = new int[n];

Note latérale : Une erreur que de nombreux débutants font est l'utilisation de tableaux de longueur variable. Il s'agit d'une extension GNU et également de Clang car elles reflètent la plupart des extensions de GCC. Il int arr[n]ne faut donc pas se fier aux éléments suivants.

Parce que le tas est beaucoup plus grand que la pile, on peut allouer / réallouer arbitrairement autant de mémoire qu'il / elle a besoin, alors que la pile a une limitation.

2. Les tableaux ne sont pas des pointeurs

En quoi est-ce un avantage que vous demandez? La réponse deviendra claire une fois que vous comprendrez la confusion / le mythe derrière les tableaux et les pointeurs. On suppose généralement qu'ils sont identiques, mais ils ne le sont pas. Ce mythe vient du fait que les pointeurs peuvent être souscrits tout comme les tableaux et à cause de la décomposition des tableaux vers les pointeurs au niveau supérieur dans une déclaration de fonction. Cependant, une fois qu'un tableau se désintègre en un pointeur, le pointeur perd sonsizeof informations. Doncsizeof(pointer) la taille du pointeur en octets, qui est généralement de 8 octets sur un système 64 bits.

Vous ne pouvez pas attribuer à des tableaux, seulement les initialiser. Par exemple:

int arr[5] = {1, 2, 3, 4, 5}; // initialization 
int arr[] = {1, 2, 3, 4, 5}; // The standard dictates that the size of the array
                             // be given by the amount of members in the initializer  
arr = { 1, 2, 3, 4, 5 }; // ERROR

D'un autre côté, vous pouvez faire ce que vous voulez avec des pointeurs. Malheureusement, comme la distinction entre les pointeurs et les tableaux est ondulée à la main en Java et en C #, les débutants ne comprennent pas la différence.

3. Polymorphisme

Java et C # ont des fonctionnalités qui vous permettent de traiter les objets comme des autres, par exemple en utilisant le asmot - clé. Donc, si quelqu'un voulait traiter un Entityobjet comme un Playerobjet, on pourrait le faire. Player player = Entity as Player;C'est très utile si vous avez l'intention d'appeler des fonctions sur un conteneur homogène qui ne devraient s'appliquer qu'à un type spécifique. La fonctionnalité peut être obtenue de la même manière ci-dessous:

std::vector<Base*> vector;
vector.push_back(&square);
vector.push_back(&triangle);
for (auto& e : vector)
{
     auto test = dynamic_cast<Triangle*>(e); // I only care about triangles
     if (!test) // not a triangle
        e.GenericFunction();
     else
        e.TriangleOnlyMagic();
}

Donc, si seulement Triangles avait une fonction Rotation, ce serait une erreur de compilation si vous essayiez de l'appeler sur tous les objets de la classe. À l'aide de dynamic_cast, vous pouvez simuler le asmot - clé. Pour être clair, si une conversion échoue, elle renvoie un pointeur non valide. Donc!test s'agit essentiellement d'un raccourci pour vérifier sitest NULL ou un pointeur non valide, ce qui signifie que la conversion a échoué.

Avantages des variables automatiques

Après avoir vu toutes les grandes choses que l'allocation dynamique peut faire, vous vous demandez probablement pourquoi personne n'utiliserait pas l'allocation dynamique tout le temps? Je vous ai déjà dit une raison, le tas est lent. Et si vous n'avez pas besoin de toute cette mémoire, vous ne devriez pas en abuser. Voici donc quelques inconvénients sans ordre particulier:

  • Il est sujet aux erreurs. L'allocation manuelle de mémoire est dangereuse et vous êtes sujet à des fuites. Si vous ne maîtrisez pas le débogueur ouvalgrind (un outil de fuite de mémoire), vous pouvez retirer vos cheveux de votre tête. Heureusement, les idiomes RAII et les pointeurs intelligents atténuent un peu cela, mais vous devez être familier avec des pratiques telles que la règle de trois et la règle de cinq. Il y a beaucoup d'informations à prendre et les débutants qui ne savent pas ou s'en moquent tomberont dans ce piège.

  • Ce n'est pas nécessaire. Contrairement à Java et C # où il est idiomatique d'utiliser le newmot - clé partout, en C ++, vous ne devez l'utiliser que si vous en avez besoin. La phrase courante va, tout ressemble à un clou si vous avez un marteau. Alors que les débutants qui commencent par C ++ ont peur des pointeurs et apprennent à utiliser les variables de pile par habitude, les programmeurs Java et C # commencent par utiliser des pointeurs sans le comprendre! Cela revient littéralement du mauvais pied. Vous devez abandonner tout ce que vous savez car la syntaxe est une chose, apprendre la langue en est une autre.

1. (N) RVO - Aka, (Nommé) Optimisation de la valeur de retour

Une optimisation que de nombreux compilateurs effectuent sont des éléments appelés élision et optimisation de la valeur de retour . Ces éléments peuvent éviter les copies inutiles, ce qui est utile pour les objets très volumineux, comme un vecteur contenant de nombreux éléments. Normalement, la pratique courante consiste à utiliser des pointeurs pour transférer la propriété plutôt que de copier les gros objets pour les déplacer . Cela a conduit à la création d'une sémantique de mouvement et de pointeurs intelligents .

Si vous utilisez des pointeurs, (N) RVO ne se produit PAS . Il est plus avantageux et moins sujet aux erreurs de tirer parti de (N) RVO plutôt que de renvoyer ou de passer des pointeurs si vous vous inquiétez de l'optimisation. Des fuites d'erreur peuvent se produire si l'appelant d'une fonction est responsable de l' deleteingénierie d'un objet alloué dynamiquement et autres. Il peut être difficile de suivre la propriété d'un objet si des pointeurs sont passés comme une patate chaude. Utilisez simplement les variables de pile car c'est plus simple et meilleur.


"Alors! Test est essentiellement un raccourci pour vérifier si le test est NULL ou un pointeur invalide, ce qui signifie que la conversion a échoué." Je pense que cette phrase doit être réécrite pour plus de clarté.
berkus

4
"La machine hype Java voudrait vous faire croire" - peut-être en 1997, mais c'est maintenant anachronique, il n'y a plus de motivation pour comparer Java à C ++ en 2014.
Matt R

15
Vieille question, mais dans le segment de code { std::string* s = new std::string; } delete s; // destructor called.... cela deletene fonctionnera sûrement pas parce que le compilateur ne saura plus ce qui sest?
badger5000

2
Je ne donne PAS -1, mais je ne suis pas d'accord avec les déclarations d'ouverture telles qu'elles sont écrites. Tout d'abord, je ne suis pas d'accord pour dire qu'il existe un "battage médiatique" - peut-être autour de l'an 2000, mais maintenant les deux langues sont bien comprises. Deuxièmement, je dirais qu'ils sont assez similaires - C ++ est l'enfant de C marié avec Simula, Java ajoute Virtual Machine, Garbage Collector et HEAVILY réduit les fonctionnalités, et C # rationalise et réintroduit les fonctionnalités manquantes de Java. Oui, cela rend les modèles et l'utilisation valide extrêmement différents, mais il est avantageux de comprendre l'infrastructure / conception commune afin que l'on puisse voir les différences.
Gerasimos R

1
@James Matta: Vous avez bien sûr raison de dire que la mémoire est de la mémoire, et ils sont tous les deux alloués à partir de la même mémoire physique, mais une chose à considérer est qu'il est très courant d'obtenir de meilleures caractéristiques de performance en travaillant avec des objets alloués par pile car la pile - ou au moins ses niveaux les plus élevés - ont une très grande chance d'être "chaud" dans le cache lorsque les fonctions entrent et sortent, tandis que le tas n'a pas un tel avantage, donc si vous pointez sur le tas, vous pouvez obtenir plusieurs échecs de cache qui vous ne le feriez probablement pas sur la pile. Mais tout ce "hasard" favorise normalement la pile.
Gerasimos R

23

C ++ vous donne trois façons de passer un objet: par pointeur, par référence et par valeur. Java vous limite avec ce dernier (la seule exception concerne les types primitifs comme int, booléen, etc.). Si vous voulez utiliser C ++ non seulement comme un jouet étrange, vous feriez mieux de faire la différence entre ces trois façons.

Java prétend qu'il n'y a pas de problème comme «qui et quand devrait détruire cela?». La réponse est: le garbage collector, Great and Awful. Néanmoins, il ne peut pas fournir une protection à 100% contre les fuites de mémoire (oui, java peut fuir la mémoire ). En fait, GC vous donne un faux sentiment de sécurité. Plus votre SUV est gros, plus votre chemin vers l'évacuateur est long.

C ++ vous laisse face à face avec la gestion du cycle de vie des objets. Eh bien, il existe des moyens de gérer cela ( famille de pointeurs intelligents , QObject dans Qt et ainsi de suite), mais aucun d'entre eux ne peut être utilisé de manière `` tirer et oublier '' comme GC: vous devriez toujours garder à l'esprit la gestion de la mémoire. Non seulement vous devez vous soucier de détruire un objet, mais vous devez également éviter de détruire le même objet plus d'une fois.

Pas encore peur? Ok: références cycliques - manipulez-les vous-même, humain. Et rappelez-vous: tuez chaque objet précisément une fois, nous, les runtimes en C ++, n'aimons pas ceux qui jouent avec les cadavres, laissons les morts seuls.

Revenons donc à votre question.

Lorsque vous passez votre objet par valeur, pas par pointeur ou par référence, vous copiez l'objet (l'objet entier, que ce soit quelques octets ou un énorme vidage de base de données - vous êtes assez intelligent pour prendre soin d'éviter ce dernier, aren ' t vous?) chaque fois que vous faites '='. Et pour accéder aux membres de l'objet, vous utilisez '.' (point).

Lorsque vous passez votre objet par pointeur, vous copiez seulement quelques octets (4 sur les systèmes 32 bits, 8 sur les systèmes 64 bits), à savoir - l'adresse de cet objet. Et pour le montrer à tout le monde, vous utilisez cet opérateur fantaisie '->' lorsque vous accédez aux membres. Ou vous pouvez utiliser la combinaison de '*' et '.'.

Lorsque vous utilisez des références, vous obtenez le pointeur qui prétend être une valeur. C'est un pointeur, mais vous accédez aux membres via '.'.

Et, pour vous épater encore une fois: lorsque vous déclarez plusieurs variables séparées par des virgules, alors (faites attention aux aiguilles):

  • Le type est donné à tout le monde
  • Le modificateur valeur / pointeur / référence est individuel

Exemple:

struct MyStruct
{
    int* someIntPointer, someInt; //here comes the surprise
    MyStruct *somePointer;
    MyStruct &someReference;
};

MyStruct s1; //we allocated an object on stack, not in heap

s1.someInt = 1; //someInt is of type 'int', not 'int*' - value/pointer modifier is individual
s1.someIntPointer = &s1.someInt;
*s1.someIntPointer = 2; //now s1.someInt has value '2'
s1.somePointer = &s1;
s1.someReference = s1; //note there is no '&' operator: reference tries to look like value
s1.somePointer->someInt = 3; //now s1.someInt has value '3'
*(s1.somePointer).someInt = 3; //same as above line
*s1.somePointer->someIntPointer = 4; //now s1.someInt has value '4'

s1.someReference.someInt = 5; //now s1.someInt has value '5'
                              //although someReference is not value, it's members are accessed through '.'

MyStruct s2 = s1; //'NO WAY' the compiler will say. Go define your '=' operator and come back.

//OK, assume we have '=' defined in MyStruct

s2.someInt = 0; //s2.someInt == 0, but s1.someInt is still 5 - it's two completely different objects, not the references to the same one

1
std::auto_ptrest obsolète, veuillez ne pas l'utiliser.
Neil

2
À peu près sûr que vous ne pouvez pas avoir une référence en tant que membre sans également fournir à un constructeur une liste d'initialisation qui inclut la variable de référence. (Une référence doit être initialisée immédiatement. Même le corps du constructeur est trop tard pour le définir, IIRC.)
cHao

20

En C ++, les objets alloués sur la pile (en utilisant une Object object;instruction dans un bloc) ne vivront que dans la portée dans laquelle ils sont déclarés. Lorsque le bloc de code termine son exécution, l'objet déclaré est détruit. Alors que si vous allouez de la mémoire sur le tas, en utilisant Object* obj = new Object(), ils continuent à vivre dans le tas jusqu'à ce que vous appeliez delete obj.

Je créerais un objet sur le tas quand j'aimerais utiliser l'objet non seulement dans le bloc de code qui l'a déclaré / alloué.


6
Object objn'est pas toujours sur la pile - par exemple des globaux ou des variables membres.
tenfour

2
@LightnessRacesinOrbit Je n'ai mentionné que les objets alloués dans un bloc, pas les variables globales et membres. La chose est que ce n'était pas clair, maintenant corrigé - ajouté "dans un bloc" dans la réponse. J'espère que ce n'est pas une fausse information maintenant :)
Karthik Kalyanasundaram

20

Mais je ne peux pas comprendre pourquoi devrions-nous l'utiliser comme ça?

Je vais comparer comment cela fonctionne à l'intérieur du corps de la fonction si vous utilisez:

Object myObject;

À l'intérieur de la fonction, votre myObject sera détruit une fois cette fonction revenue. C'est donc utile si vous n'avez pas besoin de votre objet en dehors de votre fonction. Cet objet sera placé sur la pile de threads actuelle.

Si vous écrivez à l'intérieur du corps de la fonction:

 Object *myObject = new Object;

alors l'instance de classe Object pointée par myObjectne sera pas détruite une fois la fonction terminée, et l'allocation est sur le tas.

Maintenant, si vous êtes programmeur Java, le deuxième exemple est plus proche de la façon dont l'allocation d'objets fonctionne sous java. Cette ligne: Object *myObject = new Object;équivaut à java: Object myObject = new Object();. La différence est que sous java myObject récupérera les ordures, tandis que sous c ++ il ne sera pas libéré, vous devez quelque part appeler explicitement `delete myObject; ' sinon vous introduirez des fuites de mémoire.

Depuis c ++ 11, vous pouvez utiliser des moyens sûrs d'allocations dynamiques:, new Objecten stockant les valeurs dans shared_ptr / unique_ptr.

std::shared_ptr<std::string> safe_str = make_shared<std::string>("make_shared");

// since c++14
std::unique_ptr<std::string> safe_str = make_unique<std::string>("make_shared"); 

De plus, les objets sont très souvent stockés dans des conteneurs, comme map-s ou vector-s, ils géreront automatiquement la durée de vie de vos objets.


1
then myObject will not get destroyed once function endsCe sera absolument le cas.
Courses de légèreté en orbite

6
Dans le cas du pointeur, myObjectsera toujours détruit, comme n'importe quelle autre variable locale. La différence est que sa valeur est un pointeur vers un objet, pas l'objet lui-même, et la destruction d'un pointeur stupide n'affecte pas sa pointe. Ainsi, l' objet survivra à cette destruction.
cHao

Correction de cela, les variables locales (qui incluent le pointeur) seront bien sûr libérées - elles sont sur la pile.
marcinj

13

Techniquement, c'est un problème d'allocation de mémoire, mais voici deux autres aspects pratiques de cela. Cela a à voir avec deux choses: 1) Portée, lorsque vous définissez un objet sans pointeur, vous ne pourrez plus y accéder après le bloc de code dans lequel il est défini, tandis que si vous définissez un pointeur avec "nouveau", vous pouvez y accéder de n'importe où vous avez un pointeur vers cette mémoire jusqu'à ce que vous appeliez "supprimer" sur le même pointeur. 2) Si vous voulez passer des arguments à une fonction, vous voulez passer un pointeur ou une référence pour être plus efficace. Lorsque vous passez un objet, l'objet est copié, s'il s'agit d'un objet qui utilise beaucoup de mémoire, cela peut consommer du processeur (par exemple, vous copiez un vecteur plein de données). Lorsque vous passez un pointeur, tout ce que vous passez est un entier (selon l'implémentation mais la plupart d'entre eux sont un entier).

Autre que cela, vous devez comprendre que "nouveau" alloue de la mémoire sur le tas qui doit être libéré à un moment donné. Lorsque vous n'avez pas à utiliser "nouveau", je vous suggère d'utiliser une définition d'objet régulière "sur la pile".


6

Eh bien, la question principale est pourquoi devrais-je utiliser un pointeur plutôt que l'objet lui-même? Et ma réponse, vous ne devriez (presque) jamais utiliser de pointeur au lieu d'objet, car C ++ a des références , il est plus sûr que les pointeurs et garantit les mêmes performances que les pointeurs.

Une autre chose que vous avez mentionnée dans votre question:

Object *myObject = new Object;

Comment ça marche? Il crée un pointeur de Objecttype, alloue de la mémoire pour s'adapter à un objet et appelle le constructeur par défaut, sonne bien, non? Mais en réalité, ce n'est pas si bon, si vous avez alloué dynamiquement de la mémoire (mot-clé utilisé new), vous devez également libérer de la mémoire manuellement, cela signifie que dans le code, vous devriez avoir:

delete myObject;

Cela appelle destructeur et libère de la mémoire, semble facile, mais dans les grands projets, il peut être difficile de détecter si une mémoire libérée par un thread ou non, mais à cette fin, vous pouvez essayer des pointeurs partagés , cela diminue légèrement les performances, mais il est beaucoup plus facile de travailler avec leur.


Et maintenant, une introduction est terminée et reviens à la question.

Vous pouvez utiliser des pointeurs au lieu d'objets pour obtenir de meilleures performances lors du transfert de données entre les fonctions.

Jetez un oeil, vous avez std::string(c'est aussi un objet) et il contient vraiment beaucoup de données, par exemple du gros XML, maintenant vous devez les analyser, mais pour cela vous avez une fonction void foo(...)qui peut être déclarée de différentes manières:

  1. void foo(std::string xml); Dans ce cas, vous copiez toutes les données de votre variable dans la pile de fonctions, cela prend un certain temps, donc vos performances seront faibles.
  2. void foo(std::string* xml); Dans ce cas, vous passerez le pointeur à l'objet, à la même vitesse que la size_tvariable de passage , mais cette déclaration est sujette aux erreurs, car vous pouvez passer un NULLpointeur ou un pointeur non valide. Pointeurs généralement utilisés dans Ccar il n'a pas de références.
  3. void foo(std::string& xml); Ici, vous passez une référence, fondamentalement, c'est la même chose que de passer un pointeur, mais le compilateur fait certaines choses et vous ne pouvez pas passer une référence non valide (en fait, il est possible de créer une situation avec une référence non valide, mais il trompe le compilateur).
  4. void foo(const std::string* xml); Voici la même chose que la seconde, seule la valeur du pointeur ne peut pas être modifiée.
  5. void foo(const std::string& xml); Voici la même chose que la troisième, mais la valeur de l'objet ne peut pas être modifiée.

Ce que je veux dire de plus, vous pouvez utiliser ces 5 façons de transmettre des données, quelle que soit la méthode d'allocation que vous avez choisie (avec newou régulière ).


Une autre chose à mentionner, lorsque vous créez un objet de manière régulière , vous allouez de la mémoire dans la pile, mais pendant que vous le créez avec newvous allouez un tas. Il est beaucoup plus rapide d'allouer la pile, mais c'est un peu petit pour les très grands tableaux de données, donc si vous avez besoin d'un gros objet, vous devez utiliser le tas, car vous pouvez obtenir un débordement de pile, mais généralement ce problème est résolu en utilisant des conteneurs STL et n'oubliez pas std::stringest également un conteneur, certains gars l'ont oublié :)


5

Disons que vous en avez class Aqui contiennent class BLorsque vous voulez appeler une fonction de l' class Bextérieur, class Avous obtiendrez simplement un pointeur vers cette classe et vous pourrez faire ce que vous voulez et cela changera également le contexte de class Bvotreclass A

Mais soyez prudent avec un objet dynamique


5

Il existe de nombreux avantages à utiliser des pointeurs pour s'opposer -

  1. Efficacité (comme vous l'avez déjà souligné). Passer des objets aux fonctions signifie créer de nouvelles copies d'objet.
  2. Utilisation d'objets provenant de bibliothèques tierces. Si votre objet appartient à un code tiers et que les auteurs ont l'intention d'utiliser leurs objets uniquement via des pointeurs (pas de constructeurs de copie, etc.), la seule façon de contourner cet objet est d'utiliser des pointeurs. Le passage par valeur peut provoquer des problèmes. (Problèmes de copie profonde / copie superficielle).
  3. si l'objet possède une ressource et que vous souhaitez que la propriété ne soit pas partagée avec d'autres objets.

3

Cela a été longuement discuté, mais en Java, tout est un pointeur. Il ne fait aucune distinction entre les allocations de pile et de tas (tous les objets sont alloués sur le tas), donc vous ne réalisez pas que vous utilisez des pointeurs. En C ++, vous pouvez mélanger les deux, selon vos besoins en mémoire. Les performances et l'utilisation de la mémoire sont plus déterministes en C ++ (duh).


3
Object *myObject = new Object;

Cela créera une référence à un objet (sur le tas) qui doit être supprimé explicitement pour éviter une fuite de mémoire .

Object myObject;

Cela créera un objet (myObject) de type automatique (sur la pile) qui sera automatiquement supprimé lorsque l'objet (myObject) sortira du cadre.


1

Un pointeur fait directement référence à l'emplacement mémoire d'un objet. Java n'a rien de tel. Java a des références qui font référence à l'emplacement de l'objet via des tables de hachage. Vous ne pouvez rien faire comme l'arithmétique des pointeurs en Java avec ces références.

Pour répondre à votre question, c'est juste votre préférence. Je préfère utiliser la syntaxe de type Java.


Tables de hachage? Peut-être dans certaines machines virtuelles Java, mais ne comptez pas dessus.
Zan Lynx

Qu'en est-il de la JVM fournie avec Java? Bien sûr, vous pouvez implémenter TOUT ce à quoi vous pouvez penser comme une JVM qui utilise directement des pointeurs ou une méthode qui fait des calculs de pointeurs. C'est comme dire "les gens ne meurent pas du rhume" et obtenir une réponse "Peut-être que la plupart des gens ne le font pas, mais ne comptez pas dessus!" Ha ha.
RioRicoRick

2
@RioRicoRick HotSpot implémente les références Java en tant que pointeurs natifs, voir docs.oracle.com/javase/7/docs/technotes/guides/vm/… Pour autant que je puisse voir, JRockit fait de même. Ils prennent tous deux en charge la compression OOP, mais n'utilisent jamais les tables de hachage. Les conséquences sur les performances seraient probablement désastreuses. De plus, "c'est juste votre préférence" semble impliquer que les deux ne sont que des syntaxes différentes pour un comportement équivalent, ce qui bien sûr ne l'est pas.
Max Barraclough


0

Avec des pointeurs ,

  • peut parler directement à la mémoire.

  • peut empêcher beaucoup de fuites de mémoire d'un programme en manipulant des pointeurs.


4
" en C ++, en utilisant des pointeurs, vous pouvez créer un ramasse-miettes personnalisé pour votre propre programme " qui sonne comme une terrible idée.
quant

0

Une des raisons d'utiliser des pointeurs est de s'interfacer avec les fonctions C. Une autre raison est d'économiser de la mémoire; par exemple: au lieu de passer un objet qui contient beaucoup de données et qui a un constructeur de copie gourmand en processeur à une fonction, passez simplement un pointeur sur l'objet, économisant de la mémoire et de la vitesse surtout si vous êtes dans une boucle, cependant un référence serait mieux dans ce cas, sauf si vous utilisez un tableau de style C.


0

Dans les zones où l'utilisation de la mémoire est à son maximum, les pointeurs sont pratiques. Par exemple, considérons un algorithme minimax, où des milliers de nœuds seront générés à l'aide d'une routine récursive, et les utiliser plus tard pour évaluer le prochain meilleur mouvement du jeu, la capacité de désallouer ou de réinitialiser (comme dans les pointeurs intelligents) réduit considérablement la consommation de mémoire. Alors que la variable non pointeur continue d'occuper de l'espace jusqu'à ce que son appel récursif renvoie une valeur.


0

Je vais inclure un cas d'utilisation important du pointeur. Lorsque vous stockez un objet dans la classe de base, mais il peut être polymorphe.

Class Base1 {
};

Class Derived1 : public Base1 {
};


Class Base2 {
  Base *bObj;
  virtual void createMemerObects() = 0;
};

Class Derived2 {
  virtual void createMemerObects() {
    bObj = new Derived1();
  }
};

Donc, dans ce cas, vous ne pouvez pas déclarer bObj en tant qu'objet direct, vous devez avoir un pointeur.


-5

"La nécessité est la mère de l'invention." La différence la plus importante que je voudrais souligner est le résultat de ma propre expérience de codage. Parfois, vous devez passer des objets à des fonctions. Dans ce cas, si votre objet est d'une très grande classe, le passer en tant qu'objet copiera son état (ce que vous ne voudrez peut-être pas .. ET PEUT ÊTRE GRAND AU-DESSUS), ce qui entraîne un surcoût de copie de l'objet. Tandis que le pointeur est fixé Taille de 4 octets (en supposant 32 bits). D'autres raisons sont déjà mentionnées ci-dessus ...


14
vous devriez préférer passer par référence
bolov

2
Je recommande de passer par une référence constante comme pour la variable que std::string test;nous avons, void func(const std::string &) {}mais à moins que la fonction ne doive changer l'entrée, auquel cas je recommande d'utiliser des pointeurs (pour que toute personne lisant le code le remarque &et comprenne que la fonction peut changer son entrée)
Top- Master

-7

Il existe déjà de nombreuses excellentes réponses, mais permettez-moi de vous donner un exemple:

J'ai une classe d'objets simple:

 class Item
    {
    public: 
      std::string name;
      int weight;
      int price;
    };

Je fais un vecteur pour en tenir un tas.

std::vector<Item> inventory;

Je crée un million d'objets Item et les repousse sur le vecteur. Je trie le vecteur par nom, puis je fais une recherche binaire itérative simple pour un nom d'élément particulier. Je teste le programme et cela prend plus de 8 minutes pour terminer son exécution. Ensuite, je change mon vecteur d'inventaire comme suit:

std::vector<Item *> inventory;

... et créer mes millions d'objets d'objets via new. Les seules modifications que j'apporte à mon code consistent à utiliser les pointeurs vers les éléments, à l'exception d'une boucle que j'ajoute pour le nettoyage de la mémoire à la fin. Ce programme s'exécute en moins de 40 secondes, ou mieux qu'une augmentation de vitesse de 10x. EDIT: Le code est sur http://pastebin.com/DK24SPeW Avec les optimisations du compilateur, il ne montre qu'une augmentation de 3,4x sur la machine sur laquelle je viens de le tester, ce qui est encore considérable.


2
Eh bien, comparez-vous les pointeurs alors ou comparez-vous toujours les objets réels? Je doute fort qu'un autre niveau d'indirection puisse améliorer les performances. Veuillez fournir le code! Nettoyez-vous correctement après?
Stefan

1
@stefan Je compare les données (en particulier, le champ de nom) des objets pour le tri et la recherche. Je nettoie correctement, comme je l'ai déjà mentionné dans le post. l'accélération est probablement due à deux facteurs: 1) std :: vector push_back () copie les objets, donc la version du pointeur n'a besoin de copier qu'un seul pointeur par objet. Cela a de multiples impacts sur les performances, car non seulement moins de données sont copiées, mais l'allocateur de mémoire de classe vectorielle est moins détruit.
Darren

2
Voici un code ne montrant pratiquement aucune différence pour votre exemple: le tri. Le code du pointeur est 6% plus rapide que le code sans pointeur pour le tri seul, mais dans l'ensemble, il est 10% plus lent que le code sans pointeur. ideone.com/G0c7zw
stefan

3
Mot - clé: push_back. Bien sûr, cela copie. Vous devriez avoir été emplaceen place lors de la création de vos objets (sauf si vous avez besoin qu'ils soient mis en cache ailleurs).
underscore_d

1
Les vecteurs de pointeurs se trompent presque toujours. Veuillez ne pas les recommander sans expliquer en détail les mises en garde et les avantages et les inconvénients. Vous semblez avoir trouvé un pro, qui n'est que la conséquence d'un contre-exemple mal codé, et vous l'avez déformé
Lightness Races in Orbit
En utilisant notre site, vous reconnaissez avoir lu et compris notre politique liée aux cookies et notre politique de confidentialité.
Licensed under cc by-sa 3.0 with attribution required.