====== La création des objets ======
===== Les deux phases =====
Une classe définit la structure d'un objet.
Elle définit d'une part un ensemble de champs et d'autre part un ensemble de fonctions membres qui accèdent aux champs définis au sein de la classe.
Cependant, la classe n'alloue pas de mémoire pour les champs. C'est au moment de la création de l'objet que la mémoire nécessaire à stocker l'ensemble des champs nécessaire est allouée. Une fois la mémoire allouée, il est parfois nécessaire d'initialiser l'ensemble des champs de l'objet afin de pouvoir utiliser l'objet dès que l'opération d'initialisation soit terminé.
De fait, la création d'un objet comporte deux phases :
* Phase 1 : La création d'un bloc mémoire suffisamment grand pour contenir l'ensemble des champs définis par la classe de l'objet.
* Phase 2 : L'initialisation des champs de cet objet en exécutant une séquence d'instructions pour initialiser les champs de l'objet.
===== L'allocation mémoire =====
Classiquement, la mémoire allouée pour un objet se trouve :
* soit sur la pile.
* soit sur le tas.
* soit dans la zone de mémoire globale.
Cependant, il est possible d'avoir des mécanismes d'allocation mémoire dans d'autres zones que les zones précédemment énumérées en implantant des mécanismes d'allocation sur mesure.
==== Allocation dans la pile =====
Dans ce cas, la mémoire est allouée dans la pile. L'allocation mémoire est relativement rapide puisqu'il s'agit de réserver un bloc mémoire capable de contenir l'ensemble des champs ainsi que la table des méthodes virtuelles de l'objet. La mémoire n'est pas initialisée et les différents champs contiennent des valeurs indéterminées.
void display_message()
{
std::string message = "Un message";
message += " qui est complété.";
std::cout << message << std::endl;
}
Naturellement, le bloc mémoire disparait automatiquement lorsque l'on quitte la fonction.
std::string* get_message()
{
std::string message = "Un message";
return &message;
}
int main()
{
std::string* message = get_message();
std::cout << *message << std::endl;
// Génère une segmentation fault puisque la mémoire
// l'objet message a été supprimée à la fin
// de la fonction.
return 0:
}
==== L'allocation sur le tas =====
La mémoire est allouée dans le tas. Pour ce faire, un appel au gestionnaire de mémoire du système d'exploitation est effectuée pour retourner un bloc mémoire suffisamment grand pour stocker l'ensemble des champs ainsi que la table des méthodes virtuelles associés à l'objet.
void display_message()
{
std::string* message = new std::string("Un message");
// allocation de l'objet sur le tas.
// c'est un pointeur sur un objet de type std::string
// qui est retourné.
message->append(" qui est complété.");
std::cout << message << std::endl;
delete message;
// attention: la mémoire n'est pas automatiquement
// libérée, il faut faire attention à ne pas oublier
// de libérer la mémoire une fois que l'objet n'est
// plus utilisé.
}
De fait, la mémoire allouée n'est libérée que par un appel à
l'opérateur ''delete''. Ceci signifie qu'il est parfaitement
possible de retourner un pointeur sur un objet qui aurait été
alloué par une fonction.
std::string* get_message()
{
std::string* message = new std::string("Un message");
// allocation de l'objet sur le tas.
// c'est un pointeur sur un objet de type std::string
// qui est retourné.
message->append(" qui est complété.");
return message;
}
int main()
{
std::string* message = get_message();
std::cout << *message << std::endl;
delete message;
// attention: la mémoire n'est pas automatiquement
// libérée, il faut faire attention à ne pas oublier
// de libérer la mémoire une fois que l'objet n'est
// plus utilisé.
return 0:
}
==== L'allocation dans la zone mémoire globale =====
Il est parfaitement possible de déclarer un objet comme
une variable globale. Dans ce cas, la mémoire de l'objet est
allouée au moment du chargement du segment mémoire.
std::string message = "Un message";
void extend_message()
{
message->append(" qui est complété.");
}
int main()
{
extend_message();
std::cout << message << std::endl;
return 0:
}
===== L'initialisation =====
Une fois allouée, l'objet doit être initialisé. Cette initialisation se produit après l'allocation mémoire de l'objet. L'initialisation est effectuée en appelant une méthode spéciale de l'objet qui est appelé le ** constructeur **. Un constructeur est un méthode qui a le même nom que la classe, qui n'a pas de type de retour et qui prend aucun ou plusieurs paramètres.
Le code suivant défini deux constructeurs ''Point()'' et ''Point(int, int)'' pour la classe ''Point''.
class Point
{
private:
double X, Y;
public:
Point():
{
X = 0.0;
Y = 0.0;
}
Point(int x, int y)
{
X = x;
Y = y;
}
};
Lorsque nous créons un objet sur la pile (ou en mémoire globale) :
void allocate_point_on_stack()
{
Point pointA;
// appelle le constructeur par défaut,
// ie. le constructeur Point()
Point pointB(3.4, 3.5);
// appelle le constructeur spécialisé prenant
// deux valeurs en entrées, ie. le constructeur Point(double, double)
}
La syntax est légèrement différente quand l'objet est alloué dans le tas :
void allocate_point_on_stack()
{
Point* pointA = new Point();
// appelle le constructeur par défaut,
// ie. le constructeur Point()
Point* pointB = new Point(3.4, 3.5);
// appelle le constructeur spécialisé prenant
// deux valeurs en entrées, ie. le constructeur Point(double, double)
}
Cependant il est parfaitement possible d'utiliser une syntaxe équivalente
pour allouer un objet sur la pile en écrivant l'allocation comme suit.
Cependant cette écriture n'est pas recommandée, puisqu'elle peut
parfois entrainer certaines confusions.
void allocate_point_on_stack()
{
Point pointA = Point();
// appelle le constructeur par défaut,
// ie. le constructeur Point()
Point pointB = Point(3.4, 3.5);
// appelle le constructeur spécialisé prenant
// deux valeurs en entrées, ie. le constructeur Point(double, double)
}
==== Les différents modes d'initialisation des champs ====
Dans les exemples précédents, nous avons initialisé les champs par une séquence d'instruction présente dans le corps du constructeur :
class Point
{
...
Point():
{
X = 0.0;
Y = 0.0;
}
Point(int x, int y)
{
X = x;
Y = y;
}
...
Si cette syntaxe est correcte, elle n'est pas optimale. En effet, il est souhaitable qu'avant d'exécuter un quelconque code, nous ayons initialisé l'ensemble des champs de l'objet. Pour ce faire, C++ propose de scinder le processus d'initialisation d'un constructeur en deux phases :
* Une première phase avant l'exécution du code entre les parenthèses correspondant à l'initialisation des différents champs,
* Une seconde phase consistant à l'exécution du code présent dans le corps du constructor.
Pour ce faire, le constructor en C++ s'écrit comme suit :
class Box
{
int width;
int height;
MyClass(): width(0), height(0) // Liste d'initialisateurs
{
// Corps du constructeur
}
Plus spécifiquement :
* ''width(0), height(0)'' définit une liste d'initialisateurs des champs et des [[in204:class:derivation|classes de base]]. Chaque initialisateur a pour syntaxe ''membre(paramètres)'' où ''membre'' est le nom du champ ou le type de la classe de base et où ''paramètres'' représentent le ou les paramètres qui sont passés au constructeur du membre (éventuellement aucun paramètre si c'est le membre est initialisé par défaut). Chaque initialisateur est séparer de son successeur par une ','. Les membres sont initialisés dans l'ordre dans lequel ils apparaissent dans la liste d'initialisateurs. Ainsi dans l'exemple précédent, le champ ''width'' est initialisé avant le champ ''height''.
* le code contenu dans le corps du constructeur est exécuté une fois l'ensemble des membres initialisés.
De fait, si pour un champ donné ou une classe de base, aucun initialisateur n'est spécifié dans la liste, dans ce cas, c'est l'initisateur par défaut qui est automatiquement appelé avant d'exécuter le code contenu dans le corps de la fonction.
==== Les différents types de constructeurs ====
C++ définit plusieurs types de constructeurs.
class Complex
{
private:
double re;
double im;
public:
Complex(): re(0.0), im(0.0) // Constructeur par défaut.
{}
Complex(const Complex& theSource): // Constructeur de recopie
re(theSource.re), im(theSource.im)
{}
Complex(double aFloat): // Conversion
re(aFloat), im(0.0)
{}
Complex(double theReal, double theImaginary)
re(theReal), im(theImaginary) // Constructeur spécialisé
{}
};
* [[in204:cpp:syntax:class:constructor:default|Le constructeur par défaut]]. Il s'agit d'un constructeur ne prenant aucun argument.
* [[in204:cpp:syntax:class:constructor:copy|Le constructeur de recopie]]. Ce constructeur est un constructeur servant à initialiser un objet en recopiant les informations d'un objet déjà existant et de même type.
* [[in204:cpp:syntax:class:constructor:conversion|Les constructeurs de promotion ou de conversion]]. Ces constructeurs permettent d'initialiser un objet de manière à ce qu'il soit équivalent à un objet ou un valeur ayant un autre type.
* [[in204:cpp:syntax:class:constructor:spezialized|Les constructeurs spécialisés]]. Ce sont des constructeurs qui permettent d'initialiser un objet à partir d'un ensemble de paramètres.