# Introduction
Après avoir abordé le processus de production d'un programme exécutable, nous allons maintenant nous intéresser à l'information en mémoire : les données. Ce chapitre est fondamental, car toute logique de programme repose sur la manipulation de valeurs.
Comprendre la nature des données, leur stockage et leur manipulation est fondamental pour écrire des programmes efficaces, robustes et sans bugs.
# 1. Les constantes
## 1.1. Constantes Littérales
Ce sont des valeurs directement écrites dans le code source.
> [!example] Exemples de constantes littérales
> ```c
> 10 // Littéral entier (type int)
> 3.14159 // Littéral flottant (type double par défaut)
> 3.14159F // Littéral flottant (type float)
> 'A' // Littéral caractère (type char)
> "Bonjour" // Littéral chaîne de caractères (tableau de char)
> ```
## 1.2. Constantes Symboliques
Ce sont des identifiants auxquels on associe une valeur constante. Il existe deux manières principales de les définir en C.
### 1.2.1. Utilisation de la directive `#define` (Préprocesseur)
La directive `#define` est une instruction du préprocesseur. Avant la compilation, le préprocesseur remplace toutes les occurrences de l'identifiant par sa valeur dans le code source.
La syntaxe est la suivante :
`#define IDENTIFIANT_CONSTANTE valeur`
> [!example] Exemple avec `#define`
> ```c
> #include <stdio.h>
>
> #define PI 3.14159
> #define MAX_TENTATIVES 3
> #define MESSAGE_ERREUR "Erreur: Tentatives épuisées."
>
> int main() {
> double rayon = 10.0;
> double surface = PI * rayon * rayon;
> printf("Surface du cercle : %f\n", surface);
>
> int tentatives = 0;
> while (tentatives < MAX_TENTATIVES) {
> // ... logique de tentative ...
> tentatives++;
> }
> printf("%s\n", MESSAGE_ERREUR);
> return 0;
> }
> ```
> [!warning] Limitations de `#define`
> - **Pas de type checking** : Le préprocesseur effectue un simple remplacement textuel. Il ne connaît pas les types, ce qui peut mener à des erreurs difficiles à débugger.
> - **Pas de portée (scope)** : Les constantes définies par `#define` sont visibles à partir de leur point de définition jusqu'à la fin du fichier, ou jusqu'à un `#undef`. Elles ne respectent pas la portée des blocs de code.
> - **Potentiels effets de bord** : Si la "valeur" est une expression, des problèmes peuvent survenir si elle n'est pas correctement parenthésée.
### 1.2.2. Utilisation du mot-clé `const`
Le mot-clé `const` est un qualificatif de type qui indique que la valeur d'une variable ne peut pas être modifiée après son initialisation. C'est la méthode préférée en C moderne pour définir des constantes typées.
La syntaxe est la suivante :
`const type identifiant = valeur_initiale;`
> [!example] Exemple avec `const`
> ```c
> #include <stdio.h>
>
> int main() {
> const double PI = 3.14159;
> const int MAX_TENTATIVES = 3;
> const char MESSAGE_ERREUR[] = "Erreur: Tentatives épuisées."; // Pour les chaînes
>
> double rayon = 10.0;
> double surface = PI * rayon * rayon;
> printf("Surface du cercle : %f\n", surface);
>
> // PI = 3.0; // Ceci provoquerait une erreur de compilation !
>
> // ...
> return 0;
> }
> ```
> [!note] Avantages de `const` par rapport à `#define`
> - **Sécurité de type** : Le compilateur connaît le type de la constante `const` et peut effectuer des vérifications de type.
> - **Portée (scope)** : Les constantes `const` respectent la portée des blocs de code, comme les variables normales.
> - **Débogage facilité** : Les constantes `const` existent en tant qu'entités symboliques pendant le débogage, contrairement aux `#define` qui sont remplacées avant compilation.
> - **Utilisation avec les pointeurs** : Le mot-clé `const` est essentiel pour gérer les pointeurs vers des données constantes ou des pointeurs constants, un sujet que nous aborderons plus tard.
## 1.3. Règles de Nommage des Identifiants (Constantes)
Les règles obligatoires sont les mêmes que pour les variables. Cependant, des conventions spécifiques sont souvent utilisées pour distinguer les constantes.
> [!tip] Conventions de nommage pour les constantes
> - **Pour les `#define`** : Il est d'usage de nommer les constantes avec des **lettres majuscules et des tirets bas** pour séparer les mots (ex: `PI`, `MAX_USERS`, `TAILLE_BUFFER`). Cela permet de les distinguer clairement des variables.
> - **Pour les `const`** :
> - Certains programmeurs utilisent également la convention **`ALL_CAPS_WITH_UNDERSCORES`** pour les `const` (ex: `const double PI = 3.14;`).
> - D'autres préfèrent utiliser les mêmes conventions que les variables (`camelCase` ou `snake_case`) si la constante est locale à une fonction ou a une portée limitée, considérant qu'elle est une variable dont la valeur est simplement non modifiable. La convention `ALL_CAPS` reste la plus courante pour les constantes globales ou de grande importance.
# 2. Les Variables
Une variable est un emplacement nommé en mémoire capable de stocker une valeur qui peut changer au cours de l'exécution du programme. C'est l'équivalent d'une boîte étiquetée dans laquelle vous pouvez mettre et retirer des objets (des valeurs dans notre cas).
> [!definition] Variable
> Une **variable** est une entité nommée qui représente un emplacement en mémoire vive (RAM) capable de stocker une valeur d'un certain type. Cette valeur peut être lue et modifiée au cours de l'exécution du programme.
Chaque variable en C possède trois caractéristiques essentielles :
1. **Un nom (identifiant)** : Pour la désigner et y accéder.
2. **Un type** : Qui définit la nature des valeurs qu'elle peut stocker (nombre entier, nombre décimal, caractère, etc.) et l'espace mémoire qu'elle occupe.
3. **Une valeur** : Le contenu actuel stocké à son emplacement mémoire.
## 2.1 Déclaration d'une Variable
Avant d'utiliser une variable en C, il est impératif de la **déclarer**. La déclaration indique au compilateur le nom de la variable et le type de données qu'elle pourra contenir. Cela permet au compilateur de réserver l'espace mémoire approprié et de vérifier la cohérence des opérations effectuées sur cette variable.
La syntaxe générale est la suivante :
`type identifiant;`
> [!example] Exemples de déclarations
> ```c
> int age; // Déclare une variable 'age' de type entier
> float temperature; // Déclare une variable 'temperature' de type nombre flottant
> char initiale; // Déclare une variable 'initiale' de type caractère
> double prixTotal; // Déclare une variable 'prixTotal' de type nombre flottant double précision
> _Bool estActif; // Déclare une variable 'estActif' de type booléen (C99)
> ```
> [!note] Types de données primitifs courants en C
> | Type C | Description | Taille typique (octets) | Plage de valeurs typique |
> | :--------- | :------------------------------------------------ | :---------------------- | :------------------------------------------------------------------- |
> | `char` | Caractère ASCII, petit entier | 1 | -128 à 127 ou 0 à 255 (dépend si `signed` ou `unsigned` par défaut) |
> | `int` | Entier standard | 2 ou 4 | -32,768 à 32,767 ou -2 milliards à 2 milliards |
> | `float` | Nombre à virgule flottante (simple précision) | 4 | Environ $\pm 3.4 \times 10^{38}$ (7 chiffres significatifs) |
> | `double` | Nombre à virgule flottante (double précision) | 8 | Environ $\pm 1.7 \times 10^{308}$ (15 chiffres significatifs) |
> | `_Bool` | Booléen (vrai/faux, 0 ou 1) - Standard C99+ | 1 | 0 (faux) ou 1 (vrai) |
>
> Il existe aussi des variantes comme `short int`, `long int`, `long long int`, `unsigned int`, `signed char`, etc., qui permettent de contrôler plus finement la taille et la plage de valeurs.
> [!note] Taille des types
> Les tailles typiques sont données à titre indicatif et peuvent varier légèrement selon le compilateur et l'architecture du système (32 bits vs 64 bits). La norme C garantit uniquement des tailles minimales et des relations de taille (ex: `sizeof(short) <= sizeof(int) <= sizeof(long)`). Vous pouvez utiliser l'opérateur `sizeof()` pour connaître la taille exacte d'un type ou d'une variable sur votre système.
>
> ```c
> #include <stdio.h>
>
> int main() {
> printf("Taille d'un int : %zu octets\n", sizeof(int));
> printf("Taille d'un double : %zu octets\n", sizeof(double));
> return 0;
> }
> ```
> [!tip] `_Bool` et `stdbool.h`
> Bien que `_Bool` soit le type booléen natif de C99, il est recommandé d'inclure l'en-tête `<stdbool.h>` pour utiliser les alias plus conviviaux `bool`, `true` et `false`. Avant C99, il était courant d'utiliser `int` (0 pour faux, non-0 pour vrai) pour représenter les valeurs booléennes.
## 2.2. Affectation d'une Valeur
L'**affectation** est l'opération qui consiste à stocker une valeur dans une variable. Elle est réalisée à l'aide de l'opérateur d'affectation `=`.
La syntaxe est la suivante :
`identifiant = expression;`
L'`expression` peut être une valeur littérale, une autre variable, le résultat d'un calcul, ou l'appel d'une fonction.
> [!example] Exemples d'affectation
> ```c
> int nombre;
> nombre = 10; // Affecte la valeur 10 à la variable 'nombre'
>
> float rayon = 5.5f; // Déclaration et initialisation (voir section suivante)
> float surface;
> surface = 3.14159 * rayon * rayon; // Affecte le résultat du calcul à 'surface'
>
> char grade = 'A'; // Affecte le caractère 'A' à 'grade'
>
> int a = 5;
> int b = a; // Affecte la valeur de 'a' (soit 5) à 'b'
> ```
> [!note] L-value et R-value
> En C, l'opérateur d'affectation `=` nécessite une **L-value** (une valeur qui peut être située à gauche de l'opérateur, c'est-à-dire un emplacement mémoire modifiable, comme une variable) et une **R-value** (une valeur qui peut être située à droite, c'est-à-dire une expression qui produit une valeur).
> `variable = valeur;`
## 2.3. Initialisation de variable
L'**initialisation** est l'action de donner une première valeur à une variable au moment de sa déclaration, dans une même instruction. C'est une excellente pratique de programmation qui permet d'éviter d'utiliser des variables avec des valeurs indéterminées (appelées "valeurs aléatoires" ou "garbage values").
La syntaxe est la suivante :
`type identifiant = valeur_initiale;`
> [!example] Exemples d'initialisation
> ```c
> int compteur = 0; // Déclare et initialise 'compteur' à 0
> float pi = 3.14159f; // Déclare et initialise 'pi'
> char statut = 'N'; // Déclare et initialise 'statut'
> _Bool estValide = 1; // Déclare et initialise 'estValide' à vrai
> ```
> [!warning] Variables non initialisées
> Si une variable locale (déclarée à l'intérieur d'une fonction) n'est pas initialisée, elle contiendra une valeur "aléatoire" (le contenu de la mémoire à cet emplacement au moment de la déclaration). Utiliser une telle variable peut entraîner des comportements imprévisibles et des bugs difficiles à détecter.
>
> ```c
> int x; // x n'est pas initialisé, sa valeur est indéterminée
> printf("%d\n", x); // Peut afficher n'importe quoi !
> ```
>
> Les variables globales et statiques sont automatiquement initialisées à zéro si elles ne sont pas explicitement initialisées.
## 2.4. Règles de nommage des identifiants (de variables)
Les identifiants sont les noms que vous donnez aux variables, fonctions, etc. Il existe des règles strictes et des conventions pour les nommer.
> [!property] Règles obligatoires pour les identifiants
> 1. Un identifiant doit commencer par une lettre (a-z, A-Z) ou un tiret bas (`_`).
> 2. Les caractères suivants peuvent être des lettres, des chiffres (0-9) ou des tirets bas.
> 3. Les espaces, les caractères spéciaux (comme `!`, `@`, `#`, `%`, etc.) sont interdits.
> 4. Les identifiants sont sensibles à la casse (`maVariable` est différent de `mavariable`).
> 5. Un identifiant ne peut pas être un mot-clé réservé du langage C (ex: `int`, `float`, `if`, `while`, `return`, `const`, etc.).
> 6. La longueur d'un identifiant est généralement limitée par le compilateur, mais il est recommandé de ne pas dépasser 31 caractères pour assurer la portabilité.
> [!tip] Conventions de nommage (Bonnes pratiques)
> Bien que non obligatoires, ces conventions améliorent grandement la lisibilité du code :
> - **Noms significatifs** : Choisissez des noms qui décrivent clairement le rôle de la variable (ex: `nombreEtudiants` plutôt que `n`).
> - **`camelCase`** : Le premier mot commence par une minuscule, les mots suivants par une majuscule (ex: `nombreEtudiants`, `temperatureMoyenne`).
> - **`snake_case`** : Les mots sont séparés par des tirets bas (ex: `nombre_etudiants`, `temperature_moyenne`), convention courante en Python.
> - **Éviter les identifiants commençant par un tiret bas seul (`_`)** : Ils sont souvent réservés aux identifiants internes du système ou de la bibliothèque standard.
## 3. Les Énumérations (`enum`)
Les **énumérations** (`enum`) permettent de définir un ensemble de constantes entières nommées. Elles améliorent considérablement la lisibilité du code en remplaçant des "nombres magiques" par des noms significatifs.
> [!definition] Énumération
> Une énumération est un type de données défini par l'utilisateur qui consiste en un ensemble de constantes entières nommées.
>
> Syntaxe :
> ```c
> enum NomDeLEnum {
> ELEMENT1,
> ELEMENT2,
> ELEMENT3 = 10, // On peut assigner des valeurs explicites
> ELEMENT4 // Si non spécifié, l'élément suivant prend la valeur précédente +1
> };
> ```
Par défaut, le premier élément d'une énumération a la valeur `0`, le deuxième `1`, et ainsi de suite. Si une valeur est explicitement assignée, les éléments suivants continuent à incrémenter à partir de cette valeur.
> [!example] Utilisation d'une énumération
> ```c
> #include <stdio.h>
>
> // Définition de l'énumération pour les jours de la semaine
> enum JoursSemaine {
> LUNDI, // Vaut 0
> MARDI, // Vaut 1
> MERCREDI, // Vaut 2
> JEUDI, // Vaut 3
> VENDREDI, // Vaut 4
> SAMEDI, // Vaut 5
> DIMANCHE // Vaut 6
> };
>
> // Définition d'une énumération avec valeurs explicites
> enum EtatPorte {
> OUVERT = 1,
> FERME = 0,
> VERROUILLE = 2
> };
>
> int main() {
> enum JoursSemaine aujourdhui = MERCREDI;
> enum EtatPorte porte_entree = FERME;
>
> if (aujourdhui == MERCREDI) {
> printf("Aujourd'hui est mercredi (valeur: %d)\n", aujourdhui);
> }
>
> if (porte_entree == FERME) {
> printf("La porte est fermée (valeur: %d)\n", porte_entree);
> }
>
> printf("Le samedi est le jour %d de la semaine.\n", SAMEDI);
>
> return 0;
> }
> ```
> **Sortie possible :**
> ```
> Aujourd'hui est mercredi (valeur: 2)
> La porte est fermée (valeur: 0)
> Le samedi est le jour 5 de la semaine.
> ```
> [!note] `enum` et types entiers
> Les membres d'une énumération sont des constantes de type `int`. Une variable d'un type énuméré peut stocker n'importe quelle valeur entière qui peut être représentée par un `int`, même si cette valeur ne fait pas partie des membres nommés de l'énumération. Cependant, il est fortement recommandé de n'assigner que les valeurs définies dans l'énumération pour maintenir la clarté et la sécurité du code.
# 4. Conversion de Types et Cast
En C, les opérations sont effectuées sur des données de même type. Si vous tentez d'opérer sur des données de types différents, le compilateur tentera de les convertir. Cela peut se faire de manière implicite ou explicite.
## 4.1. Conversion Implicite (Coercition)
La conversion implicite se produit automatiquement lorsque le compilateur détecte une incompatibilité de type dans une expression, mais qu'une conversion "sûre" est possible. Le compilateur convertit le type "inférieur" vers le type "supérieur" pour éviter une perte de données.
> [!property] Règles de promotion de type implicite (simplifiées)
> Lors d'opérations impliquant différents types numériques :
> 1. Les types entiers plus petits (`char`, `short`) sont promus en `int`.
> 2. Si les opérandes sont de types entiers différents, le type "inférieur" est promu au type "supérieur" (ex: `int` vers `long`, `int` vers `unsigned int`).
> 3. Si un opérande est un flottant et l'autre un entier, l'entier est converti en flottant.
> 4. Les `float` sont promus en `double` lors de la plupart des opérations arithmétiques.
>
> L'ordre de promotion typique est : `char` -> `short` -> `int` -> `unsigned int` -> `long` -> `unsigned long` -> `long long` -> `unsigned long long` -> `float` -> `double` -> `long double`.
> [!example] Exemples de conversion implicite
> ```c
> #include <stdio.h>
>
> int main() {
> int entier = 10;
> float flottant = 3.5f;
> double resultat;
>
> resultat = entier + flottant; // 'entier' (int) est converti implicitement en 'float',
> // puis le résultat de l'addition (float) est converti en 'double' pour 'resultat'.
> // La valeur de 'resultat' sera 13.5
> printf("Resultat (int + float) : %f\n", resultat);
>
> char c = 'A'; // ASCII 65
> int i = c; // 'c' (char) est converti implicitement en 'int'. 'i' vaudra 65.
> printf("Valeur de i (char to int) : %d\n", i);
>
> return 0;
> }
> ```
> [!warning] Perte de données lors de la conversion implicite
> Une conversion implicite peut entraîner une perte de données si le type de destination ne peut pas représenter la valeur du type source. Cela se produit généralement lors d'une "démotion" (conversion vers un type plus petit ou moins précis).
> ```c
> int grandEntier = 100000;
> short petitEntier = grandEntier; // grandEntier (int) est converti en short.
> // Si short fait 2 octets, 100000 dépasse sa capacité (-32768 à 32767).
> // La valeur de petitEntier sera tronquée ou incorrecte.
> printf("Petit entier : %d\n", petitEntier); // Affichera une valeur inattendue
>
> float f = 3.1415926535; // Pi avec beaucoup de décimales
> int i = f; // Le flottant est tronqué, seules la partie entière est conservée.
> // i vaudra 3.
> printf("Entier de float : %d\n", i);
> ```
## 4.2. Conversion Explicite (Cast)
La conversion explicite, ou **cast**, est une instruction que vous donnez au compilateur pour convertir explicitement une valeur d'un type à un autre. C'est une manière de forcer une conversion, même si elle pourrait potentiellement entraîner une perte de données.
La syntaxe est la suivante :
`(type_cible) expression`
> [!example] Exemples de cast
> ```c
> #include <stdio.h>
>
> int main() {
> int a = 7;
> int b = 2;
> float resultat;
>
> // Sans cast, la division entière (7 / 2) donnerait 3, puis converti en float (3.0f).
> // Avec cast, 'a' est d'abord converti en float (7.0f), puis divisé par 'b' (2, converti implicitement en 2.0f).
> resultat = (float)a / b; // resultat vaudra 3.5
> printf("Resultat (cast) : %f\n", resultat);
>
> double valeurDouble = 123.456;
> int valeurInt = (int)valeurDouble; // Conversion explicite en int (troncation)
> // valeurInt vaudra 123
> printf("Valeur int de double : %d\n", valeurInt);
>
> char c = (char)97; // Convertit l'entier 97 en caractère ASCII 'a'
> printf("Caractere de int : %c\n", c);
>
> return 0;
> }
> ```
> [!tip] Quand utiliser le cast ?
> - Pour **forcer la précision** lors d'opérations mathématiques (comme dans l'exemple de division `(float)a / b`).
> - Pour **interfacer avec des fonctions** qui attendent un type spécifique.
> - Pour **manipuler des adresses mémoire** ou des données de bas niveau (ce sera crucial avec les pointeurs).
> - Pour **supprimer un avertissement** du compilateur, mais uniquement après avoir vérifié que la conversion est intentionnelle et sûre.
> [!warning] Dangers du cast
> - **Perte de données** : Comme pour la conversion implicite, un cast peut entraîner une perte de précision ou de données si le type cible est plus petit ou moins précis que le type source.
> - **Comportement indéfini** : Dans certains cas (ex: cast d'un pointeur vers un type incompatible ou cast d'un grand nombre flottant vers un entier si l'entier ne peut pas le contenir), le comportement peut être indéfini et varier d'un compilateur à l'autre.
> - **Lisibilité** : Un usage excessif de casts peut rendre le code moins lisible et plus difficile à maintenir. Utilisez-les avec discernement.
## Conclusion
Ce chapitre vous a introduit aux fondations de la gestion des données en C : les variables et les constantes. Vous avez appris à les déclarer, les initialiser, leur affecter des valeurs et à les nommer selon les règles et les bonnes pratiques. Nous avons également exploré les mécanismes de conversion de types, qu'ils soient implicites ou explicites (cast), en soulignant l'importance de la vigilance face aux potentielles pertes de données.
La maîtrise de ces concepts est absolument essentielle. Un programme n'est, après tout, qu'une série d'opérations sur des données. Une gestion rigoureuse des types et des valeurs est la clé pour écrire du code fiable et performant.
Dans les prochains chapitres, nous construirons sur ces bases pour manipuler des structures de données plus complexes et pour interagir directement avec la mémoire via les pointeurs, où la compréhension des types et des conversions sera d'une importance capitale. Ces connaissances seront également cruciales lorsque vous aborderez des domaines comme la robotique, où la manipulation précise des données (capteurs, actionneurs) est au cœur de tout système embarqué.