Aller au contenu

Module 00 : Fondamentaux du C++

Télécharger le PDF du sujet officiel

Concepts clés :

  • Namespaces
  • Classes et Objets
  • Fonctions membres (méthodes)
  • Spécificateurs d’accès (public/private)
  • Constructeurs et Destructeurs
  • iostream (cout, cin, cerr)
  • std::string
  • Listes d’initialisation
  • Mots-clés static et const

Avant de plonger dans la syntaxe, comprenez POURQUOI ces concepts existent :

Imaginez deux bibliothèques qui définissent toutes deux une fonction appelée print(). En C, vous auriez un conflit de noms. Les namespaces résolvent ce problème en donnant à chaque bibliothèque sa propre “pièce” pour les noms.

En C, vous avez les structs (données) et les fonctions (comportement) séparément. Si vous voulez vous assurer qu’une struct est toujours dans un état valide, vous devez faire confiance à chaque programmeur pour l’utiliser correctement.

Les classes regroupent les données ET le comportement ensemble, et peuvent en plus cacher les détails internes. C’est ce qu’on appelle l’encapsulation.

Pensez à votre compte bancaire. Vous voulez que les gens connaissent votre solde (interface publique : consulter le solde), mais vous ne voulez pas qu’ils modifient directement le registre interne (données privées).

Ces trois concepts—namespaces, encapsulation et contrôle d’accès—forment la base d’un code C++ organisé et maintenable.


Comprendre les opérateurs C++ (Pour les débutants complets)

Section intitulée « Comprendre les opérateurs C++ (Pour les débutants complets) »

Avant de plonger dans le code C++, comprenons les opérateurs que vous verrez tout au long de ce module :

std::cout << "Hello" << 42 << std::endl;
  • << s’appelle “opérateur d’insertion de flux” ou “opérateur de sortie”
  • Il insère des données dans un flux de sortie (comme std::cout, qui signifie “character output”)
  • Pensez-y comme une flèche pointant dans la direction du flux de données : les données vont DES variables VERS la sortie
  • Vous pouvez chaîner plusieurs opérateurs << ensemble pour afficher plusieurs valeurs
  • std::endl insère un retour à la ligne et vide le tampon
std::cin >> number;
  • >> s’appelle “opérateur d’extraction de flux” ou “opérateur d’entrée”
  • Il extrait des données d’un flux d’entrée (comme std::cin, qui signifie “character input”)
  • Pensez-y comme une flèche pointant dans la direction du flux de données : les données vont DE l’entrée VERS la variable
std::cout // Accès à cout depuis le namespace std
ClassName::method // Accès à method depuis ClassName
  • :: est l‘“opérateur de résolution de portée”
  • Il indique au compilateur OÙ chercher un nom (variable, fonction, classe)
  • std::cout signifie “utiliser le cout qui appartient au namespace std
  • Cela évite les conflits de noms quand différentes bibliothèques utilisent les mêmes noms
object.method() // Accès à la méthode d'un objet
object.attribute // Accès à l'attribut d'un objet
  • . est l‘“opérateur d’accès aux membres” ou “opérateur point”
  • Il accède aux membres (méthodes ou attributs) d’un objet
  • Utilisez-le quand vous avez un objet réel (pas un pointeur)

L’opérateur -> (Accès aux membres via pointeur)

Section intitulée « L’opérateur -> (Accès aux membres via pointeur) »
pointer->method() // Équivalent à (*pointer).method()
pointer->attribute // Équivalent à (*pointer).attribute
  • -> est l‘“opérateur flèche” ou “opérateur d’accès aux membres via pointeur”
  • C’est un raccourci pour déréférencer un pointeur ET accéder à un membre
  • pointer->method() est exactement la même chose que (*pointer).method()
  • Utilisez-le quand vous avez un pointeur vers un objet
int* ptr = &x; // ptr stocke l'adresse mémoire de x
  • & devant une variable donne son adresse mémoire
  • C’est ainsi que vous obtenez un pointeur vers une variable
  • Ne confondez pas avec & dans les déclarations (qui crée une référence)
*ptr = 42; // Stocke 42 à l'adresse pointée par ptr
int value = *ptr; // Récupère la valeur stockée à ptr
  • * devant un pointeur le “déréférence” (accède à la valeur à cette adresse)
  • Il suit le pointeur jusqu’aux données réelles stockées en mémoire

CC++
printf()std::cout <<
scanf()std::cin >>
malloc()/free()new/delete
struct (données seules)class (données + comportement)
Les fonctions opèrent sur les donnéesLes objets ont des méthodes
// INTERDIT - vous donnera 0
printf("Hello"); // Interdit : *printf(), utilisez std::cout a la place
malloc(sizeof(int)); // Interdit : *alloc() (malloc/calloc/realloc), utilisez new a la place
free(ptr); // Interdit, utilisez delete a la place
// INTERDIT - vous donnera -42
using namespace std; // Doit préfixer avec std::
friend class Other; // Sauf si explicitement autorisé par le sujet
// INTERDIT dans les Modules 00-07 - vous donnera -42
#include <vector> // Pas de conteneurs STL avant le Module 08
#include <list> // Pas de conteneurs STL avant le Module 08
#include <map> // Pas de conteneurs STL avant le Module 08
#include <algorithm> // Pas d'algorithmes STL avant le Module 08

Les namespaces résolvent deux problèmes :

1. Collisions de noms : Dans les grands projets, deux bibliothèques peuvent définir une fonction avec le même nom.

// Sans namespaces - collision !
void print(); // Bibliothèque A
void print(); // Bibliothèque B - ERREUR !
// Avec namespaces - pas de collision
namespace LibraryA {
void print();
}
namespace LibraryB {
void print();
}
// Utilisation
LibraryA::print();
LibraryB::print();

2. Organisation du code : Les namespaces vous permettent de regrouper des symboles liés sémantiquement à travers plusieurs fichiers. Contrairement au C où l’organisation est basée sur les fichiers, les namespaces vous permettent d’organiser par signification. Un namespace peut s’étendre sur plusieurs fichiers, et les fonctions liées restent ensemble logiquement même si elles sont physiquement séparées.

Tout ce qui vient de la bibliothèque standard C++ vit dans std :

std::cout // flux de sortie
std::cin // flux d'entrée
std::cerr // flux d'erreur
std::string // classe string
std::endl // retour à la ligne + vidage du tampon
std::cout // cout du namespace std
::globalFunction() // fonction du namespace global (pas de namespace)
ClassName::method // méthode de ClassName

