Ce document compile des observations et des leçons tirées de la pratique de la programmation C, couvrant des subtilités de syntaxe, des comportements inattendus et des pièges courants.
1. Initialisation des variables et lecture des entrées
Il est crucial de vérifier si une entrée est attendue, même si elle n'est pas explicitement mentionnée dans l'énoncé d'un problème. Si les variables ne sont pas initialisées, cela peut inidquer une ambiguïté dans la description du problème.
Exemple : Calculer la longueur effective d'une chaîne. La longueur effective est le nombre de caractères avant le premier caractère nul ('\0').
#include <stdio.h>
int main()
{
int count, effective_length;
char input_string[81];
count = 0;
// Boucle pour lire les caractères jusqu'à une condition de fin (par exemple, fin de ligne ou EOF)
while (/* condition de lecture */) {
// Traitement ou comptage du caractère lu
count++;
}
// Calculer la longueur effective
effective_length = /* logique de calcul */;
printf("%d\n", effective_length);
return 0;
}
2. Priorité et associativité des opérateurs
La compréhension des tables de priorité des opérateurs est essentielle. Une erreur courante concerne la combinaison de l'opérateur d'indirection (*) et de l'opérateur d'incrémentation suffixée (++).
Considérons l'expression *p++ où p est un pointeur.
L'opérateur d'incrémentation suffixée ++ a une priorité plus élevée que l'opérateur d'indirection *. Par conséquent, p++ est évalué en premier (l'adresse contenue dans p est incrémentée), puis l'opérateur * tente d'accéder à la valeur à l'adresse mémoire résultante. Cela n'est pas équivalent à (*p)++.
La table de précédence des opérateurs dans les livres peut être incomplète ; il est recommandé de consulter des références complètes.
3. Marqueurs de fin d'entrée
Lors de la lecture depuis l'entrée standard, la fin de l'entrée est généralement marquée par EOF (End Of File), et non par le caractère nul '\0', à moins que le problème ne spécifie explicitement une autre condition.
Exemple : Conversion de minuscules en majuscules.
// Code incorrect : utilise '\0' comme marqueur de fin
while ((ch = getchar()) != '\0') {
if (ch >= 'a' && ch <= 'z')
ch = ch - 'a' + 'A';
putchar(ch);
}
// Correction : utiliser EOF comme marqueur de fin
while ((ch = getchar()) != EOF) {
if (ch >= 'a' && ch <= 'z')
ch = ch - 'a' + 'A';
putchar(ch);
}
4. Affectation d'adersses littérales aux pointeurs
L'affectation directe d'une constante entière comme adresse à un pointeur peut être possible dans certains compilateurs (comme dev-c++), mais le comportement est non standard et potentiellement dangereux. L'accès à une telle adresse peut entraîner des erreurs d'exécution (segmentation fault) si la mémoire n'est pas allouée ou protégée.
Exemple :
#include <stdio.h>
int main()
{
int variable_locale = 1;
int adresse_convertie = (int)(&variable_locale); // Conversion de l'adresse en entier
int *pointeur_vers_adresse = (int *)adresse_convertie; // Cast de l'entier vers un pointeur int*
// Afficher l'adresse interprétée par le pointeur
printf("Adresse interprétée : %d\n", (int)pointeur_vers_adresse);
// Tentative de modification via le pointeur (peut causer des erreurs)
*pointeur_vers_adresse = 2;
printf("Valeur de variable_locale après modification : %d\n", variable_locale); // Peut afficher 2 si l'accès est valide
return 0;
}
Il est préférable d'utiliser des adresses obtenues via des opérateurs d'adresse (&) ou des fonctions d'allocation mémoire (malloc).
5. Dépassement de capacité lors de l'initialisation de chaînes
Initialiser un tableau de caractères avec une chaîne littérale plus longue que la taille déclarée du tableau entraînera un dépassement de capacité.
Exemple :
// Tableau de taille 6, mais chaîne de 6 caractères + '\0' (7 octets requis)
char s[6] = "Happy\0"; // Ceci dépasse la capacité du tableau s.
6. Accès en écriture à la mémoire des chaînes constantes
Les chaînes littérales (comme "hello") sont généralement stockées dans une section de mémoire en lecture seule (souvent appelée segment de données ou read-only data segment). Tenter de modifier le contenu de cette mémoire entraînera une erreur d'exécution (segmentation fault).
char *p = "hello";
// *p = 'H'; // Ceci provoquera une erreur de segmentation car on tente de modifier une mémoire en lecture seule.
7. Affectation entre pointeurs de types différents
Les pointeurs de types différents ne peuvent pas être directement affectés l'un à l'autre, sauf s'il s'agit de pointeurs void*. Un pointeur void* peut pointer vers n'importe quel type de données, mais pour déréférancer (accéder à la valeur), il faut explicitement le convertir vers un type de pointeur approprié.
Exemple :
#include <stdio.h>
int main() {
int a = 10;
double b = 3.14;
void *generic_ptr; // void* peut pointer vers n'importe quel type
generic_ptr = &a; // ✅ int* peut être assigné à void*
generic_ptr = &b; // ✅ double* peut être assigné à void*
// int *int_ptr = generic_ptr; // ❌ Affectation directe échoue (ou génère un avertissement en C++)
// Conversion explicite nécessaire pour déréférencer
int *int_ptr_converted = (int*)generic_ptr; // ✅ Conversion explicite
// Pour accéder à la valeur, il faut convertir et utiliser l'opérateur de déréférencement :
// Si generic_ptr pointe vers un int :
// printf("%d\n", *(int*)generic_ptr);
// Essayer d'accéder à la valeur sans conversion générera une erreur de compilation :
// *generic_ptr; // Erreur : invalid type argument of unary '*' (have 'void *')
return 0;
}
8. Opérations sur les pointeurs
Les pointeurs peuvent être comparés pour l'égalité, affectés, et leur différence peut être calculée (ce qui donne un entier représentant le nombre d'éléments entre les deux pointeurs). Cependant, l'addition de pointeurs n'est pas autorisée car elle n'a pas de sémantique claire.
La soustraction de deux pointeurs (ptr2 - ptr1) retourne un type ptrdiff_t, qui est un type entier signé.
9. Initialisation de tableaux de caractères avec des chaînes multiples
Lors de l'initialisation d'un tableau de caractères avec une liste d'initialiseurs de chaînes, seule la première chaîne est utilisée. L'ajout d'une virgule après une seule chaîne est autorisé mais superflu.
Exemple :
// Ceci est valide, mais la virgule est inutile. Seule "hello" sera stockée.
char s[100] = {"hello",};
// Ceci provoquera une erreur de compilation car plusieurs chaînes sont fournies pour un tableau unidimensionnel.
// char s[100] = {"hello", " world"}; // Erreur : excess elements in struct initializer
10. Méthodes pour éviter les boucles infinies en recherche binaire
Lors de l'implémentation d'une recherche binaire, le choix de la formule pour calculer le milieu peut affecter la terminaison de la boucle, surtout lorsque l et r sont adjacents.
milieu = (gauche + droite) / 2: Dans certains cas,milieupeut être égal àgauche, ce qui peut entraîner une boucle infinie si la condition de recherche ne progresse pas.milieu = (gauche + droite + 1) / 2: Dans certains cas,milieupeut être égal àdroite.
Il est conseillé de gérer explicitement les cas où droite - gauche < 2 ou de choisir une formule qui garantit que milieu ne sera jamais égal à gauche ou droite lorsque droite - gauche > 1.
11. Lecture attentive des spécifications des problèmes
Il est crucial de lire attentivement toutes les exigences d'un problème, y compris les caractères de fin, la casse insensible, et les conditions spécifiques.
Exemple : Traitement de chaînes hexadécimales.
Le problème peut spécifier qu'une chaîne se termine par '#', et non par EOF. Il peut également indiquer que les caractères hexadécimaux sont insensibles à la casse (donc 'A' et 'a' sont équivalents).
#include <stdio.h>
int main()
{
int is_negative; // Indicateur pour le signe négatif
int i, j;
char input_buffer[81];
char hex_string[81];
i = 0;
// Lecture jusqu'au délimiteur '#'
while ((input_buffer[i] = getchar()) != '#') {
// Gérer le cas où le buffer est plein avant de rencontrer '#'
if (i < 80) {
i++;
} else {
// Gérer l'erreur de buffer plein si nécessaire
break;
}
}
input_buffer[i] = '\0'; // Terminer la chaîne lue
i = j = 0;
is_negative = 0;
while (input_buffer[i] != '\0') {
char current_char = input_buffer[i];
// Vérifier si le caractère est un chiffre hexadécimal valide (0-9, A-F, a-f)
if ((current_char >= '0' && current_char <= '9') ||
(current_char >= 'A' && current_char <= 'F') ||
(current_char >= 'a' && current_char <= 'f')) {
hex_string[j] = current_char;
j++;
} else if (j == 0 && current_char == '-') { // Vérifier le signe négatif uniquement au début
is_negative = 1;
}
i++;
}
hex_string[j] = '\0'; // Terminer la nouvelle chaîne hexadécimale
// Afficher le signe négatif si requis
if (is_negative == 1) {
putchar('-');
}
// Afficher la chaîne hexadécimale résultante
for(i = 0; hex_string[i] != '\0'; i++) {
putchar(hex_string[i]);
}
putchar('\n'); // Nouvelle ligne à la fin
return 0;
}
12. Vérification des parenthèses après les boucles for
Dans les exercices de complétion de code, il est important de vérifier si une structure for est suivie de parenthèses ({}) pour un bloc de code, ou juste d'une seule instruction.
13. Échange de valeurs avec une variable temporaire
L'échange de valeurs entre deux variables (par exemple, dans un tableau) nécessite une variable temporaire. Une erreur typique est d'oublier d'utiliser la variable temporaire pour la dernière affectation.
Exemple :
// Échange des valeurs entre a[k] et a[index]
temp = a[k];
a[k] = a[index];
a[index] = temp; // Erreur courante : écrire a[k] au lieu de temp ici.
14. Attention à la dénomination des tableaux
Ne pas supposer automatiquement qu'un tableau est nommé a ou num. Vérifiez le nom exact du tableau utilisé dans le problème.
15. Initialisation de tableaux de caractères
Il existe plusieurs syntaxes équivalentes pour initialiser un tableau de caractères de taille fixe avec une chaîne.
Si la taille du tableau est spécifiée :
// Ces 5 syntaxes sont équivalentes pour un tableau de taille 6 :
static char s[6] = {'H', 'a', 'p', 'p', 'y', '\0'};
static char s[6] = {'H', 'a', 'p', 'p', 'y'}; // Le '\0' est ajouté automatiquement si nécessaire
static char s[6] ="Happy"; // Le '\0' est ajouté automatiquement
static char s[6] ={"Happy"}; // Le '\0' est ajouté automatiquement
static char s[6] = {'H', 'a', 'p', 'p', 'y', 0}; // 0 est équivalent à '\0'
Si la taille du tableau n'est pas spécifiée, la deuxième forme (sans '\0' explicite) est incorrecte car le compilateur ne peut pas déterminer la taille nécessaire sans le terminateur nul.