Nous allons commencer par écrire les méthodes les plus importantes d'une classe : les constructeurs et le destructeur.
J'ai bien dit LES constructeurs, car on peut surcharger le constructeur (en faire plusieurs versions), et LE destructeur, car celui-ci ne peut pas être surchargé.
Je vous propose de créer 3 constructeurs et le destructeur pour commencer :
- Le constructeur par défaut (celui qui ne prend pas de paramètre). Si l'utilisateur se sert de ce constructeur, la chaîne sera vide : "".
- Un autre constructeur (une surcharge) qui prendra en paramètre une chaîne de caractères pour initialiser la ZString avec une chaîne. La ZString contiendra donc dès le départ la chaîne qu'on lui aura envoyée.
Ce constructeur recevra en paramètre un tableau de char (un char *) correspondant à la chaîne envoyée par l'utilisateur pour initialiser la ZString.
- Le constructeur de copie : quelle que soit la classe qu'on écrit, il est toujours conseillé d'écrire le constructeur de copie car il est souvent nécessaire. C'est un constructeur qui prend une référence vers un objet du même type (un const ZString &).
- Le destructeur pour supprimer le tableau de char m_chaine avant que l'objet ne soit lui-même supprimé. Cela permet d'éviter les fuites de mémoire.
On créera d'autres constructeurs par la suite, mais pour l'instant nous commençons simplement
Commençons par ajouter les prototypes de nos méthodes dans
ZString.h :
Code : C++ 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19 | #ifndef DEF_ZSTRING
#define DEF_ZSTRING
#include <iostream>
class ZString
{
public:
ZString(); // Constructeur par défaut (crée une chaîne vide "")
ZString(const char *chaine); // Constructeur surchargé (crée la chaîne envoyée)
ZString(const ZString &chaine); // Constructeur de copie
~ZString(); // Destructeur (détruit le tableau de char pour libérer la mémoire)
private:
char *m_chaine;
int m_longueur;
};
#endif
|
Bien, voilà qui est fait.
Il faut maintenant implémenter ces méthodes, rendez-vous dans le fichier ZString.cpp.
Le constructeur par défaut ZString()
On commence par implémenter le constructeur par défaut. Je vous rappelle que le but d'un constructeur est d'initialiser les attributs de la classe. La question est :
quelle valeur on va leur mettre ?
Comme on travaille sur le constructeur par défaut, vous pouvez voir que celui-ci ne prend pas de paramètre.
C'est le constructeur qui est appelé lorsqu'on crée un nouvel objet de type ZString sans préciser de paramètre.
C'est précisément ce que l'on a fait dans le main.cpp que je vous ai donné plus haut :
Code : C++1 | ZString chaine; // Appel du constructeur par défaut (aucun paramètre envoyé)
|
Que doit contenir la chaîne lorsqu'on n'envoie rien ?
Bah... rien

