Aller au contenu

Module 01 : Allocation mémoire, Références et Pointeurs

Télécharger le PDF du sujet officiel

Concepts clés :

  • Allocation Stack vs Heap
  • Opérateurs new et delete
  • Références vs Pointeurs
  • Pointeurs vers membres
  • Instructions switch

Comprendre la mémoire est crucial pour écrire du code C++ correct et efficace. Voici pourquoi ces concepts sont importants :

La stack est rapide mais limitée—comme un petit bureau où vous travaillez sur la tâche en cours. Le heap est plus grand mais nécessite une gestion manuelle—comme un entrepôt où vous stockez des objets à long terme.

Pourquoi les Références plutôt que les Pointeurs ?

Section intitulée « Pourquoi les Références plutôt que les Pointeurs ? »

Les références sont des alias plus sûrs pour des variables existantes. Elles ne peuvent pas être null et ne peuvent pas être réassignées. Utilisez-les quand vous devez passer de grands objets efficacement sans copie.

En C++, new et delete font plus qu’allouer de la mémoire—ils appellent les constructeurs et destructeurs. Cela garantit que les objets sont correctement initialisés et nettoyés.


Ce module introduit les opérateurs de gestion mémoire. Voici ce qu’ils signifient :

int* ptr = new int; // Alloue la mémoire pour un int
int* arr = new int[10]; // Alloue la mémoire pour 10 ints
  • new alloue de la mémoire sur le heap (aussi appelé “free store”)
  • Il retourne un pointeur vers la mémoire allouée
  • La mémoire allouée avec new persiste jusqu’à ce que vous la delete explicitement
  • Contrairement à malloc(), new calcule automatiquement la taille et initialise correctement les objets
delete ptr; // Libère la mémoire pour un objet
delete[] arr; // Libère la mémoire pour un tableau (NOTEZ les [] !)
  • delete libère la mémoire qui a été allouée avec new
  • delete[] (avec crochets) libère les tableaux alloués avec new[]
  • CRITIQUE : Utiliser delete sur un tableau est un comportement indéfini !
  • CRITIQUE : Utiliser delete[] sur un objet unique est un comportement indéfini !
int& ref = x; // ref est un alias (un autre nom) pour x
  • & après un type crée une référence (pas un pointeur)
  • Une référence est juste un autre nom pour une variable existante
  • Quand vous changez ref, vous changez x - c’est la même variable !
  • Les références doivent être initialisées à la déclaration
  • Les références ne peuvent pas être réassignées pour référencer une variable différente
int* ptr = &x; // ptr stocke l'adresse de x (ptr "pointe vers" x)
*ptr = 42; // Déréférence ptr pour accéder/modifier x
ptr->method(); // Si ptr pointe vers un objet, appelle sa méthode
  • Rappel : * avant un pointeur le déréférence (accède à la valeur)
  • & avant une variable obtient son adresse
  • -> est un raccourci pour déréférencement + accès membre
void (Class::*ptr)() = &Class::method; // ptr pointe vers une fonction membre
(object.*ptr)(); // Appel via objet
(objectPtr->*ptr)(); // Appel via pointeur vers objet
  • ->* combine l’opérateur flèche avec le pointeur-vers-membre
  • Il est utilisé quand vous avez un pointeur vers un objet ET un pointeur vers l’un de ses membres
  • C’est une syntaxe avancée utilisée pour les pointeurs de fonction vers des fonctions membres

Comprendre comment la mémoire est organisée vous aide à écrire un meilleur code C++ :

┌─────────────────────────────────────┐
│ MÉMOIRE HAUTE │
│ │
│ ┌──────────────────────────────┐ │
│ │ HEAP │ │ ← Allocation dynamique (new/delete)
│ │ (croît vers le bas) │ │ Grand, plus lent, nettoyage manuel
│ │ [Zombie* z] │ │
│ │ [Brain* b] ← new Brain() │ │
│ └──────────────────────────────┘ │
│ │
│ ↓ écart ↓ │
│ │
│ ┌──────────────────────────────┐ │
│ │ STACK │ │ ← Allocation automatique
│ │ (croît vers le haut) │ │ Rapide, petit, automatique
│ │ │ │
│ │ ┌────────────────────┐ │ │
│ │ │ main() │ │ │
│ │ │ int x = 42; │ │ │
│ │ │ Zombie z; │ ← constructeur appelé
│ │ │ │ │ │
│ │ └────────────────────┘ │ │
│ └──────────────────────────────┘ │
│ │
│ ┌──────────────────────────────┐ │
│ │ SEGMENT DATA │ │ ← Variables globales/statiques
│ └──────────────────────────────┘ │
│ │
│ ┌──────────────────────────────┐ │
│ │ SEGMENT CODE │ │ ← Instructions du programme
│ └──────────────────────────────┘ │
│ │
│ MÉMOIRE BASSE │
└─────────────────────────────────────┘
┌─────────────────────────────────────┐
│ Cadre de pile pour function() │
├─────────────────────────────────────┤
│ Adresse de retour │ ← Où revenir
├─────────────────────────────────────┤
│ Registres sauvegardés │ ← État précédent
├─────────────────────────────────────┤
│ Variables locales : │
│ int x = 42; → [ 42 ] │
│ Zombie z; → [ objet ] │
├─────────────────────────────────────┤
│ Paramètres de fonction │
└─────────────────────────────────────┘
La pile croît VERS LE HAUT
┌─────────────────────────────────────┐
│ Objet Contact sur la STACK │
├─────────────────────────────────────┤
│ vptr (caché) → vtable │ (seulement si la classe a des fonctions virtual)
├─────────────────────────────────────┤
│ std::string _firstName │
│ ├─ pointeur vers mémoire heap │
│ └─ info taille/capacité │
├─────────────────────────────────────┤
│ std::string _lastName │
├─────────────────────────────────────┤
│ int _age = 25; → [ 25 ] │
└─────────────────────────────────────┘

