IdentifiantMot de passe
Loading...
Mot de passe oublié ?Je m'inscris ! (gratuit)

C++ expliqué aux programmeurs C

Date de publication : 24 mars 2009


III. Les notions de classe et d'objet
III-A. Classes et Objets en POO
III-B. Classes et Objets en C++
III-C. Une question de syntaxe
III-D. La sécurité au niveau des membres
III-E. Construction et destruction d'objet
III-E-1. Constructeurs et destructeurs
III-E-3. Durée de vie d'un objet
III-E-4. Création dynamique d'objet
III-E-5. Les objets temporaires
III-E-5-a. Introduction
III-E-5-b. Quelques exemples
III-E-6. La construction par copie
III-F. Compléments
III-F-1. Les membres statiques
III-F-3. Les structures et les unions en C++


III. Les notions de classe et d'objet


III-A. Classes et Objets en POO

En POO, un objet est une représentation informatique d'un objet du monde intelligible comme une personne, une voiture, un nombre, un vecteur, etc. Un objet appartient à une classe. Par exemple, si x est un nombre, on dit que x est un objet de type "Nombre" ou encore une instance de la classe "Nombre".

Un objet regroupe des données (variables) qui servent à représenter son état et des fonctions qui servent à le manipuler. Cette association de données à manipuler et fonctions permettant de les manipuler au sein d'une même entité qu'est l'objet est la base même de la POO. C'est l'encapsulation.



III-B. Classes et Objets en C++

C++ est un langage orienté objets (LOO) c'est-à-dire un langage qui permet de programmer facilement selon les principes de la POO. En C++, le mot-clé class permet de déclarer une classe et l'opérateur '.' (point) permet d'accéder à un membre d'une instance d'une classe. D'habitude, on sépare la déclaration d'une classe de son implémentation. On met les déclarations dans un fichier d'en-tête et l'implémentation dans un fichier source.

Le programme suivant montre un exemple de définition et d'utilisation d'une classe.
double.h
#ifndef H_DOUBLE_H

#define H_DOUBLE_H

// Declaration de la classe Double.

class Double
{
public:
    /* Donnees membres. */

    double value;

    /* Fonctions membres. */
    
    // Fonction abs : Retourne la valeur absolue de 'value'.
    
    double abs();
    
    // Fonction set_d_digs : Reduit 'value' à n decimales apres la virgule.
    // Si round == true, la valeur sera arrondi au nombre le plus proche.
    // Si round = false, la valeur sera arrondi par defaut.
    
    void set_d_digs(int n, bool round = true);
};

#endif
double_prog.cpp
#include <iostream>
#include "double.h"

using namespace std;

int main()
{
    Double x;
    
    x.value = 1.618;
    
    cout << "x = " << x.value << endl;
    cout << "x.abs() = " << x.abs() << endl;
    
    x.set_d_digs(2);
    cout << "x = " << x.value << endl;
    
    x.set_d_digs(1);
    cout << "x = " << x.value << endl;
    
    x.set_d_digs(0);
    cout << "x = " << x.value << endl;

    return 0;
}
double.cpp
#include "double.h"
#include <cmath>

using namespace std;

// Implementation des fonctions membres de la classe Double.

inline double Double::abs()
{
    return fabs(value);
}

void Double::set_d_digs(int n, bool round)
{
    int m = 1;
    
    for(int i = 0; i < n; i++)
        m *= 10;
    
    value *= m;
    
    if (round)
        value += 0.5;
    
    value = floor(value);
    value /= m;
}
Voici la sortie de ce programme :
x = 1.618
x.abs() = 1.618
x = 1.62
x = 1.6
x = 2
Nous remarquons donc entre autres que ce que nous aurions écrit 'set_d_digs(&x, 2);' en C, où x serait une structure de type Double (struct Double), peut s'écrire 'x.set_d_digs(2);' en C++ comme le suggère le paradigme "orienté objets" (l'objet est mis en avant de la fonction).

Le mot-clé public suivi d'un deux-points qui précède la liste des membres de la classe Double sert à spécifier que ces membres sont "publiques", c'est-à-dire accessibles depuis n'importe quelle fonction.

Il est aussi permis de définir une fonction à l'intérieur de la déclaration d'une classe. Dans ce cas, le compilateur traitera la fonction comme si elle avait été déclarée inline. Voici donc une autre version de la déclaration, incluant cette fois-ci sa définition, de la classe Double :
class Double
{
public:
    double value;

    double abs()
    {
        return fabs(value);
    }
    
    void set_d_digs(int n, bool round = true)
    {
        int m = 1;
        
        for(int i = 0; i < n; i++)
            m *= 10;
        
        value *= m;
        
        if (round)
            value += 0.5;
        
        value = floor(value);
        value /= m;
    }
};