Si l'utilisateur n'envoie aucun texte, nous n'allons rien mettre dans l'attribut m_chaine. Il est donc inutile d'allouer un tableau de char (y'a rien à stocker !).
Ce qu'on va faire en revanche, c'est mettre le pointeur
m_chaine à NULL pour indiquer qu'il ne pointe sur rien pour le moment.
Quant à la longueur de la chaîne
m_longueur, bah elle vaudra 0 vu que pour l'instant notre objet ne contiendra aucune chaîne en mémoire
On peut donc écrire dans
ZString.cpp :
Code : C++1
2
3
4
5 | ZString::ZString()
{
m_chaine = NULL;
m_longueur = 0;
}
|
Le constructeur ZString(const char *)
Le constructeur par défaut était simple.
Les choses se corsent quand l'utilisateur envoie un paramètre lorsqu'il crée la chaîne dans main.cpp :
Code : C++1 | ZString chaine("Bonjour");
|
... ou encore (ça revient au même) :
Code : C++1 | ZString chaine = "Bonjour";
|
Lorsqu'un objet est créé de cette façon, cela appelle automatiquement le constructeur qui correspond à la signature ZString(const char *) car le fait d'écrire un texte entre guillemets dans le code source provoque la création d'un tableau de char par le compilateur.
Il va falloir écrire le code de ce constructeur dans ZString.cpp...
Mais là les choses se corsent, suivez-moi bien.
Notre but est d'initialiser nos attributs
m_chaine et
m_longueur correctement, on est bien d'accord ? C'est le but du constructeur d'initialiser des attributs.
Le problème c'est que :
- Pour m_longueur : on ne connaît pas la taille de la chaîne qu'on nous envoie ! Impossible d'initialiser m_longueur si on ne connaît pas la taille de la chaîne. On pourrait utiliser la fonction strlen de la bibliothèque C, mais notre but est que notre classe ZString soit autonome et qu'elle n'ait pas besoin de la bibliothèque du C (on est en C++ que diable !). Solution : il va falloir réécrire la fonction strlen() pour pouvoir calculer la longueur de la chaîne.
- Pour m_chaine : on nous envoie un tableau de char (appelé chaine), mais il ne faut surtout pas écrire m_chaine = chaine; ! Pourquoi ? Parce que en faisant cela, vous faites pointer notre attribut m_chaine vers un tableau qui nous a été envoyé par l'utilisateur. Qu'est-ce qui vous dit que l'utilisateur ne va pas supprimer ce tableau par la suite ? Dans un tel cas, votre pointeur m_chaine pointerait sur un tableau qui n'existe plus ! Solution : copier le tableau qu'on nous envoie et affecter m_chaine à ce tableau pour s'assurer que personne d'autre ne pourra supprimer ce tableau.
Pour le problème de l'initialisation de m_longueur je pense que vous avez compris : on ne connaît pas la longueur de la chaîne et il va nous falloir écrire une fonction qui la calcule manuellement en comptant le nombre de caractères.
Par contre, je pense que le problème de l'initialisation de m_chaine mérite plus d'explications (et même un schéma en fait).
Tout d'abord, il faut savoir que lorsqu'on envoie au constructeur une chaîne de caractères entre guillemets, un tableau de char est automatiquement créé en mémoire. Celui-ci est ensuite passé en paramètre au constructeur :
L'erreur qu'on serait tenté de faire, c'est d'assigner l'attribut m_chaine directement au tableau chaine qu'on nous envoie, avec un code comme ceci :
Code : C++1
2
3
4 | ZString::ZString(const char *chaine)
{
m_chaine = chaine; // Très mauvaise idée !
}
|
Pourquoi ? Parce qu'en faisant pointer notre attribut m_chaine vers le tableau de char qu'on nous a envoyé, on prend le risque que ce tableau de char soit supprimé par le main !
Dans ce cas, si le tableau est supprimé par le main, notre attribut m_chaine ne pointera plus sur rien et on perdra la chaîne !
La solution ?
Comme on l'a vu dans un des chapitres précédents, il faut copier la chaîne (en appelant une fonction de copie que l'on écrirera) et faire pointer m_chaine vers cette copie.
Code : C++1
2
3
4 | ZString::ZString(const char *chaine)
{
m_chaine = copie(chaine); // Bonne idée : copier la chaîne pour en avoir une version propre à la classe
}
|
Comme notre classe sera la seule à connaître la copie, elle sera sûre que personne d'autre ne la supprimera dans le programme !
Si j'insiste pour faire une copie du tableau, ce n'est pas pour rien. Il faut vraiment être sûr de travailler sur une version du tableau que nous sommes les seuls à connaître dans la classe, car sinon on prend le risque que quelqu'un d'autre la supprime sans notre autorisation.
Je vous propose d'écrire ce constructeur :
Code : C++1
2
3
4
5 | ZString::ZString(const char *chaine)
{
m_chaine = copie(chaine);
m_longueur = longueur(chaine);
}
|
Pour que ce constructeur marche, il nous faut écrire 2 fonctions :
- copie : qui copie un tableau de char et renvoie un pointeur vers la copie (équivalent de strcpy du C).
- longueur : qui calcule la longueur du tableau de char qu'on lui envoie (équivalent de strlen du C).
Ce sont des fonctions que vous avez déjà peut-être écrites si vous avez suivi mon cours de C. C'est un bon exercice que d'essayer de les réécrire.
Je vous donne la solution, sans l'expliquer, parce que ça ça ne devrait pas être nouveau pour vous (ou alors faut revoir votre cours de C sur les chaînes de caractères !) :
Code : C++ 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26 | int ZString::longueur(const char *chaine)
{
int i = 0;
while (chaine[i] != '\0')
{
i++;
}
return i;
}
char *ZString::copie(const char *chaine)
{
int taille = longueur(chaine);
char *chaineCopie = new char[taille + 1]; // +1 pour stocker \0
for (int i = 0 ; i < taille ; i++)
{
chaineCopie[i] = chaine[i];
}
chaineCopie[taille] = '\0';
return chaineCopie;
}
|
Pensez à ajouter le prototype de ces méthodes dans ZString.h
Le constructeur de copie ZString(const ZString &)
Le constructeur de copie est un constructeur très utile qui est appelé dans plusieurs cas par le compilateur. Je ne reviens pas sur ces cas mais je vous invite en revanche à relire la
partie sur le constructeur de copie dans les chapitres précédents.
Le constructeur de copie est un constructeur qui prend en paramètre une référence vers un autre objet du même type.
Voici le constructeur de copie de notre classe ZString :
Code : C++1
2
3
4
5 | ZString::ZString(const ZString &chaine)
{
m_chaine = copie(chaine.m_chaine);
m_longueur = chaine.m_longueur;
}
|
Ce constructeur est à peu de choses près identique au constructeur qu'on vient d'écrire il y a 2 minutes.
La seule différence est qu'il prend en entrée une ZString appelée
chaine. Pour récupérer le tableau de char de la ZString, il suffit d'écrire
chaine.m_chaine. Cela nous permet d'envoyer le tableau de char que les méthodes copie et longueur attendent.
Vous vous demandez peut-être pourquoi on n'a pas tout simplement écrit par exemple :
m_chaine = copie(chaine);
La réponse est simple. Dans ce constructeur :
- chaine est de type ZString (regardez le paramètre d'entrée)
- chaine.m_chaine est de type char *
Or nos méthodes copie et longueur attendent un char *, voilà pourquoi il faut dans ce cas envoyer
chaine.m_chaine.
Comment peut-on avoir le droit d'écrire chaine.m_chaine ? Je croyais que m_chaine était un attribut privé, et donc qu'on ne pouvait pas y accéder ?
Il n'aurait pas fallu créer une méthode accesseur getChaine() plutôt à la place ?
En effet, on aurait très bien pu créer une méthode accesseur
getChaine(). Faites-le si vous voulez d'ailleurs.
Normalement, on n'a pas le droit d'accéder aux membres privés d'une classe. Mais là nous sommes dans une
exception, car nous travaillons dans la même classe (nous sommes dans la classe ZString et nous essayons d'accéder à un attribut privé d'un autre objet de type ZString, ce qui est autorisé).
Le destructeur ~ZString()
On arrive maintenant au destructeur. Son rôle est de détruire les attributs alloués dynamiquement en mémoire avant que l'objet ne soit supprimé (je vous rappelle que le destructeur est automatiquement appelé lorsqu'un objet va être supprimé).
Le seul attribut alloué dynamiquement (avec un new[]), c'est m_chaine. Il faut penser à le supprimer avec un delete[].
Notre destructeur sera tout simple :
Code : C++1
2
3
4 | ZString::~ZString()
{
delete[] m_chaine;
}
|
Si on ne fait pas ça, le tableau de char m_chaine persistera en mémoire après la suppression de l'objet. Du coup, des tableaux "perdus" risqueraient de se ballader en mémoire et on assisterait à ce qu'on appelle des "fuites de mémoire". Votre programme prendrait beaucoup de place en mémoire parce qu'il aurait oublié de supprimer la mémoire dont il n'a plus besoin !
Tester le code
Il est grand temps de compiler pour vérifier qu'on n'a pas fait d'erreur. Pour le moment, on va lancer le main que je vous ai donné au tout début, ce qui va provoquer l'appel du constructeur par défaut :
Code : C++1
2
3
4
5 | int main()
{
ZString chaine;
return 0;
}
|
Compilez, lancez. La console n'affichera rien (c'est normal, tout se passe dans la mémoire) mais si vous n'avez pas de plantage c'est que c'est bon signe déjà
Testons le constructeur qui prend en paramètre un tableau de char pour initialiser la chaîne (celui qu'on a eu tant de mal à écrire, ne me dites pas que vous l'avez déjà oublié

) :
Code : C++1
2
3
4
5 | int main()
{
ZString chaine("Bonjour");
return 0;
}
|
Compilez, lancez. Toujours pas d'erreur ? C'est très bien, c'est qu'on est sur la bonne voie
Hé ! J'ai essayé de faire un cout de ma chaîne et ça ne marche pas ! Pourquoi ?
Supposons que vous essayiez le code suivant :
Code : C++1
2
3
4
5
6
7 | int main()
{
ZString chaine("Bonjour");
cout << chaine;
return 0;
}
|
Le compilateur vous répondra qu'il ne peut pas exécuter le cout car cout ne sait pas lire les objets de type ZString (pour lui c'est comme une boîte noire, il ne sait pas ce qu'il y a à l'intérieur). Il va falloir le lui apprendre en surchargeant l'opérateur << comme on l'a appris dans le chapitre sur la surcharge des opérateurs.
On verra ça un peu plus loin.
Ok, mais en attendant comment je fais pour afficher ce que contient ma chaine de type ZString ?
Comme surcharger l'opérateur << est un peu délicat et compliqué, on ne le verra que plus loin.
En attendant par contre, vous pouvez écrire une méthode afficher() dans la classe ZString qui affichera la chaîne :
Code : C++1
2
3
4 | void ZString::afficher()
{
cout << m_chaine << endl;
}
|
Tout ce que la méthode afficher() fait, c'est afficher la chaîne de caractères qu'elle stocke. Ca consiste à faire un cout de m_chaine. C'est tout bête, mais si vous ne le dites pas à l'ordinateur il ne pourra pas deviner
Dans le main, vous pouvez maintenant afficher votre chaîne !
Code : C++1
2
3
4
5
6
7 | int main()
{
ZString chaine("Bonjour");
chaine.afficher();
return 0;
}
|
Résultat :
Code : Console