void function() {
int x = 42; // Sur la stack
Zombie zombie; // Sur la stack
} // Automatiquement détruit ici

Caractéristiques :

  • Allocation/désallocation rapide
  • Taille limitée (~1-8 Mo typiquement)
  • Nettoyage automatique quand la portée se termine
  • Ne peut pas survivre à la fonction
void function() {
int* x = new int(42); // Sur le heap
Zombie* zombie = new Zombie(); // Sur le heap
// ... les utiliser ...
delete x; // Doit supprimer manuellement
delete zombie; // Doit supprimer manuellement
}

Caractéristiques :

  • Allocation plus lente
  • Grande capacité (limitée par la RAM)
  • Doit être libérée manuellement
  • Peut survivre à la fonction (retourner le pointeur)

// Allocation
int* ptr = new int; // Non initialisé
int* ptr = new int(); // Initialisé à zéro
int* ptr = new int(42); // Initialisé à 42
// Désallocation
delete ptr;
ptr = NULL; // Bonne pratique (C++98)
// Allocation
int* arr = new int[10]; // Tableau de 10 ints
Zombie* horde = new Zombie[5]; // Tableau de 5 Zombies
// Désallocation - NOTEZ LES []
delete[] arr; // DOIT utiliser delete[] pour les tableaux
delete[] horde;
// FAUX : Utiliser delete sur un tableau
int* arr = new int[10];
delete arr; // COMPORTEMENT INDÉFINI !
// FAUX : Utiliser delete[] sur un objet unique
int* ptr = new int(42);
delete[] ptr; // COMPORTEMENT INDÉFINI !
// FAUX : Double delete
int* ptr = new int(42);
delete ptr;
delete ptr; // CRASH ou corruption !
Utiliser Stack quand…Utiliser Heap quand…
Durée de vie de l’objet = portée fonctionL’objet doit survivre à la fonction
Taille connue à la compilationTaille déterminée à l’exécution
Petits objetsGrands objets
Performance critiqueRetourner de nouveaux objets

Une référence est un alias - un autre nom pour une variable existante.

int x = 42;
int& ref = x; // ref EST x (pas une copie, pas un pointeur)
ref = 100; // Change x à 100
std::cout << x; // Affiche 100
CaractéristiqueRéférencePointeur
Syntaxeint& ref = x;int* ptr = &x;
Peut être nullNonOui
Peut être réassignéNonOui
Doit être initialiséOuiNon
Accéder à la valeurref*ptr
Accéder à l’adresse&refptr
// DOIT être initialisée
int& ref; // ERREUR : les références doivent être initialisées
int& ref = x; // OK
// NE PEUT PAS être null
int& ref = NULL; // ERREUR : ne peut pas lier à null
// NE PEUT PAS être réassignée
int& ref = x;
ref = y; // Ceci ne réassigne pas ref, ça copie y dans x !
// MAUVAIS : Passage par valeur (copie tout l'objet)
void printZombie(Zombie z) {
// z est une COPIE - coûteux pour les grands objets
}
// MIEUX : Passage par pointeur
void printZombie(Zombie* z) {
if (z != NULL) // Doit vérifier null
std::cout << z->getName();
}
// MEILLEUR : Passage par référence
void printZombie(Zombie& z) {
// z est l'objet original - pas de copie
// Ne peut pas être null - pas de vérification nécessaire
std::cout << z.getName();
}
// Référence CONST - accès en lecture seule
void printZombie(const Zombie& z) {
std::cout << z.getName(); // Peut lire
// z.setName("X"); // ERREUR : z est const
}
SituationUtiliser
Jamais null, ne changera pas ce qu’il référenceRéférence
Pourrait être nullPointeur
Doit être réassignéPointeur
Retourner un nouvel objet depuis une fonctionPointeur (ou smart pointer en C++ moderne)
Paramètre optionnelPointeur (peut passer NULL)

std::string str = "HI THIS IS BRAIN";
std::string* stringPTR = &str; // Pointeur VERS str
std::string& stringREF = str; // Référence (alias) DE str
// Adresses - toutes devraient être identiques !
std::cout << &str << std::endl; // Adresse de str
std::cout << stringPTR << std::endl; // Le pointeur contient la même adresse
std::cout << &stringREF << std::endl; // Adresse de ref = adresse de str
// Valeurs - toutes devraient être identiques !
std::cout << str << std::endl; // Accès direct
std::cout << *stringPTR << std::endl; // Déréférencer le pointeur
std::cout << stringREF << std::endl; // La référence est identique à l'original

