La donnée pointée.
Le mot-clé devrait plutôt être delete_la_chose_sur_laquelle_pointe. On trouve le même abus de langage en C dans le cas du free: free(p) signifie en fait free_le_truc_sur_lequel_pointe(p).
[ Haut | Bas | Rechercher ]
Non!
Il est parfaitement légal, moral, et tout à fait sain d'utiliser malloc() et delete ou d'utiliser new et free() dans le même programme. Mais il est illégal, immoral, et totalement abject d'appeler free() sur un pointeur alloué par new, ou d'appeler delete sur un pointeur alloué par malloc().
Attention! Je reçois de temps en temps des e-mails de gens qui me disent que ça ne pose pas de problèmes sur la machine X avec le compilateur Y. Ce n'est pas pour ça que c'est correct! Les gens disent aussi parfois: "Mais c'est juste un tableau de char." Il n'en reste, il ne faut pas mélanger malloc() et delete, ou new et free(), sur le même pointeur! Si vous avez fait une allocation en utilisant p = new char[n], vous devez utiliser delete[] p; vous ne devez surtout pas utiliser free(p). De façon similaire, si vous avez fait une allocation en utilisant p = malloc(n), vous devez utiliser free(p); vous ne devez surtout pas utiliser delete[] p ou delete p! Mélanger ces appels pourrait engendrer une catastrophe à l'exécution si le code doit être porté sur une nouvelle machine, un nouveau compilateur, voire même sur une nouvelle version du même compilateur.
Vous aurez été prévenus.
[ Haut | Bas | Rechercher ]
Pour les constructeurs et destructeurs, pour un typage plus sûr, parce qu'il est redéfinissable.
[ Haut | Bas | Rechercher ]
Non!
Quand realloc() doit copier la zone allouée, il utilise une copie bit-à-bit (bitwise), ce qui mettra en lambeaux de nombreux objets C++. On doit laisser aux objets C++ le soin de se copier eux-mêmes. Ils utilisent pour cela leur constructeur de copie ou leur opérateur d'affectation.
Qui plus est, le tas (heap) utilisé par new pourrait très bien ne pas être le même que le tas utilisé par malloc() et par realloc()!
[ Haut | Bas | Rechercher ]
Non! (Mais si vous avez un vieux compilateur, vous devrez peut-être forcer le compilateur à se comporter ainsi).
Il est vraiment galère d'avoir à tester explicitement le pointeur NULL après chaque allocation par new. Devoir écrire du code comme celui ci-dessous est plutôt pénible:
Fred* p = new Fred();
if (p == NULL)
throw bad_alloc();
Et c'est encore plus pénible si votre compilateur ne supporte pas les exceptions (ou si vous ne voulez pas les utiliser):
Fred* p = new Fred();
if (p == NULL) {
cerr << "Couldn't allocate memory for a Fred" << endl;
abort();
}
Mais heureusement, si à l'exécution le système ne peut pas allouer sizeof(Fred) octets lors d'une instruction p = new Fred(), C++ lancera une exception bad_alloc. Contrairement à malloc(), new ne retourne jamais NULL!
écrivez-donc simplement:
Fred* p = new Fred(); // Pas la peine de vérifier si p vaut NULL
Attention, les vieux compilateurs ne supportent pas forcément ce mécanisme. Il faut que vous vérifiez si oui ou non votre compilateur le supporte, l'information se trouvant probablement dans la doc du compilateur, à la section qui parle de "new". Si vous avez un vieux compilateur, il est possible que vous deviez forcer le compilateur à se comporter ainsi.
[ Haut | Bas | Rechercher ]
ça sera sans doute fait de façon automatique dans une prochaine version du compilateur en question.
Mais si votre vieux compilateur ne fait pas automagiquement le test de comparaison avec NULL, il est possible de forcer le système d'exécution (runtime system) à le faire en spécifiant une fonction de "new handler" (to handle en anglais veut dire dans ce contexte: se charger de, s'occuper de). Cette fonction "new handler" peut se comporter comme vous le souhaitez, elle peut par exemple afficher un message et appeler abort() pour terminer le programme, ou bien faire des delete sur certains objets et rendre la main à l'appelant (dans ce cas, operator new essaiera à nouveau d'allouer de la mémoire), ou encore lancer une exception grâce à throw, etc.
L'exemple suivant de "new handler" affiche un message et appelle abort(). Le "new handler" est spécifié en utilisant set_new_handler():
#include <new.h>
// Pour avoir accès à set_new_handler
#include <stdlib.h>
// Pour avoir accès à abort()
#include <iostream.h>
// Pour avoir accès à cerr
void myNewHandler()
{
// C'est votre propre "new handler". Donnez-lui le comportement
que vous voulez.
cerr << "Attempt to allocate memory failed!" << endl;
abort();
}
int main()
{
set_new_handler(myNewHandler);
// Permet de spécifier votre propre "new handler"
// ...
}
Après l'exécution de l'appel à set_new_handler(), l'operator new appelera votre fonction myNewHandler() s'il ne peut plus allouer de mémoire. Cela signifie que new ne renverra jamais NULL:
Fred* p = new Fred(); // Pas besoin de vérifier si p vaut NULL
Note: n'utilisez abort() qu'en dernier ressort. Si votre compilateur supporte la gestion des exceptions, vous devriez lancer une exception plutôt que d'appeler abort().
Note: si le constructeur d'un objet global ou statique alloue de la mémoire, new n'utilisera pas la fonction myNewHandler() dans la mesure où ce constructeur sera appelé avant l'exécution de main(). Il n'y a pas hélas de moyen simple de garantir que set_new_handler() soit appelée avant la première utilisation de new. Même si, par exemple, vous placez l'appel à set_new_handler() dans le constructeur d'un objet global, vous ne pourrez pas savoir si le module ("l'unité de compilation") qui contient cet objet global verra ses constructeurs exécutés en premier, en dernier, ou quelque part entre les deux. Et vous n'aurez donc pas la garantie que l'appel à set_new_handler() sera exécuté avant n'importe quel constructeur d'un objet global.
[ Haut | Bas | Rechercher ]
Non!
Le langage C++ garantit que delete p n'a aucun n'effet si p vaut NULL. Dans la mesure où il y a un risque que vous vous trompiez en écrivant le test, et dans la mesure où la plupart des méthodologies de test vous obligent à tester effectivement tous les cas où il y a une branche, le mieux est de ne pas faire ce test if redondant.
Incorrect:
if (p != NULL)
delete p;
Correct:
delete p;
[ Haut | Bas | Rechercher ]
L'opération delete p s'effectue en deux étapes: d'abord le destructeur est appelé, ensuite la mémoire est libérée. Le code généré pour un delete p ressemble en gros à ceci (si p est de type Fred*):
// Code original: delete p;
if (p != NULL) {
p->~Fred();
operator delete(p);
}
L'instruction p->~Fred() appelle le destructeur de l'objet Fred sur lequel pointe p.
L'instruction operator delete(p) appelle la primitive de désallocation de mémoire, void operator delete(void* p). Cette primitive est du même genre que free(void* p). (Notez cependant que ces deux primitives ne sont pas interchangeables; entre autres, il n'est même pas garantit qu'elles utilisent le même tas (heap)!).
[ Haut | Bas | Rechercher ]
Non.
Si, dans l'instruction p = new Fred(), une exception survient lors de l'exécution du constructeur de Fred, le langage C++ garantit que les sizeof(Fred) octets de mémoire qui ont été alloués vont être automagiquement rendus au tas (heap).
Plus précisément, l'opération new Fred() s'effectue en deux étapes:
Le code qui sera effectivement généré ressemble en gros à ceci:
// Code
original: Fred* p = new Fred();
Fred* p = (Fred*) operator new(sizeof(Fred));
try {
new(p) Fred();
// Placement new
} catch (...) {
operator delete(p);
// Désallouer la mémoire
throw;
// Relancer l'exception
}
L'instruction commentée comme étant un "Placement new " appelle le constructeur de Fred. Le pointeur p devient le pointeur this à l'intérieur du constructeur Fred::Fred().
[ Haut | Bas | Rechercher ]
Utilisez p = new T[n] et delete[] p:
Fred* p = new Fred[100];
// ...
delete[] p;
Quand vous avez alloué un tableau d'objets en utilisant new (grâce à un [n] dans l'expression-new), vous devez absolument utiliser les [] dans l'instruction delete. Cette syntaxe est nécessaire parce qu'il n'y a pas de différence syntaxique entre un pointeur sur quelque chose et un pointeur sur un tableau de quelque chose (C++ a hérité ça de C).
[ Haut | Bas | Rechercher ]
All life comes to a catastrophic end.
Il incombe au programmeur, et non au compilateur, d'associer correctement les new T[n] à des delete[]. Si vous vous trompez, le compilateur ne génèrera pas de message d'erreur, ni à la compilation, ni à l'exécution. Vous risquez probablement de corrompre le tas. Voire pire. Il y a des chances pour que votre programme plante.
[ Haut | Bas | Rechercher ]
Non!
Certains programmeurs croient que les [] dans un delete[] p ont pour seul but de permettre au compilateur d'appeler les destructeurs pour tous les éléments du tableau. Ce raisonnement les conduit à penser qu'un tableau d'un type de base (comme char ou int) peut être détruit sans que l'on doive utiliser les []. Ainsi, ils partent du principe que le code qui suit est valide:
void userCode(int n)
{
char* p = new char[n];
// ...
delete p;
// < ERREUR! Devrait être delete[] p !
}
Ce code n'est pas valide, et a des chances de provoquer une catastrophe à l'exécution. Il faut savoir que l'instruction delete p appelle la fonction operator delete(void*), alors que l'instruction delete[] p appelle elle la fonction operator delete[](void*). S'il est vrai que, par défaut, operator delete[](void*) appelle operator delete(void*), un utilisateur a tout à fait le droit de remplacer operator delete[](void*) par une autre fonction qui aurait un comportement différent (dans un tel cas, l'utilisateur remplacera probablement aussi le code de new de la fonction operator new[](size_t)). Au final, si le code de delete[] a été remplacé et n'est pas compatible avec le code de delete, et si vous avez appelé la mauvaise fonction (par exemple, si vous avez utilisé delete p au lieu de delete[] p), il y a grand risque de catastrophe à l'exécution.
[ Haut | Bas | Rechercher ]
Réponse courte: c'est magique.
Réponse longue: le run-time C++ stocke le nombre d'objets, n, à un endroit où il peut le retrouver grâce uniquement au pointeur p. Il y a deux techniques classiques qui permettent de faire ça. Ces techniques, qui sont toutes deux utilisées dans des compilateurs payants, présentent chacune des inconvénients, et ni l'une ni l'autre n'est parfaite. Elles consistent à:
[ Haut | Bas | Rechercher ]
En étant prudent, il n'y a pas de problème à ce qu'un objet se suicide (delete this).
Par "prudent", j'entends que:
Les avertissements habituels s'appliquent au cas où votre pointeur this est un pointeur sur une classe de base et que vous n'avez pas de destructeur virtuel .
[ Haut | Bas | Rechercher ]
De nombreuses techniques peuvent être utilisées, tout dépend du degré de flexibilité que vous souhaitez au niveau du dimensionnement du tableau. à un extrême, si vous connaissez toutes les dimensions du tableau à la compilation, vous pouvez simplement allouer statiquement un tableau à plusieurs dimensions (comme en C):
class Fred {
/*...*/ };
void someFunction(Fred& fred);
void manipulateArray()
{
const unsigned nrows = 10;
// Le nombre de lignes est une constante de compilation
const unsigned ncols = 20;
// Le nombre de colonnes est une constante de compilation
Fred matrix[nrows][ncols];
for (unsigned i = 0; i < nrows; ++i) {
for (unsigned j = 0; j < ncols; ++j) {
// Voilà comment on accède à l'élément
(i,j):
someFunction( matrix[i][j] );
// Pas besoin de delete explicite, on peut faire
simplement "return":
if (today == "Tuesday" && moon.isFull())
return;
// Traitement écourté les mardi de pleine lune
(full moon = pleine lune)
}
}
// Pas besoin non plus de faire un delete explicite
à la fin de la fonction
}
Plus fréquemment, la taille de la matrice n'est connue qu'à l'exécution mais on sait qu'il s'agit d'une matrice rectangulaire. Dans ce cas, il est nécessaire de se servir du tas ("heap", "freestore") tout en sachant que l'on peut allouer un seul bloc mémoire pour tous les éléments.
void manipulateArray(unsigned nrows, unsigned ncols)
{
Fred* matrix = new Fred[nrows * ncols];
// Puisqu'on a utilisé ci-dessus un pointeur "brut",
il faut faire
// TRèS attention à ne pas oublier de
faire un delete.
// C'est pourquoi on attrape toutes les exceptions:
try {
// Voilà comment on accède à l'élément
(i,j):
for (unsigned i = 0; i < nrows; ++i) {
for (unsigned j = 0; j < ncols; ++j) {
someFunction( matrix[i*ncols + j] );
}
}
// Si vous voulez écourter le traitement les mardi de
pleine lune, assurez-vous
// qu'il y a bien un delete avant TOUS les points
de sortie de la fonction:
if (today == "Tuesday" && moon.isFull()) {
delete[] matrix;
return;
}
// ...
}
catch (...) {
// N'oubliez
pas le delete dans le cas où une exception est lancée:
delete[] matrix;
throw;
// Relancer l'exception courante
}
// N'oubliez pas non plus le delete à la
fin de la fonction:
delete[] matrix;
}
à l'autre extrême, il est des cas où vous n'avez pas la garantie que la matrice soit rectangulaire. Par exemple, si par chaque ligne peut avoir une longueur différente alors les lignes devront être allouées séparément. Dans la fonction ci-dessous, ncols[i] est le nombre de colonnes dans la ligne i, avec i variant de 0 à nrows-1 inclus.
void manipulateArray(unsigned nrows, unsigned ncols[])
{
Fred** matrix = new Fred*[nrows];
for (unsigned i = 0; i < nrows; ++i)
matrix[i] = new Fred[ ncols[i] ];
// Puisqu'on a utilisé ci-dessus un pointeur "brut",
il faut faire
// TRèS attention à ne pas oublier de faire un
delete.
// C'est pourquoi on attrape toutes les exceptions:
try {
// Voilà comment on accède à l'élément
(i,j):
for (unsigned i = 0; i < nrows; ++i) {
for (unsigned j = 0; j < ncols[i]; ++j) {
someFunction( matrix[i][j] );
}
}
// Si vous voulez écourter le traitement les mardi de
pleine lune, assurez-vous
// qu'il y a bien un delete avant TOUS les points
de sortie de la fonction:
if (today == "Tuesday" && moon.isFull()) {
for (unsigned i = nrows; i > 0; --i)
delete[] matrix[i-1];
delete[] matrix;
return;
}
// ...
}
catch (...) {
// N'oubliez
pas le delete dans le cas où une exception est lancée:
for (unsigned i = nrows; i > 0; --i)
delete[] matrix[i-1];
delete[] matrix;
throw;
// Relancer l'exception courante
}
// N'oubliez pas non plus le delete à la
fin de la fonction.
// Notez que la destruction se fait dans l'ordre inverse de l'allocation:
for (i = nrows; i > 0; --i)
delete[] matrix[i-1];
delete[] matrix;
}
Le fait de faire matrix[i-1] dans la boucle de destruction peut sembler bizarre. ça permet en fait d'éviter que i (qui est un unsigned) prenne une valeur très grande quand il vaut 0 et qu'il est décrémenté par la boucle for.
Pour finir, gardez à l'esprit que les pointeurs et les tableaux vous veulent du mal. Il vous sera en général très bénéfique d'encapsuler vos pointeurs dans une classe possédant une interface sûre et simple. La FAQ suivante donne un exemple de cette technique.
[ Haut | Bas | Rechercher ]
Si.
C'est parce qu'il utilise des pointeurs que le code de la FAQ précédente est si retors et si source d'erreurs, et nous savons que les pointeurs et les tableaux nous veulent du mal. La solution consiste à encapsuler les pointeurs dans une classe possédant une interface sûre et simple. On peut par exemple définir une classe Matrix représentant une matrice rectangulaire, ce qui va simplifier grandement le code que l'utilisateur aura a écrire si on le compare aucode de la FAQ précédente pour une matrice rectangulaire:
// Le code de la classe Matrix se trouve plus bas...
void someFunction(Fred& fred);
void manipulateArray(unsigned nrows, unsigned ncols)
{
Matrix matrix(nrows, ncols);
// Construit une Matrix nommée matrix
for (unsigned i = 0; i < nrows; ++i) {
for (unsigned j = 0; j < ncols; ++j) {
// Voilà comment on accède à l'élément
(i,j):
someFunction( matrix(i,j) );
// Pas besoin de delete explicite, on peut faire
simplement "return":
if (today == "Tuesday" && moon.isFull())
return;
// Traitement écourté les mardi de pleine lune
(full moon = pleine lune)
}
}
// Pas besoin non plus de faire un delete explicite
à la fin de la fonction
}
Ce qu'il est important de noter, c'est qu'il n'est pas nécessaire d'écrire du code pour récupérer la mémoire. Il n'y a pas par exemple d'instruction delete dans le code ci-dessus, et pourtant il n'y aura pas de fuite mémoire (à la seule condition que le destructeur de la classe Matrix fasse correctement son travail).
Voici comment Matrix rend une telle chose possible:
class Matrix {
public:
Matrix(unsigned nrows, unsigned ncols);
// La
méthode ci-dessus lance un objet BadSize si l'une
des deux tailles vaut zéro
class BadSize { };
// Application de la Loi des Trois Soeurs:
~Matrix();
Matrix(const Matrix& m);
Matrix& operator= (const Matrix& m);
// Méthodes d'accès à l'élément
(i,j):
Fred& operator() (unsigned i, unsigned j);
const Fred& operator() (unsigned i, unsigned j) const;
// Les
deux méthodes ci-dessus lancent un objet BoundsViolation si
i est trop grand ou si j est trop grand
class BoundsViolation { };
private:
Fred* data_;
unsigned nrows_, ncols_;
};
inline Fred& Matrix::operator() (unsigned row, unsigned col)
{
if (row >= nrows_ || col >= ncols_) throw BoundsViolation();
return data_[row*ncols_ + col];
}
inline const Fred& Matrix::operator() (unsigned row, unsigned col) const
{
if (row >= nrows_ || col >= ncols_) throw BoundsViolation();
return data_[row*ncols_ + col];
}
Matrix::Matrix(unsigned nrows, unsigned ncols)
: data_ (new Fred[nrows * ncols]),
nrows_ (nrows),
ncols_ (ncols)
{
if (nrows == 0 || ncols == 0)
throw BadSize();
}
Matrix::~Matrix()
{
delete[] data_;
}
Notez que cette classe Matrix accomplit deux choses: elle transfère la responsabilité du code retors de gestion mémoire depuis l'utilisateur (p.ex., main()) vers l'intérieur de la classe, et elle rend le programme globalement plus léger. Si on suppose que Matrix est un tant soit peu réutilisable, rendre la tâche plus facile aux utilisateurs de Matrix en déplaçant la complexité vers l'implémentation de la classe, revient à rendre la tâche plus facile au plus grand nombre au détriment de quelques-uns. Et si vous avez vu Star Trek 3, vous savez que le bien du plus grand nombre l'emporte sur le bien de quelques-uns, et à fortiori sur le bien d'un seul.
[ Haut | Bas | Rechercher ]
Si, il suffit d'utiliser les templates :
template<class T>
// Voir la section sur
les templates pour plus d'explications
class Matrix {
public:
Matrix(unsigned nrows, unsigned ncols);
// La
méthode ci-dessus lance un objet BadSize si l'une
des deux tailles vaut zéro
class BadSize { };
// Application de la Loi des trois Soeurs:
~Matrix();
Matrix(const Matrix<T>& m);
Matrix<T>& operator= (const Matrix<T>& m);
// Méthodes d'accès à l'élément
(i,j):
T& operator() (unsigned i, unsigned j);
const T& operator() (unsigned i, unsigned j) const;
// Les
deux méthodes ci-dessus lancent un objet BoundsViolation si
i est trop grand ou si j est trop grand
class BoundsViolation { };
private:
T* data_;
unsigned nrows_, ncols_;
};
template<class T>
inline T& Matrix<T>::operator() (unsigned row, unsigned col)
{
if (row >= nrows_ || col >= ncols_) throw BoundsViolation();
return data_[row*ncols_ + col];
}
template<class T>
inline const T& Matrix<T>::operator() (unsigned row, unsigned col) const
{
if (row >= nrows_ || col >= ncols_) throw BoundsViolation();
return data_[row*ncols_ + col];
}
template<class T>
inline Matrix<T>::Matrix(unsigned nrows, unsigned ncols)
: data_ (new T[nrows * ncols]),
nrows_ (nrows),
ncols_ (ncols)
{
if (nrows == 0 || ncols == 0)
throw BadSize();
}
template<class T>
inline Matrix<T>::~Matrix()
{
delete[] data_;
}
Et voici un exemple d'utilisation de cette classe template :
#include "Fred.hpp"
// Pour la définition de la classe Fred
void doSomethingWith(Fred& fred);
void sample(unsigned nrows, unsigned ncols)
{
Matrix<Fred> matrix(nrows, ncols);
// Construit une Matrix<Fred> nommée
matrix
for (unsigned i = 0; i < nrows; ++i) {
for (unsigned j = 0; j < ncols; ++j) {
doSomethingWith( matrix(i,j) );
}
}
}
[ Haut | Bas | Rechercher ]
Use the standard vector template, and make a vector of vector.
The following uses a
#include <vector> template<class T> // See section on templates for more class Matrix { public: Matrix(unsigned nrows, unsigned ncols); // Throws a BadSize object if either size is zero class BadSize { }; // No need for any of The Big Three! // Access methods to get the (i,j) element: T& operator() (unsigned i, unsigned j); const T& operator() (unsigned i, unsigned j) const; // These throw a BoundsViolation object if i or j is too big class BoundsViolation { }; private: std::vector<vector<T> > data_; }; template<class T> inline T& Matrix<T>::operator() (unsigned row, unsigned col) { if (row >= nrows_ || col >= ncols_) throw BoundsViolation(); return data_[row][col]; } template<class T> inline const T& Matrix<T>::operator() (unsigned row, unsigned col) const { if (row >= nrows_ || col >= ncols_) throw BoundsViolation(); return data_[row][col]; } template<class T> Matrix<T>::Matrix(unsigned nrows, unsigned ncols) : data_ (nrows) { if (nrows == 0 || ncols == 0) throw BadSize(); for (unsigned i = 0; i < nrows; ++i) data_[i].resize(ncols); }
[ Haut | Bas | Rechercher ]
Oui, au sens où la STL contient une classe template vector qui permet cela.
Non, au sens où on doit spécifier à la compilation la taille les tableaux C++ natifs.
Oui, au sens où, même pour les tableaux C++ natifs, la taille de la première dimension peut être spécifiée à l'exécution. Si seule la première dimension doit varier, il suffit de demander à new de construire un tableau de tableaux plutôt qu'un tableau de pointeurs sur des tableaux:
const unsigned ncols = 100;
// ncols = nombre de colonnes dans le tableau
class Fred {
/*...*/ };
void manipulateArray(unsigned nrows)
// nrows = nombre de lignes dans le tableau
{
Fred (*matrix)[ncols] = new Fred[nrows][ncols];
// ...
delete[] matrix;
}
Il n'est pas possible de faire ça si une autre dimension que la première doit varier à l'exécution.
Mais SVP, n'utilisez les tableaux que si vous ne pouvez pas faire autrement. Les tableaux vous veulent du mal. Si possible, utilisez plutôt des objets.
[ Haut | Bas | Rechercher ]
Utilisez l'idiome du Constructeur Nommé.
Comme il se doit avec l'idiome du Constructeur Nommé, tous les constructeurs sont private: ou protected:, et on définit une ou plusieurs méthodes public static create() (les "constructeurs nommés"), une par constructeur. Les méthodes create() allouent les objets grâce à new. Et puisque les constructeurs ne sont pas public, il n'y a pas d'autre moyen de créer des objets de cette classe.
class Fred {
public:
// Les méthodes create() sont les "constructeurs
nommés":
static Fred* create() { return new Fred(); }
static Fred* create(int i) { return new Fred(i); }
static Fred* create(const Fred& fred) { return new Fred(fred); }
// ...
private:
// Les
constructeurs sont eux private ou protected:
Fred();
Fred(int i);
Fred(const Fred& fred);
// ...
};
Les méthodes Fred::create() sont alors la seule façon de créer des objets Fred:
int main()
{
Fred* p = Fred::create(5);
// ...
delete p;
}
Si Fred doit être dérivée, assurez-vous bien que les constructeurs sont dans la section protected:.
Notez aussi que, si une classe Wilma doit avoir un objet Fred pour donnée membre, il est possible de rendre Wilma amie de Fred.. Mais on dévie alors du but initial, qui était d'obliger tous les objets Fred à être alloués par new.
[ Haut | Bas | Rechercher ]
Si tout ce dont vous avez besoin est de pouvoir avoir plusieurs pointeurs sur le même objet tout en étant assuré que cet objet soit détruit automagiquement quand le dernier pointeur disparaît, alors vous pouvez utilisez une technique similaire à la classe de "pointeur intelligent" ("smart pointer") suivante:
// Fred.h
class FredPtr;
class Fred {
public:
Fred() : count_(0)
/*...*/ { } // Tous
les constructeurs initialisent count_ à 0 !
// ...
private:
friend FredPtr;
// Une classe amie
unsigned count_;
//
count_ doit être initialisé à 0 par
tous les constructeurs
// count_ représente le nombre d'objets
FredPtr qui pointent sur this
};
class FredPtr {
public:
Fred* operator-> () { return p_; }
Fred& operator* () { return *p_; }
FredPtr(Fred* p) : p_(p) { ++p_->count_; }
// p ne doit pas valoir NULL
~FredPtr() { if (--p_->count_ == 0) delete p_; }
FredPtr(const FredPtr& p) : p_(p.p_) { ++p_->count_; }
FredPtr& operator= (const FredPtr& p)
{
// NE CHANGEZ PAS L'ORDRE DE CES INSTRUCTIONS!
// (Cet ordre gère correctement le cas de l'auto-affectation)
++p.p_->count_;
if (--p_->count_ == 0) delete p_;
p_ = p.p_;
return *this;
}
private:
Fred* p_;
// p_ ne vaut jamais NULL
};
Vous pouvez évidemment utiliser une classe imbriquée, et transformer FredPtr en Fred::Ptr.
Notez qu'il est possible d'être moins restrictif sur le "p_ ne vaut jamais NULL" en faisant quelques tests supplémentaires dans le constructeur, dans le constructeur de copie, dans l'opérateur d'affectation et dans le destructeur. Si vous prenez cette voie, vous auriez aussi intérêt à faire le test p_ != NULL dans les opérateurs "*" et "->" (au pire faites un assert()). Par ailleurs, je serais tenté de vous déconseiller de fournir une méthode operator Fred*(), car une telle méthode pourrait donner à l'utilisateur accidentellement accès au Fred*.
L'une des contraintes implicites est qu'un objet FredPtr doit pointer uniquement sur des objets Fred qui ont été alloués par new. Si vous voulez le garantir, vous pouvez rendre private tous les constructeurs de Fred, et fournir pour chaque constructeur une méthode public (static) create() qui alloue l'objet Fred par un new et qui renvoie un FredPtr (pas un Fred*). Ainsi, la seule façon de créer un objet Fred est de demander un FredPtr ("Fred* p = new Fred()" devra être remplacé par "FredPtr p = Fred::create()"). Et personne ne peut corrompre accidentellement le mécanisme de comptage de références.
Si, par exemple, Fred avait les constructeurs Fred::Fred() et Fred::Fred(int i, int j), on changerait la classe Fred comme suit:
class Fred {
public:
static FredPtr create() { return new Fred(); }
static FredPtr create(int i, int j) { return new Fred(i,j); }
// ...
private:
Fred();
Fred(int i, int j);
// ...
};
Au final, vous disposez maintenant d'un moyen de faire du comptage de référence simple, qui vous permet de donner une "sémantique de pointeur" a un objet donné. Les utilisateurs de la classe Fred se servent d'objets FredPtr, lesquels se comportent plus ou moins comme des pointeurs Fred*. L'avantage est que les utilisateurs peuvent faire autant de copies qu'ils le souhaitent de leurs "pointeurs intelligents" FredPtr, tout en étant assurés que les objets Fred pointés vont être automagiquement détruits quand le dernier objet FredPtr object disparaîtra.
Si vous préférez fournir à vos utilisateurs une "sémantique de référence" plutôt qu'une "sémantique de pointeur," utilisez le comptage de références avec "copie sur écriture" ("copy on write").
[ Haut | Bas | Rechercher ]
La FAQ précédente a présenté un mécanisme de comptage de références simple qui offre une sémantique de pointeur. Cette FAQ présente une approche qui offre aux utilisateurs une sémantique de référence.
L'idée de base est de laisser croire aux utilisateurs qu'ils font des copies d'objets Fred, alors qu'en réalité l'implémentation ne fait la copie que quand un utilisateur essaie de modifier l'objet Fred sous-jacent.
La classe Fred::Data stocke toutes les données qui seraient normalement placées dans la classe Fred. Fred::Data possède une donnée membre supplémentaire, count_, qui gère le compteur de références. La classe Fred elle-même devient finalement une "référence intelligente" qui pointe (en interne) sur un objet Fred::Data.
class Fred {
public:
Fred();
// Un constructeur
par défaut
Fred(int i, int j);
// Un constructeur normal
Fred(const Fred& f);
Fred& operator= (const Fred& f);
~Fred();
void sampleInspectorMethod() const;
// Ne modifie pas l'objet (this)
void sampleMutatorMethod();
// Modifie l'objet (this)
// ...
private:
class Data {
public:
Data();
Data(int i, int j);
Data(const Data& d);
// étant donné que seule Fred peut
accéder à l'objet Fred::Data, vous
// pouvez si vous le souhaitez rendre public les données
de Fred::Data.
// Si ça ne vous plaît pas, mettez les données
en private
// et faites de Fred une classe amie en utilisant
friend Fred;
// ...
unsigned count_;
// count_ représente le nombre d'objets Fred qui
pointent sur this
// count_ doit être initialisé à
1 par tous les constructeurs
// (on commence à 1 car l'objet Fred qui
a créé ce Fred::data pointe dessus)
};
Data* data_;
};
Fred::Data::Data() : count_(1)
/*initialiser les autres données*/ { }
Fred::Data::Data(int i, int j) : count_(1)
/*initialiser les autres données*/ { }
Fred::Data::Data(const Data& d) : count_(1)
/*initialiser les autres données*/ { }
Fred::Fred() : data_(new Data()) { }
Fred::Fred(int i, int j) : data_(new Data(i, j)) { }
Fred::Fred(const Fred& f)
: data_(f.data_)
{
++ data_->count_;
}
Fred& Fred::operator= (const Fred& f)
{
// NE CHANGEZ PAS L'ORDRE DE CES INSTRUCTIONS!
// (Cet ordre gère
correctement
l'auto-affectation)
++ f.data_->count_;
if (--data_->count_ == 0) delete data_;
data_ = f.data_;
return *this;
}
Fred::~Fred()
{
if (--data_->count_ == 0) delete data_;
}
void Fred::sampleInspectorMethod() const
{
// Cette méthode garantit (elle est "const") de
ne rien changer à *data_
// On utilise "data_->..." pour accéder aux données
}
void Fred::sampleMutatorMethod()
{
// Cette méthode pourrait modifier les données
de *data_
// Il faut donc d'abord vérifier si this est
le seul objet pointant sur *data_
if (data_->count_ > 1) {
Data* d = new Data(*data_);
// Appeler le copie-constructeur de Fred::Data
-- data_->count_;
data_ = d;
}
assert(data_->count_ == 1);
// à partir de là, la méthode accède
normalement à "data_->..."
}
Si le constructeur par défaut de Fred est appelé assez fréquemment, il est possible d'éviter tous ces appels à new en partageant un objet Fred::Data, commun à tous les Freds construits par Fred::Fred(). Afin d'éviter les problèmes d'ordre d'initialisation des static, cet objet Fred::Data partagé est créé dans une fonction, "lors de la première utilisation". Voici les changements qu'il faudrait apporter au code ci-dessus pour utiliser cette deuxième approche (notez que le destructeur de l'objet partagé Fred::Data n'est jamais appelé. Si cela pose un problème et si vous voulez détruire l'objet, priez pour que vous n'ayez pas de problèmes dans l'ordre d'initialisation des static. Si c'est le cas, contentez-vous alors de la première approche):
class Fred {
public:
// ...
private:
// ...
static Data* defaultData();
};
Fred::Fred()
: data_(defaultData())
{
++ data_->count_;
}
Fred::Data* Fred::defaultData()
{
static Data* p = NULL;
if (p == NULL) {
p = new Data();
++ p->count_;
// Pour s'assurer qu'il n'atteigne jamais zéro
}
return p;
}
Note: si votre classe Fred doit être une classe de base, vous pouvez utiliser le comptage de référence pour une hiérarchie de classes.
[ Haut | Bas | Rechercher ]
La FAQ précédente a présenté un mécanisme de comptage qui offre une sémantique de référence, mais ce pour une classe isolée plutôt que pour une hiérarchie de classes. Cette FAQ étend la technique précédente à une hiérarchie de classes. La différence de base est que Fred::Data est maintenant la racine d'une hiérarchie et possède des fonctions virtuelles. Notez que la classe Fred elle-même n'aura pas de fonctions virtuelles.
On utilise l'Idiome du Constructeur Virtuel pour copier les objets Fred::Data. Pour choisir la classe dérivée à créer, le code ci-dessous utilise l'Idiome du Constructeur Nommé, mais d'autres techniques sont possibles (un switch dans le constructeur, etc). Le code suppose qu'il existe deux classes dérivées: Der1 et Der2. Les méthodes des classes dérivées ne savent pas qu'il y a comptage de références.
class Fred {
public:
static Fred create1(String s, int i);
static Fred create2(float x, float y);
Fred(const Fred& f);
Fred& operator= (const Fred& f);
~Fred();
void sampleInspectorMethod() const;
// Ne modifie pas l'objet (this)
void sampleMutatorMethod();
// Modifie l'objet (this)
// ...
private:
class Data {
public:
Data() : count_(1) { }
Data(const Data& d) : count_(1) { }
// Ne PAS copier le membre 'count_'!
Data& operator= (const Data&) { return *this; }
// Ne PAS copier le membre 'count_'!
virtual ~Data() { assert(count_ == 0); }
// Un
destructeur virtuel
virtual Data* clone() const = 0;
// Un
constructeur virtuel
virtual void sampleInspectorMethod() const = 0;
// Une fonction virtuelle pure
virtual void sampleMutatorMethod() = 0;
private:
unsigned count_;
// Pas besoin de rendre count_ protected
friend Fred;
// Donner à Fred accès à count_
};
class Der1 : public Data {
public:
Der1(String s, int i);
virtual void sampleInspectorMethod() const;
virtual void sampleMutatorMethod();
virtual Data* clone() const;
// ...
};
class Der2 : public Data {
public:
Der2(float x, float y);
virtual void sampleInspectorMethod() const;
virtual void sampleMutatorMethod();
virtual Data* clone() const;
// ...
};
Fred(Data* data);
// Crée
une "référence intelligente" Fred qui possède
*data
// Ce constructeur est privé afin de forcer l'utilisation
d'une méthode createXXX()
// Contrainte: data ne doit pas valoir NULL
Data* data_;
// Invariant: data_ ne vaut jamais NULL
};
Fred::Fred(Data* data) : data_(data) { assert(data != NULL); }
Fred Fred::create1(String s, int i) { return Fred(new Der1(s, i)); }
Fred Fred::create2(float x, float y) { return Fred(new Der2(x, y)); }
Fred::Data* Fred::Der1::clone() const { return new Der1(*this); }
Fred::Data* Fred::Der2::clone() const { return new Der2(*this); }
Fred::Fred(const Fred& f)
: data_(f.data_)
{
++ data_->count_;
}
Fred& Fred::operator= (const Fred& f)
{
// NE CHANGEZ PAS L'ORDRE DE CES INSTRUCTIONS!
//
(Cet ordre gère correctement l'auto-affectation)
++ f.data_->count_;
if (--data_->count_ == 0) delete data_;
data_ = f.data_;
return *this;
}
Fred::~Fred()
{
if (--data_->count_ == 0) delete data_;
}
void Fred::sampleInspectorMethod() const
{
// Cette méthode garantit (elle est "const") de
ne rien changer à *data_
// On se contente donc simplement de "passer la main" à
*data_:
data_->sampleInspectorMethod();
}
void Fred::sampleMutatorMethod()
{
// Cette méthode pourrait modifier les données
de *data_
// Il faut donc d'abord vérifier si this est
le seul objet pointant sur *data_
if (data_->count_ > 1) {
Data* d = data_->clone();
// L'idiome
du Constructeur Virtuel
-- data_->count_;
data_ = d;
}
assert(data_->count_ == 1);
// On "passe ensuite la main" à *data_:
data_->sampleInspectorMethod();
}
Vous devrez bien sûr implémenter, de la façon qui convient, les constructeurs et les méthodes sampleXXX de Fred::Der1 et de Fred::Der2.
[ Haut | Bas | Rechercher ]
No, and (normally) no.
There are two basic approaches to subverting the reference counting mechanism:
And even if we closed all those holes, C++ has those wonderful pieces of syntax called pointer casts. Using a pointer cast or two, a sufficiently motivated programmer can normally create a hole that's big enough to drive a proverbial truck through. (By the way, pointer casts are evil.)
So the lessons here seems to be: (a) you can't prevent espionage no matter how hard you try, and (b) you can easily prevent mistakes.
So I recommend settling for the "low hanging fruit": use the easy-to-build and easy-to-use mechanisms that prevent mistakes, and don't bother trying to prevent espionage. You won't succeed, and even if you do, it'll (probably) cost you more than it's worth.
So if we can't use the C++ language itself to prevent espionage, are there other ways to do it? Yes. I personally use old fashioned code reviews for that. And since the espionage techniques usually involve some bizarre syntax and/or use of pointer-casts and unions, you can use a tool to point out most of the "hot spots."
[ Haut | Bas | Rechercher ]
Yes.
Compared with the "smart pointer" techniques (see [16.21], the two kinds of garbage collector techniques (see [16.26]) are:
[ Haut | Bas | Rechercher ]
In general, there seem to be two flavors of garbage collectors for C++:
Conservative garbage collectors. These know little or nothing about the layout of the stack or of C++ objects, and simply look for bit patterns that appear to be pointers. In practice they seem to work with both C and C++ code, particularly when the average object size is small. Here are some examples, in alphabetical order:
Hybrid garbage collectors. These usually scan the stack conservatively, but require the programmer to supply layout information for heap objects. This requires more work on the programmer's part, but may result in improved performance. Here are some examples, in alphabetical order:
Since garbage collectors for C++ are normally conservative, they can sometimes leak if a bit pattern "looks like" it might be a pointer to an otherwise unused block. Also they sometimes get confused when pointers to a block actually point outside the block's extent (which is illegal, but some programmers simply must push the envelope; sigh) and (rarely) when a pointer is hidden by a compiler optimization. In practice these problems are not usually serious, however providing the collector with hints about the layout of the objects can sometimes ameliorate these issues.
[ Haut | Bas | Rechercher ]
[ Haut | Bas | Rechercher ]
Ecrire à l'auteur,
au traducteur,
ou en savoir plus sur la traduction.
C++ FAQ Lite fr |
Table des matières |
Index |
A propos de l'auteur |
© |
Téléchargez votre propre copie ]
Dernière révision Sun Apr 13 23:54:17 PDT 2003