III-C. Une question de syntaxe

Un LOO permet de créer et de manipuler facilement des objets. Un objet possède des données membres et des fonctions membres qui servent à manipuler les données membres donc l'objet (car les données membres représentent l'état de l'objet). Lorsqu'on écrit x.set_d_digs(2, true), x est l'objet (de type Double) manipulé et set_d_digs est la fonction membre appelée. En C++ (et le principe est le même pour n'importe quel LOO), lorsque le compilateur rencontre une telle expression, il la transforme en interne en quelque chose comme Double@set_d_digs(&x, 2, true). Double@set_d_digs est la procédure effectivement appelée. Elle reçoit en argument, tout d'abord, l'adresse de l'objet manipulé et ensuite, les arguments passés par l'utilisateur. C'est ce qui se passe pour tout appel de fonction membre (à quelques exceptions près que nous découvrirons plus bas). Cet argument caché qui pointe vers l'objet manipulé est accessible, en C++, depuis l'intérieur de n'importe quelle fonction membre (en considérant les mêmes exceptions citées plus haut) via le mot-clé this. Ainsi, l'implémentation des fonctions membres de Double aurait pu également s'écrire :
inline double Double::abs()
{
    return fabs(this->value);
}

void Double::set_d_digs(int n, bool round)
{
    int m = 1;
    
    for(int i = 0; i < n; i++)
        m *= 10;
    
    this->value *= m;
    
    if (round)
        this->value += 0.5;
    
    this->value = floor(this->value);
    this->value /= m;
}
this permet en particulier de lever l'ambiguïté lorsqu'un paramètre utilise le même nom qu'une donnée membre comme nous pourrons le constater dans les exemples à venir.


III-D. La sécurité au niveau des membres

Dans les exemples précédents, les membres de la classe (Double) étaient tous publiques, ce qui signifie qu'il était possible d'accéder à ces membres depuis n'importe quelle fonction. Souvent, on a pourtant besoin de rendre "privés" (c'est-à-dire visible que par les autres membres) certains membres de la classe afin d'éviter de mauvaises manipulations de la part de l'utilisateur. Pour rendre certains membres privés, il suffit de faire précéder leur déclaration du mot-clé private. L'utilisateur n'a pas accès aux membres privés d'une classe. En C++, les membres d'une classe sont par défaut privés (private).

Supposons donc que nous voulions créer une classe Robot avec comme données membres les variables listées ci-dessous :
std::string name; // Nom du robot.
int pos_x, pos_y; // Coordonnees de la position actuelle du robot.
Direction dir; // Direction regardee par le robot.
Où Direction est un type énuméré défini par :
enum Direction {
    DIR_NORD, DIR_OUEST, DIR_SUD, DIR_EST
};
Pour déplacer le robot, l'utilisateur devra appeler une fonction Move() qui aura pour effet de le faire avancer d'un pas dans la direction qu'il regarde. Pour le faire changer de direction, on utilisera les fonctions RotateLeft() et RotateRight(). Il est donc clair que les membres pos_x, pos_y et dir doivent être déclarés privés afin que l'utilisateur ne puisse pas les modifier directement, c'est-à-dire sans passer par les fonctions citées ci-dessus. Il en est de même pour name car ce membre ne doit jamais changé une fois fixé.

Or, comme il a déjà été maintes fois dit, un membre privé n'est visible que par un membre de la classe. Si nous voulons retrouver l'accès à ces membres (privés), nous devons alors ajouter à la classe des fonctions appelés muteurs qui serviront à les modifier et des accesseurs qui serviront à les lire. L'intérêt d'utiliser des accesseurs et muteurs au lieu de déclarer les membres publiques est que le muteur peut faire des contrôles et/ou traitements supplémentaires avant et/ou après la modification de la donnée. Ajoutons donc à la classe Robot les accesseurs get_name(), get_pos_x(), get_pos_y() et get_dir(). Pour pos_x, pos_y et dir, nous n'allons pas définir des muteurs pour car nous avons déjà les fonctions Move(), RotateLeft() et RotateRight() qui font bien leur job. Jusqu'à présent, nous n'avons toujours pourtant pas de fonction permettant de spécifier le nom d'un robot (qui n'est pas directement accessible à l'utilisateur car il s'agit d'un membre privé). Devons-nous donc écrire un muteur pour ce membre ? Certainement pas car le nom doit être tout simplement fixé dès la création. Une fois fixé, il ne doit jamais plus être changé. Ce n'est donc pas d'un muteur dont nous avons besoin mais d'un initialiseur, une fonction qui ne sera appelée que pendant la création de l'objet (et qui ne pourra plus jamais être appelée). La fonction qui permet de réaliser les différentes initialisations nécessaires à la création d'un objet s'appelle le constructeur d'objet. Il existe également une autre fonction spéciale, appelée à chaque destruction d'un objet, qu'on appelle destructeur.

