ELCa11 - BE #1 — Découverte du langage C++¶
Cours associé : Cours 1 — Initiation au C++ (voir le polycopié, section Cours 1).
Objectifs¶
- Découvrir la syntaxe du C++ par l’exemple : variables, contrôle de flux, fonctions, conteneurs, pointeurs.
- Prendre en main l’environnement de développement Qt Creator (voir le tuto Création d’un projet C++/Qt).
Prérequis¶
- Bases de programmation impérative (boucles, fonctions, types) — typiquement acquises en Python ou autre langage en première année.
- Aucune connaissance préalable du C ou du C++ n’est supposée.
Principe¶
Le BE est organisé en 6 séquences progressives. À partir de la séquence 3, un fil rouge : travailler sur une série de nombres et en calculer des statistiques.
Pour chaque séquence :
- Notions à maîtriser — ce qu’il faut apprendre ;
- Exemples — programmes commentés, à comprendre et exécuter ;
- Points à noter / Questions à se poser — à discuter avec l’enseignant ;
- Programme à réaliser — exercice à coder.
Séquence 1 — Premier programme C++¶
Notions à maîtriser
- Structure minimale d’un programme C++ ;
- directives
#includeet espace de nomsstd::; - types primitifs :
int,double,char,bool,std::string; - affichage avec
std::cout; - opérateurs arithmétiques (
+,-,*,/,%) et de comparaison (==,<,>,<=,>=,!=) ; - instruction conditionnelle
if (…) { … } else { … }.
Exemple 1.1 — Moyenne de deux notes¶
/* ===========================================
Exemple 1.1 — Calcul d'une moyenne
=========================================== */
#include <iostream>
int main() {
int note1 = 12;
int note2 = 15;
// Attention : int / int donne un int. Le 2.0 force le calcul en double.
double moyenne = (note1 + note2) / 2.0;
std::cout << "Note 1 : " << note1 << std::endl;
std::cout << "Note 2 : " << note2 << std::endl;
std::cout << "Moyenne : " << moyenne << std::endl;
if (moyenne >= 10.0) {
std::cout << "Élève reçu." << std::endl;
} else {
std::cout << "Élève recalé." << std::endl;
}
return 0;
}
Points à noter
- Tout programme C++ possède une fonction
int main()qui retourne un entier (0= succès). #include <…>charge un en-tête de bibliothèque (ici, les fonctions d’entrée/sortie).std::coutest le flux de sortie standard ;<<est l’opérateur d’écriture sur ce flux.std::endlinsère un retour à la ligne. Équivalent simple :"\n".
Questions à se poser
- Que vaut
moyennesi on remplace2.0par2? - Comment afficher les deux notes sur une même ligne ?
- Comment ajouter un cas “mention bien” pour
moyenne >= 14?
Exemple 1.2 — Équation du second degré¶
/* ===========================================
Exemple 1.2 — Résolution de a·x² + b·x + c = 0
=========================================== */
#include <iostream>
#include <cmath> // pour std::sqrt
int main() {
int a = 1, b = -3, c = 2;
double delta = b * b - 4.0 * a * c;
if (delta < 0) {
std::cout << "Pas de racine réelle." << std::endl;
} else if (delta > 0) {
double x1 = (-b + std::sqrt(delta)) / (2.0 * a);
double x2 = (-b - std::sqrt(delta)) / (2.0 * a);
std::cout << "x1 = " << x1 << std::endl;
std::cout << "x2 = " << x2 << std::endl;
} else {
double x = -b / (2.0 * a);
std::cout << "Racine double : x = " << x << std::endl;
}
return 0;
}
Points à noter
- L’en-tête
<cmath>rend disponibles les fonctions mathématiques (std::sqrt,std::pow,std::sin, …). else ifchaîne les conditions sans imbrication profonde.
Questions à se poser
- Pourquoi écrit-on
4.0 * a * cplutôt que4 * a * c? - Que faut-il ajouter pour gérer le cas où
a == 0(l’équation devient linéaire) ?
Programme à réaliser¶
Soit la série de notes suivante, codée en dur :
Écrire un programme qui, pour chaque note :
- affiche la note et indique si elle est en-dessous ou au-dessus de la moyenne (10) ;
- si elle est au-dessus de 14, ajoute la mention “très bien” ;
- en fin de programme, affiche le nombre total de notes au-dessus de 10.
Indication : utilisez une boucle. Dans la prochaine séquence, on verra for plus en détail ; pour cet exercice, voici la syntaxe à utiliser :
Séquence 2 — Boucles et mise en forme¶
Notions à maîtriser
- Boucles :
while,do-while,for; - mise en forme de la sortie :
<iomanip>(std::setw,std::fixed,std::setprecision) ; - déclaration d’une constante avec
const.
Exemple 2.1 — Table de conversion Fahrenheit → Celsius¶
Trois manières équivalentes d’écrire la même boucle.
/* ===========================================
Exemple 2.1 — Conversion Fahrenheit → Celsius
de 0 à 100 F par pas de 20 F
=========================================== */
#include <iostream>
int main() {
const int inf = 0, sup = 100, pas = 20;
std::cout << "--- avec while ---" << std::endl;
double fahr = inf;
while (fahr <= sup) {
double celsius = (5.0 / 9.0) * (fahr - 32.0);
std::cout << fahr << " F -> " << celsius << " C" << std::endl;
fahr += pas;
}
std::cout << "--- avec do-while ---" << std::endl;
fahr = inf;
do {
double celsius = (5.0 / 9.0) * (fahr - 32.0);
std::cout << fahr << " F -> " << celsius << " C" << std::endl;
fahr += pas;
} while (fahr <= sup);
std::cout << "--- avec for ---" << std::endl;
for (double f = inf; f <= sup; f += pas) {
double celsius = (5.0 / 9.0) * (f - 32.0);
std::cout << f << " F -> " << celsius << " C" << std::endl;
}
return 0;
}
Points à noter
const int inf = 0;rend la variable non modifiable. À utiliser pour toute valeur qui ne doit pas changer.- Les trois boucles font la même chose, mais s’expriment différemment :
while: condition testée avant le corps. Le corps peut ne jamais s’exécuter ;do-while: condition testée après. Le corps s’exécute au moins une fois ;for: la plus compacte pour un parcours avec compteur.
fahr += pas;est équivalent àfahr = fahr + pas;.
Questions à se poser
- Si on prend
inf = 100etsup = 0, que fait chaque boucle ? - Pourquoi déclarer
fà l’intérieur duforest-il préférable à réutiliserfahr?
Mise en forme avec <iomanip>¶
L’affichage par défaut n’est pas très lisible. L’en-tête <iomanip> permet de formater les colonnes :
#include <iostream>
#include <iomanip>
int main() {
const int inf = 0, sup = 100, pas = 20;
std::cout << std::fixed << std::setprecision(2); // toujours 2 décimales
for (double f = inf; f <= sup; f += pas) {
double c = (5.0 / 9.0) * (f - 32.0);
std::cout << std::setw(8) << f
<< " -> "
<< std::setw(8) << c << std::endl;
}
return 0;
}
Questions à se poser
- À quoi sert
std::setw(8)? Son effet est-il permanent ou ponctuel (sur la valeur suivante uniquement) ? - À quoi servent
std::fixedetstd::setprecision(2)? Combien de temps leur effet dure-t-il ?
Programme à réaliser¶
Soit la constante const int N = 10;.
Écrire un programme qui affiche, sous forme de tableau aligné, pour les N premiers entiers strictement positifs :
- la valeur de l’entier ;
- son carré ;
- sa racine carrée (avec 3 décimales).
Indications
- L’en-tête
<cmath>donne accès àstd::sqrt. - Pour le carré d’un entier,
i * iest plus simple questd::pow(i, 2). - Utilisez
<iomanip>pour aligner les colonnes.
Sortie attendue (extrait) :
Séquence 3 — Conteneurs std::vector et std::string¶
Notions à maîtriser
std::vector<T>: création, accès, ajout, taille ;std::string: création, concaténation, accès ;- parcours avec une boucle
forindexée et avec une boucleforrange-based ; - déduction de type avec
auto; - génération de nombres aléatoires avec
<random>.
Exemple 3.1 — std::vector<int>¶
/* ===========================================
Exemple 3.1 — Le conteneur std::vector
=========================================== */
#include <iostream>
#include <vector>
int main() {
// Trois manières de créer un vector
std::vector<int> v1; // vide
std::vector<int> v2 = { 3, 1, 4, 1, 5, 9 }; // par liste de valeurs
std::vector<int> v3(10, 0); // 10 éléments tous à 0
// Ajout en fin
v1.push_back(7);
v1.push_back(8);
v1.push_back(9);
// Taille et accès
std::cout << "v1 contient " << v1.size() << " éléments." << std::endl;
std::cout << "v2[2] = " << v2[2] << std::endl;
// Parcours indexé classique
std::cout << "v2 (indexé) : ";
for (std::size_t i = 0; i < v2.size(); ++i) {
std::cout << v2[i] << " ";
}
std::cout << std::endl;
// Parcours moderne (range-based for) avec déduction de type (auto)
std::cout << "v2 (range) : ";
for (auto x : v2) {
std::cout << x << " ";
}
std::cout << std::endl;
return 0;
}
Points à noter
std::vector<T>est un tableau dynamique : il peut grandir avecpush_back.v.size()retourne le nombre d’éléments (typestd::size_t, non signé).- L’accès par
v[i]ne vérifie pas les bornes ; pour un accès vérifié, utiliserv.at(i)(lance une exception en cas de dépassement — on y reviendra dans le BE #4). - La boucle range-based
for (auto x : v)est plus lisible que la version indexée et évite les erreurs d’indice. Elle fonctionne avec tout conteneur standard.
Questions à se poser
- Quelle est la différence de comportement entre
std::vector<int> v(10);etstd::vector<int> v;? - Que se passe-t-il si on écrit
v[100]alors que le vecteur a 10 éléments ?
Exemple 3.2 — std::string¶
/* ===========================================
Exemple 3.2 — Le type std::string
=========================================== */
#include <iostream>
#include <string>
int main() {
std::string nom = "Centrale";
std::string ville = "Lyon";
// Concaténation avec +
std::string complet = nom + " " + ville;
std::cout << complet << std::endl;
// Taille et accès caractère par caractère
std::cout << "Longueur : " << complet.length() << std::endl;
std::cout << "Premier : " << complet[0] << std::endl;
// Une chaîne se parcourt comme un vector
for (char c : complet) {
std::cout << c << "-";
}
std::cout << std::endl;
return 0;
}
Points à noter
std::stringse manipule comme un type primitif : affectation, comparaison (==,!=), concaténation (+).- En interne, c’est essentiellement un
std::vector<char>: on peut accéder à chaque caractère par[i]et le parcourir avec une boucle range-based.
Exemple 3.3 — Génération aléatoire avec <random>¶
Pour éviter d’utiliser std::cin, on remplit nos séries avec des valeurs tirées au hasard.
/* ===========================================
Exemple 3.3 — Génération de valeurs aléatoires
=========================================== */
#include <iostream>
#include <vector>
#include <random>
int main() {
// Générateur pseudo-aléatoire avec graine fixe (résultats reproductibles)
std::mt19937 gen(42);
// Distribution uniforme : entiers entre 0 et 100 inclus
std::uniform_int_distribution<int> dist(0, 100);
// Remplir un vector de 10 entiers aléatoires
std::vector<int> serie;
for (int i = 0; i < 10; ++i) {
serie.push_back(dist(gen));
}
// Affichage
std::cout << "Série : ";
for (auto x : serie) {
std::cout << x << " ";
}
std::cout << std::endl;
return 0;
}
Points à noter
std::mt19937est un générateur pseudo-aléatoire de bonne qualité. La graine42rend les résultats reproductibles d’une exécution à l’autre — pratique pour déboguer.- Pour des tirages vraiment aléatoires, on peut remplacer
std::mt19937 gen(42);parstd::mt19937 gen(std::random_device{}());. std::uniform_int_distribution<int>(a, b)produit des entiers entreaetbinclus.
Questions à se poser
- Que se passe-t-il si on exécute deux fois le programme avec la graine
42? Avecstd::random_device{}()? - Comment changer le programme pour tirer des nombres entre
-50et+50?
Programme à réaliser¶
Soit const int N = 20;.
Écrire un programme qui :
- Génère un vecteur de
Nentiers aléatoires entre0et100(graine fixe42pour avoir des résultats reproductibles). - Affiche la série.
- Calcule et affiche le minimum, le maximum et la moyenne de la série.
Indications
- Initialisez
minà un grand entier (par exemple, le premier élément du vector) etmaxà un petit entier (idem) ; mettez-les à jour au fil du parcours. - La moyenne est de type
double. Forcez la division en flottants pour ne pas perdre la partie décimale.
Sortie attendue (avec graine 42, valeurs indicatives — la sortie exacte dépend de l’implémentation de <random> et peut varier selon le compilateur) :
Séquence 4 — Fonctions¶
Notions à maîtriser
- Définition et appel d’une fonction ;
- passage des paramètres par valeur, par référence (
&) et par adresse (*, pointeur) ; - paramètre
const T&pour passer un objet sans le copier ni le modifier ; - fonction
void(qui ne retourne rien) et fonction qui retourne une valeur.
Exemple 4.1 — Passage par valeur¶
/* ===========================================
Exemple 4.1 — Passage par valeur (copie)
=========================================== */
#include <iostream>
int somme(int a, int b) {
return a + b;
}
void incrementer(int x) { // x est une COPIE locale
x = x + 1;
std::cout << "Dans la fonction, x = " << x << std::endl;
}
int main() {
int s = somme(3, 4);
std::cout << "somme(3, 4) = " << s << std::endl;
int n = 10;
incrementer(n);
std::cout << "Après appel, n = " << n << std::endl; // n vaut toujours 10
return 0;
}
Points à noter
- Par défaut, les arguments d’une fonction sont passés par valeur : la fonction reçoit une copie de la variable de l’appelant. Modifier cette copie ne change pas l’original.
- Le type de retour précède le nom :
int somme(...),void incrementer(...). Le mot-clévoidsignifie “ne retourne rien”.
Exemple 4.2 — Passage par référence et par adresse¶
/* ===========================================
Exemple 4.2 — Permutation par référence et par pointeur
=========================================== */
#include <iostream>
void permuter_ref(int& a, int& b) { // référence : a et b désignent les variables de l'appelant
int tmp = a;
a = b;
b = tmp;
}
void permuter_ptr(int* a, int* b) { // pointeur : *a et *b accèdent aux valeurs pointées
int tmp = *a;
*a = *b;
*b = tmp;
}
int main() {
int x = 1, y = 2;
permuter_ref(x, y);
std::cout << "Après ref : x = " << x << ", y = " << y << std::endl;
permuter_ptr(&x, &y);
std::cout << "Après ptr : x = " << x << ", y = " << y << std::endl;
return 0;
}
Points à noter
- Un paramètre référence
int& asignifie :aest un alias de la variable passée en argument. Toute modification deamodifie la variable de l’appelant. - Un paramètre pointeur
int* areçoit l’adresse d’une variable. On y accède en déréférençant :*a. À l’appel, on prend l’adresse avec&x. - Référence et pointeur ont le même effet, mais la syntaxe est plus simple côté appelant avec les références.
Questions à se poser
- Peut-on appeler
permuter_ref(3, 4)? Pourquoi ? - Peut-on appeler
permuter_ptr(nullptr, &y)? Que se passe-t-il alors ?
Exemple 4.3 — Calcul de statistiques par une fonction¶
/* ===========================================
Exemple 4.3 — Fonction qui retourne plusieurs valeurs
via des paramètres de sortie (références)
=========================================== */
#include <iostream>
#include <vector>
// Le vector est passé par const& : pas de copie, lecture seule.
// min, max, moyenne sont passés par référence : ce sont les sorties.
void stats(const std::vector<int>& v, int& min, int& max, double& moyenne) {
min = v[0];
max = v[0];
long somme = 0;
for (auto x : v) {
if (x < min) min = x;
if (x > max) max = x;
somme += x;
}
moyenne = (double)somme / v.size();
}
int main() {
std::vector<int> serie = { 13, 92, 35, 82, 14, 70 };
int minVal, maxVal;
double moy;
stats(serie, minVal, maxVal, moy);
std::cout << "Min : " << minVal << std::endl;
std::cout << "Max : " << maxVal << std::endl;
std::cout << "Moy : " << moy << std::endl;
return 0;
}
Points à noter
const std::vector<int>& vsignifie : on passe le vecteur sans le copier (&= référence) et on s’engage à ne pas le modifier (const). C’est la manière standard de transmettre un gros objet à une fonction qui ne le modifie pas.(double)somme / v.size()force la division en flottants. Sans le cast,somme / v.size()ferait une division entière.
Programme à réaliser¶
Reprendre la série aléatoire de la séquence 3 (graine 42, N = 20 entiers entre 0 et 100).
Écrire deux variantes équivalentes d’une fonction qui calcule la moyenne et la variance d’une série :
stats_ref(const std::vector<int>& v, double& moyenne, double& variance)— passage par référence ;stats_ptr(const std::vector<int>& v, double* moyenne, double* variance)— passage par adresse.
Rappel : la variance se calcule en deux passes — on calcule d’abord la moyenne, puis on calcule la moyenne des carrés des écarts à cette moyenne.
Vérifier que les deux fonctions donnent le même résultat.
Sortie attendue (valeurs indicatives) :
Série : 13 92 35 82 14 70 ...
[stats_ref] moyenne = 51.40, variance = 942.84
[stats_ptr] moyenne = 51.40, variance = 942.84
Séquence 5 — Pointeurs et allocation dynamique¶
Notions à maîtriser
- Variables pointeurs : déclaration, opérateurs
&(adresse) et*(déréférencement) ; - valeur nulle d’un pointeur :
nullptr; - allocation dynamique d’un tableau 1D :
new T[n]/delete[]; - bonne pratique : libérer toute mémoire allouée et remettre le pointeur à
nullptr.
Exemple 5.1 — Variables pointeurs¶
/* ===========================================
Exemple 5.1 — Adresse, pointeur, déréférencement
=========================================== */
#include <iostream>
int main() {
int x = 42;
int* p = &x; // p contient l'adresse de x
std::cout << "x = " << x << std::endl;
std::cout << "&x = " << &x << std::endl; // l'adresse de x
std::cout << "p = " << p << std::endl; // contient la même adresse
std::cout << "*p = " << *p << std::endl; // la valeur pointée par p
*p = 100; // modifie x à travers p
std::cout << "Après *p = 100, x vaut " << x << std::endl;
int* q = nullptr; // pointeur nul : ne pointe sur rien
if (q == nullptr) {
std::cout << "q ne pointe sur rien." << std::endl;
}
return 0;
}
Points à noter
int* pdéclare un pointeur sur unint. À ce stade,pn’est pas initialisé : il vaut une adresse quelconque. Toujours initialiser un pointeur (avec une vraie adresse ou avecnullptr).- L’opérateur
&(adresse) appliqué à une variable retourne son adresse mémoire. - L’opérateur
*(déréférencement) appliqué à un pointeur accède à la valeur pointée. nullptr(C++11) est la valeur conventionnelle pour “pointeur invalide”. Préférable à l’ancienNULLou0.
Questions à se poser
- Que se passe-t-il si l’on écrit
int* p; *p = 5;(pointeur non initialisé) ? - Que se passe-t-il si l’on écrit
int* p = nullptr; *p = 5;?
Exemple 5.2 — Allocation dynamique d’un tableau 1D¶
/* ===========================================
Exemple 5.2 — Tableau 1D alloué dynamiquement
=========================================== */
#include <iostream>
int main() {
int n = 5;
int* tab = new int[n]; // alloue n entiers sur le tas
for (int i = 0; i < n; ++i) {
tab[i] = i * i;
}
for (int i = 0; i < n; ++i) {
std::cout << "tab[" << i << "] = " << tab[i] << std::endl;
}
delete[] tab; // libère la mémoire (impératif !)
tab = nullptr; // bonne pratique : invalide le pointeur
return 0;
}
Points à noter
new T[n]réserve sur le tas (mémoire dynamique) un tableau denéléments de typeTet retourne un pointeur sur le premier élément.delete[] tablibère la mémoire correspondante. Toute mémoire allouée parnew[]doit être libérée pardelete[]— sinon, fuite mémoire.- Le crochet est essentiel :
delete tab(sans[]) sur un tableau cause un comportement indéfini.
Programme à réaliser¶
Reprendre la série aléatoire de la séquence 3, mais cette fois sans utiliser std::vector : les N = 20 entiers seront stockés dans un tableau 1D alloué dynamiquement.
- Allouer dynamiquement un tableau de
Nentiers. - Le remplir avec des entiers aléatoires entre 0 et 100 (graine
42). - Écrire une fonction
void stats(const int* tab, int N, double& moyenne, double& variance)qui calcule la moyenne et la variance. - Afficher la série, la moyenne et la variance.
- Libérer la mémoire.
Indications
- Le pointeur seul ne connaît pas la taille du tableau qu’il pointe : il faut systématiquement passer la taille
Nen paramètre des fonctions. - N’oubliez pas le
delete[]à la fin dumain(). Sinon : fuite mémoire.
Sortie attendue (valeurs indicatives, identiques à celles de la séquence 4) :
Note : en C++ moderne,
std::vector<int>(vu en séquence 3) est presque toujours préférable ànew int[N] / delete[]pour des raisons de sûreté et de simplicité (pas de risque d’oublier ledelete, taille connue, redimensionnement automatique). On utilise les pointeurs nus principalement pour comprendre les mécanismes sous-jacents et pour interfacer avec du code existant. L’allocation dynamique en 2 dimensions sera abordée au BE #2, dans le contexte d’une classeDamier.
Séquence 6 — Synthèse¶
Cette dernière séquence n’introduit pas de nouvelle notion. Objectif : combiner tout ce que vous avez vu dans un programme complet.
Programme à réaliser¶
Simulation de 3 dés différents lancés chacun N = 1000 fois :
- un dé classique à 6 faces (
D6, valeurs entre 1 et 6) ; - un dé à 10 faces (
D10, valeurs entre 1 et 10) ; - un dé à 20 faces (
D20, valeurs entre 1 et 20).
- Pour chaque dé, générer la série des
Nlancers (graines respectives42,2024,7). - Stocker chaque série dans un
std::vector<int>(ou un tableau alloué dynamiquement, au choix). - Pour chaque dé, calculer min, max, moyenne et variance à l’aide d’une fonction
stats(...). - Afficher un tableau récapitulatif aligné de la forme :
Dé | Min | Max | Moyenne | Variance
--------+-------+-------+---------+---------
D6 | 1 | 6 | 3.51 | 2.91
D10 | 1 | 10 | 5.43 | 8.32
D20 | 1 | 20 | 10.62 | 33.15
- Indiquer en fin de programme le dé qui présente la plus grande variance observée. Comparer ensuite aux valeurs théoriques pour un tirage uniforme entre
1etn:
| Dé | Moyenne théorique | Variance théorique |
|---|---|---|
| D6 | 3.5 | 2.917 |
| D10 | 5.5 | 8.250 |
| D20 | 10.5 | 33.250 |
Rappel : pour une variable uniforme discrète entre
1etn, la moyenne vaut(n+1)/2et la variance vaut(n²−1)/12.
Bonus : afficher un histogramme ASCII des résultats du D6, montrant la fréquence d’apparition de chaque face de 1 à 6.
1 : ##############
2 : ##############
3 : ###############
4 : #################
5 : ##############
6 : #############
Aller plus loin — vers les classes¶
Vous remarquez probablement que vous traitez ces 3 séries de la même manière, en répétant les mêmes appels de fonctions sur des std::vector différents. Cela suggère qu’il serait pratique de regrouper les données (le vecteur des lancers) et les opérations (stats, affichage) dans une seule entité. C’est précisément l’idée des classes, que nous découvrirons au BE #2.