5. Référence vs Pointeur dans les classes (ex03)

Section intitulée « 5. Référence vs Pointeur dans les classes (ex03) »
class HumanA {
private:
std::string _name;
Weapon& _weapon; // Référence - DOIT toujours avoir une arme
public:
// DOIT initialiser la référence dans le constructeur
HumanA(std::string name, Weapon& weapon)
: _name(name), _weapon(weapon) {}
void attack() {
std::cout << _name << " attaque avec " << _weapon.getType();
}
};
// Utilisation
Weapon club("club");
HumanA bob("Bob", club); // DOIT fournir une arme à la construction
class HumanB {
private:
std::string _name;
Weapon* _weapon; // Pointeur - pourrait ne pas avoir d'arme
public:
HumanB(std::string name) : _name(name), _weapon(NULL) {}
void setWeapon(Weapon& weapon) {
_weapon = &weapon;
}
void attack() {
if (_weapon)
std::cout << _name << " attaque avec " << _weapon->getType();
else
std::cout << _name << " n'a pas d'arme";
}
};
// Utilisation
HumanB jim("Jim"); // Pas d'arme initialement
jim.setWeapon(club); // Arme ajoutée plus tard
  • Référence : Quand l’objet DOIT exister à la construction et pendant toute la durée de vie
  • Pointeur : Quand l’objet pourrait ne pas exister, ou pourrait changer

void sayHello() { std::cout << "Hello"; }
void sayBye() { std::cout << "Bye"; }
// Pointeur de fonction
void (*funcPtr)() = &sayHello;
funcPtr(); // Appelle sayHello()
funcPtr = &sayBye;
funcPtr(); // Appelle sayBye()
class Harl {
public:
void debug() { std::cout << "Debug"; }
void info() { std::cout << "Info"; }
void warning() { std::cout << "Warning"; }
void error() { std::cout << "Error"; }
void complain(std::string level) {
// Tableau de pointeurs de fonctions membres
void (Harl::*funcs[4])() = {
&Harl::debug,
&Harl::info,
&Harl::warning,
&Harl::error
};
std::string levels[4] = {"DEBUG", "INFO", "WARNING", "ERROR"};
for (int i = 0; i < 4; i++) {
if (level == levels[i]) {
(this->*funcs[i])(); // Appelle la fonction membre
return;
}
}
}
};
// Déclaration
void (ClassName::*pointerName)(parameters);
// Assignation
pointerName = &ClassName::methodName;
// Appel via objet
(object.*pointerName)(args);
// Appel via pointeur vers objet
(objectPtr->*pointerName)(args);

switch (expression) {
case value1:
// code
break;
case value2:
// code
break;
default:
// code si pas de correspondance
}
// SANS break - tombe dans le case suivant
switch (level) {
case 3: // WARNING
std::cout << "Message warning\n";
// Pas de break - tombe !
case 2: // INFO
std::cout << "Message info\n";
// Pas de break - tombe !
case 1: // DEBUG
std::cout << "Message debug\n";
break;
}
// Si level = 3 : affiche Warning, Info, Debug
// Si level = 2 : affiche Info, Debug
// Si level = 1 : affiche Debug
// Ne peut faire un switch que sur des types intégraux en C++98
switch (number) { ... } // OK
switch (character) { ... } // OK
switch (string) { ... } // ERREUR en C++98 !
// Pour les chaînes, doit utiliser if-else
if (str == "DEBUG") { ... }
else if (str == "INFO") { ... }

Puisque C++98 ne peut pas faire de switch sur les chaînes, convertissez d’abord les chaînes en entiers :

// Convertir chaîne en index pour switch
int getLevel(const std::string& level) {
std::string levels[4] = {"DEBUG", "INFO", "WARNING", "ERROR"};
for (int i = 0; i < 4; i++) {
if (level == levels[i])
return i;
}
return -1; // Non trouvé
}
void complain(const std::string& level) {
switch (getLevel(level)) {
case 0:
std::cout << "Message debug" << std::endl;
break;
case 1:
std::cout << "Message info" << std::endl;
break;
case 2:
std::cout << "Message warning" << std::endl;
break;
case 3:
std::cout << "Message error" << std::endl;
break;
default:
std::cout << "Niveau inconnu" << std::endl;
}
}

C’est plus propre qu’une longue chaîne if-else et permet le comportement fall-through.


#include <fstream>
#include <string>
std::ifstream inFile("input.txt");
if (!inFile.is_open()) {
std::cerr << "Impossible d'ouvrir le fichier" << std::endl;
return 1;
}
std::string line;
while (std::getline(inFile, line)) {
std::cout << line << std::endl;
}
inFile.close();
std::ofstream outFile("output.txt");
if (!outFile.is_open()) {
std::cerr << "Impossible de créer le fichier" << std::endl;
return 1;
}
outFile << "Hello, World!" << std::endl;
outFile << "Ligne 2" << std::endl;
outFile.close();
std::ifstream inFile("input.txt");
std::string content;
std::string line;
while (std::getline(inFile, line)) {
content += line;
content += "\n";
}

