1. Introduction
Quand vous déclarez une variable, vous demandez littéralement au programme : «Alloue de la place pour une valeur». Et alors surgit la question embarrassante : que contient-elle au départ ? Pour certains types c'est prévisible, mais pour les nombres en variables locales c'est souvent «quelque chose qui reste en mémoire après une aventure précédente». Et c'est là que commence la magie… mais pas la bonne magie : celle qui casse des deadlines.
Imaginez une situation simple : vous écrivez un calculateur du coût d'un achat. Si vous avez oublié d'initialiser la remise ou la somme, le résultat peut être étrange : parfois zéro, parfois un nombre énorme, parfois «tout va bien»… jusqu'au moment où il faut rendre le devoir. L'initialisation est un moyen de rendre le comportement déterministe : vous savez par quoi vous commencez.
Initialisation et affectation
Ce sujet semble évident, jusqu'à ce que vous commenciez à lire du code (d'autrui ou le vôtre la semaine suivante). L'initialisation, c'est quand une variable est créée directement avec une valeur initiale. L'affectation, c'est quand une variable existe déjà et que vous changez son contenu. Dans le langage courant on appelle souvent les deux «affectation», mais en C++ ce sont des opérations et des règles différentes.
Mini-exemple montrant la différence temporelle :
#include <iostream>
int main() {
int x = 10; // initialisation
x = 25; // affectation
std::cout << x << '\n'; // 25
}
Pourquoi cela importe ? Parce que certaines constructions (par exemple les «accolades sécurisées» {}) agissent précisément au moment de l'initialisation. Et d'autres variables ne peuvent pas être laissées non initialisées selon les règles du langage (cela deviendra particulièrement important plus tard).
2. Formes d'initialisation : =, (), {}
En C++ une même intention («donner une valeur de départ») peut s'exprimer de différentes manières. Ce n'est pas parce que le langage voulait vous compliquer la vie (même si parfois on a cette impression), mais parce que C++ a évolué longtemps et a pris soin de conserver la compatibilité. Dans le C++ moderne il est important de ne pas seulement «connaître trois façons», mais de comprendre leurs différences et laquelle permet d'attraper des erreurs plus tôt.
Pendant notre cours, une modélisation suffisante est : il y a l'initialisation par copie avec =, l'initialisation directe avec des parenthèses (), et l'initialisation par liste (braced / list-initialization) avec des accolades {}. Dans la norme c'est vraiment un sujet vaste, et les formulations concernant = et braced-init-list ont été précisées séparément.
Comparaison sur un exemple :
#include <iostream>
int main() {
int a = 10;
int b(10);
int c{10};
std::cout << a << ' ' << b << ' ' << c << '\n'; // 10 10 10
}
Pour l'instant les trois variables sont identiques. Mais des différences vont apparaître ensuite.
Initialisation par copie =
L'initialisation par copie est la plus familière : «type, nom, égal, expression». Elle se lit bien et est pratique, donc vous la verrez tout le temps. Le principal danger ici est que = permet parfois au compilateur d'effectuer une conversion silencieuse — sans signaler «attention, on perd des données», notamment quand on convertit un nombre à virgule en entier.
Exemple où tout semble «normal», mais la partie décimale disparaît sans avertissement :
#include <iostream>
int main() {
int euros = 3.99; // la partie fractionnaire est tronquée
std::cout << euros << '\n'; // 3
}
Ce n'est pas toujours une erreur : parfois il faut effectivement enlever la partie fractionnaire. Mais il est important de savoir qu'avec = on peut facilement rater ça à l'œil, surtout si l'expression est longue.
Initialisation directe ()
Les parenthèses sont historiquement très populaires en C++ et souvent utilisées pour «donner directement» une valeur. Sur les types de base (int, double) elles ressemblent à = et se comportent généralement de façon prévisible. À ce stade du cours, retenez que () est une autre forme d'initialisation, et elle peut aussi «avaler» certaines conversions.
Exemple :
#include <iostream>
int main() {
double price(19.95);
int qty(2);
std::cout << price << ' ' << qty << '\n'; // 19.95 2
}
Il existe un piège classique (à connaître pour l'avenir, mais utile dès maintenant) : l'écriture int x(); ne crée pas une variable, elle déclare une fonction. Cela peut sembler une plaisanterie du compilateur, mais c'est la réalité du C++ (on l'appelle même le most vexing parse). Donc pour une «initialisation vide» avec des parenthèses mieux vaut ne pas jouer avec ça.
Initialisation par liste {}
Les accolades {} sont devenues dans le C++ moderne presque un moyen universel d'initialiser, car elles offrent une propriété importante : elles interdisent de nombreuses conversions dangereuses de narrowing. Autrement dit, {} force souvent le compilateur à s'arrêter et à dire : «Attends, es-tu sûr de vouloir perdre une partie des données ? »
De plus {} fournit une forme pratique «vide» : int x{}; — et vous obtenez systématiquement zéro. Pour un débutant c'est un peu comme une ceinture de sécurité : on peut toujours se mettre dans une situation problématique, mais il y aura moins de surprises aléatoires.
Exemple de base :
#include <iostream>
int main() {
int counter{}; // 0
double total{}; // 0.0
std::cout << counter << ' ' << total << '\n'; // 0 0
}
Accolades vides {} : zéro pour les nombres et vide pour les chaînes
Parfois vous ne connaissez pas à l'avance la valeur d'une variable parce qu'elle viendra de l'entrée. Mais même dans ce cas il est utile de donner un état initial correct pour ne pas lire «des ordures». Les accolades vides {} font cela très bien : pour les nombres ce sera zéro, pour bool ce sera false, pour char ce sera '\0', pour std::string — une chaîne vide.
Comparaison pratique :
#include <iostream>
#include <string>
int main() {
int a{}; // 0
double b{}; // 0.0
std::string name{}; // ""
std::cout << a << '\n'; // 0
std::cout << b << '\n'; // 0
std::cout << '[' << name << ']' << '\n'; // []
}
Et maintenant un anti-exemple (ne pas reproduire à la maison, en prod ni nulle part) : si vous écrivez int x; et que vous essayez immédiatement d'afficher x, vous obtiendrez une valeur indéterminée. Autrement dit le programme peut afficher n'importe quoi, et ce n'est pas un «aléa» utilisable mais simplement une erreur. Le compilateur n'est pas obligé de vous sauver.
4. Narrowing : quand une partie de l'information est perdue
Le mot narrowing se traduit par «rétrécissement» : vous prenez une valeur d'un «conteneur large» et la mettez dans un «conteneur étroit». Exemple typique : de double vers int : la partie fractionnaire ne tient pas et est supprimée. Autre exemple : d'un grand int vers un petit char : la valeur peut ne pas tenir dans la plage. Un autre cas : essayer de stocker un nombre négatif dans un unsigned, où les négatifs n'existent pas conceptuellement.
L'initialisation par liste {} est justement connue pour interdire nombre de ces conversions de narrowing à la compilation, et c'est une des raisons d'utiliser couramment la list-initialization dans le style moderne.
Exemple double → int : la partie fractionnaire disparaît
Voyons d'abord le cas où = laisse passer l'erreur silencieusement, alors que {} non.
#include <iostream>
int main() {
int x = 3.14; // compile, deviendra 3
int y{3.14}; // ne compile pas : narrowing
std::cout << x << '\n'; // 3
}
Et c'est là qu'une bonne habitude apparaît : si vous voyez int x = 3.14;, arrêtez-vous et demandez-vous : «je veux vraiment 3 et pas 3.14 ?». Parfois la réponse est oui, mais dans ce cas il vaut mieux rendre l'intention explicite (nous verrons plus tard comment clarifier ces intentions pour la lisibilité).
Exemple int → char : ça tient ou pas
char est un petit type (généralement 1 octet). Il conserve un caractère, mais techniquement c'est un nombre dans une petite plage. Si vous essayez d'y mettre un nombre élevé, vous aurez des comportements de débordement et des caractères inattendus.
{} aide souvent à détecter cette situation :
#include <iostream>
int main() {
int code = 1000;
char c1 = code; // peut compiler, mais la valeur sera étrange
char c2{code}; // généralement ne compile pas : narrowing
std::cout << code << '\n'; // 1000
}
Nous n'affichons pas le char ici, car le «caractère étrange» dépend beaucoup de l'encodage et de l'implémentation. L'important est autre : les accolades cherchent à ne pas vous laisser perdre le sens des données par accident.
Exemple -1 → unsigned : le moins devient un grand nombre
Sans entrer dans signed/unsigned, retenez que l'idée simple est : unsigned ne contient que des nombres non négatifs. Si vous essayez d'y mettre -1, le compilateur doit «convertir» la valeur d'une manière ou d'une autre. Le résultat devient alors un très grand nombre, ce qui effraie souvent les débutants plus que le commentaire du professeur «refais».
Les accolades bloquent fréquemment ce scénario dès la compilation :
#include <iostream>
int main() {
unsigned u1 = -1; // peut compiler, mais donnera un grand nombre
unsigned u2{-1}; // généralement ne compile pas : narrowing
std::cout << "Check passed.\n"; // Check passed.
}
Il est important de saisir l'idée : {} est un moyen d'inciter le compilateur à devenir votre partenaire de sécurité, plutôt qu'un complice silencieux des bugs.
5. Mini-application : MiniCheck
Pour que le sujet ne reste pas un ensemble de règles «parce que c'est comme ça», construisons un petit extrait console de notre application pédagogique. Appelons-le MiniCheck : un programme qui demande le prix, la quantité et la remise, puis affiche le total. Nous n'écrivons pas encore de belles fonctions ni de modules, tout vit dans main comme dans nos premières leçons — c'est simple et transparent.
Déclaration soignée des variables
Commençons par déclarer proprement les variables : tout ce qui est numérique s'initialise avec {}.
#include <iostream>
int main() {
double price{}; // prix par unité
int qty{}; // quantité
int discount{}; // remise en pourcentage
std::cin >> price >> qty >> discount;
}
Calculs sans surprises d'entiers
Ajoutons maintenant les calculs. Il est important de choisir des types afin que le résultat ne soit pas «cassé» par une division entière. Ainsi pour les pourcentages nous utilisons un coefficient double, plutôt que de diviser un int par un int.
#include <iostream>
int main() {
double price{};
int qty{};
int discount{};
std::cin >> price >> qty >> discount;
double subtotal = price * qty;
double k = (100 - discount) / 100.0;
double total = subtotal * k;
std::cout << total << '\n';
}
Notez le petit détail : nous avons écrit 100.0 pour que l'expression devienne flottante. Ce n'est pas de la «magie», mais un contrôle direct du type de l'expression via le littéral (nous aborderons aussi les littéraux plus tard, mais aujourd'hui l'idée suffit).
Rendons maintenant l'affichage un peu plus convivial et ajoutons des valeurs intermédiaires pour vérifier la logique visuellement :
#include <iostream>
int main() {
double price{};
int qty{};
int discount{};
std::cin >> price >> qty >> discount;
double subtotal = price * qty;
double total = subtotal * (100 - discount) / 100.0;
std::cout << "Subtotal: " << subtotal << '\n';
std::cout << "Total: " << total << '\n';
}
Où est le rapport avec l'initialisation ? Le fait que même si vous modifiez plus tard le code et commencez à imprimer subtotal avant le calcul (ou ajoutez une nouvelle variable), vos valeurs de départ ne seront pas «des ordures». De plus, si vous décidez, par exemple, de stocker la remise comme double, les accolades vous avertiront honnêtement si vous commencez à restreindre les types par erreur.
Comment choisir la forme d'initialisation
Au début on voudrait «une seule bonne règle pour tous les cas». En C++ cela n'existe pas, mais on peut se mettre d'accord sur un ensemble de bonnes habitudes donnant un bon compromis entre lisibilité et sécurité. Pour un débutant, le principe le plus utile est : par défaut utilisez {} — particulièrement pour les nombres — et utilisez = quand c'est évident et lisible, et soyez prudent avec ().
Si vous voulez une petite logique, voici un schéma simple :
flowchart TD
A["Il faut créer une variable"] --> B{"Y a-t-il une valeur initiale ?"}
B -->|Non| C["Utiliser T x{}; état nul ou vide"]
B -->|Oui| D{"Y a-t-il risque de narrowing ?"}
D -->|Oui ou incertain| E["Utiliser T x{expr}; le compilateur vérifiera"]
D -->|Non, tout tient| F["On peut T x = expr; ou T x(expr);"]
Et oui, il est normal de «ne pas être sûr». En programmation c'est une émotion de base. L'essentiel est que face au doute vous choisissiez la forme qui vous aide et ne complique pas la vie.
6. Erreurs typiques avec l'initialisation et le narrowing
Dans ce domaine les erreurs sont généralement pas «complexes» mais «discrètes» : le programme compile, parfois fonctionne, puis soudain fait quelque chose d'étrange. Le plus souvent le problème n'est pas la formule mais le fait qu'une variable a commencé sa vie sans valeur correcte ou a discrètement perdu des données lors d'un narrow. Il est donc utile d'apprendre à reconnaître ces bugs à leur style.
Erreur n°1 : déclarer une variable et oublier de lui donner une valeur de départ.
Les débutants écrivent souvent int sum; et pensent que c'est automatiquement zéro. Dans les variables locales ce n'est pas le cas : la valeur peut être n'importe quoi. Le moyen le plus simple pour corriger ça par habitude est d'écrire int sum{}; puis d'incrémenter sum pendant les calculs.
Erreur n°2 : confondre initialisation et affectation et penser que = signifie toujours «après».
La ligne int x = 10; — ce n'est pas «créé puis assigné», c'est une initialisation. Les règles et conversions autorisées en dépendent. Si vous gardez en tête «la première valeur est donnée à la naissance», le code devient plus lisible.
Erreur n°3 : perdre la partie fractionnaire et le remarquer seulement avec un mauvais résultat.
int x = 3.99; compile et devient 3. Si ce n'était pas intentionnel, l'erreur est sournoise : elle chuchote. L'habitude d'utiliser {} pour initialiser les nombres attrape souvent ces cas immédiatement, car beaucoup de situations de narrowing deviennent des erreurs de compilation avec les accolades.
Erreur n°4 : compresser un grand nombre dans un petit type, surtout dans char.
Quand on transforme un int en char, il est facile d'obtenir un caractère inattendu ou un autre nombre à cause de la plage. Même si le compilateur a autorisé la conversion, c'est presque toujours un signe qu'il faut s'arrêter et vérifier : est-ce que char est vraiment le bon conteneur pour cette information ?
Erreur n°5 : utiliser () «pour zéro» et déclarer accidentellement une fonction.
Une écriture comme int x(); ressemble à «crée x», mais le compilateur la lit comme la déclaration d'une fonction x qui retourne un int. Si vous voyez cela, c'est probablement une coquille, et il vaut mieux écrire int x{};.
GO TO FULL VERSION