#include <iostream>
int main() {
std::cout << "Hello, World!" << std::endl;
int x = 42;
std::cout << "La réponse est " << x << std::endl;
// Chaînage de plusieurs valeurs
std::cout << "a=" << 1 << ", b=" << 2 << std::endl;
return 0;
}
#include <iostream>
#include <string>
int main() {
int number;
std::cout << "Entrez un nombre : ";
std::cin >> number;
std::string name;
std::cout << "Entrez votre nom : ";
std::cin >> name; // Ne lit que jusqu'à l'espace !
// Pour une ligne complète :
std::getline(std::cin, name);
return 0;
}
// PROBLÈME : mélanger cin >> et getline
int age;
std::string name;
std::cin >> age; // Laisse '\n' dans le tampon
std::getline(std::cin, name); // Lit une ligne vide !
// SOLUTION : vider le tampon
std::cin >> age;
std::cin.ignore(); // Ignore le '\n' restant
std::getline(std::cin, name);
#include <iostream>
#include <iomanip>
int main() {
// Définir la largeur du champ
std::cout << std::setw(10) << "Hello" << std::endl; // " Hello"
// Alignement droite/gauche
std::cout << std::right << std::setw(10) << "Hi" << std::endl; // " Hi"
std::cout << std::left << std::setw(10) << "Hi" << std::endl; // "Hi "
// Caractère de remplissage
std::cout << std::setfill('.') << std::setw(10) << "Hi" << std::endl; // "........Hi"
return 0;
}

// Style C (dangereux, mémoire manuelle)
char* str = (char*)malloc(100);
strcpy(str, "Hello");
// Doit se rappeler de libérer !
// Style C++ (sûr, automatique)
std::string str = "Hello";
// Mémoire gérée automatiquement
#include <string>
std::string s = "Hello";
// Longueur
s.length(); // 5
s.size(); // 5 (même chose)
// Accès aux caractères
s[0]; // 'H'
s.at(0); // 'H' (avec vérification des limites)
// Concaténation
s + " World"; // "Hello World"
s.append(" World"); // Modifie s
// Comparaison
s == "Hello"; // true
s < "World"; // true (lexicographique)
// Sous-chaînes
s.substr(0, 3); // "Hel"
s.substr(2); // "llo"
// Recherche
s.find("ll"); // 2 (index)
s.find("xyz"); // std::string::npos (non trouvé)
// Remplacement (mais rappel : interdit dans ex04 !)
s.replace(0, 2, "YY"); // "YYllo"
// Effacement
s.empty(); // false
s.clear(); // s est maintenant ""
std::string s = "Hello";
// Basée sur l'index
for (size_t i = 0; i < s.length(); i++) {
std::cout << s[i];
}
// C++98 n'a pas de boucles for basées sur les plages !
// Ceci est du C++11 : for (char c : s) { } // INTERDIT

En C++, class et struct peuvent tous deux avoir des variables et fonctions membres. La seule différence est le spécificateur d’accès par défaut :

Caractéristiqueclassstruct
Accès par défautprivatepublic
Héritage par défautprivatepublic
Membres accessibles ?Seulement dans la classeDe partout

Exemple pratique :

class MyClass {
int x; // PRIVATE par défaut - accessible seulement dans la classe
void foo(); // PRIVATE par défaut
};
struct MyStruct {
int x; // PUBLIC par défaut - accessible de partout
void foo(); // PUBLIC par défaut
};
MyClass c;
c.x = 5; // ERREUR : x est private !
MyStruct s;
s.x = 5; // OK : x est public

Quand utiliser chacun :

  • Utilisez class quand vous avez besoin d’encapsulation (cacher l’implémentation)
  • Utilisez struct pour les structures de données simples (comme les structs C)
  • En pratique, les deux fonctionnent pareil - c’est une question de convention

Disposition mémoire visuelle :

Objet MyClass : Objet MyStruct :
+-------------+ +-------------+
| _firstName | | _firstName | <-- Tous les membres
| _lastName | | _lastName | sont accessibles
| _phone | | _phone |
+-------------+ +-------------+
(private) (public)
Contact.hpp
#ifndef CONTACT_HPP
#define CONTACT_HPP
#include <string>
class Contact {
private:
// Attributs (membres de données)
std::string _firstName;
std::string _lastName;
std::string _phoneNumber;
public:
// Constructeur
Contact();
// Destructeur
~Contact();
// Fonctions membres (méthodes)
void setFirstName(std::string name);
std::string getFirstName() const;
void display() const;
};
#endif
Contact.cpp
#include "Contact.hpp"
#include <iostream>
// Implémentation du constructeur
Contact::Contact() {
std::cout << "Contact créé" << std::endl;
}
// Implémentation du destructeur
Contact::~Contact() {
std::cout << "Contact détruit" << std::endl;
}
// Setter
void Contact::setFirstName(std::string name) {
this->_firstName = name;
}
// Getter (const - ne modifie pas l'objet)
std::string Contact::getFirstName() const {
return this->_firstName;
}
// Méthode d'affichage
void Contact::display() const {
std::cout << "Nom : " << _firstName << " " << _lastName << std::endl;
}
SpécificateurAccès
privateAccessible seulement dans la classe
publicAccessible de partout
protectedAccessible dans la classe et les classes dérivées

Règle générale : Rendez les attributs private, fournissez des getters/setters publics.

Chaque fonction membre non-static a accès à un pointeur spécial appelé this. Il pointe vers l’instance courante - l’objet sur lequel la méthode a été appelée. C’est ainsi que les fonctions membres savent à quelles données d’objet accéder.

class Example {
private:
int value;
public:
void setValue(int value) {
// 'value' fait référence au paramètre
// 'this->value' fait référence au membre
this->value = value;
}
// this est implicite - ces deux sont équivalents :
int getValue() { return value; }
int getValue() { return this->value; }
};

Pourquoi this est important : Quand vous appelez obj.setValue(42), le compilateur passe &obj comme premier argument caché. Dans la fonction, this == &obj.

class Example {
private:
int _value;
public:
// Peut modifier l'objet
void setValue(int v) { _value = v; }
// Ne peut pas modifier l'objet (const à la fin)
int getValue() const { return _value; }
};

class MyClass {
public:
MyClass() {
std::cout << "Constructeur par défaut appelé" << std::endl;
}
};
// Utilisation
MyClass obj; // Appelle le constructeur par défaut
class Contact {
private:
std::string _name;
int _age;
public:
Contact(std::string name, int age) {
_name = name;
_age = age;
}
};
// Utilisation
Contact c("John", 25);
class Contact {
private:
std::string _name;
int _age;
public:
// SANS liste d'initialisation (assignation dans le corps)
Contact(std::string name, int age) {
_name = name; // D'abord construit par défaut, puis assigné
_age = age;
}
// AVEC liste d'initialisation (initialisation directe)
Contact(std::string name, int age) : _name(name), _age(age) {
// Les membres sont initialisés avant l'exécution du corps
}
};

Pourquoi utiliser les listes d’initialisation ?

  1. Plus efficace : Sans liste d’init, les membres sont construits par défaut PUIS assignés (2 opérations). Avec liste d’init, les membres sont directement initialisés (1 opération).

    // SANS liste d'init - DEUX opérations par membre :
    Contact(std::string name) {
    // 1. _name est construit par défaut (chaîne vide)
    // 2. _name reçoit la valeur de 'name'
    _name = name;
    }
    // AVEC liste d'init - UNE opération par membre :
    Contact(std::string name) : _name(name) {
    // _name est directement construit avec 'name'
    }
  2. Requis pour les membres const (on ne peut pas assigner à const après construction)

  3. Requis pour les membres référence (doivent être liés à l’initialisation)

  4. Requis pour les membres sans constructeur par défaut