Cet exercice vous demande de créer une classe Zombie avec deux fonctions :

  • newZombie(std::string name) : Crée un zombie sur le HEAP, retourne un pointeur
  • randomChump(std::string name) : Crée un zombie sur la STACK, s’annonce lui-même

L’insight clé : quand la mémoire doit-elle survivre au-delà d’un appel de fonction ?

Étape 1 - Comprendre l’exigence de durée de vie

Posez-vous : “L’appelant a-t-il besoin du zombie après le retour de la fonction ?”

  • newZombie : OUI → Allocation heap (survit au retour de fonction)
  • randomChump : NON → Allocation stack (nettoyage auto)

Étape 2 - Concevoir la classe Zombie

  • Attribut privé _name
  • Constructeur qui définit le nom
  • Destructeur qui affiche “[nom] is dead”
  • Méthode announce() qui affiche “[nom]: BraiiiiiiinnnzzzZ…”

Étape 3 - Implémenter les deux stratégies d’allocation

Étape 1 - Classe Zombie :

Zombie.hpp
#ifndef ZOMBIE_HPP
#define ZOMBIE_HPP
#include <string>
class Zombie {
private:
std::string _name;
public:
Zombie(std::string name);
~Zombie();
void announce() const;
};
#endif

Étape 2 - Implémentation :

Zombie.cpp
#include "Zombie.hpp"
#include <iostream>
Zombie::Zombie(std::string name) : _name(name) {}
Zombie::~Zombie() {
std::cout << _name << " is dead." << std::endl;
}
void Zombie::announce() const {
std::cout << _name << ": BraiiiiiiinnnzzzZ..." << std::endl;
}

Étape 3 - Les deux fonctions d’allocation :

newZombie.cpp
#include "Zombie.hpp"
Zombie* newZombie(std::string name) {
return new Zombie(name); // Heap - l'appelant doit delete !
}
randomChump.cpp
#include "Zombie.hpp"
void randomChump(std::string name) {
Zombie zombie(name); // Stack - détruit auto à la fin de la fonction
zombie.announce();
}
// Destructeur appelé ICI automatiquement
LigneCodePourquoi
new Zombie(name)Alloue sur le heapLa mémoire survit au retour de fonction
return new Zombie(...)Retourne le pointeurL’appelant obtient la propriété
Zombie zombie(name)Allocation stackRapide, nettoyage automatique
Fin de randomChumpDestructeur s’exécuteVariables stack nettoyées

1. Fuites mémoire avec newZombie

// FAUX - fuite mémoire !
newZombie("Bob"); // Créé mais jamais supprimé
// JUSTE
Zombie* z = newZombie("Bob");
z->announce();
delete z; // Nettoyage !

2. Retourner un pointeur stack (comportement indéfini)

// FAUX - retourne l'adresse d'un objet mort !
Zombie* badZombie(std::string name) {
Zombie z(name);
return &z; // z meurt ici, le pointeur est dangling !
}

3. Oublier le message du destructeur

Le sujet exige que le destructeur affiche un message. Les évaluateurs vérifieront !

Fenêtre de terminal
# Compiler
c++ -Wall -Wextra -Werror *.cpp -o zombie
# Tester le zombie heap
./zombie
# Devrait voir : message announce, puis plus tard "X is dead" quand supprimé
# Surveiller les fuites mémoire avec valgrind
valgrind --leak-check=full ./zombie
Zombie.hpp
#ifndef ZOMBIE_HPP
#define ZOMBIE_HPP
#include <string>
class Zombie {
private:
std::string _name;
public:
Zombie(std::string name);
~Zombie();
void announce() const;
};
Zombie* newZombie(std::string name);
void randomChump(std::string name);
#endif

Créez une fonction zombieHorde(int N, std::string name) qui :

  • Alloue N zombies en une seule allocation
  • Initialise tous les zombies avec le même nom
  • Retourne un pointeur vers le premier zombie

Contrainte clé : UNE allocation, pas N appels séparés à new.

Étape 1 - Utiliser la syntaxe d’allocation de tableau

new Zombie[N] alloue N zombies de manière contiguë.

Étape 2 - Gérer l’exigence du constructeur par défaut

L’allocation de tableau appelle le constructeur par défaut. Vous devez en ajouter un !

Étape 3 - Initialiser les noms après l’allocation

Puisque le constructeur par défaut ne peut pas prendre de paramètres, ajoutez une méthode setName().

Étape 1 - Ajouter constructeur par défaut et setter :

Zombie.hpp
class Zombie {
private:
std::string _name;
public:
Zombie(); // Constructeur par défaut pour allocation tableau
Zombie(std::string name);
~Zombie();
void setName(std::string name); // Définir le nom après construction
void announce() const;
};

Étape 2 - Implémenter zombieHorde :

