Je viens d'un environnement 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 sur 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 le faire de cette façon. Je suppose que cela a à voir avec l'efficacité et la rapidité puisque nous avons un accès direct à l'adresse de la mémoire. Ai-je raison?
Il est très regrettable que vous voyiez si souvent une allocation dynamique. Cela montre juste combien il y a de mauvais programmeurs C++.
En un sens, vous avez deux questions regroupées en une. La première est quand devrions-nous utiliser l'allocation dynamique (en utilisant new
)? La seconde est quand devrions-nous utiliser des pointeurs?
Le message important à retenir est que vous devez (utilisez toujours 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 une allocation dynamique manuelle et/ou d'utiliser des pointeurs bruts.
Dans votre question, vous avez montré deux manières de créer un objet. La principale différence est la durée de stockage de l'objet. Lorsque Object myObject;
est inséré dans un bloc, l’objet est créé avec une durée de stockage automatique, ce qui signifie qu’il sera automatiquement détruit lorsqu’il sortira de sa portée. Lorsque vous exécutez new Object()
, l'objet a une durée de stockage dynamique, ce qui signifie qu'il reste actif jusqu'à ce que vous l'ayez explicitement 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:
Lorsque vous avez absolument besoin d'une allocation dynamique, vous devez l'encapsuler dans un pointeur intelligent ou un autre type effectuant RAII (comme les conteneurs standard). Les pointeurs intelligents fournissent la sémantique de propriété des objets alloués dynamiquement. Jetez un coup d'œil à std::unique_ptr
et std::shared_ptr
, par exemple. Si vous les utilisez correctement, vous pouvez presque entièrement éviter de gérer vous-même la mémoire (voir la règle de zéro ).
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 auparavant, préférez toujours les alternatives, sauf si vous avez vraiment besoin de pointeurs.
_ {Vous avez besoin de la sémantique de référence}. Parfois, vous souhaitez transmettre un objet à l'aide d'un pointeur (quelle que soit la manière dont il a été alloué), car vous souhaitez que la fonction à laquelle vous le transmettez ait accès à cet objet spécifique (et non une copie de celui-ci). Cependant, dans la plupart des situations, vous devriez préférer les types de référence aux pointeurs, car ils sont spécifiquement conçus pour ces objectifs. 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 auparavant, si vous acceptez de transmettre une copie de l'objet, vous n'avez pas besoin de la sémantique de référence.
_ {Vous avez besoin d'un 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.
Vous voulez représenter qu'un objet est facultatif en permettant à un nullptr
d'être transmis lorsque l'objet est omis. S'il s'agit d'un argument, vous devriez utiliser des arguments par défaut ou des surcharges de fonctions. Sinon, vous devriez préférer utiliser un type qui encapsule ce comportement, tel que std::optional
(introduit en C++ 17 - avec les normes C++ antérieures, utilisez boost::optional
).
Vous voulez 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 forward 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 améliorer considérablement le temps de compilation. Voir le idiome Pimpl .
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 à faire est de vous assurer que vos pointeurs bruts ne sont pas lâchés au dernier moment. Vous pouvez obtenir un pointeur brut à partir d'un pointeur intelligent, par exemple, en utilisant sa fonction membre get
. Si une bibliothèque effectue à votre place une allocation qu’elle attend de la désallocation via un handle, vous pouvez souvent l’envelopper dans un pointeur intelligent avec un suppresseur personnalisé qui désallouera l’objet de manière appropriée.
Il existe de nombreux cas d'utilisation pour les pointeurs.
Comportement polymorphe. Pour les types polymorphes, les pointeurs (ou références) sont utilisés pour éviter le découpage en tranches:
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 de copier. Pour les types non polymorphes, un pointeur (ou une référence) évitera de copier un objet potentiellement coûteux
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 définitivement et autorisera 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 sur une ressource à l'aide de l'opérateur new
est un anti-pattern 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 aucune manière être impliqué dans la propriété, que ce soit par la création directe ou implicitement par les valeurs de retour. Voir aussi ce Q & A du C++ FAQ .
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 conservée. 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.
Il existe de nombreuses excellentes réponses à cette question, notamment les cas d'utilisation importants des déclarations anticipées, du polymorphisme, etc., mais je pense qu'une partie de "l'âme" de votre question n'a pas de réponse - à savoir ce que les différentes syntaxes signifient en Java et en C++.
Examinons la situation en comparant les deux langues:
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 à ceci est:
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 manière 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 de penser à cela est que - plus ou moins - Java (implicitement) gère les pointeurs sur les objets, tandis que C++ peut gérer soit les pointeurs sur les objets, soit les objets eux-mêmes . Il existe des exceptions à cela - pour Par exemple, si vous déclarez des types "primitifs" Java, ce sont des valeurs réelles qui sont copiées et non des pointeurs . Donc,
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 cette question de manière satisfaisante. L'idée générale est que, en C++, vous avez beaucoup plus de contrôle sur la durée de vie des objets et sur leur lieu de résidence.
Le point de départ: la construction Object * object = new Object()
est en réalité ce qui se rapproche le plus de la sémantique typique de Java (ou de C #).
Une autre bonne raison d'utiliser des pointeurs serait pour les déclarations en aval . Dans un projet suffisamment important, ils peuvent vraiment accélérer le temps de compilation.
Java n'a rien à voir avec C++, contrairement à l'exagération. La machine _ hype Java aimerait vous faire croire que, parce que Java a la même syntaxe que C++, les langages sont similaires. Rien ne peut être plus éloigné de la vérité. Cette information erronée explique en partie pourquoi les programmeurs Java vont en C++ et utilisent une syntaxe semblable à celle de Java sans comprendre les implications de leur code.
Mais je ne peux pas comprendre pourquoi devrions-nous le faire de cette façon. Je suppose que cela a à voir avec l'efficacité et la rapidité puisque nous avons un accès direct à l'adresse de la 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 comparée au tas. Les destructeurs des variables de stockage automatiques (ou variables de pile) sont appelés une fois qu'ils sont hors de portée. Par exemple:
{
std::string s;
}
// s is destroyed here
Par contre, si vous utilisez un pointeur alloué dynamiquement, son destructeur doit être appelé manuellement. delete
appelle ce destructeur pour vous.
{
std::string* s = new std::string;
}
delete s; // destructor called
Cela n'a rien à voir avec la syntaxe new
qui prévaut en C # et en Java. Ils sont utilisés à des fins complètement différentes.
1. Vous n'avez pas besoin de connaître la taille du tableau à l'avance
Un des premiers problèmes que rencontrent 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 changer la taille des tableaux. Par exemple:
char buffer[100];
std::cin >> buffer;
// bad input = buffer overflow
Bien sûr, si vous utilisez plutôt un std::string
, std::string
se redimensionne de manière interne, ce qui ne devrait pas poser de problème. Mais la solution à ce problème est essentiellement 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 commise par de nombreux débutants est l'utilisation de tableaux de longueurs variables. Il s’agit d’une extension GNU et également de Clang car elles reflètent de nombreuses extensions de GCC. Donc, le
int arr[n]
suivant ne doit pas être utilisé.
Parce que le tas est beaucoup plus gros que la pile, on peut allouer/réallouer autant de mémoire que nécessaire, 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 aurez compris 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 de la même manière que les tableaux et qu'en raison de leur décomposition, ils deviennent des pointeurs au niveau supérieur d'une déclaration de fonction. Cependant, une fois qu'un tableau se décompose en un pointeur, celui-ci perd ses informations sizeof
. Donc, sizeof(pointer)
donnera la taille du pointeur en octets, qui est généralement de 8 octets sur un système 64 bits.
Vous ne pouvez pas affecter de tableaux, mais 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'autre part, vous pouvez faire ce que vous voulez avec des pointeurs. Malheureusement, la distinction entre les pointeurs et les tableaux étant agitée à la main dans Java et en C #, les débutants ne comprennent pas la différence.
3. Polymorphisme
Java et C # disposent de fonctionnalités vous permettant de traiter les objets comme un autre, en utilisant par exemple le mot clé as
. Donc, si quelqu'un veut traiter un objet Entity
comme un objet Player
, on peut faire Player player = Entity as Player;
Ceci est très utile si vous souhaitez appeler des fonctions sur un conteneur homogène qui ne devrait s'appliquer qu'à un objet. 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 Rotate, ce serait une erreur du compilateur si vous tentiez de l'appeler sur tous les objets de la classe. En utilisant dynamic_cast
, vous pouvez simuler le mot clé as
. Pour être clair, si une conversion échoue, elle renvoie un pointeur non valide. Donc, !test
est essentiellement un raccourci pour vérifier si test
est NULL ou un pointeur non valide, ce qui signifie que la conversion a échoué.
Après avoir vu toutes les grandes choses que l'allocation dynamique peut faire, vous vous demandez probablement pourquoi personne ne l'utiliserait PAS 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. Donc, voici quelques inconvénients sans ordre particulier:
C'est sujet aux erreurs. L'allocation de mémoire manuelle est dangereuse et vous êtes sujet aux fuites. Si vous ne maîtrisez pas bien le débogueur ou valgrind
(outil de fuite de mémoire), vous risquez de vous arracher les cheveux de la tête. Heureusement, les idiomes de RAII et les pointeurs intelligents atténuent quelque peu cet inconvénient, mais vous devez être familiarisé avec des pratiques telles que The Rule of Three et The Rule of Five. Il faut beaucoup d'informations, et les débutants qui ne le savent pas ou qui s'en moquent vont tomber dans ce piège.
Ce n'est pas nécessaire. Contrairement à Java et à C # où il est idiomatique d'utiliser le mot clé new
partout, en C++, vous ne devriez l'utiliser que si vous en avez besoin. L’expression courante est que 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 # début en utilisant des pointeurs sans le comprendre! C’est littéralement partir 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, optimisation de la valeur de retour (nommée)
Une optimisation de nombreux compilateurs sont des choses appelées élision et optimisation de la valeur de retour. Ces choses peuvent éviter des 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 transfert de propriété plutôt que de copier les gros objets bouge toi autour d'eux. Cela a conduit à la création de déplacer la sémantique et pointeurs intelligents.
Si vous utilisez des pointeurs, le (N) RVO fait NE PAS se produire. Il est plus bénéfique et moins sujet aux erreurs de tirer parti de (N) RVO plutôt que de renvoyer ou de passer des indicateurs si l'optimisation vous préoccupe. Des fuites d'erreur peuvent se produire si l'appelant d'une fonction est responsable de delete
ing d'un objet alloué dynamiquement, etc. Il peut être difficile de suivre la propriété d'un objet si des pointeurs sont échangés comme une patate chaude. Il suffit d’utiliser des variables de pile car c’est plus simple et meilleur.
C++ vous donne trois façons de passer un objet: par pointeur, par référence et par valeur. Java vous limite avec le dernier (la seule exception concerne les types primitifs tels que int, boolean, etc.). Si vous souhaitez utiliser le C++ et pas seulement comme un jouet étrange, vous feriez mieux de connaître la différence entre ces trois méthodes.
Java prétend qu'il n'existe pas de problème tel que "qui et quand devrait détruire cela?". La réponse est: Le ramasse-miettes, grand et affreux. Néanmoins, il ne peut pas fournir une protection à 100% contre les fuites de mémoire (oui, Java peut libérer de la mémoire ). En fait, GC vous donne un faux sentiment de sécurité. Plus votre VUS est gros, plus vous vous dirigez vers l'évacuateur.
C++ vous laisse face à face avec la gestion du cycle de vie des objets. Eh bien, il existe des moyens de gérer cela ( pointeurs intelligents famille, QObject dans Qt et ainsi de suite), mais aucun d’entre eux ne peut être utilisé de manière "feu et oublie" comme GC: vous devriez toujours Gardez à l'esprit la gestion de la mémoire. Non seulement devez-vous vous préoccuper de détruire un objet, vous devez également éviter de détruire le même objet plusieurs fois.
Pas encore peur? Ok: références cycliques - manipulez-les vous-même, humain. Et rappelez-vous: tuez chaque objet avec précision une fois, notre environnement d'exécution C++ n'aime pas ceux qui gâchent les cadavres, laissons les morts morts.
Donc, revenons à votre question.
Lorsque vous transmettez votre objet par valeur, et non par pointeur ou par référence, vous copiez l'objet (l'objet entier, qu'il s'agisse de quelques octets ou d'un énorme dump de base de données - vous êtes assez malin pour vous en occuper, aren ' t-vous?) chaque fois que vous faites '='. Et pour accéder aux membres de l'objet, vous utilisez '.' (point).
Lorsque vous transmettez votre objet par pointeur, vous ne copiez que quelques octets (4 sur les systèmes 32 bits, 8 sur les systèmes 64 bits), à savoir - l'adresse de cet objet. Et pour montrer cela à tout le monde, vous utilisez cet opérateur '->' de 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 souffler une fois de plus votre esprit: lorsque vous déclarez plusieurs variables séparées par des virgules, alors (surveillez les mains):
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
Mais je ne peux pas comprendre pourquoi devrions-nous l'utiliser comme ça?
Je vais comparer son fonctionnement dans le corps de la fonction si vous utilisez:
Object myObject;
Dans la fonction, votre myObject
sera détruite une fois que cette fonction est 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 dans le corps de la fonction:
Object *myObject = new Object;
alors l'instance de classe Object désignée par myObject
ne 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'objet fonctionne sous Java. Cette ligne: Object *myObject = new Object;
est équivalente à Java: Object myObject = new Object();
. La différence est que sous Java, myObject sera récupéré, alors 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 méthodes sûres d’allocation dynamique: new Object
, en 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, tels que map-s ou vector-s, ils géreront automatiquement la durée de vie de vos objets.
En C++, les objets alloués sur la pile (à l'aide de l'instruction Object object;
dans un bloc) ne vivront que dans la portée dans laquelle ils sont déclarés. Lorsque le bloc de code est exécuté, les objets déclarés sont détruits. 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'aimerai utiliser l'objet non seulement dans le bloc de code qui l'a déclaré/attribué.
Techniquement, il s’agit d’un problème d’allocation de mémoire, mais voici deux autres aspects pratiques de ceci . Cela concerne deux choses: 1) La portée, lorsque vous définissez un objet sans pointeur, vous ne serez plus en mesure de le faire. pour y accéder après le bloc de code dans lequel il est défini, alors que si vous définissez un pointeur avec "nouveau", vous pourrez y accéder depuis n'importe où si vous avez un pointeur sur 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 afin d’être plus efficace. Lorsque vous transmettez un objet, l'objet est copié. S'il s'agit d'un objet utilisant beaucoup de mémoire, cela risque de consommer beaucoup de ressources de la CPU (par exemple, vous copiez un vecteur plein de données). Lorsque vous passez un pointeur, tout ce que vous passez est un int (dépendant de l'implémentation, mais la plupart d'entre eux sont un int).
En dehors de 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 standard "sur la pile".
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 un pointeur à la place d'un objet, car le C++ a references , 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 type Object
, alloue de la mémoire à un objet et appelle le constructeur par défaut, ça sonne bien, non? Mais en réalité, ce n’est pas si bon, si vous allouez dynamiquement de la mémoire (mot-clé utilisé new
), vous devez également libérer de la mémoire manuellement, c’est-à-dire que, dans le code, vous devriez avoir:
delete myObject;
Cela appelle destructeur et libère de la mémoire, cela semble facile; toutefois, dans les grands projets, il peut être difficile de détecter si un thread libère de la mémoire ou non, mais vous pouvez pour cela essayer des pointeurs partagés , ces performances diminuent légèrement, mais beaucoup plus facile de travailler avec eux.
Et maintenant, une introduction est terminée et revenons à la question.
Vous pouvez utiliser des pointeurs au lieu d'objets pour obtenir de meilleures performances lors du transfert de données entre fonctions.
Regardez, vous avez std::string
(c'est aussi un objet) et il contient vraiment beaucoup de données, par exemple un gros XML, vous devez maintenant l'analyser, mais pour cela vous avez la fonction void foo(...)
qui peut être déclarée de différentes manières:
void foo(std::string xml);
Dans ce cas, vous allez copier toutes les données de votre variable dans la pile de fonctions, cela prend un certain temps et vos performances seront donc faibles.void foo(std::string* xml);
Dans ce cas, vous passerez un pointeur à un objet, à la même vitesse que la variable size_t
, mais cette déclaration est sujette aux erreurs, car vous pouvez passer un pointeur NULL
ou un pointeur invalide. Les pointeurs habituellement utilisés dans C
car il n'y a pas de références.void foo(std::string& xml);
Ici, vous passez une référence, c'est en gros le même que le pointeur de passage, mais le compilateur fait des choses et vous ne pouvez pas passer de référence non valide (en fait, il est possible de créer une situation avec une référence non valide, mais c'est un tricheur de compilateur).void foo(const std::string* xml);
Here est identique à second, seule la valeur du pointeur ne peut pas être modifiée.void foo(const std::string& xml);
Here est identique au troisième, mais la valeur de l'objet ne peut pas être modifiée.Quoi de plus que je veux mentionner, vous pouvez utiliser ces 5 façons de transmettre des données quelle que soit la méthode d’allocation choisie (avec new
ou normal).
Une autre chose à mentionner, lorsque vous créez un objet de la manière régulière, vous allouez de la mémoire dans la pile, mais pendant que vous le créez avec new
, vous allouez le tas. Il est beaucoup plus rapide d'allouer une pile, mais c'est un peu petit pour de très grands tableaux de données, donc si vous avez besoin d'un gros objet, vous devriez utiliser tas, car vous pourriez avoir un débordement de pile, mais ce problème est généralement résolu avec les conteneurs STL et n'oubliez pas que std::string
est aussi un conteneur, certains gars l'ont oublié :)
Il y a de nombreux avantages à utiliser des pointeurs pour objecter -
Supposons que vous avez class A
qui contient class B
Lorsque vous voulez appeler une fonction de class B
en dehors de class A
, vous obtiendrez simplement un pointeur sur cette classe et vous pourrez faire ce que vous voulez. Cela changera également le contexte de class B
dans votre class A
Mais attention aux objets dynamiques
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), vous ne réalisez donc pas que vous utilisez des pointeurs. En C++, vous pouvez mélanger les deux, en fonction de vos besoins en mémoire. L'utilisation des performances et de la mémoire est plus déterministe en C++ (duh).
Object *myObject = new Object;
Cela créera une référence à un objet (sur le tas) qui doit être supprimé explicitement pour éviter fuite de mémoire .
Object myObject;
Cela créera un objet (myObject) de type automatic (sur la pile) qui sera automatiquement supprimé lorsque l'objet (myObject) sortira de sa portée.
Un pointeur fait directement référence à l'emplacement mémoire d'un objet. Java n'a rien comme ça. Java a des références qui référencent l'emplacement de l'objet à travers des tables de hachage. Vous ne pouvez rien faire comme l'arithmétique de pointeur 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.
Une des raisons d'utiliser des pointeurs est l'interface avec les fonctions C. Une autre raison est d'économiser de la mémoire; Par exemple, au lieu de passer un objet contenant beaucoup de données et possédant un constructeur de copie gourmand en ressources processeur, passez simplement un pointeur sur l'objet, ce qui économise de la mémoire et de la vitesse, surtout si vous êtes dans une boucle. référence serait mieux dans ce cas, sauf si vous utilisez un tableau de style C.
Je vais inclure un cas d'utilisation important de pointeur. Lorsque vous stockez un objet dans la classe de base, 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.
Dans les zones où l'utilisation de la mémoire est primordiale, les pointeurs sont pratiques. Par exemple, considérons un algorithme minimax, dans lequel des milliers de nœuds seront générés à l'aide d'une routine récursive, et les utilisera ultérieurement pour évaluer le meilleur coup suivant dans le jeu. La possibilité 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 à occuper de l'espace jusqu'à ce que son appel récursif renvoie une valeur.
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.