class Example {
private:
const int _id; // DOIT utiliser liste d'init
std::string& _ref; // DOIT utiliser liste d'init
public:
// C'est la SEULE façon :
Example(int id, std::string& ref) : _id(id), _ref(ref) {}
};
class FileHandler {
private:
int* _data;
public:
FileHandler() {
_data = new int[100]; // Allocation
}
~FileHandler() {
delete[] _data; // Nettoyage
std::cout << "FileHandler détruit, mémoire libérée" << std::endl;
}
};

Partagés entre TOUTES les instances d’une classe.

// Header
class Counter {
private:
static int _count; // Déclaration
public:
Counter() { _count++; }
~Counter() { _count--; }
static int getCount() { return _count; }
};
// Source (DOIT être défini hors de la classe)
int Counter::_count = 0; // Définition + initialisation
// Utilisation
Counter a;
Counter b;
Counter c;
std::cout << Counter::getCount(); // 3

Peuvent être appelées sans objet. Ne peuvent pas accéder aux membres non-statiques.

class Math {
public:
static int add(int a, int b) {
return a + b;
}
};
// Utilisation - pas d'objet nécessaire
int result = Math::add(5, 3);

Philosophie de conception : const est plus qu’une fonctionnalité du langage - c’est une discipline pour écrire du code correct. En marquant tout ce qui ne devrait pas changer comme const, vous :

  • Détectez les bugs à la compilation (modifications accidentelles)
  • Documentez l’intention (les lecteurs savent ce qui ne changera pas)
  • Permettez les optimisations du compilateur
  • Rendez le code plus facile à comprendre dans les systèmes complexes

Règle générale : Utilisez const par défaut. Ne le retirez que quand vous avez besoin de modifier quelque chose.

const int MAX = 100; // Ne peut pas être modifié
MAX = 200; // ERREUR !
void print(const std::string& s) {
// s ne peut pas être modifié
// Passé par référence (efficace, pas de copie)
std::cout << s << std::endl;
}
class Example {
public:
int getValue() const { // Promet de ne pas modifier l'objet
return _value;
}
};
class Example {
private:
std::string _name;
public:
// Retourne une référence const - l'appelant ne peut pas modifier
const std::string& getName() const {
return _name;
}
};

Chaque fichier header DOIT avoir des gardes d’inclusion pour éviter la double inclusion :

Contact.hpp
#ifndef CONTACT_HPP
#define CONTACT_HPP
class Contact {
// ...
};
#endif

Pourquoi ?

// Sans gardes :
#include "Contact.hpp"
#include "Contact.hpp" // ERREUR : Contact redéfini !
// Avec gardes :
#include "Contact.hpp" // Définit CONTACT_HPP, inclut la classe
#include "Contact.hpp" // CONTACT_HPP déjà défini, ignoré

NAME = program
CXX = c++
CXXFLAGS = -Wall -Wextra -Werror -std=c++98
SRCS = main.cpp Contact.cpp PhoneBook.cpp
OBJS = $(SRCS:.cpp=.o)
all: $(NAME)
$(NAME): $(OBJS)
$(CXX) $(CXXFLAGS) -o $(NAME) $(OBJS)
%.o: %.cpp
$(CXX) $(CXXFLAGS) -c $< -o $@
clean:
rm -f $(OBJS)
fclean: clean
rm -f $(NAME)
re: fclean all
.PHONY: all clean fclean re

Cette structure evite aussi le “relink” : si aucun fichier .cpp n’a change, make ne relancera pas l’etape finale de link.

LigneCodeExplication
1NAME = programDéfinit une variable NAME contenant le nom de l’exécutable final. Utiliser une variable permet de changer facilement le nom du programme à un seul endroit.
2CXX = c++Définit le compilateur C++ à utiliser. c++ est la commande standard qui pointe généralement vers g++ ou clang++. Utiliser une variable permet de changer facilement de compilateur.
3CXXFLAGS = -Wall -Wextra -Werror -std=c++98Flags du compilateur : -Wall active tous les avertissements courants, -Wextra active des avertissements supplémentaires, -Werror traite les avertissements comme des erreurs (la compilation échoue sur tout avertissement), -std=c++98 impose le standard C++98 (requis par 42).
5SRCS = main.cpp Contact.cpp PhoneBook.cppListe tous les fichiers sources à compiler. Chaque fichier .cpp sera compilé en son propre fichier objet.
6OBJS = $(SRCS:.cpp=.o)Référence de substitution : Prend la liste SRCS et remplace .cpp par .o. Résultat : main.o Contact.o PhoneBook.o. Cela crée la liste des fichiers objets.
8all: $(NAME)La cible par défaut. Quand vous exécutez make sans arguments, il construit all, qui dépend de $(NAME) (l’exécutable).
10-11$(NAME): $(OBJS)Règle de liaison : L’exécutable dépend de tous les fichiers objets. Si un fichier .o est plus récent que l’exécutable, cette règle s’exécute.
11$(CXX) $(CXXFLAGS) -o $(NAME) $(OBJS)La commande de liaison : appelle le compilateur avec les flags, -o $(NAME) spécifie le nom du fichier de sortie, $(OBJS) liste tous les fichiers objets à lier ensemble.
13-14%.o: %.cppRègle de pattern : Définit comment construire N’IMPORTE QUEL fichier .o à partir de son fichier .cpp correspondant. Le % est un joker qui correspond à n’importe quel nom de fichier.
14$(CXX) $(CXXFLAGS) -c $< -o $@La commande de compilation : -c signifie compiler seulement (ne pas lier), $< est une variable automatique représentant le premier prérequis (le fichier .cpp), $@ est la cible (le fichier .o).
16-17clean:Une cible pour supprimer les fichiers objets. N’a pas de prérequis, donc s’exécute toujours quand appelée.
17rm -f $(OBJS)Supprime tous les fichiers objets. -f signifie “forcer” (ne pas errorer si les fichiers n’existent pas).
19-20fclean: clean”Nettoyage complet” : exécute d’abord clean (son prérequis), puis supprime aussi l’exécutable.
20rm -f $(NAME)Supprime l’exécutable final.
22re: fclean all”Refaire” : exécute fclean d’abord, puis all. Cela force une recompilation complète depuis zéro.
24.PHONY: all clean fclean reDéclare ces cibles comme “phony” (pas des fichiers réels). Sans cela, si un fichier nommé clean existait, make clean n’exécuterait pas la règle.
VariableSignification
$@La cible de la règle (ce qu’on construit)
$<Le premier prérequis (première dépendance)
$^Tous les prérequis (toutes les dépendances)
  1. Compilation incrémentale : Ne recompile que les fichiers qui ont changé
  2. Pas de relinking : Si aucun fichier objet n’a changé, l’exécutable n’est pas reconstruit
  3. Maintenance facile : Changez NAME ou SRCS à un seul endroit
  4. Cibles de nettoyage : clean, fclean, et re suivent les conventions standard de 42