zombieHorde.cpp
#include "Zombie.hpp"
Zombie* zombieHorde(int N, std::string name) {
if (N <= 0)
return NULL;
// Allocation unique pour tous les N zombies
Zombie* horde = new Zombie[N];
// Initialiser le nom de chaque zombie
for (int i = 0; i < N; i++)
horde[i].setName(name);
return horde;
}
LigneCodePourquoi
new Zombie[N]Allocation tableauBloc contigu unique
horde[i].setName(name)Initialise chacunConstructeur par défaut a laissé nom vide
return hordeRetourne premier élémentLe tableau se dégrade en pointeur

1. Utiliser delete au lieu de delete[]

Zombie* horde = new Zombie[N];
// FAUX - comportement indéfini !
delete horde;
// JUSTE - doit utiliser delete[] pour les tableaux
delete[] horde;

2. Faire N allocations séparées

// FAUX - N allocations, pas une !
for (int i = 0; i < N; i++)
horde[i] = new Zombie(name);
// JUSTE - allocation unique
Zombie* horde = new Zombie[N];

3. Retourner un tableau stack

// FAUX - le tableau stack meurt au retour !
Zombie* zombieHorde(int N, std::string name) {
Zombie horde[N]; // VLA sur stack (aussi non-standard !)
return horde; // Pointeur dangling !
}
Fenêtre de terminal
# Tester création et nettoyage de horde
int main() {
Zombie* horde = zombieHorde(5, "Walker");
for (int i = 0; i < 5; i++)
horde[i].announce();
delete[] horde; // Devrait voir 5 messages "is dead"
return 0;
}
zombieHorde.cpp
#include "Zombie.hpp"
Zombie* zombieHorde(int N, std::string name) {
if (N <= 0)
return NULL;
Zombie* horde = new Zombie[N];
for (int i = 0; i < N; i++)
horde[i].setName(name);
return horde;
}

Démontrez que les références sont des alias en montrant :

  • Une variable string
  • Un pointeur vers cette string
  • Une référence vers cette string

Les trois devraient afficher la même adresse et la même valeur.

C’est une simple démonstration. L’insight clé : une référence EST la variable originale, juste avec un autre nom.

main.cpp
#include <iostream>
#include <string>
int main() {
std::string str = "HI THIS IS BRAIN";
std::string* stringPTR = &str; // Pointeur stocke l'adresse
std::string& stringREF = str; // Référence EST str
// Afficher les adresses - toutes devraient être identiques
std::cout << "Adresse de str : " << &str << std::endl;
std::cout << "Adresse dans stringPTR : " << stringPTR << std::endl;
std::cout << "Adresse de stringREF : " << &stringREF << std::endl;
// Afficher les valeurs - toutes devraient être identiques
std::cout << "Valeur de str : " << str << std::endl;
std::cout << "Valeur via stringPTR : " << *stringPTR << std::endl;
std::cout << "Valeur de stringREF : " << stringREF << std::endl;
return 0;
}
LigneCodePourquoi
std::string* stringPTR = &strStocker adressePointeur contient adresse mémoire
std::string& stringREF = strCréer aliasRéférence EST l’original
&strObtenir adresseOpérateur address-of
&stringREFObtenir adresseMême que &str !
*stringPTRDéréférencerObtenir valeur à l’adresse
stringREFUtiliser directementPas de déréférencement nécessaire

1. Confondre les significations de &

// Dans une déclaration : & signifie "type référence"
std::string& ref = str;
// Dans une expression : & signifie "adresse de"
std::cout << &str;

2. Penser que la référence est une copie

std::string& ref = str;
ref = "CHANGED";
// str est aussi "CHANGED" - c'est la même chose !
Fenêtre de terminal
./brain
# La sortie devrait montrer la même adresse 3 fois
# La sortie devrait montrer la même valeur 3 fois
main.cpp
#include <iostream>
#include <string>
int main() {
std::string str = "HI THIS IS BRAIN";
std::string* stringPTR = &str;
std::string& stringREF = str;
std::cout << &str << std::endl;
std::cout << stringPTR << std::endl;
std::cout << &stringREF << std::endl;
std::cout << str << std::endl;
std::cout << *stringPTR << std::endl;
std::cout << stringREF << std::endl;
return 0;
}

Créez des classes démontrant quand utiliser une référence vs un pointeur comme membre de classe :

  • HumanA : A toujours une arme → utiliser une référence
  • HumanB : Pourrait ne pas avoir d’arme → utiliser un pointeur

L’arme est partagée - quand elle change, les deux humains voient le changement.

Étape 1 - Comprendre la décision de conception

HumanAHumanB
Doit avoir une arme à la constructionPeut ne pas avoir d’arme
L’arme ne change jamaisL’arme peut être définie plus tard
Utiliser Weapon& (référence)Utiliser Weapon* (pointeur)

Étape 2 - Règle d’initialisation des références

Les références DOIVENT être initialisées à la construction. Ne peuvent pas être NULL.

Étape 3 - Les pointeurs peuvent être NULL

Les pointeurs peuvent commencer à NULL et être assignés plus tard.

Étape 1 - Classe Weapon :

