C’est parti pour la pratique ! Vous allez ici apprendre à définir une classe en C++ et à l’instancier.
Commencez par ouvrir le dépôt d’exercices dans VSCode.
Ouvrez le fichier chap-02/1-first_class.cpp
, dans lequel on vous fournit le code de la fonction main
.
int main()
{
// Person p;
// p.set_name("Batman");
// p.set_age(23);
// std::cout << "Person named '" << p.get_name() << "' is " << p.get_age() << " years old." << std::endl;
return 0;
}
Vous allez procéder ici en suivant plus ou moins la méthodologie Test Driven Development (ou TDD), qui consiste à écrire le code des tests avant d’écrire le code appelé. Dans votre cas, il ne s’agira pas réellement de TDD (si vous voulez vraiment voir en quoi cela consiste, vous pouvez trouver plein d’exemples sur Internet), mais vous suivrez les étapes ci-dessous afin de vous habituer à écrire le code appelé uniquement à partir de ce que le code appelant requiert :
main
.main
soit décommenté.Pourquoi vous faire faire l’exercice de cette manière ? Déjà, cela vous permet de découvrir un peu des méthodologies qui sont employées en entreprise. Ensuite, parce que le TDD a de gros avantages :
- Vous n’implémentez que du code utile (principes KISS et YAGNI) : pas besoin de réfléchir à ce que vous devez fournir, puisque le code appelant vous dit quoi fournir, et vous perdez moins de temps à écrire du code qui au final ne sert à rien (= code mort).
- Vous ne délivrez que du code testé.
- Vous avez moins d’opportunités de régression, c’est-à-dire d’introduire des bugs dans du code qui fonctionnait à la base, lors d’un refactoring par exemple.
Voici la syntaxe permettant de définir une classe A
:
class A
{
};
Attention à ne pas oublier le point virgule ;
après l’accolade fermante. Il s’agit d’une erreur classique.
Décommentez la première instruction du main
, permettant d’instancier une variable p
de type Person
, et définissez la classe correspondante juste au-dessus du main
.
Compilez et testez que le programme se lance correctement.
#include <iostream>
class Person
{
};
int main()
{
Person p;
...
}
Pour la compilation, on peut se placer dans le répertoire chap-02
et exécuter :
g++ -std=c++17 -o 1-first_class 1-first_class.cpp
Pour définir des membres dans une classe, il faut dans un premier temps choisir leur visibilité. En C++, cela se fait à l’aide des mots-clefs public
, private
et protected
. Dans ce chapitre, nous n’utiliserons que les deux premiers.
Voici un exemple de classe définissant des fonctions-membres publiques et des attributs privés :
class SomeClass
{
public:
void fcn1(int p1, int p2)
{
// code
}
int fcn2(bool p1)
{
// code
}
private:
int _member1 = 0;
std::string _member2;
};
- Nous avons mis la partie publique avant la partie privée, mais on peut très bien faire l’inverse. Il est même possible d’écrire un nouveau bloc public derrière le bloc privé. Restez cependant cohérents et lisibles.
- Ici, toutes les fonctions sont publiques, et tous les attributs sont privés, mais on peut très bien mettre la définition d’une fonction dans le bloc privé ou la définition d’un attribut dans le bloc public.
- Nous avons préfixé les attributs avec _
. Ce n’est ni obligatoire de préfixer, ni d’utiliser _
comme préfixe (certains utilisent m_
ou my
), mais c’est toujours pratique de le faire pour distinguer les attributs des paramètres de même nom.
Vous allez maintenant décommenter l’instruction permettant d’appeler la fonction set_name
sur p
.
Ajoutez ensuite un attribut _name
à la classe Person
et implémentez la fonction-membre set_name
permettant de modifier cet attribut.
Quelle visibilité avez-vous choisi pour _name
et set_name
?
Compilez et testez que le code s’exécute correctement.
Pour les visibilités, _name
doit être privé, car on y accède uniquement depuis une fonction-membre.
En revanche, set_name
doit être publique, car on l’utilise depuis le main
, donc en dehors de la définition de la classe.
Voici le nouveau code de la classe Person
:
class Person
{
public:
void set_name(std::string name) { _name = name; }
private:
std::string _name;
};
Comment s’assurer que le code est juste, sachant que la ligne qui affiche le résultat est toujours commentée ?
C’est la raison pour laquelle je vous ai demandé d’installer un vrai IDE. Nous allons voir comment inspecter les valeurs du programme au cours de l’exécution à l’aide du débuggeur.
Pour commencer, il faut recompiler le programme en ajoutant l’option -g
.
Cette option sert à ajouter les symboles de debug à l’intérieur des fichiers produits par la compilation.
g++ -g -std=c++17 -o 1-first_class 1-first_class.cpp
Ensuite, pour pouvoir lancer le programme depuis VSCode, vous devez configurer le fichier .vscode/launch.json
.
Si ce n’est pas encore fait, rendez vous sur cette section, et remplacez bien le paramètre program
par le chemin de l’exécutable.
Par exemple, si vous avez suivi la correction, vous pouvez indiquer :
"program": "${workspaceFolder}/chap-02/1-first_class",
Une fois l’environnement correctement configuré, il faut ajouter un breakpoint, c’est-à-dire un point d’arrêt : cela permet au programme de se mettre en pause, juste avant l’exécution d’une instruction particulière.
Par défaut, VSCode propose des raccourcis clavier pour les différentes fonctions du débuggeur. Les plus utiles sont :
Ici, vous allez placer un breakpoint sur l’instruction return 0;
du main
.
Pour cela, placez votre curseur sur la ligne en question et appuyez sur F9. Vous pouvez aussi cliquer directement à gauche du numéro de ligne.
Utilisez ensuite F5 pour lancer le programme. L’éditeur devrait prendre cette apparence, indiquant que le programme est en pause juste avant l’exécution de l’instruction surlignée :
Ouvrez maintenant le panneau d’exécution en allant dans View > Run
.
Ce panneau contient 3 sections :
8 + 3 * 5
pour obtenir le résultat du calcul, ou p._name
pour obtenir la valeur de l’attribut _name
de p
, ou encore &p
pour récupérer l’adresse de p
.main
, mais nous y reviendrons.Dans la section des Variables, si vous pouvez constater comme sur le screenshot que p._name
vaut bien "Batman"
, alors c’est que votre code est correct.
Appuyez ensuite sur F5 pour reprendre l’exécution du programme.
Décommentez l’instruction faisant l’appel à set_age
, et définissez la fonction ainsi que l’attribut correspondants.
Quel type avez-vous utilisé pour l’âge de Batman ?
Pour _age
, le mieux est d’utiliser un entier non-signé. En effet, ce n’est pas possible d’avoir un âge négatif, donc utiliser un unsigned int
plutôt qu’un int
permet de réduire la possibilité d’avoir un mauvais usage. Vous pouvez également décider d’utiliser un unsigned short
, plutôt que unsigned int
, puisque même Batman ne vivra pas si vieux.
Je déconseille l’utilisation de unsigned char
cependant, car char
est associé à la notion de caractères. Donc bien que l’espace soit suffisant pour contenir un âge humain, il est plus clair d’utiliser un vrai type entier.
Évidemment, ce genre de bonnes pratiques ne peut s’appliquer que dans le cas où il n’y a pas de contrainte critique d’utilisation de la mémoire.
Voici le nouveau code :
class Person
{
public:
void set_name(const std::string& name) { _name = name; }
void set_age(unsigned int age) { _age = age; }
private:
std::string _name;
unsigned int _age;
};
Testez à nouveau votre code avec le débuggeur. En plus du breakpoint final, ajoutez un breakpoint supplémentaire de manière à vous arrêter juste après l’exécution du set_name
et juste avant l’exécution du set_age
.
Pour s’arrêter au bon endroit, il faut placer le breakpoint sur la ligne de l’instruction p.set_age(23);
.
Si vous inspectez la valeur de p._age
avant l’exécution de set_age
, il est possible que celle-ci soit complètement aléatoire. Eh oui, de la même manière que les variables locales de types fondamentaux, il faut également initialiser les attributs de types fondamentaux de vos classes. Faites les changements nécessaires pour que l’âge de Batman vaille 0 tant que celui-ci n’a pas été modifié. Testez à nouveau.
private:
std::string _name;
unsigned int _age = 0u;
0u
permet de faire référence au 0
entier non-signé. Cela n’a pas beaucoup d’importance ici, puisque _age
est explicitement typé, mais si on écrivait dans une fonction auto v = 0u;
alors v
serait de type unsigned int
plutôt que de type int
.
Il ne reste plus qu’une seule ligne à décommenter. Vous devrez ici implémenter deux accesseurs (ou getters), un pour le nom et un pour l’âge.
Les accesseurs sont des fonctions qui ne sont pas censées modifier l’état de l’objet lorsqu’elles sont appelées. En C++, on spécifie cette garantie en plaçant le mot-clef const
à la fin de la signature de la fonction.
public:
int get_value() const { return _value; }
Dès lors qu’une fonction-membre est marquée const
, le compilateur va vérifier qu’aucune modification n’est effectuée sur les attributs de l’objet. Le code suivant ne compilera donc pas :
class SomeClass
{
public:
int set_and_get(int value) const
{
_value = value;
return _value;
}
private:
int _value = 0;
};
Il est extrêmement facile d’omettre d’ajouter const
aux fonctions qui ne sont pas censées modifier l’état de l’objet.
Parfois, ces indications ne sont pas nécessaires pour compiler et exécuter le programme, et c’est donc difficile de prendre l’habitude de les ajouter.
Cependant, même si le compilateur peut s’en passer, inclure le mot-clé const
est une information précieuse pour les programmeurs qui liront votre code. Cela permet d’identifier rapidement ce qui peut être modifié et ce qui restera constant pendant l’exécution.
Par conséquent, il est fortement recommandé de se relire systématiquement après l’ajout d’une fonction-membre et de se demander si cette fonction est censée modifier la classe. Si ce n’est pas le cas, pensez à ajouter const
💪
Revenons à notre exercice. Décommentez la dernière instruction du main
, implémentez les deux accesseurs nécessaires (sans oublier leur const
🙃) et vérifiez que le programme fonctionne.
public:
std::string get_name() const { return _name; }
unsigned int get_age() const { return _age; }
...
;
public:
ou private:
devant un groupe de champs pour indiquer s’ils sont publics ou privés.