1. Pourquoi parler de mémoire ?
Un programme range ses données à deux endroits.
| La pile (stack) | Le tas (heap) | |
|---|---|---|
| Contenu | Données de taille fixe et connue : nombres, booléens, tableaux | Données de taille variable : textes, listes qui grandissent |
| Rapidité | Très rapide | Plus lent : il faut demander de la place |
| Libération | Automatique à la fin de la fonction | Quelqu’un doit la libérer |
La vraie question est donc : qui libère la mémoire du tas, et quand ? Trois réponses existent.
| Approche | Langages | Inconvénient |
|---|---|---|
| Le programmeur libère à la main | C, C++ | Oubli (fuite), libération en double, utilisation après libération : des failles |
| Un ramasse-miettes libère ce qui ne sert plus | Python, Java, C#, JavaScript | Coût à l’exécution, pauses imprévisibles |
| Le compilateur vérifie des règles de possession | Rust | Des règles à apprendre : c’est l’objet de ce niveau |
2. String et &str
Rust a deux types de texte courants, et ce niveau est le bon moment pour les distinguer.
fn main() {
let fixe: &str = "Bonjour"; // texte écrit dans le programme, non modifiable
let mut dynamique = String::from("Bon"); // texte possédé, stocké dans le tas
dynamique.push_str("jour");
dynamique.push('!');
println!("{fixe} / {dynamique} ({} octets)", dynamique.len());
}
String | &str | |
|---|---|---|
| Nature | Un texte possédé, modifiable s’il est mut | Une vue empruntée sur un texte existant |
| Stockage | Dans le tas | Là où se trouve le texte observé |
| Créer | String::from("…"), "…".to_string(), format!(…) | Un littéral "…", ou &une_string |
| Paramètre de fonction | Rarement | Presque toujours |
3. Les trois règles de la possession
fn main() {
{
let s = String::from("temporaire"); // s possède le texte
println!("{s}");
} // fin de portée : le texte est libéré
// println!("{s}"); ← erreur : s n'existe plus ici
}
Le déplacement (move)
Que se passe-t-il quand on affecte une String à une autre variable ?
fn main() {
let s1 = String::from("Chevrollier");
let s2 = s1;
println!("{s1}");
}
error[E0382]: borrow of moved value: `s1`
--> src/main.rs:4:15
|
2 | let s1 = String::from("Chevrollier");
| -- move occurs because `s1` has type `String`, which does not implement the `Copy` trait
3 | let s2 = s1;
| -- value moved here
4 | println!("{s1}");
| ^^^^ value borrowed here after move
|
help: consider cloning the value if the performance cost is acceptable
|
3 | let s2 = s1.clone();
| ++++++++
Le texte n’a pas été copié : sa possession a été déplacée de s1 vers s2, et s1 n’est plus utilisable. Sans cette règle, s1 et s2 libéreraient chacune le même texte à la fin du bloc : une double libération.
Copier explicitement : clone
fn main() {
let s1 = String::from("Chevrollier");
let s2 = s1.clone(); // copie complète dans le tas : deux propriétaires, deux textes
println!("{s1} et {s2}");
let a = 5;
let b = a; // les types simples sont copiés automatiquement
println!("{a} et {b}");
}
Les types de taille fixe (entiers, flottants, bool, char, et les tuples qui n’en contiennent que) sont copiés : ils implémentent le trait Copy. Tous les autres sont déplacés.
La possession et les fonctions
fn afficher(texte: String) { // la fonction prend possession
println!("{texte}");
} // texte est libéré ici
fn main() {
let nom = String::from("Lina");
afficher(nom);
// afficher(nom); ← erreur E0382 : nom a été déplacé dans le premier appel
}
Passer une String à une fonction la déplace, exactement comme une affectation. Rendre la valeur à chaque fois serait pénible : c’est pourquoi on emprunte.
4. Emprunter : les références
Une référence permet d’utiliser une valeur sans en prendre possession. On dit qu’on emprunte la valeur. Elle s’écrit avec &.
fn longueur(texte: &String) -> usize { // emprunt en lecture
texte.len()
} // rien n'est libéré : texte n'était qu'emprunté
fn main() {
let nom = String::from("Chevrollier");
let n = longueur(&nom);
println!("{nom} contient {n} octets"); // nom est toujours utilisable
}
Pour modifier une valeur empruntée, il faut une référence mutable &mut, et la variable d’origine doit elle-même être mut :
fn ajouter_suffixe(texte: &mut String) {
texte.push_str(" - BTS SIO");
}
fn main() {
let mut classe = String::from("SIO1");
ajouter_suffixe(&mut classe);
println!("{classe}");
}
La règle d’emprunt
fn main() {
let mut s = String::from("texte");
let r1 = &s;
let r2 = &s; // plusieurs lecteurs : autorisé
println!("{r1} {r2}"); // dernière utilisation de r1 et r2
let r3 = &mut s; // un écrivain : autorisé, les lecteurs ne servent plus
r3.push_str(" modifié");
println!("{r3}");
}
fn main() {
let mut s = String::from("texte");
let lecteur = &s;
let ecrivain = &mut s; // erreur E0502 : on ne peut pas écrire pendant une lecture
ecrivain.push('!');
println!("{lecteur}");
}
Pourquoi cette règle ? Imagine que lecteur observe le texte pendant qu’ecrivain le fait grandir : le texte peut être déplacé ailleurs dans le tas pour avoir plus de place, et lecteur pointerait alors vers une zone libérée. La règle d’emprunt rend cette situation impossible à écrire.
Pas de référence pendante
fn creer() -> &String { // erreur E0106 : emprunt de quoi ?
let s = String::from("local");
&s
} // s est libérée ici : la référence désignerait du vide
La solution est de rendre la valeur elle-même : fn creer() -> String { String::from("local") }. La possession est alors transmise à l’appelant.
5. Les tranches (slices)
Une tranche est une référence vers une partie d’une collection. &str est d’ailleurs une tranche de texte.
fn premier_mot(texte: &str) -> &str {
for (i, c) in texte.char_indices() {
if c == ' ' {
return &texte[..i]; // du début jusqu'à l'espace (exclu)
}
}
texte // pas d'espace : tout le texte
}
fn main() {
let phrase = String::from("Rust est exigeant");
let mot = premier_mot(&phrase); // &String est accepté là où &str est attendu
println!("{mot}");
let notes = [12, 15, 9, 17, 11];
let milieu: &[i32] = ¬es[1..4]; // tranche de tableau : 15, 9, 17
println!("{:?}", milieu);
}
Le texte renvoyé par premier_mot est emprunté à phrase : le compilateur interdira de modifier ou de libérer phrase tant que mot sert encore. Dans d’autres langages, un indice calculé sur un texte qui change ensuite devient silencieusement faux.
6. Mini-projet : l’analyseur de phrase
7. Bilan
Exercices corrigés
Face à une erreur de possession, le réflexe du débutant est d’ajouter .clone() partout. Ça compile, mais ça contourne la question. Les corrigés montrent systématiquement la solution par emprunt, puis expliquent quand clone() est vraiment justifié.
Exercice 2.1 — Compile ou pas ?
Voir le corrigé
| Programme | Compile ? | Raison |
|---|---|---|
| A | Oui | Un i32 est Copy : x reste utilisable. |
| B | Non | a a été déplacée dans b (E0382 sur le println!). |
| C | Oui | clone() crée une copie : deux propriétaires distincts. |
| D | Oui | Un tuple de types Copy est lui-même Copy. |
| E | Non | Le tuple contient une String, il n’est pas Copy : il est déplacé. |
Explication
La question à se poser est toujours : ce type est-il Copy ? Tout ce qui contient une donnée dans le tas (String, Vec…) ne l’est pas, et un type composé n’est Copy que si tous ses éléments le sont.
Exercice 2.2 — Emprunter plutôt que prendre
Voir le corrigé
fn compter_voyelles(texte: &str) -> usize {
let mut n = 0;
for c in texte.chars() {
if "aeiouyàâäéèêëîïôöùûüÿAEIOUYÀÂÄÉÈÊËÎÏÔÖÙÛÜŸ".contains(c) {
n += 1;
}
}
n
}
fn mettre_en_majuscules(texte: &mut String) {
*texte = texte.to_uppercase();
}
fn main() {
let mut texte = String::from("Élève en BTS SIO");
println!("{} voyelles", compter_voyelles(&texte));
mettre_en_majuscules(&mut texte);
println!("{texte}");
println!("{} voyelles", compter_voyelles("littéral accepté aussi"));
}
Explication
L’étoile de *texte = … signifie « la valeur désignée par la référence » : on remplace le contenu de la String d’origine. Si compter_voyelles prenait une String, l’appel déplacerait texte, qui ne serait plus utilisable ensuite ; et on ne pourrait pas lui passer directement un littéral.
Exercice 2.3 — Réparer
Voir le corrigé
// A : saluer emprunte au lieu de prendre possession
fn saluer(nom: &str) { println!("Bonjour {nom}"); }
fn main() {
let nom = String::from("Sam");
saluer(&nom);
saluer(&nom);
}
// B : deux &mut simultanés (E0499) → on les utilise l'un après l'autre
fn main() {
let mut liste = String::from("a");
let r1 = &mut liste;
r1.push('b');
let r2 = &mut liste; // r1 ne sert plus : autorisé
r2.push('c');
println!("{liste}");
}
// C : on vide le texte pendant qu'une tranche le lit (E0502)
fn main() {
let mut texte = String::from("abc");
let debut = &texte[0..1];
println!("{debut}"); // on termine la lecture…
texte.clear(); // …avant de modifier
println!("[{texte}]");
}
// D : le compilateur ne sait pas de quel paramètre vient le résultat (E0106)
fn plus_long<'a>(a: &'a str, b: &'a str) -> &'a str {
if a.len() >= b.len() { a } else { b }
}
fn main() {
println!("{}", plus_long("Rust", "Python"));
}
Explication
A, B et C violent chacun une règle précise : déplacement, double écriture, écriture pendant une lecture. Pour D, la fonction renvoie une référence qui provient soit de a, soit de b : l’annotation 'a (une durée de vie) dit au compilateur que le résultat vit au plus aussi longtemps que les deux paramètres. Le niveau 5 y revient.
Mini-projet
Voir le corrigé
// &str : on lit seulement
fn nombre_mots(texte: &str) -> usize {
texte.split_whitespace().count()
}
// &str en entrée et en sortie : le résultat est une tranche de l'original
fn mot_le_plus_long(texte: &str) -> &str {
let mut meilleur = "";
for mot in texte.split_whitespace() {
if mot.chars().count() > meilleur.chars().count() {
meilleur = mot;
}
}
meilleur
}
// String en sortie : on fabrique un texte qui n'existait pas
fn initiales(texte: &str) -> String {
let mut resultat = String::new();
for mot in texte.split_whitespace() {
if let Some(c) = mot.chars().next() {
resultat.push_str(&c.to_uppercase().to_string());
}
}
resultat
}
// &mut String : on modifie le texte de l'appelant
fn censurer(texte: &mut String, mot: &str) {
let etoiles = "*".repeat(mot.chars().count());
*texte = texte.replace(mot, &etoiles);
}
fn main() {
let mut phrase = String::from("le langage Rust protège la mémoire du programme");
println!("{} mots", nombre_mots(&phrase));
println!("plus long : {}", mot_le_plus_long(&phrase));
println!("initiales : {}", initiales(&phrase));
censurer(&mut phrase, "mémoire");
println!("{phrase}");
}
Explication
La signature de chaque fonction raconte ce qu’elle fait de la donnée : lire (&str), modifier (&mut String) ou produire (String). On compte les caractères avec chars().count() et non len(), qui compte les octets : « mémoire » fait 7 caractères mais 8 octets.
Pour aller plus loin
if let Some(c) = … est une écriture raccourcie de match que tu étudieras au niveau 3.
🧠 Testez vos connaissances
5 questions. Répondez puis cliquez sur « Corriger ». Rien n’est enregistré : c’est de l’auto-évaluation.
© 2026 Bruno Jousseaume. Ce cours est publié sous licence CC BY-NC-SA 4.0 : vous pouvez le partager et l’adapter en citant l’auteur et un lien vers cette page, sans usage commercial, et en partageant vos adaptations sous la même licence.