Weapon.hpp
#ifndef WEAPON_HPP
#define WEAPON_HPP
#include <string>
class Weapon {
private:
std::string _type;
public:
Weapon(std::string type);
const std::string& getType() const;
void setType(std::string type);
};
#endif

Étape 2 - HumanA avec référence :

HumanA.hpp
class HumanA {
private:
std::string _name;
Weapon& _weapon; // Référence - doit toujours exister
public:
HumanA(std::string name, Weapon& weapon);
void attack() const;
};
HumanA.cpp
HumanA::HumanA(std::string name, Weapon& weapon)
: _name(name), _weapon(weapon) {} // DOIT utiliser liste d'initialisation !
void HumanA::attack() const {
std::cout << _name << " attaque avec son " << _weapon.getType() << std::endl;
}

Étape 3 - HumanB avec pointeur :

HumanB.hpp
class HumanB {
private:
std::string _name;
Weapon* _weapon; // Pointeur - peut être NULL
public:
HumanB(std::string name);
void setWeapon(Weapon& weapon);
void attack() const;
};
HumanB.cpp
HumanB::HumanB(std::string name) : _name(name), _weapon(NULL) {}
void HumanB::setWeapon(Weapon& weapon) {
_weapon = &weapon;
}
void HumanB::attack() const {
if (_weapon)
std::cout << _name << " attaque avec son " << _weapon->getType() << std::endl;
else
std::cout << _name << " n'a pas d'arme !" << std::endl;
}
LigneCodePourquoi
Weapon& _weaponMembre référenceDoit exister, ne peut pas être NULL
: _weapon(weapon)Liste d’initLes références DOIVENT être initialisées ici
Weapon* _weaponMembre pointeurPeut être NULL, défini plus tard
_weapon = &weaponStocker adresseConvertir référence en pointeur
if (_weapon)Vérification NULLLe pointeur pourrait ne pas être défini
_weapon->getType()Opérateur flècheDéréférencement + accès membre

1. Ne pas utiliser la liste d’initialisation pour la référence

// FAUX - référence non initialisée !
HumanA::HumanA(std::string name, Weapon& weapon) {
_name = name;
_weapon = weapon; // ERREUR : _weapon devait déjà exister !
}
// JUSTE - utiliser la liste d'initialisation
HumanA::HumanA(std::string name, Weapon& weapon)
: _name(name), _weapon(weapon) {}

2. Ne pas vérifier NULL pour le pointeur

// FAUX - crash si pas d'arme !
void HumanB::attack() const {
std::cout << _weapon->getType(); // Déréférencement NULL !
}
// JUSTE - vérifier d'abord
if (_weapon)
std::cout << _weapon->getType();
Fenêtre de terminal
# Tester que les changements d'arme affectent les deux humains
int main() {
Weapon club("crude spiked club");
HumanA bob("Bob", club);
HumanB jim("Jim");
jim.setWeapon(club);
bob.attack();
jim.attack();
club.setType("some other type of club");
bob.attack(); // Devrait montrer la nouvelle arme !
jim.attack(); // Devrait montrer la nouvelle arme !
}
Weapon.cpp
#include "Weapon.hpp"
Weapon::Weapon(std::string type) : _type(type) {}
const std::string& Weapon::getType() const {
return _type;
}
void Weapon::setType(std::string type) {
_type = type;
}

Créez un programme qui remplace toutes les occurrences de la chaîne s1 par s2 dans un fichier :

  • Lire le contenu du fichier
  • Remplacer TOUTES les occurrences (pas seulement la première)
  • Écrire dans <filename>.replace
  • INTERDIT : fonction std::string::replace()

Étape 1 - Lire le fichier entier dans une chaîne

Utiliser std::ifstream et std::getline() pour lire ligne par ligne.

Étape 2 - Implémenter le remplacement sans replace()

Utiliser find() pour localiser les occurrences, append() ou substr() pour construire le résultat.

Étape 3 - Écrire dans le fichier de sortie

Utiliser std::ofstream pour créer <filename>.replace.

Étape 1 - L’algorithme de remplacement :

std::string replaceAll(const std::string& content,
const std::string& s1,
const std::string& s2) {
if (s1.empty())
return content; // Éviter boucle infinie !
std::string result;
std::size_t pos = 0;
std::size_t found;
while ((found = content.find(s1, pos)) != std::string::npos) {
result.append(content, pos, found - pos); // Avant la correspondance
result.append(s2); // Remplacement
pos = found + s1.length(); // Passer après la correspondance
}
result.append(content, pos, std::string::npos); // Reste
return result;
}

Étape 2 - E/S de fichiers :