Lors de la lecture d’entrée dans une boucle, vous devez gérer l’EOF (Ctrl+D sur Unix, Ctrl+Z sur Windows) :

std::string line;
// std::getline retourne le flux, qui se convertit en false sur EOF
while (std::getline(std::cin, line)) {
// Traiter la ligne
std::cout << "Reçu : " << line << std::endl;
}
// La boucle se termine quand EOF est atteint
// Vous pouvez aussi vérifier explicitement :
if (std::cin.eof()) {
std::cout << "Fin de l'entrée atteinte" << std::endl;
}
#include <cstdlib> // pour std::exit
std::string input;
std::cout << "Entrez une valeur : ";
if (!std::getline(std::cin, input)) {
std::cout << std::endl; // Affiche un retour à la ligne pour une sortie propre
std::exit(0); // Quitte le programme gracieusement
}

Quand vous avez besoin d’une collection de taille fixe où les nouveaux éléments remplacent les plus anciens :

class PhoneBook {
private:
Contact _contacts[8]; // Tableau de taille fixe
int _index; // Prochaine position d'écriture
int _count; // Total de contacts stockés
public:
PhoneBook() : _index(0), _count(0) {}
void addContact(const Contact& c) {
_contacts[_index] = c;
// Bouclage avec modulo
_index = (_index + 1) % 8; // 0,1,2,3,4,5,6,7,0,1,2...
// Compter jusqu'au max
if (_count < 8)
_count++;
}
};
int index = 0;
int size = 8;
index = (index + 1) % size; // 0 -> 1
index = (index + 1) % size; // 1 -> 2
// ... après 7 :
index = (index + 1) % size; // 7 -> 0 (boucle !)

C++ étend le concept de pointeur pour permettre des pointeurs vers des membres de classe (attributs et fonctions membres). C’est différent des pointeurs normaux.

class Sample {
public:
int value;
};
// Pointeur vers attribut membre
int Sample::*ptr = &Sample::value;
// Utilisation - besoin d'une instance pour déréférencer
Sample s;
s.value = 42;
std::cout << s.*ptr << std::endl; // Affiche 42
Sample* sp = &s;
std::cout << sp->*ptr << std::endl; // Affiche aussi 42
class Sample {
public:
void display() { std::cout << "Hello" << std::endl; }
void greet(std::string name) { std::cout << "Salut " << name << std::endl; }
};
// Pointeur vers fonction membre (sans paramètres)
void (Sample::*funcPtr)() = &Sample::display;
// Utilisation
Sample s;
(s.*funcPtr)(); // Appelle s.display()
Sample* sp = &s;
(sp->*funcPtr)(); // Appelle aussi display()
// Pointeur vers fonction membre avec paramètres
void (Sample::*greetPtr)(std::string) = &Sample::greet;
(s.*greetPtr)("World"); // Appelle s.greet("World")
  • Callbacks : Sélectionner quelle fonction membre appeler à l’exécution
  • Conception pilotée par les données : Accéder à différents attributs selon la configuration
  • Algorithmes génériques : Écrire des fonctions qui fonctionnent sur n’importe quel membre

  1. Utiliser using namespace std; - Interdit à 42 (note -42)
  2. Utiliser le mot-clé friend - Interdit sauf si explicitement autorisé (note -42)
  3. Utiliser les conteneurs/algorithmes STL avant le Module 08 - Interdit (note -42)
  4. Oublier les gardes d’inclusion - Cause des erreurs de compilation (note 0)
  5. Mettre l’implémentation dans les headers - Note 0 (sauf templates)
  6. Ne pas terminer la sortie par un retour à la ligne - Requis par le sujet
  7. Utiliser printf/scanf - Interdit à 42 (note 0)
  8. Oublier const sur les getters - Mauvaise pratique
  9. Ne pas initialiser les membres - Comportement indéfini
  10. Fuites mémoire - Toujours apparier new avec delete

Appliquons maintenant ces concepts aux exercices du module.


L’exercice Megaphone vous demande de créer un programme qui :

  • Prend des arguments en ligne de commande et les convertit tous en majuscules
  • Concatène tous les arguments sans ajouter d’espaces entre eux
  • Affiche un message par défaut * LOUD AND UNBEARABLE FEEDBACK NOISE * quand aucun argument n’est donné

C’est votre premier programme C++. L’objectif est d’apprendre les E/S de base avec std::cout et de comprendre comment fonctionnent les arguments en ligne de commande en C++.

Avant d’écrire du code, réfléchissez au problème :

  1. Comprendre argc/argv : argc est le nombre d’arguments, argv est un tableau de chaînes C. argv[0] est toujours le nom du programme.
  2. Gérer le cas limite en premier : Vérifier si aucun argument n’a été donné (argc == 1).
  3. Traiter chaque argument : Boucler à partir de l’index 1 pour sauter le nom du programme.
  4. Convertir caractère par caractère : Pour chaque argument, parcourir chaque caractère et convertir en majuscule.
  5. Afficher à la fin : Imprimer un seul retour à la ligne après tous les caractères.

Étape 1 - Squelette de base :

#include <iostream>
int main(int argc, char **argv)
{
// TODO: implémenter
return 0;
}

Étape 2 - Gérer le cas sans arguments :

#include <iostream>
int main(int argc, char **argv)
{
if (argc == 1)
{
std::cout << "* LOUD AND UNBEARABLE FEEDBACK NOISE *" << std::endl;
return 0;
}
// TODO: gérer les arguments
return 0;
}

Étape 3 - Implémentation complète :

#include <iostream>
#include <cctype>
int main(int argc, char **argv)
{
if (argc == 1)
{
std::cout << "* LOUD AND UNBEARABLE FEEDBACK NOISE *" << std::endl;
return 0;
}
for (int i = 1; i < argc; i++)
{
for (int j = 0; argv[i][j]; j++)
{
std::cout << (char)toupper(argv[i][j]);
}
}
std::cout << std::endl;
return 0;
}
LigneCodePourquoi
1#include <iostream>Bibliothèque E/S C++. Utilisez ceci au lieu de stdio.h. Nous donne std::cout.
2#include <cctype>Wrapper C++ pour les fonctions de caractères. Fournit toupper().
4int main(int argc, char **argv)Signature standard pour les programmes recevant des arguments en ligne de commande.
6if (argc == 1)Pourquoi 1, pas 0 ? Parce que argv[0] est toujours le nom du programme, donc 1 = “pas d’arguments réels”.
8std::cout << ... << std::endl;Envoie le message vers stdout. std::endl vide le tampon et ajoute un retour à la ligne.
9return 0;Sortie anticipée après avoir affiché le message par défaut.
12for (int i = 1; i < argc; i++)Commence à 1 pour sauter argv[0] (le nom du programme).
14for (int j = 0; argv[i][j]; j++)Boucle jusqu’au terminateur nul. argv[i][j] est le j-ième caractère du i-ième argument.
16(char)toupper(argv[i][j])toupper() retourne un int, donc on caste en char pour un affichage correct.
19std::cout << std::endl;Affiche le retour à la ligne final seulement après tous les caractères.
  1. Utiliser printf au lieu de std::cout

    • Habitude du C ! Dans les modules C++, vous devez utiliser std::cout pour la sortie.
    • Faux : printf("hello");
    • Juste : std::cout << "hello";
  2. Oublier que toupper() retourne int

    • Sans le cast, vous pourriez afficher des codes ASCII au lieu de caractères.
    • Faux : std::cout << toupper(c);
    • Juste : std::cout << (char)toupper(c);
  3. Oublier le cas sans argument

    • Le sujet exige explicitement d’afficher * LOUD AND UNBEARABLE FEEDBACK NOISE * quand il n’y a pas d’arguments.
    • Toujours tester avec ./megaphone (sans args).
  4. Ajouter des espaces entre les arguments

    • Le sujet veut que les arguments soient concaténés directement.
    • ./megaphone "Hello" "World" devrait afficher HELLOWORLD, pas HELLO WORLD.

