Chapitre 5 • forme canonique
Cartouche
| Champ | Valeur |
|---|---|
| Auteur·e | Élise |
| Édition | 2023-09-22 |
| Durée | une journée |
| Taille des équipes | 1 personne |
| Rendu | via git, dépôt 2023_cpp_c5, droits en lecture à delivery_collector |
Barème
| Critère | Points |
|---|---|
| Étape 1 : construction | 3 |
| Étape 2 : std::cout | 3 |
| Étape 3 : ré-assignation | 3 |
| Étape 4 : modification d’un char | 4 |
| Étape 5 : itération façon C | 3 |
| Étape 6 : itération façon C++ | 4 |
| Total des critères | 20 |
| Note maximale | 20 |
| Fautes de norme | -0,5 par ligne |
Règlement
La réalisation de cet exercice est assujettie aux règles en vigueur dans l’école en matière de triche et de normalisation. Tricher vous expose à de graves sanctions.
Vous devez être seul·e auteur·e de votre rendu mais vous êtes autorisés et encouragés à communiquer dans le cadre de ce sujet.
Introduction
Forme canonique
Lorsque vous manipulez des types scalaires, vous vous attendez à pouvoir faire des opérations du genre variable_a = variable_b; ou encore, en C++ int a(b);
Ces syntaxes s’appellent respectivement une assignation (ou une affectation) et une construction par copie.
Dans une assignation, on écrase le contenu d’une variable par le résultat de l’expression en deuxième opérande.
Dans une construction par copie, on créé une nouvelle variable avec immédiatement la même valeur qu’une autre (ou une expression du même type).
Ces opérations, vous pouvez les faire sans trop réfléchir avec des classes de la bibliothèque standard. Comme avec une chaîne de caractères par exemple.
int main(void)
{
// construction par copie (avec un autre type source)
std::string str_1("hello");
// construction par copie
std::string str_2(str_1);
// construction par défaut (dans ce cas, initialisée à vide)
std::string str_3;
// affectation
// (on écrase ce qu'il y avait dans la chaîne (rien)
// et on le remplace par un copie de ce qu'il y
// avait dans str_1 ("hello"))
str_3 = str_1;
// affectation
// (on écrase le "hello" contenu et on le remplace par "toto")
str_2 = "tata";
// affectation
// (pareil mais avec un `std::string &` au lieu d'un `char *`)
str_1 = str_2;
}
Ces fonctionnalités n’existent pas nécessairement automatiquement, en particulier lorsqu’on fait usage d’allocation dynamique (comme le font std::string ou std::vector).
Pour que ces assignations et ces copies soient possibles, ces classes implémentent la forme canonique de Coplien, qu’on appelle plus simplement forme de Coplien ou forme canonique.
Cette forme se présente ainsi :
class T {
T();
T(const T &other);
~T();
T &operator=(const T &rhs);
}
Pour être conforme à la forme canonique, une classe doit implémenter :
- un constructeur par défaut ;
- un constructeur par copie ;
- un destructeur ; et
- une surcharge de l’opérateur
=qui permet l’affectation.
Après une copie d’un objet, les deux objets doivent pouvoir exister indépendamment. Cela signifie que les deux objets ne peuvent pas partager une même allocation mémoire. Le deuxième objet doit faire ses propres allocations. Il doit copier le contenu pointé par les allocations mémoire du premier objet. Il ne doit pas copier les adresses du premier objet.
Allocation dynamique
Au stade où nous en sommes, il est probable que vous ayez l’impression de ne pas avoir besoin d’allocation dynamique en C++.
Pour cause, l’allocation dynamique depuis le début ce n’est pas vous qui vous en occupez mais les objets de la STL que vous utilisez.
std::string, std::vector, etc. toutes ces classes font de l’allocation dynamique pour accommoder une taille variable d’éléments que vous essayez de stocker.