main.cpp
int main(int argc, char** argv) {
if (argc != 4) {
std::cerr << "Usage : " << argv[0] << " <fichier> <s1> <s2>" << std::endl;
return 1;
}
std::string filename = argv[1];
std::ifstream inFile(filename.c_str()); // .c_str() pour C++98
if (!inFile.is_open()) {
std::cerr << "Erreur : Impossible d'ouvrir le fichier" << std::endl;
return 1;
}
// Lire le contenu du fichier
std::string content;
std::string line;
bool first = true;
while (std::getline(inFile, line)) {
if (!first)
content += '\n';
content += line;
first = false;
}
inFile.close();
// Remplacer et écrire
std::string result = replaceAll(content, argv[2], argv[3]);
std::ofstream outFile((filename + ".replace").c_str());
outFile << result;
outFile.close();
return 0;
}
LigneCodePourquoi
content.find(s1, pos)Trouver occurrence suivanteCommencer la recherche depuis pos
std::string::nposValeur “non trouvé”Retournée quand pas de correspondance
result.append(content, pos, found - pos)Ajouter sous-chaîneTout avant la correspondance
pos = found + s1.length()Sauter après la correspondanceNe pas re-trouver la même occurrence
.c_str()String vers C-stringLes flux de fichiers C++98 en ont besoin

1. Utiliser std::string::replace() interdit

// FAUX - interdit !
str.replace(pos, s1.length(), s2);
// JUSTE - utiliser find + append
result.append(content, pos, found - pos);
result.append(s2);

2. Ne pas gérer s1 vide

// Sans cette vérification : boucle infinie !
if (s1.empty())
return content;

3. Ne remplacer que la première occurrence

// FAUX - ne trouve que la première
size_t pos = content.find(s1);
// ... remplace une fois
// JUSTE - boucler jusqu'à plus de correspondance
while ((found = content.find(s1, pos)) != std::string::npos) {
// ... remplacer chaque
}
Fenêtre de terminal
# Créer fichier de test
echo "Hello World World" > test.txt
# Remplacer "World" par "42"
./sed test.txt World 42
# Vérifier le résultat
cat test.txt.replace
# Devrait montrer : "Hello 42 42"
main.cpp
#include <iostream>
#include <fstream>
#include <string>
std::string replaceAll(const std::string& content,
const std::string& s1,
const std::string& s2) {
if (s1.empty())
return content;
std::string result;
std::size_t pos = 0;
std::size_t found;
while ((found = content.find(s1, pos)) != std::string::npos) {
result.append(content, pos, found - pos);
result.append(s2);
pos = found + s1.length();
}
result.append(content, pos, std::string::npos);
return result;
}
int main(int argc, char** argv) {
if (argc != 4) {
std::cerr << "Usage : " << argv[0] << " <fichier> <s1> <s2>" << std::endl;
return 1;
}
std::ifstream inFile(argv[1]);
if (!inFile.is_open()) {
std::cerr << "Erreur : Impossible d'ouvrir le fichier" << std::endl;
return 1;
}
std::string content, line;
bool first = true;
while (std::getline(inFile, line)) {
if (!first) content += '\n';
content += line;
first = false;
}
inFile.close();
std::string result = replaceAll(content, argv[2], argv[3]);
std::ofstream outFile((std::string(argv[1]) + ".replace").c_str());
outFile << result;
outFile.close();
return 0;
}

Créez une classe Harl qui se plaint à différents niveaux :

  • DEBUG, INFO, WARNING, ERROR
  • Exigence du sujet : eviter une “foret” de if/else if/else (utiliser des pointeurs vers fonctions membres)

La solution : utiliser des pointeurs vers fonctions membres.

Étape 1 - Déclarer un tableau de pointeurs de fonctions membres

void (Harl::*funcs[4])();

Ceci déclare un tableau de 4 pointeurs vers des fonctions membres de Harl.

Étape 2 - Mapper les niveaux aux fonctions

Créer des tableaux parallèles : un pour les pointeurs de fonction, un pour les chaînes de niveau.

Étape 3 - Boucler et correspondre

Trouver le niveau correspondant, appeler la fonction correspondante.

Étape 1 - Structure de la classe Harl :

Harl.hpp
#ifndef HARL_HPP
#define HARL_HPP
#include <string>
class Harl {
private:
void debug();
void info();
void warning();
void error();
public:
void complain(std::string level);
};
#endif

Étape 2 - Utilisation des pointeurs de fonction :

Harl.cpp
void Harl::complain(std::string level) {
// Tableau de pointeurs de fonctions membres
void (Harl::*funcs[4])() = {
&Harl::debug,
&Harl::info,
&Harl::warning,
&Harl::error
};
// Tableau parallèle de noms de niveaux
std::string levels[4] = {"DEBUG", "INFO", "WARNING", "ERROR"};
// Trouver et appeler
for (int i = 0; i < 4; i++) {
if (level == levels[i]) {
(this->*funcs[i])(); // Appeler la fonction membre via pointeur
return;
}
}
}
LigneCodePourquoi
void (Harl::*funcs[4])()Déclarer tableau4 pointeurs vers méthodes Harl
&Harl::debugObtenir adresse fonctionAdresse de fonction membre
(this->*funcs[i])()Appeler via pointeurDéréférencer + appeler

1. Utiliser forêt if/else

// FAUX - interdit !
if (level == "DEBUG")
debug();
else if (level == "INFO")
info();
// ...
// JUSTE - utiliser pointeurs de fonction
(this->*funcs[i])();

2. Mauvaise syntaxe pour appel de pointeur de fonction membre

