Table of Contents

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 :

L'allocation mémoire

Classiquement, la mémoire allouée pour un objet se trouve :

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 :

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 :

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é
    {}
};