Testez votre programme avec les exemples exacts du sujet :

Fenêtre de terminal
./megaphone "shhhhh... I think the students are asleep..."
# Attendu : SHHHHH... I THINK THE STUDENTS ARE ASLEEP...
./megaphone Damnit " ! " "Sorry students, I thought this thing was off."
# Attendu : DAMNIT ! SORRY STUDENTS, I THOUGHT THIS THING WAS OFF.
./megaphone
# Attendu : * LOUD AND UNBEARABLE FEEDBACK NOISE *

Cas limites supplémentaires à vérifier :

Fenêtre de terminal
./megaphone ""
# Attendu : (ligne vide - juste un retour à la ligne)
./megaphone "123" "!@#"
# Attendu : 123!@# (les non-lettres passent inchangées)
megaphone.cpp
#include <iostream>
#include <cctype>
int main(int argc, char **argv)
{
if (argc == 1)
{
std::cout << "* LOUD AND UNBEARABLE FEEDBACK NOISE *" << std::endl;
return 0;
}
for (int i = 1; i < argc; i++)
{
for (int j = 0; argv[i][j]; j++)
{
std::cout << (char)toupper(argv[i][j]);
}
}
std::cout << std::endl;
return 0;
}

Votre premier vrai programme C++ avec des classes. Cet exercice vous enseigne les fondamentaux orientés objet : encapsulation, conception de classes, et comment les objets interagissent.

L’exercice PhoneBook vous demande de construire un système de gestion de contacts avec ces contraintes :

Deux classes nécessaires :

  • Contact : Stocke les données d’une personne (prénom, nom, surnom, numéro de téléphone, secret le plus sombre)
  • PhoneBook : Gère une collection de jusqu’à 8 contacts

Exigences principales :

  • Tableau fixe de 8 contacts - PAS de vectors, PAS d’allocation dynamique (new/delete)
  • Trois commandes : ADD, SEARCH, EXIT
  • Tampon circulaire : Quand plein, le contact le plus ancien est remplacé
  • Pas de champs vides : Chaque champ doit avoir du contenu
  • Affichage SEARCH : Colonnes de 10 caractères, alignées à droite, tronquées avec ”.” si trop long
  • Recommande : gerer EOF (Ctrl+D) proprement (le traiter comme EXIT)

La commande SEARCH a deux phases :

  1. Afficher un tableau formaté montrant tous les contacts
  2. Demander un index, puis afficher les détails complets de ce contact

Réfléchissez à la conception avant de coder :

Étape 1 - Concevoir la classe Contact d’abord

Posez-vous : Quelles données un contact contient-il ? De quelles méthodes a-t-il besoin ?

  • 5 attributs string privés (les champs)
  • Un flag pour savoir si l’emplacement est utilisé (_isEmpty)
  • Des setters pour stocker les données, des getters pour les récupérer
  • Tout privé sauf les méthodes d’interface

Étape 2 - Concevoir la classe PhoneBook

Posez-vous : Comment gérer 8 contacts ? Comment savoir où ajouter le suivant ?

  • Tableau fixe : Contact _contacts[8]
  • Suivre où ajouter : _currentIndex (cycle 0→7→0)
  • Suivre combien existent : _totalContacts (plafond à 8)

Étape 3 - Planifier la boucle principale

while (true) {
lire commande
si ADD → appeler phonebook.addContact()
si SEARCH → appeler phonebook.searchContacts()
si EXIT → break
}

Étape 4 - Gérer les cas limites

  • Rejet des entrées vides (boucler jusqu’à non-vide)
  • Gestion EOF (std::getline retourne false)
  • Index invalide (vérifier plage et format)

Insight clé - Le tampon circulaire :

Vous avez besoin de deux compteurs :

  • _currentIndex : Où ajouter le PROCHAIN contact (boucle avec modulo 8)
  • _totalContacts : Combien de contacts existent (maximum 8)

Quand on ajoute le contact #9, _currentIndex sera 0, remplaçant le contact le plus ancien.

Étape 1 - Classe Contact (le conteneur de données) :

Contact.hpp
#ifndef CONTACT_HPP
#define CONTACT_HPP
#include <string>
class Contact {
private:
std::string _firstName;
std::string _lastName;
std::string _nickname;
std::string _phoneNumber;
std::string _darkestSecret;
bool _isEmpty;
public:
Contact();
~Contact();
// Setters - prennent const reference pour l'efficacité
void setFirstName(const std::string& firstName);
void setLastName(const std::string& lastName);
void setNickname(const std::string& nickname);
void setPhoneNumber(const std::string& phoneNumber);
void setDarkestSecret(const std::string& secret);
void setIsEmpty(bool isEmpty);
// Getters - marqués const (promesse de ne pas modifier l'objet)
std::string getFirstName() const;
std::string getLastName() const;
std::string getNickname() const;
std::string getPhoneNumber() const;
std::string getDarkestSecret() const;
bool isEmpty() const;
};
#endif

Pourquoi ces choix de conception ?

DécisionRaison
Attributs privésEncapsulation - le code externe ne peut pas corrompre les données
const std::string& paramsÉvite de copier la chaîne, plus efficace
const sur les gettersPromet que ces méthodes ne modifieront pas l’objet
Flag _isEmptyPermet de vérifier si un emplacement est utilisé
Préfixe underscoreConvention courante pour identifier les membres privés

Étape 2 - Classe PhoneBook (le gestionnaire) :

PhoneBook.hpp
#ifndef PHONEBOOK_HPP
#define PHONEBOOK_HPP
#include "Contact.hpp"
#include <string>
class PhoneBook {
private:
Contact _contacts[8]; // Tableau fixe - PAS de vectors !
int _currentIndex; // Où ajouter le suivant (0-7, boucle)
int _totalContacts; // Combien sont stockés (max 8)
// Méthodes helper privées - détails d'implémentation internes
std::string _truncate(const std::string& str) const;
void _displayContactRow(int index) const;
std::string _getInput(const std::string& prompt) const;
public:
PhoneBook();
~PhoneBook();
void addContact();
void searchContacts() const;
};
#endif