Cependant, ces fonctions n’utilisent pas malloc et free. Elles utilisent new et delete. À la différence de leur équivalent en C, new et delete ne sont pas des fonctions. Ce sont des mots clés. Leur utilité réside dans le fait qu’elles sont capable d’appeler les constructeurs et destructeurs des données pour lesquelles ont fait une allocation mémoire.
Voici quelques exemples d’utilisation :
#include <string>
#include <iostream>
#include <cstring>
int main(void)
{
{
std::string *str_ptr{new std::string};
*str_ptr = "toto";
std::cout << *str_ptr << "\n";
delete str_ptr;
}
{
std::string *str_ptr;
str_ptr = new std::string("toto");
std::cout << *str_ptr << "\n";
delete str_ptr;
}
{
char *c_str;
c_str = new char[5];
std::strcpy(c_str, "toto");
std::cout << c_str << "\n";
delete[] c_str;
}
}
Tâches
Étape 1 : construction d’une chaîne
Implémentez la classe stu::string qui représente une chaîne de caractères.
Cette classe doit répondre aux exigences suivantes :
- construction par défaut avec chaîne vide ;
- construction par copie avec un
const stu::string &; - construction par copie avec un
const char *; - toute mémoire allouée doit être libérée avant de devenir inatteignable (soit à la destruction, soit lors d’une nouvelle assignation du pointeur) ;
- une modification sur une
stu::stringresultant d’une copie ne doit pas modifier l’original ; - une modification sur une
stu::stringayant été copiée ne doit pas modifier la copie non plus ; - fonction
dumpaffiche le contenu de la chaîne sur la sortie standard ; - votre tableau de caractères doit être privé.
Étape 2 : std::cout
L’objectif de cette étape c’est de pouvoir afficher votre stu::string comme une std::string, avec les opérateurs de flux et std::cout.
#include <iostream>
#include "string.hpp"
int main(void)
{
stu::string str{"hello"};
std::cout << str << "\n";
}
Vous devez écrire une surcharge de l’opérateur <<. Dans votre fichier .hpp, en dehors de la déclaration de la classe, vous devez ajouter :
#include <ostream>
std::ostream &operator<<(std::ostream &, const stu::string &);
Enfin, dans votre fichier .cpp vous devez implémenter cette fonction.
Une fois réussi, supprimez la fonction membre dump de votre stu::string et retirez votre inclusion d’iostream dans votre fichier .hpp.
Étape 3 : ré-assignation
Pour réussir cette étape, il doit être possible de changer la chaîne contenue dans votre stu::string avec une simple assignation.
int main(void)
{
stu::string str("toto");
stu::string str2("tata");
str2 = str;
str = "hello";
std::cout << "should be \"hello\"" << str << "\n";
std::cout << "should be \"toto\"" << str2 << "\n";
}
Une assignation doit remplacer entièrement le contenu précédemment stocké dans la chaîne. Si vous réassignez un pointeur que vous avez précédemment alloué, n’oublier pas de le libérer.
Dans la déclaration de votre classe, vous pouvez surcharger l’opérateur d’assignation en ajoutant une fonction membre avec la signature suivante :
class string {
// [...]
string &operator=(const char *rhs);
string &operator=(const string &rhs);
// [...]
};
Étape 4 : modification d’un caractère
Pour cette étape, on va ajouter la possibilité de faire ceci :
int main(void)
{
stu::string str("hello");
str[1] = 'a';
std::cout << "should print hallo: " << str << "\n";
}
On a encore besoin de faire une surcharge d’opérateur, mais cette fois-ci sur operator[]. À votre avis quel sera le type du paramètre de cet opérateur ?
Et selon vous quel outil du langage devons-nous utiliser pour que la ligne str[1] = 'a' permette de modifier un caractère au sein de la zone mémoire à l’intérieur du stu:string ?
Étape 5 : itération façon C
Faites en sorte que le code suivant fonctionne :
void print_by_char(const stu::string &str)
{
unsigned int idx{0};
auto len{str.size()};
while (idx < len) {
std::cout << str[idx];
idx += 1;
}
std::cout << "\n";
}
int main(void)
{
stu::string str("hello");
print_by_char(str);
}
Étape 6 : itération façon C++
Pour cette dernière étape, implémentez une classe d’itérateur pour votre chaîne de caractères. Vous êtes complètement libres de son implémentation dès lors que le comportement ci-dessous est satisfait.
void print_with_iter(const stu::string &str)
{
auto iter{str.begin()};
while (iter != str.end()) {
std::cout << *iter;
iter += 1;
}
std::cout << "\n";
}
int main(void)
{
stu::string str("hello");
print_with_iter(str);
}