Le C++ permet de définir plusieurs constructeurs grâce à la surcharge des fonctions. L'utilisateur choisit donc le constructeur qu'il veut utiliser pendant la création de l'objet. Il ne doit cependant y avoir qu'un destructeur tout au plus par classe. En C++, un constructeur est une fonction membre portant le même nom que la classe et dont la valeur de retour n'est pas spécifiée (et non pas qui retourne void !). Le destructeur porte aussi le même nom que la classe mais ce nom doit commencer par ~. Il ne spécifie pas de valeur de retour et ne requiert aucun argument. Comme les constructeurs et les destructeurs sont utilisés pour construire et détruire des objets, il sont généralement déclarés publiques mais cela n'est absolument pas imposé par le langage.

On pourrait maintenant se précipiter à écrire notre classe Robot, mais nous pouvons encore faire mieux !

Premièrement, nous avons créé le type Direction pour énumérer les différentes directions utilisables avec le membre dir. Le problème, c'est qu'il est possible que nous souhaiterions plus tard créer un autre type de même nom pour une autre classe, éventuellement avec des valeurs complètement différentes. La solution, c'est de le faire (le type (Direction)) membre de la classe. Comme il doit être visible même depuis l'extérieur, nous devons le déclarer publique. Ici, la classe joue donc, en quelque sorte, tout simplement le rôle d'un espace de noms.

Deuxièmement, qu'adviendra t-il de nos objets si nous les déclarions par exemple const ? Cette question est plus importante qu'à ce qu'elle paraît. En effet, lorsqu'un objet est déclaré const, ses données membres ne peuvent être écrites que pendant les phases de construction et de destruction. Une conséquence immédiate est que, en dehors de ces phases particulières que sont sa construction et sa destruction, toute fonction membre susceptible de modifier une donnée membre ne pourra pas ête appelée. Pour qu'une fonction membre puisse être appelée depuis un objet const, il faut qu'elle ait été elle aussi déclarée const, ce qui signifie pour le compilateur que vous garantissez que cette fonction ne modifie et ne tentera jamais de modifier l'objet, et le compilateur s'en assurera d'ailleurs en générant une erreur lorsque vous tentez de modifier une donnée membre à l'intérieur d'une fonction const. Pour rendre une fonction const, il suffit de placer le mot-clé const juste après la parenthèse fermante de la liste d'arguments de la fonction. Cela doit se faire aussi bien dans la déclaration que dans la définition de la fonction. En particulier, tous les accesseurs doivent donc être déclarés const. C'est aussi ce que nous aurions du faire avec la fonction abs() de notre classe Double.

Voici donc enfin la version finale de notre classe Robot avec un exemple simple d'utilisation :
robot.h
#ifndef H_ROBOT_H

#define H_ROBOT_H

#include <string>

class Robot
{
    /* Types */
public:
    enum Direction {
        DIR_NORD, DIR_OUEST, DIR_SUD, DIR_EST
    };

    /* Donnees membres */
private:
    std::string name;
    int pos_x, pos_y;
    Direction dir;

    /* Fonctions membres */
public:
    // Constructeurs et destructeur

    Robot(std::string name, int x = 0, int y = 0, Direction d = DIR_NORD); // Constructeur
    Robot(std::string name, Direction d); // Constructeur
    ~Robot(); // Destructeur

    // Accesseurs et muteurs

    const std::string & get_name() const { return name; }
    int get_pos_x() const { return pos_x; }
    int get_pos_y() const { return pos_y; }
    Direction get_dir() const { return dir; }

    void Move();
    void RotateLeft();
    void RotateRight();

    // Utilitaires

    void PrintPos() const; // Affiche la position actuelle du Robot.
    void PrintDir() const; // Afiche la direction regardee par le Robot.
    void Say(std::string text) const; // Fait parler le robot.
};

#endif
robot_prog.cpp
#include <iostream>
#include "robot.h"

using namespace std;