Pourquoi Contact _contacts[8] et pas un vector ?

Le sujet interdit explicitement les conteneurs de la STL. Cela vous force à comprendre comment fonctionnent les tableaux de taille fixe en C++. Le tableau est alloué sur la pile comme partie de l’objet PhoneBook lui-même.

Étape 3 - La logique du tampon circulaire :

void PhoneBook::addContact() {
Contact newContact;
// Collecter tous les champs (le helper rejette les entrées vides)
newContact.setFirstName(_getInput("Entrez le prénom : "));
newContact.setLastName(_getInput("Entrez le nom : "));
newContact.setNickname(_getInput("Entrez le surnom : "));
newContact.setPhoneNumber(_getInput("Entrez le numéro de téléphone : "));
newContact.setDarkestSecret(_getInput("Entrez le secret le plus sombre : "));
newContact.setIsEmpty(false);
// Stocker à l'index courant (écrase le plus ancien si plein)
_contacts[_currentIndex] = newContact;
// Tampon circulaire : bouclage avec modulo
_currentIndex = (_currentIndex + 1) % 8;
// Compter (plafond à 8)
if (_totalContacts < 8)
_totalContacts++;
std::cout << "Contact ajouté avec succès !" << std::endl;
}

L’astuce du modulo expliquée :

(_currentIndex + 1) % 8 fait cycler l’index : 0→1→2→3→4→5→6→7→0→1→2…

_currentIndex+ 1% 8Résultat
011 % 81
677 % 87
788 % 80 ← boucle !

Étape 4 - Formatage de l’affichage SEARCH :

std::string PhoneBook::_truncate(const std::string& str) const {
if (str.length() > 10)
return str.substr(0, 9) + "."; // 9 chars + point = 10
return str;
}
void PhoneBook::_displayContactRow(int index) const {
std::cout << std::setw(10) << std::right << index << "|";
std::cout << std::setw(10) << std::right << _truncate(_contacts[index].getFirstName()) << "|";
std::cout << std::setw(10) << std::right << _truncate(_contacts[index].getLastName()) << "|";
std::cout << std::setw(10) << std::right << _truncate(_contacts[index].getNickname()) << std::endl;
}

Comprendre std::setw et std::right :

  • std::setw(10) - Définit la largeur à 10 caractères pour la prochaine sortie
  • std::right - Aligne le contenu à droite dans cette largeur

Exemple : "John" devient " John" (6 espaces + 4 chars = 10)

Validation d’entrée avec gestion EOF :

std::string PhoneBook::_getInput(const std::string& prompt) const {
std::string input;
while (true) {
std::cout << prompt;
if (!std::getline(std::cin, input)) { // Retourne false sur EOF
std::cout << std::endl;
std::exit(0); // Sortie propre sur Ctrl+D
}
if (!input.empty())
break;
std::cout << "Le champ ne peut pas être vide. Veuillez réessayer." << std::endl;
}
return input;
}
LignePourquoi
std::getline(std::cin, input)Lit toute la ligne incluant les espaces (contrairement à std::cin >>)
if (!std::getline(...))getline retourne false quand EOF (Ctrl+D) est rencontré
std::exit(0)Termine le programme proprement - requis pour la gestion EOF
if (!input.empty())Rejette les chaînes vides, continue à demander

Validation d’index pour SEARCH :

// Obtenir et valider l'index
std::string indexStr;
std::cout << "Entrez l'index pour voir les détails : ";
if (!std::getline(std::cin, indexStr)) {
std::cout << std::endl;
return;
}
// Validation stricte : doit être un chiffre unique 0-7
if (indexStr.length() != 1 || indexStr[0] < '0' || indexStr[0] > '7') {
std::cout << "Index invalide." << std::endl;
return;
}
int index = indexStr[0] - '0'; // Convertit char digit en int
if (index >= _totalContacts) {
std::cout << "Index invalide." << std::endl;
return;
}
LignePourquoi
indexStr.length() != 1N’accepte que les entrées d’un seul caractère
indexStr[0] < '0' || indexStr[0] > '7'Doit être un chiffre 0-7
indexStr[0] - '0'Conversion caractère en int : '5' - '0' = 5
index >= _totalContactsNe peut pas accéder à un contact qui n’existe pas

1. Utiliser vectors ou allocation dynamique

Le sujet l’INTERDIT EXPLICITEMENT. Vous échouerez l’évaluation.

// FAUX - interdit
std::vector<Contact> _contacts;
Contact* _contacts = new Contact[8];
// JUSTE - tableau fixe
Contact _contacts[8];

2. Mauvais formatage d’affichage

// FAUX - pas de formatage
std::cout << firstName << "|" << lastName << std::endl;
// JUSTE - 10 chars, aligné à droite, tronqué
std::cout << std::setw(10) << std::right;
if (firstName.length() > 10)
std::cout << firstName.substr(0, 9) + ".";
else
std::cout << firstName;

3. Ne pas gérer correctement le tampon circulaire

// FAUX - incrémente juste
_currentIndex++;
// JUSTE - boucle
_currentIndex = (_currentIndex + 1) % 8;

4. Accepter les champs vides

// FAUX - accepte tout
std::getline(std::cin, input);
contact.setFirstName(input);
// JUSTE - boucle jusqu'à non-vide
while (input.empty()) {
std::cout << "Le champ ne peut pas être vide : ";
std::getline(std::cin, input);
}

5. Ne pas gérer EOF (Ctrl+D)

// FAUX - ignore EOF, le programme bloque
std::getline(std::cin, input);
// JUSTE - vérifie la valeur de retour et sort
if (!std::getline(std::cin, input)) {
std::cout << std::endl;
std::exit(0);
}

6. Rendre les attributs publics

// FAUX - casse l'encapsulation
class Contact {
public:
std::string firstName; // N'importe qui peut modifier directement !
};
// JUSTE - privé avec accesseurs
class Contact {
private:
std::string _firstName;
public:
void setFirstName(const std::string& name);
std::string getFirstName() const;
};
Fenêtre de terminal
# Tester la fonctionnalité ADD
> ADD
Entrez le prénom : John
Entrez le nom : Doe
Entrez le surnom : JD
Entrez le numéro de téléphone : 555-1234
Entrez le secret le plus sombre : Aime la pizza à l'ananas
Contact ajouté avec succès !
# Tester le rejet des champs vides
> ADD
Entrez le prénom :
Le champ ne peut pas être vide. Veuillez réessayer.
Entrez le prénom : Jane
# Tester SEARCH sans contacts
> SEARCH
PhoneBook est vide.
# Tester le formatage d'affichage SEARCH
> SEARCH
Index|First Name| Last Name| Nickname
--------------------------------------------
0| John| Doe| JD
Entrez l'index pour voir les détails : 0
Prénom : John
Nom : Doe
Surnom : JD
Numéro de téléphone : 555-1234
Secret le plus sombre : Aime la pizza à l'ananas
# Tester la troncation (nom > 10 chars)
# Ajouter un contact avec prénom "Christopher"
> SEARCH
# Devrait afficher "Christoph." (9 chars + point)
# Tester le tampon circulaire (ajouter 9 contacts)
# Le contact #9 devrait remplacer le contact #0
# Tester index invalide
> SEARCH
Entrez l'index pour voir les détails : 9
Index invalide.
> SEARCH
Entrez l'index pour voir les détails : abc
Index invalide.
# Tester la gestion EOF
> (Appuyez sur Ctrl+D)
# Le programme devrait quitter proprement avec un retour à la ligne

