Les constructeurs sont une sorte de fonction "init". Ils transforment une pile de bits arbitraire en un objet vivant. Au minimum ils initialisent les champs de l'objet. Ils peuvent également allouer des ressources (mémoire, fichiers, sémaphores, sockets, etc..).
"ctor" est une abréviation typique pour le constructeur.
[ Haut | Bas | Section précédente | Section suivante ]
Supposez que list soit le nom d'une certaine classe. Si la fonction f() déclare un objet local de type list et appelé x:
void f()
{
list x; /
/ objet local nommé x ( de la classe list)
/ / ...
}
La fonction g() en revanche déclare une fonction appelée x() qui retourne un objet de type list:
void g()
{
list x(); / / fonction nommé x (qui retourne
une list)
/ / ...
}
[ Haut | Bas | Section précédente | Section suivante ]
Que les dragons me viennent en aide : si vous appelez un autre constructeur, le compilateur initialise un objet local provisoire; il n'initialise pas l'objet this. Vous pouvez combiner les deux constructeurs en utilisant un paramètre par défaut, ou vous pouvez partager leur code commun dans une fonction membre privée init().
[ Haut | Bas | Section précédente | Section suivante ]
class Fred {
public:
Fred();
/ / constructeur par défaut: peut s'appeler sans
args
/
/ ...
};
Toutefois il est possible (et probable) qu'un constructeur par défaut prenne des arguments, s' ils sont spécifiés par défaut:
class Fred {
public:
Fred(int i=3, int j=5);
/ / constructeur par défaut: peut s'appeler sans
args
/
/ ...
};
[ Haut | Bas | Section précédente | Section suivante ]
Il n'y a aucun moyen de demander au compilateur d'appeler un constructeur différent. Si votre class Fred n'a pas de constructeur par défaut , une tentative de créer un tableau de Fred, se soldera par une erreur de compilation.
classe Fred {
public:
Fred(int i, int j);
/ / ... supposent
qu' il n'y a aucun constructeur
de défaut dedans classe
Fred ...
};
main()
{
Fred a[10 ];
/ / ERREUR: Fred n'a pas un constructeur par défaut
Fred * p = new Fred[10
]; / / ERREUR: Fred n'a pas un constructeur
de défaut
}
Cependant si vous créez un vector<Fred STL plutôt qu'un tableau standard de Fred (ce que vous devriez faire de toute façon puisque les tableaux sont mauvais ), vous n'avez plus besoin d'avoir un constructeur par défaut dans class Fred, puisque vous passez un objet Fred au vector pour initialiser les éléments:
#include <vector
using
namespace std;
main()
{
vector<Fred a(10, Fred(5,7));
/ / 10 Fred objets dans le vecteur a
seront initialisé avec Fred(5,7).
/ / ...
}
[ Haut | Bas | Section précédente | Section suivante ]
Par exemple, ce constructeur initialise l'objet membre x_ en utilisant une liste d'initialisation: Fred::Fred() : x_(quoiquecesoit) { }. D'un point de vue exécution, il est important de noter que l'expression quoiquecesoit ne générera pas nécessairement la création d'un objet séparé et sa copie dans x_: si les types sont identiques le résultat de ... quoiquecesoit... sera construit directement à dans x_.
En revanche le constructeur suivant utilise l'affectation: Fred::Fred() { x _ = quoiquecesoit; }. Dans ce cas-ci l'expression quoiquecesoit contraint la création d'un objet séparé et provisoire, passé ensuite a l'opérateur d'assignation de x_, avant d'être détruit au ;. C'est inefficace.
Il y a encore une autre source d'inefficacité : dans le deuxième cas (l'affectation), le constructeur par défaut de l'objet (implicitement appelé devant le corps du constructeur "{") pourrait, par exemple, assigner une quantité de mémoire par défaut ou ouvrir un fichier par défaut. Tout ce travail pourrait être pour rien si l'expression quoiquecesoit et/ou l'opérateur d'affectation donnait lieu a la fermeture du fichier et/ou la libération de cette mémoire (par exemple, si le constructeur par défaut n'assignait pas un bloc de mémoire assez grand ou s' il ouvrait le mauvais fichier).
Conclusion: toutes choses égales par ailleurs, votre code tournera plus vite si vous utilisez les listes d'initialisation plutôt que l'assignation.
Note: Il n'y a pas de différence de performance si le type de x_ est de base, comme int, ou char *, ou float. Mais même dans ce cas, ma préférence personnelle est d'initialiser ses données dans la liste d'initialization plutô que par affectation par soucis de consitence. Un autre argument lié à a la symetrie: la valeur des membres de données const et non statiques ne peuvent pas être modifiées dans le constructeur, donc pour conserver la symetrie, je recommende d'initialiser tout dans la list d'initialisation.
[ Haut | Bas | Section précédente | Section suivante ]
Certains pensent qu'on ne devrait pas utiliser le pointeur this dans un constructeur parce que l'objet n'est pas completement formé. Pourtant il possible d'utiliser le pointeur this dans le corp du constructeur et même dans la liste d'initializaton si on est prudent.
Voilà quelque chose qui fonctionne toujours : le (corps du) constructeur (ou une fonction appelée depuis le constructeur) peuvent acceder aux membres de donnée déclarés dans une classe de base et/ou aux membres de donnée déclarés dans la classe elle-même en toute fiabilité. C'est parce que tous ces membres de donnée sont garantis avoir été completement construits au moment ou le (corps du) constructeur commence à être executer.
Voilaà quelque chose qui ne fonctionne jamais : le (corps du) constructeur (ou une fonction appelée par lui) ne peut pas descendre dans une classe dérivée en appelant une méthode virtual qui est redéfinie dans une classe dérivée. Si votre but était d'executer le code de la fonction virtuelle, ça ne fonctionnera pas. Notez que vous n'obtiendrez pas la version de la classe dérivée indépendemment de la manière d'appeler la fonction membre virtuelle : en utilisant explicitement this (e.g. this->method(), ou implicitement sans utiliser le pointeur this (e.g.method()), ou même en appelant quelque autre fonction qui appelle la fonction membre virtuelle en question a partir du pointeur this. La clé est que même si l'appeleur est en train de construire un objet d'un type dérivé, pendant la construction de la classe de base, votre objet n'appartient pas encore à cette classe dérivée. Vous êtes prevenus.
Voilà quelque chose qui fonctionne parfois: si vous passez n'importe quel membre de donnée de l'objet à au constructeur d'initialisation d'un autre membre de donnée, vous devez vous assurer que l'autre membre de donnée a déjà été initialisé. La bonne nouvelle est que vous pouvez déterminer si l'autre membre de donn&eacte;e a (ou non) déjà été initialisé en utilisant des règles du langage indépendantes du compilateur que vous utilisez. La mauvaise nouvelle est qu'il vous faut connaître ces règles (e.g. les sous-objets de la classe de base sont initialisés en premier (vérifier l'ordre si vous avez de l'héritage multiple et/ou de l'héritage virtuel!), ensuite viennent les membres de donnée définis dans la classe qui est initialisée dans l'ordre dans lequel ils apparaissent dans la déclaration de la classe). Si vous ne connaissez pas ces règles alors ne passez aucun membre de donnée depuis l'objet this (cela ne dépend pas de l'utilisation explicite de this->) vers l'initialiseur d'un autre membre de donné! Et si vous connaissez ces règles, s'il vous plait faites attention.
[ Haut | Bas | Section précédente | Section suivante ]
Le problème est que les constructeurs ont toujours le même nom que la classe. Par conséquent la seule voie de différencier entre les divers constructeurs d'une classe se fait via la liste de paramètres. Mais s' il y a beaucoup de constructeurs, les différences entre les constructeurs devient quelque peu subtile et sujette a erreur.
Avec l'idiom du constructeur nommé, vous déclarez les constructeurs de toute la classe dans l'une des sections private: ou protected:. Vous fournissez des méthodes declarée static dans la section public: qui renvoient un objet. Ces méthodes statiques sont connus comme "constructors nommé". En général il y a une telle méthode statique pour chaque manière différente de construire l'objet.
Par exemple, supposez que nous construisions une classe Point qui représente une position sur le plan X/Y. Il s'avère qu'il y a deux façon d'indiquer une coordonnée dans un espace bi-dimensionel : coordonnées rectangulaires (X+Y), coordonnées polaires (Distance+Angle). (ne vous inquiétez pas si vous ne pouvez pas vous rappeler ces derniers; les conditions particulières des systèmes de coordonnées représenatant un point n'importent pas; l'important est qu'il y a plusieurs façons de créer un point). Malheureusement les paramètres pour ces deux systèmes de coordonnées ont identiques: deux réels. Ceci créerait une ambiguïté dans les constructeurs surchargés:
class Point {
public:
Point(float x, float y); // Coordonnées rectangulaires
Point(float r, float
a); // Coordinnées polaires (distance et angle)
// ERROR: Surcharge
ambiguë: Point::Point(float,float)
};
main()
{
Point p = Point(5.7, 1.2); // Ambigu: De quel système de
coordonnées parle-t-on?
}
Une manière de résoudre cette ambiguïté est d'utiliser l'idiom du constructeur nommé:
#include <math.h // Pour avoir sin() et cos()
class Point {
public:
static Point rectangular(float x, float y);
// Coords rectangulaires
static Point polar(float radius, float angle); //
Coords polaires
// Ces méthodes static sont les "constructeurs només"
// ...
private:
Point(float x, float y);
// coordonnées rectangulaires
float x_, y_;
};
inline Point::Point(float x, float y)
: x_(x), y_(y) { }
inline Point Point::rectangular(float
x, float y)
{ return Point(x,
y); }
inline Point Point::polar(float
radius, float angle)
{ return
Point(radius*cos(angle), radius*sin(angle)); }
Maintenant les utilisateurs du point ont une syntaxe claire et non ambiguë pour créer des points dans l'un ou l'autre système de coordonnées:
main()
{
Point p1 = Point::rectangular(5.7, 1.2); // Evidemment rectangulaire
Point p2 = Point::polar(5.7,
1.2); // Evidemment polaire
}
Faîtes attention à déclarer vos constructeurs dans la section protected: si vous vous attendez à ce que Fred ait des classes dérivées.
L'idiom du constructeur nommé peut aussi être utilisé pour vous assurer que les objets d'une classe sont toujours créés avec new .
[ Haut | Bas | Section précédente | Section suivante ]
Fred.h:
class Fred
{
public:
Fred();
// ...
private:
int i_;
static int j_;
};
Fred.cpp (ou Fred.C ou autre ):
Fred::Fred()
: i_(10) // OK: vous
pouvez (et vous devriez) initialiser les données membre de cette
façon
j_(42) // Error: vous ne pouvez pas initialiser une donnée
static comme ça.
{
//
...
}
// Vous devez définir les données static de
cette façon:
int Fred::j_
= 42;
[ Haut | Bas | Section précédente | Section suivante ]
// Fred.h
class Fred {
public:
// ...
private:
static int j_; //
Fred::j_ : donnée membre declaré static
// ...
};
Le générateur de lien vous grondera "Fred::j_ is not defined" (Fred::j_ n'est pas défini) à moins que vous ne définissiez (par opposition à déclariez) Fred::j_ dans (exactement) un de vos fichiers source :
// Fred.cpp
#include "Fred.h"
int Fred::j_ = quelque_expression_evaluant_un_int;
// Alternativement, si vous désirez utiliser la valeur par défaut
0 pour les ints static :
// int Fred::j_;
La place habituelle pour définir une donnée membre static de la classe Fred est dans le fichier Fred.cpp (ou Fred.C ou l'extension de fichier que vous utilisez).
[ Haut | Bas | Section précédente | Section suivante ]
Le fiasco de l'ordre d'initialisation des static est un moyen subtile et un aspect habituellement mal compris du C++. Malheureusement il est très difficile à détecter -- les erreurs se manifeste avant que le main() commence.
En bref, supposez que vous avez deux objets static x et y qui sont définis dans deux fichiers sources séparés, disons x.cpp et y.cpp. Supposez maintenant que le constructeur de l'objet y appelle une méthode de l'objet x.
Voilà. C'est aussi simple que ça.
La tragédie est que vous avez 50%-50% de chances de mourir. Si il arrive que l'unité de compilation correspondant à x.cpp soit initialisée avant celle correspondant à y.cpp, tout va bien. Mais si l'unité de compilation correspondant à y.cpp est initialisée d'abord, alors le constructeur de y sera en route avant le constructeur de x, et vous êtes cuît. C'est à dire que le constructeur de y appelera une méthode de l'objet x, alors que l'objet x n'a pas encore été construit.
Si vous pensez que c'est "excitant" de jouer à la roulette russe avec la moitié du barillet chargé, vous pouvez vous arrêter de lire ici. Si au contraire vous aimez augmenter vos chances de survie en prévenant les désastres de manière systématique, vous serez probablement intéressé par la prochaîne FAQ .
Note: Le fiasco de l'ordre d'initialisation des static ne s'applique pas au types de données prédefinis/intrinsèques comme int ou char*. Par exemple si vous créez un objet staticfloat, il n'y a jamais de problèmes avec l'ordre d'initializarion. Les seules fois où l'ordre d'initialisation statique est vraiment un fiasco est lorsque vos objets globaux ou static ont un constructeur.
[ Haut | Bas | Section précédente | Section suivante ]
Par exemple, supposez que vous ayez deux classes, Fred et Barney. Il y a un objet Fred global appelé x, et un objet Barney global appelé y. Le constructeur de Barney invoque la méthode goBowling() (va jouer au bowling) de l'objet x. Le fichier x.cpp définie l'objet x :
// Fichier x.cpp
#include
"Fred.hpp"
Fred x;
Le fichier y.cpp définie l'objet y:
// Fichier y.cpp
#include
"Barney.hpp"
Barney y;
Pour être complet, le constructeur de Barney pourraît ressembler à quelque chose comme :
// Fichier Barney.cpp
#include "Barney.hpp"
Barney::Barney()
{
// ...
x.goBowling();
// ...
}
Comme décrit ci-dessus , le désastre intervient si y est construit avant x, ce qui arrive 50% du temps puisqu'ils sont dans deux fichiers sources différents.
Il y a beaucoup de solutions à ce problème, mais une solution très simple et completement portable est de remplacer l'objet (de type Fred) global x, par une fonction globale x(), qui retourne par réference l'objet Fred.
// File x.cpp
#include "Fred.hpp"
Fred& x()
{
static Fred* ans = new
Fred();
return
*ans;
}
Puisque les objet locaux static sont construits la première fois (et seulement la première fois) que le flux de contrôle passe sur la déclaration, l'instruction ci-dessus new Fred() sera non seulement executée une fois : la première fois que x() est appelée, mais chaque appel suivant retournera le même objet de type Fred (celui pointé par ans). Tout ce qu'il reste à faire est de changer x en x():
// Fichier Barney.cpp
#include "Barney.hpp"
Barney::Barney()
{
// ...
x().goBowling();
// ...
}
Le nom est Construction à la première utilisation car cela fait exactement ce que ça dit : l'objet global Fred est construit à sa première utilisation.
Le défaut de cette approche est que l'objet Fred n'est jamais detruit. Le livre C++ FAQ contient une seconde technique qui solutionne ce souscis (mais au risque de générer un fiasco dans l'ordre de de-initialisation des variables statiques").
Notez que vous avez pas besoin de faire ça pour les types intrinsèques/prédefinis commeint ou char*. Par exemple si vous créez un staticfloat ou un float global, il n'y a pas besoin de l'emballer dans une fonction. La seule fois où l'initialisation statique peut vraiment tourner au fiasco est lorsque vos objets static ou globaux ont un constructeur.
[ Haut | Bas | Section précédente | Section suivante ]
Supposez que vous avez une classe X possedant un objet staticFred :
// Fichier X.hpp
class X {
public:
// ...
private:
static Fred x_;
};
Naturellement ce membre static est initialisé séparement :
// Fichier X.cpp
#include "X.hpp"
Fred X::x_;
Naturellement aussi l'objet Fred sera utilisé dans une ou plusieurs des méthodes de X:
void X::someMethod()
{
x_.goBowling();
}
Mais maintenant le "scenario catastrophe" est que quelqu'un, quelque part, appelle de quelque manière cette méthode avant que l'objet Fred soit construit. Par exemple, si quelqu'un crée un objet static X et invoque la méthode someMethod() pendant l'initialisation static, alors vous êtes à la merci du compilateur : c'est à dire si le compilateur construira X::x_ avant ou après que someMethod() soit appelé. (Notez que le comité ANSI/ISO C++ travaille sur ce problème, mais que les compilateurs ne sont pas en général n'implantent pas ces changements; surveillez cet espace pour une mise-à-jour dans le futur.)
Dans tous les cas, c'est toujours portable et sûre de modifier le membre de donnée X::x_static en une fonction membre static:
// Fichier X.hpp
class X {
public:
// ...
private:
static Fred& x();
};
Naturellement ce membre static est initialisé séparement:
// Fichier X.cpp
#include "X.hpp"
Fred& X::x()
{
static Fred* ans = new
Fred();
return
*ans;
}
Il ne reste plus qu'a remplacer x_ par x():
void X::someMethod()
{
x().goBowling();
}
Si vous êtes super sensible à la performance de votre programme et que vous êtes souieux de délai introduit par un appel de fonction suplémentaire à chaque invoquation de X::someMethod() vous pouvez mettre un static Fred& à la place. Comme vous vous en souvenez, les variables locales static sont seulement initialisées une fois (la première fois que le flux de contrôle passe sur la déclaration), ceci appelera donc X::x() une fois seulement au premier appel de X::someMethod() :
void X::someMethod()
{
static Fred& x = X::x();
x.goBowling();
}
Notez que vous avez pas besoin de faire ça pour les types intrinsèques/prédefinis commeint ou char*. Par exemple si vous créez un staticfloat ou un float global, il n'y a pas besoin de l'emballer dans une fonction. La seule fois où l'initialisation statique peut vraiment tourner au fiasco est lorsque vos objets static ou globaux ont un constructeur.
[ Haut | Bas | Section précédente | Section suivante ]
Lancer (throw) une exception. Voir [17.1] pour les détails.
[ Haut | Bas | Section précédente | Section suivante ]
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 le 12 Nov 2002