int main()
{
    Robot toto("toto"); // Ou : 'Robot toto = Robot("toto");'.

    toto.PrintPos();
    toto.PrintDir();
    
    const Robot tata("tata", 0, -2);
    
    tata.PrintPos(); // PrintPos est une fonction const donc on peut l'appeler.
    tata.PrintDir(); // Pareil pour PrintDir.
    
    // On ne peut par contre pas ecrire tata.Move() par exemple car Move() n'est pas une fonction const.
    
    for(int i = 0; i < 3; i++)
        toto.Move();

    toto.PrintPos();

    while(toto.get_dir() != Robot::DIR_SUD)
        toto.RotateLeft();

    toto.PrintDir();

    toto.Move();
    toto.PrintPos();

    toto.Say("Au revoir !");
    tata.Say("Au revoir !");

    return 0;
}
robot.cpp
#include "robot.h"
#include <iostream>
#include <string>

using namespace std;

Robot::Robot(string name, int x, int y, Direction d)
{
    cout << "Constructeur 1 de Robot appele (Robot = " << name << ")." << endl;

    this->name = name;
    this->pos_x = x;
    this->pos_y = y;
    this->dir = d;
}

Robot::Robot(string name, Direction d)
{
    cout << "Constructeur 2 de Robot appele (Robot = " << name << ")." << endl;

    this->name = name;
    this->pos_x = 0;
    this->pos_y = 0;
    this->dir = d;
}

Robot::~Robot()
{
    cout << "Destructeur de Robot appele (Robot = " << name << ")." << endl;
}

void Robot::Move()
{
    cout << name << " avance." << endl;

    switch(dir)
    {
    case DIR_NORD:
        pos_y++; break;

    case DIR_OUEST:
        pos_x--; break;

    case DIR_SUD:
        pos_y--; break;

    case DIR_EST:
        pos_x++; break;
    }
}

void Robot::RotateLeft()
{
    cout << name << " tourne a gauche." << endl;

    int new_dir = dir + 1;
    
    if (new_dir > DIR_EST)
        new_dir = DIR_NORD;

    dir = (Direction)new_dir;
}

void Robot::RotateRight()
{
    cout << name << " tourne a droite." << endl;

    int new_dir = dir - 1;
    
    if (new_dir < DIR_NORD)
        new_dir = DIR_EST;

    dir = (Direction)new_dir;
}

void Robot::PrintPos() const
{
    cout << "Etat de " << name << " : pos = (" << pos_x << ", " << pos_y << ")." << endl;
}

void Robot::PrintDir() const
{
    static const char * t[] = {"NORD", "OUEST", "SUD", "EST"};

    cout << "Etat de " << name << " : dir = " << t[dir] << "." << endl;
}

void Robot::Say(string text) const
{
    cout << name << " dit : " << text << endl;
}
Voici la sortie du programme :
Constructeur 1 de Robot appele (Robot = toto).
Etat de toto : pos = (0, 0).
Etat de toto : dir = NORD.
Constructeur 1 de Robot appele (Robot = tata).
Etat de tata : pos = (0, -2).
Etat de tata : dir = NORD.
toto avance.
toto avance.
toto avance.
Etat de toto : pos = (0, 3).
toto tourne a gauche.
toto tourne a gauche.
Etat de toto : dir = SUD.
toto avance.
Etat de toto : pos = (0, 2).
toto dit : Au revoir !
tata dit : Au revoir !
Destructeur de Robot appele (Robot = tata).
Destructeur de Robot appele (Robot = toto).
Nous n'avons pas encore tout dit des différentes spécifications d'accès existantes en C++ mais nous en savons déjà assez pour continuer notre lecture. Le section suivante discutera des mécanismes de construction et de destruction d'objet.


III-E. Construction et destruction d'objet


III-E-1. Constructeurs et destructeurs

Nous avons vu tout à l'heure comment définir un constructeur pour une classe et comment créer un objet avec. Nous avons également vu qu'il était possible de définir plusieurs constructeurs pour une même classe grâce à la surcharge des fonctions mais qu'il n'était possible de définir qu'un destructeur tout au plus. Si aucun constructeur n'a eté défini, le compilateur ajoutera automatiquement un constructeur appelé constructeur par défaut ou encore le constructeur sans argument car ce constructeur ne requiert pas d'argument. 'Double x;' était un exemple de création d'objet en utilisant le constructeur sans argument. De même, 'Double t[2];' crée un tableau t de deux éléments de type Double construits à l'aide du constructeur sans argument. Lorsqu'un constructeur est défini, le compilateur n'ajoute plus automatiquement le constructeur par défaut. Dans l'exemple où nous avons défini un constructeur pour la classe Robot donc, si on avait tenté de créer un objet Robot à l'aide du constructeur sans argument, le compilateur aurait généré une erreur car aucun constructeur pouvant s'utiliser sans argument n'a été défini ! Une solution consisterait donc à rendre optionnel l'argument name et une autre à définir explicitement un constructeur sans argument pour la classe mais aucune des deux en fait ne peut être réellement appliquée car l'argument name ne peut être facultatif.