Contact.hpp

Contact.hpp
#ifndef CONTACT_HPP
#define CONTACT_HPP
#include <string>
class Contact {
private:
std::string _firstName;
std::string _lastName;
std::string _nickname;
std::string _phoneNumber;
std::string _darkestSecret;
bool _isEmpty;
public:
Contact();
~Contact();
void setFirstName(const std::string& firstName);
void setLastName(const std::string& lastName);
void setNickname(const std::string& nickname);
void setPhoneNumber(const std::string& phoneNumber);
void setDarkestSecret(const std::string& secret);
void setIsEmpty(bool isEmpty);
std::string getFirstName() const;
std::string getLastName() const;
std::string getNickname() const;
std::string getPhoneNumber() const;
std::string getDarkestSecret() const;
bool isEmpty() const;
};
#endif

Contact.cpp

Contact.cpp
#include "Contact.hpp"
Contact::Contact() : _isEmpty(true) {}
Contact::~Contact() {}
void Contact::setFirstName(const std::string& firstName) { _firstName = firstName; }
void Contact::setLastName(const std::string& lastName) { _lastName = lastName; }
void Contact::setNickname(const std::string& nickname) { _nickname = nickname; }
void Contact::setPhoneNumber(const std::string& phoneNumber) { _phoneNumber = phoneNumber; }
void Contact::setDarkestSecret(const std::string& secret) { _darkestSecret = secret; }
void Contact::setIsEmpty(bool isEmpty) { _isEmpty = isEmpty; }
std::string Contact::getFirstName() const { return _firstName; }
std::string Contact::getLastName() const { return _lastName; }
std::string Contact::getNickname() const { return _nickname; }
std::string Contact::getPhoneNumber() const { return _phoneNumber; }
std::string Contact::getDarkestSecret() const { return _darkestSecret; }
bool Contact::isEmpty() const { return _isEmpty; }

PhoneBook.hpp

PhoneBook.hpp
#ifndef PHONEBOOK_HPP
#define PHONEBOOK_HPP
#include "Contact.hpp"
#include <string>
class PhoneBook {
private:
Contact _contacts[8];
int _currentIndex;
int _totalContacts;
std::string _truncate(const std::string& str) const;
void _displayContactRow(int index) const;
std::string _getInput(const std::string& prompt) const;
public:
PhoneBook();
~PhoneBook();
void addContact();
void searchContacts() const;
};
#endif

PhoneBook.cpp

PhoneBook.cpp
#include "PhoneBook.hpp"
#include <iostream>
#include <iomanip>
#include <cstdlib>
PhoneBook::PhoneBook() : _currentIndex(0), _totalContacts(0) {}
PhoneBook::~PhoneBook() {}
std::string PhoneBook::_truncate(const std::string& str) const {
if (str.length() > 10)
return str.substr(0, 9) + ".";
return str;
}
void PhoneBook::_displayContactRow(int index) const {
std::cout << std::setw(10) << std::right << index << "|";
std::cout << std::setw(10) << std::right << _truncate(_contacts[index].getFirstName()) << "|";
std::cout << std::setw(10) << std::right << _truncate(_contacts[index].getLastName()) << "|";
std::cout << std::setw(10) << std::right << _truncate(_contacts[index].getNickname()) << std::endl;
}
std::string PhoneBook::_getInput(const std::string& prompt) const {
std::string input;
while (true) {
std::cout << prompt;
if (!std::getline(std::cin, input)) {
std::cout << std::endl;
std::exit(0);
}
if (!input.empty())
break;
std::cout << "Le champ ne peut pas être vide. Veuillez réessayer." << std::endl;
}
return input;
}
void PhoneBook::addContact() {
Contact newContact;
newContact.setFirstName(_getInput("Entrez le prénom : "));
newContact.setLastName(_getInput("Entrez le nom : "));
newContact.setNickname(_getInput("Entrez le surnom : "));
newContact.setPhoneNumber(_getInput("Entrez le numéro de téléphone : "));
newContact.setDarkestSecret(_getInput("Entrez le secret le plus sombre : "));
newContact.setIsEmpty(false);
_contacts[_currentIndex] = newContact;
_currentIndex = (_currentIndex + 1) % 8;
if (_totalContacts < 8)
_totalContacts++;
std::cout << "Contact ajouté avec succès !" << std::endl;
}
void PhoneBook::searchContacts() const {
if (_totalContacts == 0) {
std::cout << "PhoneBook est vide." << std::endl;
return;
}
// Afficher l'en-tête
std::cout << std::setw(10) << "Index" << "|";
std::cout << std::setw(10) << "First Name" << "|";
std::cout << std::setw(10) << "Last Name" << "|";
std::cout << std::setw(10) << "Nickname" << std::endl;
std::cout << std::string(44, '-') << std::endl;
// Afficher tous les contacts
for (int i = 0; i < _totalContacts; i++)
_displayContactRow(i);
// Obtenir l'index de l'utilisateur
std::cout << "Entrez l'index pour voir les détails : ";
std::string indexStr;
if (!std::getline(std::cin, indexStr)) {
std::cout << std::endl;
return;
}
// Valider l'index
if (indexStr.length() != 1 || indexStr[0] < '0' || indexStr[0] > '7') {
std::cout << "Index invalide." << std::endl;
return;
}
int index = indexStr[0] - '0';
if (index >= _totalContacts) {
std::cout << "Index invalide." << std::endl;
return;
}
// Afficher les détails complets du contact
std::cout << "Prénom : " << _contacts[index].getFirstName() << std::endl;
std::cout << "Nom : " << _contacts[index].getLastName() << std::endl;
std::cout << "Surnom : " << _contacts[index].getNickname() << std::endl;
std::cout << "Numéro de téléphone : " << _contacts[index].getPhoneNumber() << std::endl;
std::cout << "Secret le plus sombre : " << _contacts[index].getDarkestSecret() << std::endl;
}

main.cpp

main.cpp
#include "PhoneBook.hpp"
#include <iostream>
#include <cstdlib>
int main() {
PhoneBook phonebook;
std::string command;
while (true) {
std::cout << "Entrez une commande (ADD, SEARCH, EXIT) : ";
if (!std::getline(std::cin, command)) {
std::cout << std::endl;
break;
}
if (command == "ADD")
phonebook.addContact();
else if (command == "SEARCH")
phonebook.searchContacts();
else if (command == "EXIT")
break;
}
return 0;
}