// FAUX
funcs[i](); // Manque this
this->funcs[i](); // Mauvaise syntaxe
*funcs[i](); // Mauvaise syntaxe
// JUSTE
(this->*funcs[i])();
Fenêtre de terminal
./harl DEBUG
# Devrait afficher message debug
./harl ERROR
# Devrait afficher message error
./harl INVALID
# Ne devrait rien afficher (ou gérer gracieusement)
Harl.cpp
#include "Harl.hpp"
#include <iostream>
void Harl::debug() {
std::cout << "[ DEBUG ]" << std::endl;
std::cout << "I love having extra bacon..." << std::endl;
}
void Harl::info() {
std::cout << "[ INFO ]" << std::endl;
std::cout << "I cannot believe adding extra bacon costs more..." << std::endl;
}
void Harl::warning() {
std::cout << "[ WARNING ]" << std::endl;
std::cout << "I think I deserve to have some extra bacon for free..." << std::endl;
}
void Harl::error() {
std::cout << "[ ERROR ]" << std::endl;
std::cout << "This is unacceptable! I want to speak to the manager now." << std::endl;
}
void Harl::complain(std::string level) {
void (Harl::*funcs[4])() = {
&Harl::debug,
&Harl::info,
&Harl::warning,
&Harl::error
};
std::string levels[4] = {"DEBUG", "INFO", "WARNING", "ERROR"};
for (int i = 0; i < 4; i++) {
if (level == levels[i]) {
(this->*funcs[i])();
return;
}
}
}

Créez un filtre de log utilisant switch avec fall-through :

  • Étant donné un niveau minimum, afficher ce niveau ET tous les niveaux au-dessus
  • DEBUG → affiche DEBUG, INFO, WARNING, ERROR
  • WARNING → affiche WARNING, ERROR
  • Invalide → affiche message spécial

Étape 1 - Convertir string en int

C++98 ne peut pas faire de switch sur les strings. Convertir le nom de niveau en index (0-3).

Étape 2 - Utiliser fall-through

Ne pas utiliser break entre les cases - laisser l’exécution “tomber” vers les cases suivants.

Étape 1 - Convertir niveau en int :

int getLevel(const std::string& level) {
std::string levels[4] = {"DEBUG", "INFO", "WARNING", "ERROR"};
for (int i = 0; i < 4; i++)
if (level == levels[i])
return i;
return -1; // Niveau invalide
}

Étape 2 - Switch avec fall-through :

switch (getLevel(argv[1])) {
case 0:
harl.debug();
// PAS DE BREAK - fall through !
case 1:
harl.info();
// PAS DE BREAK - fall through !
case 2:
harl.warning();
// PAS DE BREAK - fall through !
case 3:
harl.error();
break; // Seulement break à la fin
default:
std::cout << "[ Probably complaining about insignificant problems ]" << std::endl;
}
LigneCodePourquoi
case 0:Niveau DEBUGPoint d’entrée pour DEBUG
Pas de breakFall-throughContinuer vers case suivant
case 3: ... break;Niveau ERRORS’arrêter après ERROR
default:Niveau invalideCatch-all

1. Ajouter break à chaque case

// FAUX - ne filtre pas correctement !
case 0:
harl.debug();
break; // S'arrête ici, ne montre pas INFO/WARNING/ERROR
// JUSTE - pas de break pour fall-through
case 0:
harl.debug();
// fall through
case 1:
harl.info();

2. Essayer de faire un switch sur une string

// FAUX - C++98 ne supporte pas ça !
switch (level) { // level est std::string
case "DEBUG":
// ...
}
// JUSTE - convertir en int d'abord
switch (getLevel(level)) {
case 0:
// ...
}
Fenêtre de terminal
./harlFilter DEBUG
# Devrait afficher : DEBUG, INFO, WARNING, ERROR
./harlFilter WARNING
# Devrait afficher : WARNING, ERROR
./harlFilter ERROR
# Devrait afficher : ERROR seulement
./harlFilter INVALID
# Devrait afficher : [ Probably complaining about insignificant problems ]
main.cpp
#include "Harl.hpp"
#include <iostream>
int getLevel(const std::string& level) {
std::string levels[4] = {"DEBUG", "INFO", "WARNING", "ERROR"};
for (int i = 0; i < 4; i++)
if (level == levels[i])
return i;
return -1;
}
int main(int argc, char** argv) {
if (argc != 2) {
std::cerr << "Usage : " << argv[0] << " <level>" << std::endl;
return 1;
}
Harl harl;
switch (getLevel(argv[1])) {
case 0:
harl.debug();
case 1:
harl.info();
case 2:
harl.warning();
case 3:
harl.error();
break;
default:
std::cout << "[ Probably complaining about insignificant problems ]" << std::endl;
}
return 0;
}

// Objet unique
Type* ptr = new Type(args);
delete ptr;
// Tableau
Type* arr = new Type[size];
delete[] arr;
Type& ref = original; // Créer alias
// ref est maintenant indistinguable de original
void (Class::*ptr)(args) = &Class::method;
(object.*ptr)(args);
std::ifstream in("fichier");
std::ofstream out("fichier");
std::getline(in, str);
out << content;

Continuez votre parcours C++ :

Visitez le Glossaire pour les définitions de :