III-E-3. Durée de vie d'un objet

Objet C++ ou pas, les données et instructions qui constituent un programme se trouvent toujours en "mémoire" (la mémoire vue des applications) durant son exécution. Pour les "données" (c'est-à-dire les variables ou les données allouées dynamiquement, ce qui inclut également les "objets"), nous avons déjà vu en C que leur durées de vie dépendent de la manière dont elles sont été créées. En guise de petit rappel :

  • Les objets automatiques (en clair, les variables locales automatiques) sont placés sur la pile (stack). Ils sont créés au moment où le programme rencontre leurs définitions et détruits au moment où le programme quitte le bloc dans lequel ils ont été créés.

  • Les objets créés dynamiquement sont placés sur le tas (heap). Ils continuent de vivre tant qu'ils n'ont pas été explicitement détruits.

  • Les objets statiques (variable globale ou statique) sont placés sur la mémoire statique. Cette mémoire est allouée au lancement du programme (avant même que main() soit appelée) puis libérée à sa terminaison (après que main() ait retourné). Les objets statiques vivent donc pratiquement le temps d'exécution du programme. Toutefois, si le programme s'est terminé de manière prématurée, leurs destructeurs ne sont pas appelés.

En C++, il y a aussi les objets temporaires (qui sont en fait des objets automatiques "cachés") dont nous en reparlerons tout à l'heure.

Le programme suivant met en évidence les instants de création et destruction d'objets dans un programme.
#include <iostream>
#include <string>

using namespace std;

static bool dans_main = false;

class MyObj
{
private:
    string name;

public:
    MyObj(string name)
    {
        const char * s = dans_main ? "Dans main() : " : "Hors de main() : ";
        cout << s << "Construction de l'objet " << name << "." << endl;
        this->name = name;
    }
    
    ~MyObj()
    {
        const char * s = dans_main ? "Dans main() : " : "Hors de main() : ";
        cout << s << "Destruction de l'objet " << name << "." << endl;
    }
};

MyObj A("A");

int main()
{
    dans_main = true;
    
    MyObj B("B");
    
    cout << "Debut de for()." << endl;

    for(int i = 0; i < 2; i++)
    {
        MyObj C("C");
    }
    
    cout << "Fin de for()." << endl;
    
    dans_main = false;

    return 0;
}
Sortie :
Hors de main() : Construction de l'objet A.
Dans main() : Construction de l'objet B.
Debut de for().
Dans main() : Construction de l'objet C.
Dans main() : Destruction de l'objet C.
Dans main() : Construction de l'objet C.
Dans main() : Destruction de l'objet C.
Fin de for().
Dans main() : Destruction de l'objet B.
Hors de main() : Destruction de l'objet A.
Le paragraphe suivant discute des objets créés dynamiquement.


III-E-4. Création dynamique d'objet

Nous avons déjà eu l'occasion d'utiliser les opérateurs new et delete pour allouer puis libérer dynamiquement de la mémoire. Si malloc et free permettent de faire autant, ils ne sont pas très adaptés à la création d'objets car ils ne font qu'allouer/libérer de la mémoire alors que new appelle le constructeur après avoir alloué la mémoire et delete appelle le destructeur avant de la libérer.

Voici un exemple de création et de destruction dynamiques d'objet :
#include <iostream>
#include <string>
#include <new>
#include "robot.h"

using namespace std;

int main()
{
    try
    {
        Robot * pRobot = new Robot("toto");
        string s = pRobot->get_name();
        pRobot->PrintPos();
        pRobot->PrintDir();
        pRobot->Say("Bonjour tout le monde !");
        delete pRobot;
        cout << s << " a bien ete detruit." << endl;
    }
    catch(const bad_alloc & e)
    {
        cout << "La Construction du Robot a echoue : " << e.what() << "." << endl;
    }
    catch(...)
    {
        cout << "Le programme doit etre arrete en raison d'une exception non geree." << endl;
    }

    cout << "A bientot !" << endl;

    return 0;
}
Sortie normale du programme :
Constructeur 1 de Robot appele (Robot = toto).
Etat de toto : pos = (0, 0).
Etat de toto : dir = NORD.
toto dit : Bonjour tout le monde !
Destructeur de Robot appele (Robot = toto).
toto a bien ete detruit.
A bientot !
Pour construire dynamiquement un objet de type X en utilisant un constructeur qui ne requiert pas d'argument, les écritures 'new X()' (appel explicite du constructeur) et 'new X' (allocation d'un objet de type X (automatiquement suivi d'un appel au constructeur)) sont toutes correctes et équivalentes. A noter cependant que la création statique d'objet en utilisant ce même type de constructeur ne peut pas s'écrire 'X x();' car cette écriture correspond à la déclaration d'une fonction x qui requiert aucun argument et qui retourne un objet X.


III-E-5. Les objets temporaires


III-E-5-a. Introduction
Les objets temporaires sont des objets créés puis détruits aussitôt qu'ils ne sont plus utilisés. Considérons par exemple l'instruction 'MyObj("A");'. Il s'agit d'un simple appel au constructeur de la classe MyObj. Utilisé ainsi, ce constructeur, après avoir correctement initialisé l'objet créé, va retourner une référence vers cet objet. C'est d'ailleurs la principale raison pour laquelle on ne spécifie pas le retour d'un constructeur : c'est au compilateur de décider de la manière de créer l'objet et de ce que le constructeur doit retourner.

Avec un appel tel que 'MyObj("X")' (sans new), le compilateur génèrera, comme nous venons juste de le voir, le code qui crée l'objet (sur la pile) suivi d'un appel au constructeur invoqué qui retournera ensuite une référence vers l'objet nouvellement créé. En outre, l'expression (MyObj("X")) sera traitée par le compilateur comme une lvalue si elle est utilisée en paramètre d'une fonction ou à gauche de l'opérateur = et comme une rvalue dans les autres cas. La raison principale pour laquelle elle est traitée comme une lvalue lorsqu'elle est utilisé en paramètre d'une fonction c'est que cela permet de faciliter les appels de fonctions qui requièrent une référence en paramètre.

Dans 'MyObj("A");', un objet est créé mais il n'est pas utilisé. Le compilateur ajoutera alors automatiquement une instruction qui aura pour effet de le détruire sur le champ. Cela est mis en évidence grâce au programme suivant :
#include <iostream>
#include <string>

using namespace std;

class MyObj
{
private:
    string name;

public:
    MyObj(string name)
    {
        cout << "Construction de l'objet " << name << "." << endl;
        this->name = name;
    }
    
    ~MyObj()
    {
        cout << "Destruction de l'objet " << name << "." << endl;
    }
};

int main()
{
    cout << "Debut du programme." << endl;
    MyObj("A");
    MyObj("B");
    cout << "Fin du programme." << endl;

    return 0;
}
Sortie du programme :
Debut du programme.
Construction de l'objet A.
Destruction de l'objet A.
Construction de l'objet B.
Destruction de l'objet B.
Fin du programme.
La création d'objets temporaires permet souvent de simplifier une expression. Par exemple, l'opérateur + n'est défini ni pour les tableaux ni pour les pointeurs alors qu'il l'est pour les "strings". On peut alors profiter du mécanisme de gestion des objets temporaires du C++ pour concaténer rapidement (en termes de lignes de code) des chaînes C comme le montre l'exemple suivant :
#include <iostream>
#include <string>

using namespace std;

int main()
{
    const char * p = "azerty", * q = "uiop";
    
    string s = string(p) + q;
    // Ou 'string s = string(s) + string(q);', etc.
    // Mais 'string s = p + q;' ne marche pas !
    	
    cout << s << endl;
    
    return 0;
}

III-E-5-b. Quelques exemples
Dans une instruction telle que 'MyObj A = MyObj("A");' :

  • Un objet "temp" est créé par l'appel MyObj("A"). Une référence vers cet objet est retournée.
  • L'objet ainsi créé est copié par A. Il ne s'agit cependant pas d'une simple affectation, il s'agit bien d'une construction d'un nouvel objet A à partir d'un objet existant ("temp"), c'est-à-dire en copiant cet objet.
  • L'objet "temp" a fini d'être copié. Il ne sert plus à rien, il sera automatiquement détruit.

Maintenant, soit f une fonction qui reçoit par valeur un objet MyObj en argument. Lors d'un appel tel que f(MyObj("A")) :

  • Un objet "temp" est créé par l'appel MyObj("A"). Une référence vers cet objet est retournée.
  • L'objet ainsi créé est copié par l'argument x de la fonction f (l'objet étant passé par valeur). Ici encore, nous rappelons qu'il s'agit bien d'une construction d'un nouvel objet x à partir d'un objet existant ("temp").
  • L'objet "temp" a fini d'être copié. Il ne sert plus à rien, il sera automatiquement détruit.
  • L'objet x est détruit au retour de f.

Soit également g une fonction qui reçoit par référence un objet MyObj en argument. Lors d'un appel tel que g(MyObj("A")) :

  • Un objet "temp" est créé par l'appel MyObj("A"). Une référence vers cet objet est retournée.
  • La fonction créé une référence vers cet objet. Il n'y a cependant pas de création de nouvel objet car g ne fait que créer une référence vers "temp" et non copier "temp".
  • L'objet "temp" est maintenu en vie jusqu'au retour de g, instant de sa destrcution.

Nous constatons que dans certains cas, le compilateur doit créer (plus précisément, générer le code pour créer) un objet x en copiant un objet existant y. Dans de tels cas, un constructeur spécial est appelé pour créer l'objet. C'est le constructeur par copie.


III-E-6. La construction par copie

Lorsqu'on créé un objet x en copiant un objet existant y, le constructeur par copie est utilisé pour créer l'objet. Ce constructeur possède comme unique argument une référence vers l'objet à copier. Si le constructeur par copie n'est pas défini, le compilateur ajoute un constructeur qui se contente d'effectuer une simple copie de bits.

Voici un exemple de définition d'une classe possédant un constructeur par copie.
#include <iostream>
#include <string>
#include <sstream>

using namespace std;

class MyObj
{
private:
    string label, name;
    static int count;

public:
    MyObj(string label);
    MyObj(const MyObj & x); // Constructeur par copie
    ~MyObj();

    void Say(string text) const;

private:
    void Create(); // Routine principale de construction
};

int MyObj::count = 0;

void f(MyObj x)
{
    x.Say("Dans f.");
}

void g(const MyObj & x)
{
    x.Say("Dans g.");
}

int main()
{
    cout << "Construction de Obj1." << endl;
    MyObj Obj1("toto"); // Creation d'un objet Obj1 avec pour label "toto".
    cout << "Construction de Obj1 terminee." << endl << endl;

    cout << "Passage de Obj1 a f." << endl;
    f(Obj1); // Obj1 sera copie par x. x sera Obj2.
    cout << "Retour dans main()." << endl << endl;

    cout << "Passage de Obj1 a g." << endl;
    g(Obj1); // Pas de nouvel objet cree. x sera juste une reference a Obj1.
    cout << "Retour dans main()." << endl << endl;

    cout << "Construction de Obj3." << endl;
    MyObj Obj3 = Obj1; // Creation d'un objet Obj3 en copiant Obj1.
    cout << "Construction de Obj3 terminee." << endl << endl;

    cout << "Construction de Obj5." << endl;
    MyObj Obj5 = MyObj("tata"); // Creation d'un objet Obj4 temporaire + Creation de Obj5 en copiant Obj4.
    cout << "Construction de Obj5 terminee." << endl << endl;

    cout << "Construction de Obj7." << endl;
    const MyObj & Obj7 = MyObj("tata"); // <=> const MyObj Obj7 = MyObj("tata");
    cout << "Construction de Obj7 terminee." << endl << endl;

    return 0;
}

MyObj::MyObj(string label)
{
    cout << "Constructeur 1 de MyObj appele - ";
    this->label = label;
    this->Create();
}

MyObj::MyObj(const MyObj & x)
{
    cout << "Constructeur par copie de MyObj appele - ";
    this->label = x.label;
    this->Create();
}

void MyObj::Create()
{
    ostringstream s;

    count++;
    s << label << " (Obj " << count << ")";
    name = s.str();

    cout << "Construction de " << name << "." << endl;
}

MyObj::~MyObj()
{
    cout << "Destructeur de MyObj appele - Destruction de " << name << "." << endl;
}

void MyObj::Say(string text) const
{
    cout << name << " dit : " << text << endl;
}
Voici la sortie de ce programme :
Construction de Obj1.
Constructeur 1 de MyObj appele - Construction de toto (Obj 1).
Construction de Obj1 terminee.

Passage de Obj1 a f.
Constructeur par copie de MyObj appele - Construction de toto (Obj 2).
toto (Obj 2) dit : Dans f.
Destructeur de MyObj appele - Destruction de toto (Obj 2).
Retour dans main().

Passage de Obj1 a g.
toto (Obj 1) dit : Dans g.
Retour dans main().

Construction de Obj3.
Constructeur par copie de MyObj appele - Construction de toto (Obj 3).
Construction de Obj3 terminee.

Construction de Obj5.
Constructeur 1 de MyObj appele - Construction de tata (Obj 4).
Constructeur par copie de MyObj appele - Construction de tata (Obj 5).
Destructeur de MyObj appele - Destruction de tata (Obj 4).
Construction de Obj5 terminee.

Construction de Obj7.
Constructeur 1 de MyObj appele - Construction de tata (Obj 6).
Constructeur par copie de MyObj appele - Construction de tata (Obj 7).
Destructeur de MyObj appele - Destruction de tata (Obj 6).
Construction de Obj7 terminee.

Destructeur de MyObj appele - Destruction de tata (Obj 7).
Destructeur de MyObj appele - Destruction de tata (Obj 5).
Destructeur de MyObj appele - Destruction de toto (Obj 3).
Destructeur de MyObj appele - Destruction de toto (Obj 1).

III-F. Compléments


III-F-1. Les membres statiques

Un membre statique est une variable ou une fonction déclarée à l'intérieur d'une classe mais dont l'utilisation ne nécessite pourtant aucune instanciation de la classe. Il reste unique indépendament du nombre d'instances de la classe. Ici encore (comme lorsque nous avons déclaré Direction), la classe joue donc, en quelque sorte, tout simplement le rôle d'un espace de noms. Les fonctions membres statiques ne recoivent pas, bien entendu, l'argument this. Elles ne peuvent donc accéder qu'aux autres membres statiques.

Voici un exemple d'utilisation de membres statiques :
#include <iostream>
#include <string>

using namespace std;

static bool dans_main = false;

class MyObj
{
    /* Membres statiques */
private:
    static int count; // Declare une variable globale 'count' comme membre prive de la classe X (MyObj::count).
    
public:
    static int get_count() const
    {
        return count;
    }
    
    /* Membres non statiques */
private:
    string name;

public:
    MyObj(string name)
    {
        cout << "Construction de l'objet " << name << "." << endl;
        this->name = name;
        count++;
    }
    
    ~MyObj()
    {
        cout << "Destruction de l'objet " << name << "." << endl;
        count--;
    }
};

int MyObj::count = 0; // Definition de MyObj::count.

int main()
{
    cout << "Debut du programme." << endl;
    
    cout << "Nombre d'objets en vie : " << MyObj::get_count() << "." << endl;
    
    MyObj * pA = new MyObj("A");
    MyObj * pB = new MyObj("B");
    MyObj * pC = new MyObj("C");
    
    cout << "Nombre d'objets en vie : " << MyObj::get_count() << "." << endl;
    
    delete pC;
    cout << "Nombre d'objets en vie : " << MyObj::get_count() << "." << endl;
    
    delete pB;
    cout << "Nombre d'objets en vie : " << MyObj::get_count() << "." << endl;
    
    delete pA;
    cout << "Nombre d'objets en vie : " << MyObj::get_count() << "." << endl;
    
    cout << "Fin du programme." << endl;

    return 0;
}
Sortie :
Debut du programme.
Nombre d'objets en vie : 0.
Construction de l'objet A.
Construction de l'objet B.
Construction de l'objet C.
Nombre d'objets en vie : 3.
Destruction de l'objet C.
Nombre d'objets en vie : 2.
Destruction de l'objet B.
Nombre d'objets en vie : 1.
Destruction de l'objet A.
Nombre d'objets en vie : 0.
Fin du programme.
La séparation des déclarations des membres statiques des membres non statiques comme dans l'exemple ci-dessus est surtout une affaire de présentation du code c'est-à-dire de style mais en aucune manière une obligation.


III-F-3. Les structures et les unions en C++

En C++, les structures et les unions ont été promues en classes. Les unions sont toutefois une forme très limitée de classe c'est pourquoi nous n'en parlerons que très peu. Les structures quant à elles sont de véritables classes et la seule différence entre une "class" et une structure en C++ est que dans une "class", les membres sont par défaut privés (private) et que dans une structure, ils sont par défaut publiques (public). Par exemple :
#include <iostream>
#include <string>

using namespace std;

enum Sexe {
    SX_M, SX_F
};

struct Personne {

    /* Pas besoin de mettre 'public:' car les membres d'une structure sont deja par defaut publiques. */
    
    // Donnees membres.
    
    string nom;
    int age;
    Sexe sx;
    
    // Fonctions membres
    
    const char * Sex();
};

int main()
{
    Personne jean;

    jean.nom = "Jean";
    jean.age = 24;
    jean.sx = SX_M;

    cout << "Nom : " << jean.nom << endl;
    cout << "Age : " << jean.age << endl;
    cout << "Sexe : " << jean.Sex() << endl;

    return 0;
}

const char * Personne::Sex()
{
    return sx == SX_M ? "masculin" : "feminin";
}
 

Valid XHTML 1.1!Valid CSS!

Les sources présentées sur cette page sont libres de droits et vous pouvez les utiliser à votre convenance. Par contre, la page de présentation constitue une œuvre intellectuelle protégée par les droits d'auteur. Copyright © 2009 Melem. Aucune reproduction, même partielle, ne peut être faite de ce site ni de l'ensemble de son contenu : textes, documents, images, etc. sans l'autorisation expresse de l'auteur. Sinon vous encourez selon la loi jusqu'à trois ans de prison et jusqu'à 300 000 € de dommages et intérêts.