Note : C’est un exercice BONUS. Complétez-le après ex00 et ex01 si vous voulez de la pratique supplémentaire avec les membres statiques et la rétro-ingénierie.

Cet exercice est différent. Au lieu de construire à partir de zéro, on vous donne des morceaux et vous devez déduire les parties manquantes. Vous recevez :

  • Account.hpp - La déclaration complète de la classe
  • tests.cpp - Le code de test qui utilise la classe
  • Un fichier log - La sortie attendue montrant ce qui devrait se passer

Votre travail : Écrire Account.cpp pour que les tests produisent une sortie correspondant au log (sauf les timestamps).

Ce qui vous est donné :

Le fichier header Account.hpp déclare :

  • Des membres statiques suivant l’état global (_nbAccounts, _totalAmount, etc.)
  • Des membres d’instance pour chaque compte (_accountIndex, _amount, etc.)
  • Une méthode privée _displayTimestamp()
  • Constructeur, destructeur, et diverses méthodes

Ce que vous devez déterminer :

  1. Comment initialiser les membres statiques (ils DOIVENT être définis quelque part)
  2. Comment formater le timestamp du log : [YYYYMMDD_HHMMSS]
  3. Le format exact de sortie (paires clé:valeur séparées par des points-virgules)
  4. Ce que chaque méthode devrait afficher et quand

Étape 1 - Analyser le fichier header

Lisez chaque ligne de Account.hpp. Listez ce que vous devez implémenter :

  • 4 membres statiques nécessitent une initialisation
  • 4 méthodes getter statiques
  • 1 méthode d’affichage statique (displayAccountsInfos)
  • Constructeur et destructeur
  • makeDeposit, makeWithdrawal, checkAmount, displayStatus
  • _displayTimestamp privée

Étape 2 - Étudier le fichier log

Le log montre EXACTEMENT quelle sortie produire. Observations clés :

  • Chaque ligne commence par un timestamp : [19920104_091532]
  • Le format est séparé par des points-virgules : index:0;amount:42;created
  • Les appels au destructeur se font dans l’ordre INVERSE de la construction

Étape 3 - Mapper les lignes du log aux méthodes

Pattern du logMéthode
accounts:N;total:N;deposits:N;withdrawals:NdisplayAccountsInfos()
index:N;amount:N;createdConstructeur
index:N;amount:N;closedDestructeur
index:N;p_amount:N;deposit:N;amount:N;nb_deposits:NmakeDeposit()
index:N;p_amount:N;withdrawal:refusedmakeWithdrawal() (échec)
index:N;p_amount:N;withdrawal:N;amount:N;nb_withdrawals:NmakeWithdrawal() (succès)

Étape 1 - Initialisation des membres statiques :

#include "Account.hpp"
// CRITIQUE : Les membres statiques DOIVENT être initialisés hors de la classe
// Cela alloue le stockage réel pour eux
int Account::_nbAccounts = 0;
int Account::_totalAmount = 0;
int Account::_totalNbDeposits = 0;
int Account::_totalNbWithdrawals = 0;

Pourquoi hors de la classe ? Les membres statiques sont partagés par TOUTES les instances. Le header ne fait que les déclarer ; vous devez les définir (allouer le stockage pour eux) exactement une fois dans un fichier .cpp.

Étape 2 - Formatage du timestamp :

#include <ctime>
#include <iomanip>
void Account::_displayTimestamp() {
std::time_t now = std::time(NULL);
std::tm* local = std::localtime(&now);
std::cout << "["
<< (local->tm_year + 1900) // Année depuis 1900
<< std::setfill('0') << std::setw(2) << (local->tm_mon + 1) // Mois 0-11
<< std::setfill('0') << std::setw(2) << local->tm_mday
<< "_"
<< std::setfill('0') << std::setw(2) << local->tm_hour
<< std::setfill('0') << std::setw(2) << local->tm_min
<< std::setfill('0') << std::setw(2) << local->tm_sec
<< "] ";
}

Comprendre <ctime> :

FonctionBut
std::time(NULL)Obtient l’heure courante en secondes depuis epoch
std::localtime(&now)Convertit en struct heure locale (tm)
tm->tm_yearAnnées depuis 1900 (ajouter 1900 pour l’année réelle)
tm->tm_monMois 0-11 (ajouter 1 pour format humain)

Étape 3 - Constructeur et destructeur :

Account::Account(int initial_deposit) : _amount(initial_deposit), _nbDeposits(0), _nbWithdrawals(0) {
_accountIndex = _nbAccounts++;
_totalAmount += initial_deposit;
_displayTimestamp();
std::cout << "index:" << _accountIndex
<< ";amount:" << _amount
<< ";created" << std::endl;
}
Account::~Account() {
_displayTimestamp();
std::cout << "index:" << _accountIndex
<< ";amount:" << _amount
<< ";closed" << std::endl;
}

Étape 4 - Méthodes de dépôt et retrait :

void Account::makeDeposit(int deposit) {
_displayTimestamp();
std::cout << "index:" << _accountIndex
<< ";p_amount:" << _amount
<< ";deposit:" << deposit;
_amount += deposit;
_nbDeposits++;
_totalAmount += deposit;
_totalNbDeposits++;
std::cout << ";amount:" << _amount
<< ";nb_deposits:" << _nbDeposits << std::endl;
}
bool Account::makeWithdrawal(int withdrawal) {
_displayTimestamp();
std::cout << "index:" << _accountIndex
<< ";p_amount:" << _amount
<< ";withdrawal:";
if (withdrawal > _amount) {
std::cout << "refused" << std::endl;
return false;
}
_amount -= withdrawal;
_nbWithdrawals++;
_totalAmount -= withdrawal;
_totalNbWithdrawals++;
std::cout << withdrawal
<< ";amount:" << _amount
<< ";nb_withdrawals:" << _nbWithdrawals << std::endl;
return true;
}
  1. Oublier d’initialiser les membres statiques

    • Sans initialisation, vous aurez des erreurs de liaison
    • Doit être fait dans le fichier .cpp, pas le header
  2. Mauvais format de timestamp

    • Le mois est 0-11, pas 1-12
    • L’année est depuis 1900, pas l’année réelle
  3. Oublier de mettre à jour les totaux globaux

    • Chaque dépôt/retrait doit mettre à jour les compteurs statiques
  4. Ordre des destructeurs

    • Les objets sont détruits dans l’ordre inverse de leur construction
    • Vérifiez que votre log correspond à cet ordre

Comparez votre sortie avec le fichier log fourni. Les timestamps seront différents, mais tout le reste doit correspondre exactement :

Fenêtre de terminal
# Compilez et exécutez
c++ -Wall -Wextra -Werror -std=c++98 *.cpp -o account
./account > my_output.log
# Comparez (ignorez les timestamps)
diff <(sed 's/\[[0-9_]*\]/[TIMESTAMP]/g' 19920104_091532.log) \
<(sed 's/\[[0-9_]*\]/[TIMESTAMP]/g' my_output.log)

Si les fichiers correspondent (sauf timestamps), votre implémentation est correcte !