Principes des Langages de Programmation
Les pointeurs, les structures et les fonctions
John Samuel
CPE Lyon
Année: 2026-2027 Courriel: john.samuel@cpe.fr
2.1. Objectifs
Objectifs
Les pointeurs
Les structures
Les unions
Introduction aux fonctions
2.2. Microprocesseur
Architecture
L'architecture d'une machine détermine la gestion de la mémoire, la taille des types,
l'ordre d'exécution des instructions et la taille des registres — d'où des ajustements
nécessaires d'une plate-forme à l'autre.
Une architecture n-bit peut adresser au plus 2n octets. En pratique les processeurs 64 bits actuels n'implémentent que 48 ou 57 bits d'adresse virtuelle : la limite théorique n'est jamais atteinte.
2.2. Microprocesseur
Le nombre de bits d'une machine est étroitement lié à la mémoire de plusieurs manières :
Taille des données en mémoire : Le nombre de bits détermine la quantité de données que la
machine peut manipuler en une seule opération. Par exemple, sur une architecture 32 bits, les
entiers sont généralement stockés sur 4 octets (32 bits), tandis que sur une architecture 64 bits,
ils sont stockés sur 8 octets (64 bits). Cela influe sur la capacité de stockage et la manipulation
des données en mémoire.
Adressage mémoire : Le nombre de bits dans une adresse mémoire détermine la taille de
l'espace d'adressage de la machine. Sur une architecture 32 bits, vous avez théoriquement un espace
d'adressage de 2^32 octets (4 Go), tandis qu'une architecture 64 bits offre un espace d'adressage de
2^64 octets (plusieurs exaoctets). Cela affecte la capacité de la machine à adresser et à accéder à
la mémoire.
2.2. Microprocesseur
Le nombre de bits d'une machine est étroitement lié à la mémoire de plusieurs manières :
Performance : Les architectures 64 bits permettent souvent de manipuler de plus grandes
quantités de données en une seule opération, ce qui peut améliorer les performances dans certaines
situations. Cependant, cela peut également entraîner une utilisation de la mémoire plus intensive,
ce qui nécessite de prendre en compte la gestion de la mémoire.
Compatibilité : Les programmes écrits pour une architecture donnée (32 bits ou 64 bits)
peuvent ne pas être compatibles avec une autre. La taille des pointeurs et des données affecte la
manière dont le code interagit avec la mémoire, ce qui peut nécessiter des adaptations lors de la
migration d'une architecture à l'autre.
2.2. Microprocesseur
Le nombre de bits d'une architecture détermine la quantité de mémoire adressable.
La capacité de la mémoire vive (RAM) d'un ordinateur est de 4 Go, 8 Go, 16 Go, etc., et elle
détermine la quantité de données que l'ordinateur peut stocker temporairement pour un traitement rapide.
2.4. Mémoire Virtuelle
La mémoire virtuelle étend l'utilisation de la mémoire de masse en utilisant des adresses
virtuelles et physiques.
Adresses: Les adresses virtuelles et les adresses physiques
Les adresses virtuelles sont utilisées par les programmes.
Les adresses physiques sont utilisées par les puces mémoire.
L'unité de gestion mémoire traduit les adresses virtuelles en adresses physiques pour
faciliter l'accès à la mémoire.
La conversion des adresses virtuelles en adresses physiques
2.5. Bit de poids fort et faible
Dans une représentation binaire donnée,
Le bit de poids fort (MSB, acronyme de "Most Significant Bit") est le bit le plus à gauche et
il représente la plus grande puissance de 2 dans la valeur binaire. Il a la plus grande valeur
lorsque tous les autres bits sont à zéro.
Le bit de poids faible (LSB, acronyme de "Least Significant Bit") est le bit le plus à droite
et il représente la plus petite puissance de 2 dans la valeur binaire. Il a la plus petite valeur
lorsque tous les autres bits sont à zéro.
MSB et LSB (8 bit)
2.6. Endianness (Boutisme)
Dans une représentation binaire sur 8 bits (octet)
MSB (Bit de poids fort) : C'est le bit le plus à gauche de l'octet, généralement le 7e bit si
l'on
compte de 0 à 7. Il représente la plus grande puissance de 2 dans cet octet, soit 2^7. Sa valeur
maximale est 128 lorsque ce bit est à 1.
LSB (Bit de poids faible) : C'est le bit le plus à droite de l'octet, généralement le 0e bit
si l'on
compte de 0 à 7. Il représente la plus petite puissance de 2, soit 2^0, et sa valeur maximale est 1
lorsque ce bit est à 1.
MSB et LSB (8 bit)
2.6. Endianness (Boutisme)
Dans une représentation binaire sur 16 bits
MSB (Bit de poids fort) : C'est le bit le plus à gauche des 16 bits, généralement le 15e bit
si l'on compte de 0 à 15. Il représente la plus grande puissance de 2 dans ces 16 bits, soit 2^15.
Sa valeur maximale est 32 768 lorsque ce bit est à 1.
LSB (Bit de poids faible): C'est le bit le plus à droite des 16 bits, généralement le 0e bit
si l'on compte de 0 à 15. Il représente la plus petite puissance de 2, soit 2^0, et sa valeur
maximale est 1 lorsque ce bit est à 1.
MSB et LSB (16 bit)
2.6. Endianness (Boutisme)
Endianness (Boutisme)
L'ordre d'organisation des octets en mémoire.
Big-endian (grand-boutiste) : Convention où l'octet le plus significatif est stocké en
premier dans la mémoire.
Little-endian (petit-boutiste) : Convention où l'octet le moins significatif est stocké en
premier dans la mémoire.
Remarque: Dans ce cours, nous utilisons la convention petit-boutiste.
Boutisme: grand-boutiste et petit-boutiste
2.7. Pointeurs
charc = 'a'; char *my_char_addr = &c;
Un pointeur est une variable qui stocke l'adresse mémoire d'une autre variable.
Un pointeur de type char nommé my_char_addr est défini et reçoit l'adresse mémoire de la variable
c en utilisant l'opérateur d'adresse '&'.
my_char_addr fait référence à l'emplacement mémoire où la valeur
'a' est stockée.
Remarque:my_char_addr = 0xab71
une variable char
2.7. Pointeurs
Pointeurs, adresses virtuelles et physiques
Pointeur en C : Un pointeur en C est une variable qui stocke une adresse mémoire. Il est
utilisé pour référencer et accéder à des données situées à cette adresse mémoire.
Adresse Physique : L'adresse physique est l'adresse réelle à laquelle les données sont
stockées en mémoire physique (RAM). Contrairement à l'adresse virtuelle, elle dépend de
l'architecture matérielle de l'ordinateur.
Gestion de la Mémoire : Le système d'exploitation et l'unité de gestion mémoire (MMU) sont
responsables de la traduction des adresses virtuelles en adresses physiques. Cela permet au système
d'exploitation de gérer la mémoire de manière efficace, notamment en utilisant des techniques telles
que la pagination ou la segmentation.
2.7. Pointeurs
Pointeurs, adresses virtuelles et physiques
Protection de la Mémoire : L'utilisation d'adresses virtuelles permet de mettre en place des
mécanismes de protection de la mémoire. Par exemple, un programme ne peut pas accéder directement à
la mémoire d'un autre programme en utilisant des adresses virtuelles, ce qui renforce la sécurité et
l'isolation des processus.
Portabilité du Code : L'utilisation d'adresses virtuelles rend le code portable entre
différentes architectures matérielle, car il peut utiliser des pointeurs en C avec des adresses
virtuelles sans se soucier des détails de l'adresse physique sous-jacente.
2.7. Pointeurs
Pointeurs, adresses virtuelles et physiques
Les pointeurs en C utilisent des adresses virtuelles pour accéder à la mémoire, tandis que l'unité
de gestion mémoire du système d'exploitation traduit ces adresses virtuelles en adresses physiques
pour un accès réel à la mémoire.
Cela permet une gestion efficace de la mémoire, la protection des processus et la portabilité du
code entre différentes plates-formes matérielles.
2.7. Pointeurs
short *
shorts = 0xa478; short *my_short_addr = &s;
Ce code initialise une variable short s avec la valeur hexadécimale
0xa478, puis crée un pointeur my_short_addr qui référence l'adresse mémoire de s.
Remarque:my_short_addr = 0xab71.
une variable short
2.7. Pointeurs
int *
inti = 0xa47865ff; int *my_int_addr = &i;
Ce code initialise une variable int i avec la valeur hexadécimale
0xa47865ff, puis crée un pointeur my_int_addr qui référence l'adresse mémoire de i.
Remarque:my_int_addr = 0xab71
une variable int
2.7. Pointeurs
long int *
long intli = 0xa47865ff; long int *my_long_int_addr = &li;
Ce code initialise une variable long int li avec la valeur hexadécimale
0xa47865ff, puis crée un pointeur my_long_int_addr qui référence l'adresse mémoire de li.
Remarque:my_long_int_addr = 0xab71
une variable long int
2.7. Pointeurs: les nombres en flottant
IEEE 754
float (32-bit)
signe: bit 31
exposant: bits 23-30
mantisse: bits 0-22
une variable float
2.7. Pointeurs: les nombres en flottant
IEEE 754
float (32-bit): Pour stocker un numéro en utilisant la conversion IEEE
754, voici les étapes :
Déterminez le signe : Si le numéro est positif, le bit de signe est 0 ; s'il est négatif, le
bit de signe est 1.
Exprimez le numéro en binaire normalisé : Convertissez le numéro en une forme binaire
normalisée, c'est-à-dire sous la forme "1.xxxx" où "xxxx" représente la partie fractionnaire en
binaire. Par exemple, pour le nombre 9.5 (1001.1), la forme normalisée en binaire est "1.0011".
Déterminez l'exposant : L'exposant est le décalage nécessaire pour normaliser le numéro. Par
exemple, pour "1001.1", l'exposant est 3 car il faut décaler la virgule de trois positions vers la
gauche pour obtenir "1.0011".
Appliquez le biais : Ajoutez le biais à l'exposant pour éviter les valeurs négatives. Pour
les float 32 bits, le biais est 127. Donc, si l'exposant est 3, l'exposant stocké est 3 + 127 = 130
en binaire.
Stockez le résultat : Combinez le signe, l'exposant et la mantisse (la partie fractionnaire)
pour obtenir la représentation binaire complète du numéro en virgule flottante.
2.7. Pointeurs: les nombres en flottant
IEEE 754
float (32-bit): Pour stocker un numéro en utilisant la conversion IEEE
754, voici les étapes pour stocker le nombre 9.5:
Le signe est 0 (positif).
La forme binaire est "1001.1", donc la forme normalisée est "1.0011 × 23".
L'exposant est 3.
En ajoutant le biais (127), l'exposant stocké est 130 en binaire, soit "10000010".
La mantisse est "00110000000000000000000" (les bits fractionnaires après le point).
2.7. Pointeurs: les nombres en flottant
IEEE 754
double (64-bit)
signe: bit 63
exposant: bits 52-62
mantisse: bits 0-51
Remarque: IEEE 754
une variable double
2.7. Pointeurs: les nombres en flottant
IEEE 754
double (64-bit):
La conversion d'un nombre en double précision (double) se fait généralement en plusieurs étapes :
Normalisation : Le nombre est converti en une forme binaire normalisée de la forme "1.xxxx"
(partie fractionnaire après la virgule) multipliée par une puissance de 2.
Calcul de l'exposant : L'exposant est déterminé en fonction du décalage nécessaire pour
normaliser le nombre. Pour 9.5 = 1.0011 × 23, l'exposant est 3.
Application du biais : Un décalage, appelé "biais", est ajouté à l'exposant pour le rendre
positif. Le biais est de 1023.
Représentation des signes, exposants et mantisses : Le bit le plus significatif est utilisé
pour le signe (0 pour positif, 1 pour négatif), suivie de la représentation de l'exposant (sur un
certain nombre de bits) et de la mantisse (fraction binaire après le point, sur un certain nombre de
bits).
Assemblage des bits : Les bits du signe, de l'exposant et de la mantisse sont combinés pour
former la représentation binaire du nombre en double précision.
char*my_char_addr
= &c : Un pointeur my_char_addr de type char est déclaré et reçoit l'adresse mémoire de la
variable c à l'aide de l'opérateur d'adresse &. À présent, my_char_addr pointe vers l'emplacement en mémoire où la valeur 'a' est stockée.
printf
("%c",
*my_char_addr)
: L'opérateur de déréférencement (*) est utilisé avec *my_char_addr
pour accéder à la valeur stockée à l'adresse mémoire pointée par my_char_addr, qui est 'a'. La
fonction printf affiche ensuite cette valeur 'a' à l'écran.
L'opérateur de déréférencement (*) est utilisé pour accéder et afficher la valeur de la variable
c à travers le pointeur my_char_addr.
2.8. L'opérateur de déréférenciation
Pour distinguer l'opérateur * en tant qu'opérateur de déréférencement, vous pouvez vous appuyer sur le
contexte dans lequel il est utilisé.
Déclaration de pointeur : Lorsque vous déclarez un pointeur, l'opérateur * est utilisé pour
indiquer qu'il s'agit d'un pointeur. Par exemple :
char*my_char_addr
= &c;
Ici, * fait partie de la déclaration, ce n'est pas un déréférencement.
Utilisation du pointeur : pour accéder à la valeur pointée, * est bien l'opérateur de
déréférencement :
* donne ici la valeur pointée par my_char_addr, c'est-à-dire
celle de c.
2.8. L'opérateur de déréférenciation
L'opérateur * est utilisé pour manipuler un objet pointé lorsqu'il est utilisé avec un pointeur.
Exemple 1
charc
= 'a'; char*my_char_addr
= &c; c
= 'b'; *my_char_addr
= 'c'; printf
("%c",
c);
//le caractère
pointé par my_char_addr prend la valeur 'c'
L'opérateur * est utilisé avec *my_char_addr pour accéder et modifier la
valeur stockée à l'adresse mémoire pointée par my_char_addr. Cela
permet de changer la valeur de la variable c en modifiant directement
la mémoire à l'emplacement indiqué par le pointeur.
2.8. L'opérateur de déréférenciation
L'opérateur * est utilisé pour manipuler un objet pointé lorsqu'il est utilisé avec un pointeur.
Exemple 2
inti
= 0x20; int*my_int_addr
= &i; *my_int_addr
= 1; *my_int_addr
= *my_int_addr
+ 1;
//i = 0x2 i
= i
+ 3;
//i = 0x5 printf
("%x",
*my_int_addr); // 5
L'opérateur * est utilisé avec *my_int_addr pour accéder et modifier la
valeur stockée à l'adresse mémoire pointée par my_int_addr. Cela permet
de changer la valeur de la variable i en modifiant directement la
mémoire à l'emplacement indiqué par le pointeur.
2.9. Les pointeurs génériques
void *
chara
= 'a'; char*cptr
= &a; inti
= 1; cptr
= &i;
$ gcc ...
error: assignment to ‘char *’ from incompatible pointer type ‘int *’ [-Wincompatible-pointer-types]
cptr = &i;
2.9. Les pointeurs génériques
void *
Un pointeur générique est un type spécial de pointeur qui peut contenir l'adresse mémoire de
n'importe quel type de données, c'est-à-dire qu'il peut pointer vers n'importe quel type de variable. Le
terme "générique" signifie que le pointeur n'a pas de type de données spécifique associé.
chara
= 'a';
char*cptr
= &a; void*vptr
= &a;
inti
= 1;
vptr
= &i;
floatf
= 1;
vptr
= &f;
L'avantage principal d'un pointeur générique est sa polyvalence. Vous pouvez l'utiliser pour stocker
l'adresse d'une variable de n'importe quel type,
2.10. Conversion de type
Sans perte d'information
La conversion de type (« cast ») change le type d'une variable. Vers un type
plus large de même signe, la valeur est conservée : elle est simplement étendue.
char → short →
int → long int : aucune
perte, chaque type étant au moins aussi large que le précédent.
Si la valeur d'origine est négative, c'est l'extension signée qui s'applique : le
bit de signe est recopié dans les octets ajoutés.
Tout le reste — vers un type plus étroit, ou de signe différent — demande une vérification.
shorts
= 0x7fff; inti
= s; // i vaut 32767
Contre-exemple :short s = 0xffff;
ne convertit rien : 65535 ne tient pas dans un short signé (max 32767). Le résultat est
défini par l'implémentation — sur x86, s vaut −1.
2.10. Conversion de type
Avec perte d'information
int->
float : Cette conversion peut entraîner une perte d'information,
car les types int et float sont représentés différemment en mémoire. Les valeurs entières sont
converties en valeurs à virgule flottante, ce qui signifie qu'elles peuvent perdre de la précision.
Un float n'a que 24 bits de mantisse : au-delà de 224 (16 777 216), il ne peut
plus représenter tous les entiers, et la valeur est arrondie (et non tronquée).
Exemple
inti
= 16777217; floatf
= i;
Remarque:f vaut 16777216, pas 16777217 : l'entier
voisin n'est pas représentable. (int) f == i est faux.
2.10. Conversion de type
Les conversions explicites
Les conversions explicites, également appelées typecasts, sont des opérations dans lesquelles vous
spécifiez explicitement le type de données vers lequel vous souhaitez convertir une variable.
Taille des tableaux fixe : Les tableaux 'prenom', 'nom', 'rue' et 'ville' sont définis avec
une taille fixe de 135 éléments. Cela signifie que la base de données peut contenir un maximum de
135 étudiants. Si vous avez besoin de gérer plus d'étudiants, vous devez redimensionner manuellement
ces tableaux, ce qui peut être fastidieux et source d'erreurs.
Taille fixe des champs : Les champs 'prenom', 'nom', 'rue' et 'ville' sont également définis
avec une taille fixe de 30 caractères. Cela peut poser problème si les noms ou les adresses des
étudiants dépassent cette limite, car les données seront tronquées, ce qui peut entraîner une perte
d'information.
2.11. Les structures
Base de données de gestion des étudiants
Manque de flexibilité : Cette base de données utilise des tableaux multidimensionnels pour
stocker les informations des étudiants. Cela peut rendre la gestion des données moins flexible,
notamment pour l'ajout ou la suppression d'étudiants.
Aucune validation des données : Cette base de données n'inclut pas de mécanisme de validation
des données. Par exemple, il n'y a pas de contrôle sur la validité des valeurs des notes (elles
pourraient être en dehors de la plage attendue) ni sur les chaînes de caractères (par exemple, des
adresses incorrectes pourraient être saisies).
2.11. Les structures
Une structure (struct) est une construction de données en programmation permettant de
regrouper plusieurs
variables de types différents sous un seul nom, facilitant ainsi la manipulation et la gestion de
données complexes.
Flexibilité : Vous pouvez gérer un nombre variable d'étudiants en ajoutant ou en supprimant
des éléments dans le tableau de structures.
Structure de données plus appropriée : L'utilisation de structures permet de regrouper les
données d'un étudiant en un seul objet, ce qui est plus naturel et lisible.
2.11. Les structures
La déclaration et l'utilisation d'une structure
Ce code crée une variable d'une structure etudiant, et l'initialise avec
les données spécifiques à un étudiant nommé Dupont Pierre.
struct etudiant dupont = {
"Pierre", "Dupont", "Boulevard du 11 novembre 1918", "Villeurbanne", 19};
2.11. Les structures
La déclaration et l'utilisation d'une structure
Ce code vise à définir une variable d'une structure etudiant pour stocker les informations d'un étudiant
particulier nommé "Dupont Pierre", avec une adresse et une note.
struct etudiant dupont;
strcpy(dupont.prenom, "Pierre"); strcpy(dupont.nom, "Dupont"); strcpy(dupont.rue, "Boulevard du 11 novembre 1918"); strcpy(dupont.ville, "Villeurbanne");
dupont.notes = 19;
2.11. Les structures
Tableaux de structures
Ce code crée un tableau de structures nommé etudiant_cpe pouvant contenir
jusqu'à 135 étudiants, puis il initialise le premier étudiant du tableau avec les données spécifiques à
"Dupont Pierre" notamment son prénom, nom, adresse et note.
struct etudiant etudiant_cpe[135];
strcpy(etudiant_cpe[0].prenom, "Pierre"); strcpy(etudiant_cpe[0].nom, "Dupont"); strcpy(etudiant_cpe[0].rue, "Boulevard du 11 novembre 1918"); strcpy(etudiant_cpe[0].ville, "Villeurbanne");
etudiant_cpe[0].notes = 19;
2.11. Les structures
Une structure dans une structure
Ce code définit deux structures, adresse et etudiant, où la structure etudiant
contient une structure adresse comme l'un de ses membres. Cela permet de
stocker les informations de l'étudiant, y compris son prénom, nom et adresse, de manière organisée et
hiérarchique.
Ce code définit une structure etudiant qui inclut une
structure imbriquée
adresse. Cela permet de regrouper les informations relatives à un
étudiant, telles que son prénom, nom et notes, ainsi que ses données d'adresse, y compris la rue et la
ville, de manière structurée et organisée.
Ce code crée un tableau de structures etudiant nommé etudiant_cpe pouvant contenir jusqu'à 135 étudiants. Il utilise la
structure imbriquée adresse pour stocker les détails de l'adresse de
l'étudiant.
struct etudiant etudiant_cpe[135];
strcpy(etudiant_cpe[0].prenom, "Pierre"); strcpy(etudiant_cpe[0].nom, "Dupont"); strcpy(etudiant_cpe[0].adresse.rue, "Boulevard du 11 novembre 1918"); strcpy(etudiant_cpe[0].adresse.ville, "Villeurbanne");
etudiant_cpe[0].notes = 19;
2.12. Les unions
Union
Une union se déclare comme une structure, mais ses
membres partagent le même espace mémoire. Sa taille est celle de son membre le plus grand — et
non la somme des tailles, comme pour une structure.
Remarque: les quatre membres commencent à la même adresse. Un
seul contenu est écrit, et les trois lectures le retrouvent. C'est exactement ce qui distingue une
union d'une structure.
free(c.ccontent); // le membre libéré doit être celui qui a été alloué return 0; }
2.12. Les unions
Point3D : deux vues des mêmes octets
L'union point3d donne deux façons de désigner les
mêmes 12 octets : un tableau value[3], ou trois champs nommés
x, y,
z. p.value[0] et
p.x désignent le même entier.
union point3d{ int value[3]; struct{ int x; int y; int z;
};
};
Remarque: la structure anonyme (C11) permet d'écrire
p.x et non p.s.x. C'est ici que la
relecture d'un membre non écrit est volontaire et bien définie en C : les deux vues décrivent
la même disposition mémoire.
2.12. Les unions
/* Fichier: union.c * la déclaration et l'utilisation d'une union
*/ #include<stdio.h> // en-têtes(headers)
Remarque:sizeof(union
point3d) vaut 12 — la taille du plus grand membre, pas leur somme.
2. Question
Écrivez une structure pour la représentation d'une couleur RGB
(rouge, vert, bleu : chaque couleur prend un octet) en utilisant
union et struct, de façon à
pouvoir accéder soit aux trois composantes séparément, soit aux trois octets d'un coup.
/* Fichier: bonjour2.c * affiche un message à l'écran en utilisant une variable
* auteur: John Samuel * Ceci est un commentaire
sur plusieurs lignes */
#include <stdio.h> // en-têtes(headers)
intmain()
{ intannee = 2026;
// déclaration d'une variable printf("Bonjour le Monde!!! C'est l'année %d", annee); return 0; }
2.13. Fonctions
Les fonctions sont utilisées pour encapsuler des morceaux de code spécifiques dans un programme afin
d'effectuer des opérations ou de calculer des résultats de manière modulaire et réutilisable.
Prototype: Le but de ce prototype est de fournir aux autres parties du programme des informations
sur la manière d'appeler la fonction "add" et sur les types de données qu'elle attend et renvoie.
int
add(
int,
int);
L'implémentation: Plutôt que de répéter le même code d'addition partout où vous en avez besoin
dans votre programme, vous pouvez simplement appeler la fonction "add".
int
add(
inta,
intb
) { returna
+ b;
}
2.13. Fonctions
Les fichiers .h servent à déclarer les prototypes de fonctions et les structures de données pour
permettre une séparation claire entre l'interface publique et la définition de fonctions dans les
fichiers .c.
Prototype (operators.h)
int
add(
int,
int);
L'implémentation (operators.c)
int
add(
inta,
intb
) { returna
+ b;
}
2.13. Fonctions
La déclaration d'une fonction consiste en un prototype qui spécifie le type de retour et les types des
paramètres entre parenthèses, tandis que l'implémentation de la fonction utilise la même signature avec
des noms de variables pour les paramètres et inclut le code à exécuter.
Prototype
type
fonction(
[type,]*);
L'implémentation
type
fonction(
[typevariable,]*)
{ [returnvaleur];
}
Remarque:type: void, les types de
base, les types composés
2.13. Fonctions
Les fichiers .h sont inclus à l'aide de #include pour permettre
l'accès aux déclarations de fonctions, structures et variables définies dans le fichier d'en-tête, ce
qui permet d'utiliser ces fonctionnalités dans d'autres parties du programme.
/* Fichier: bonjour3.c * affiche un message à l'écran en utilisant print
* auteur: John Samuel */
#include"nom.h" // en-têtes(headers)
intmain()
{ print(
"Bonjour le Monde!!!", 19); charmessage[] =
"Pierre"; print("Je suis ", 8); print(message, 19); return 0; }
2.13. Fonctions
La compilation
$ gcc -o bonjour bonjour3.c nom.c
L'exécution
$./bonjour Bonjour le Monde!!!Je suis Pierre
Remarque: un programme ne peut contenir qu'une seule fonction nommée 'main', quel que soit le nombre de fichiers .c liés ensemble.
2.14. L'interface en ligne de commande
Les outils Linux
L'interface en ligne de commande (CLI) est un moyen d'interagir avec un système d'exploitation ou
un logiciel en utilisant du texte, généralement en saisissant des commandes dans un terminal.
Les commandes sont des instructions données à un système via une interface en ligne de commande,
et les arguments sont des informations spécifiques fournies avec ces commandes pour indiquer ce
qu'elles doivent faire
$ ls -l
$ cd dossier
$ cat fichier
ls -l utilise la commande "ls" pour lister les fichiers avec
l'argument "-l" qui spécifie une liste détaillée
cd dossier utilise la commande "cd" pour changer de répertoire vers
"dossier"
cat fichier utilise la commande "cat" pour afficher le contenu du
fichier "fichier."
2.14. L'interface en ligne de commande
/* Fichier: bonjour4.c * affiche un message à l'écran en utilisant les arguments de la ligne de commandes.
* auteur: John Samuel */
#include"nom.h" // en-têtes(headers)
intmain(intargc, char ** argv)
{ print("Bonjour le Monde. Je suis ", 26); // sinon argv[4] est hors du tableau if (argc < 5) return 1; print(argv[1], 20); print(argv[2], 20); print(argv[3], 20); print(argv[4], 20); return 0; }
2.14. L'interface en ligne de commande
La compilation
$ gcc -o bonjour bonjour4.c nom.c
L'exécution
$./bonjour Pierre Dupont Lyon 69001
Bonjour le Monde. Je suis Pierre Dupont Lyon 69001
La fonction main prend deux arguments, intargc et char ** argv, qui permettent de passer des arguments lors de l'exécution
du programme.
Le programme peut ainsi accéder et utiliser ces arguments pour effectuer des actions spécifiques en
fonction de ces valeurs. Dans l'exemple donné, le programme "bonjour4.c"
semble utiliser les arguments pour afficher un message de salutation personnalisé en fonction des noms
et de l'adresse passés en arguments lors de son exécution.
2.14. L'interface en ligne de commande
intargc représente le nombre
total d'arguments passés au programme, y compris le nom du programme lui-même. Dans cet exemple,
argc serait égal à 5 : le nom du programme plus les quatre arguments.
char ** argv est un tableau de pointeurs vers des chaînes de
caractères (tableau de chaînes de caractères) où chaque élément pointe vers un argument passé au
programme. Dans cet exemple, argv contiendrait les éléments suivants
:
argv[0] : Pointe vers le nom du programme, qui serait "bonjour" dans ce cas.
argv[1] : Pointe vers "Pierre"
argv[2] : Pointe vers "Dupont"
argv[3] : Pointe vers "Lyon"
argv[4] : Pointe vers "69001"
argv[5] : Pointe vers NULL (marquant la fin de la liste des
arguments)
2.14. L'interface en ligne de commande
La compilation
2.14. L'interface en ligne de commande
/* Fichier: bonjour4.c
* affiche un message à l'écran en utilisant
* les arguments de la ligne de commandes.
*/
#include"nom.h" // en-têtes(headers)
intmain(intargc, char ** argv)
{ print("Bonjour le Monde. Je suis ", 26); if ( argc == 2 ) { print(argv[1], 20); } return 0; }
Remarque:argv[0] est normalement le nom du fichier exécutable (e.g., bonjour)
2.15. La manipulation d'une chaîne de caractères
/* Fichier: string.c * manipulation d'une chaîne de caractères.
* auteur: John Samuel */
Objectif : L'objectif de la fonction strlen est de calculer la
longueur (le nombre de caractères) d'une chaîne de caractères, c'est-à-dire le nombre de caractères
qu'elle contient avant d'atteindre le caractère nul '\0' qui marque la fin de la chaîne.
2.15. La manipulation d'une chaîne de caractères
La compilation
$ gcc -o strlen string.c
1ere Exécution
$./strlen
Bonjour. Votre nom? John
La taille: 4
2.15. La manipulation d'une chaîne de caractères
La compilation
2eme Exécution
Lorsqu'un nom excessivement long est saisi, ce programme provoque une erreur de dépassement de pile
(stack smashing).
/* Fichier: string.c * manipulation d'une chaîne de caractères.
* auteur: John Samuel */ #include<stdio.h> // en-têtes(headers) #include<string.h> intmain(intargc, char ** argv)
{ charnom[10]; printf("Bonjour. Votre nom? "); scanf("%9s", nom); printf("La taille: %zu\n", strnlen(nom, sizeof(nom))); return 0; }
Objectif : strnlen calcule la longueur d'une chaîne sans
jamais lire au-delà de la taille annoncée : il renvoie au plus sizeof(nom), même si le
tableau ne contient aucun '\0'.
Attention :strnlen ne corrige pas le dépassement — il
protège seulement la lecture. Le débordement de la page précédente venait de l'écriture
faite par scanf("%s", nom). C'est pourquoi le format est passé à
"%9s" ici : une largeur maximale (taille du tampon moins un) est la
seule chose qui empêche réellement l'écriture hors limites.
2.15. La manipulation d'une chaîne de caractères
/* Fichier: string.c * scanf avec une largeur maximale : "%9s"
* auteur: John Samuel */ #include<stdio.h> // en-têtes(headers) #include<string.h> intmain(intargc, char ** argv)
{ charnom[10]; charsortie[100]; printf("Bonjour. Votre nom? "); scanf("%9s", nom); sprintf(sortie, "La taille: %zu\n", strnlen(nom, sizeof(nom))); return 0; }
Objectif:scanf("%9s", nom) lit une chaîne de caractères d'au plus 9 caractères depuis l'entrée
standard et la stocke dans la variable nom, garantissant ainsi qu'aucun
dépassement de mémoire ne se produit.
Remarque: L'objectif de ce code est de formater une chaîne de caractères
contenant la taille d'une autre chaîne (nom) limitée à un maximum de 10
caractères, puis de stocker ce résultat dans la variable sortie.
La fonction atoi en C a pour objectif de convertir une
chaîne de
caractères représentant un nombre entier en une valeur entière de type int. Elle ignore les espaces
initiaux, lit les chiffres jusqu'à ce qu'elle atteigne un caractère non numérique, puis retourne la
valeur entière correspondante.
Attention :atoi ne signale
aucune erreur : atoi("abc") et atoi("0") renvoient tous deux 0, et
un dépassement est un comportement indéfini. Pour une saisie utilisateur, préférez
strtol, qui indique où la lecture s'est arrêtée et positionne errno.
La fonction sscanf permet de lire et de convertir des
valeurs depuis une
chaîne de caractères en fonction d'un format spécifié, stockant ces valeurs dans des variables, ce qui
facilite l'extraction de données structurées à partir de chaînes de caractères formatées. La fonction
sscanf analyse la chaîne numstr en recherchant des entiers et un nombre
à virgule flottante délimités par des espaces, puis stocke ces valeurs dans les variables inum1, inum2, et fnum3.
La fonction sscanf analyse la chaîne numstr en
recherchant des entiers
et un nombre à virgule flottante délimités par des points-virgules, puis stocke ces valeurs dans les variables
inum1, inum2, et fnum3.
2.15. La manipulation d'une chaîne de caractères
La compilation
$ gcc -o strnlen string2.c
1ere Exécution
$./strnlen
Bonjour. Votre nom? John
La taille: 4
2.15. La manipulation d'une chaîne de caractères
strcat concatène deux chaînes de caractères
strncat concatène deux chaînes de caractères avec une
taille maximum donnée par l'utilisateur.
strcpy copie une chaîne de caractères dans une autre
strncpy copie au plus n caractères.
Attention : si la source est plus longue que n, le résultat
n'est pas terminé par '\0' — ce n'est donc pas une version sûre de
strcpy. Préférez snprintf.
strcmp compare deux chaînes de caractères
strncmp compare deux chaînes de caractères avec une
taille maximum donnée par l'utilisateur
2.15. La manipulation d'une chaîne de caractères
/* Fichier: string3.c * manipulation d'une chaîne de caractères.
*/
void *malloc(
size_t size); void *calloc(
size_t nmemb,
size_t size); voidfree(
void * ptr); // désallocation ou libération de mémoire
malloc : Cette fonction alloue dynamiquement une région de mémoire
de la taille spécifiée size en octets, et elle renvoie un pointeur
vers le début de cette mémoire allouée. L'objectif est de permettre la création de structures de
données dynamiques lorsqu'on ne connaît pas la taille exacte à l'avance.
calloc : Cette fonction alloue dynamiquement une région de mémoire
pour un tableau de nmemb éléments, chacun de size octets, initialisée à zéro, et elle renvoie un pointeur
vers le début de cette mémoire allouée. Elle est couramment utilisée pour allouer de la mémoire pour
des tableaux.
free : Cette fonction libère la mémoire précédemment allouée
dynamiquement à l'aide de malloc ou calloc. Son objectif est de libérer la mémoire inutilisée afin
d'éviter les fuites de mémoire et d'optimiser l'utilisation des ressources mémoire dans un
programme.
2.16. Allocation dynamique de mémoire
/* Fichier: memory.c * Allocation dynamique de mémoire
* auteur: John Samuel */
intmain(intargc, char ** argv)
{ char *content = calloc(10, sizeof(char)); if (content == NULL) return 1; // 9 et non 10 : strncat ajoute encore le '\0'
strncat(content, "Bonjour", 9); free(content); // libération de mémoire return 0; }
2. Question
Écrivez un programme qui réserve et libère un espace mémoire pour un tableau d'entiers (int, long int,
short ou long long int) et un tableau de nombres en flottant (float, double ou long double) en utilisant
malloc, calloc et free.
2.17. Les variables constantes
Une variable constante
L'objectif d'une variable constante est de déclarer une valeur immuable qui ne peut pas être
modifiée après son initialisation, afin d'assurer l'intégrité des données et de prévenir les erreurs
potentielles.
/* Une variable constante */ #include <stdio.h>
intmain() { const intannee = 2017; // une variable constante annee = 2019; // tentative de modification d'une variable constante printf("C'est l'année %d", annee); return 0; }
2.17. Les variables constantes
Erreur pendant la compilation
Les variables constantes sont des valeurs en lecture seule, et toute tentative de modification de
leur valeur après leur initialisation entraîne une erreur de compilation pour préserver leur
immutabilité.
$ gcc bonjour.c
const.c: In function ‘main’:
const.c:5:8: error: assignment of read-only variable ‘annee’
annee = 2019;
2.18. La portée des variables
Une variable globale
Les variables globales sont visibles et modifiables depuis toutes les fonctions du programme. Il
est généralement recommandé de limiter l'utilisation de variables globales et de les utiliser
avec précaution pour éviter des effets secondaires indésirables dans un programme complexe.
/* affiche un message à l'écran en utilisant une variable globale
*/
#include <stdio.h> intannee = 2017; // une variable globale
/* affiche un message à l'écran en utilisant une variable globale */
#include <stdio.h> intmain()
{ printf("C'est l'année %d", annee); return 0; } intannee = 2017; // une variable globale
Dans la fonction main(), l'affichage de la valeur de annee est tenté avant sa déclaration et son initialisation, ce qui génère
une erreur de compilation. Le compilateur ne sait pas ce qui est tenté d'être affiché avant que la
variable annee ne soit définie.
2.18. La portée des variables
Erreur pendant la compilation
$ gcc bonjour.c
bonjour.c: In function ‘main’:
bonjour.c:5:30: error: ‘annee’ undeclared (first use in this function)
5 | printf("C'est l'année %d", annee);
| ^~~~
bonjour.c:5:30: note: each undeclared identifier is reported only once for each function it appears in
L'erreur de compilation indique que la variable annee n'a pas été
déclarée avant son utilisation dans la fonction main().
2.18. La portée des variables
Une variable locale
/* affiche un message à l'écran en utilisant une variable locale */ #include <stdio.h> intannee = 2017; // une variable globale intmain()
{ intannee = 2018; // une variable locale printf("C'est l'année %d", annee); //affiche 2018 return 0; }
Déclarée en dehors de main(), annee est une variable globale : visible depuis toute fonction
du programme. Déclarée à l'intérieur, c'est une variable locale, visible de
main() seulement.
C'est la portée des variables : la locale masque la globale de même nom.
2.18. La portée des variables
Une variable locale
/* affiche un message à l'écran en utilisant une variable locale */ #include <stdio.h> intannee = 2017; // une variable globale
intmain()
{ intannee = 2018; // une variable locale
{ intannee = 2019; // une variable locale printf("C'est l'année %d", annee); //affiche 2019
} printf("C'est l'année %d", annee); //affiche 2018 return 0; }
2.19. Le passage de paramètres
1. Passage par valeur
void
echange(
inta,
intb
) { inttemp
= a; a
= b; b
= temp;
} intmain()
{ inta = 10, b = 20; echange(a, b); printf("a: %d, b: %d", a, b); //affiche 10, 20 return 0; }
Passage par valeur : la fonction reçoit des copies. Les valeurs de
a et b ne sont pas modifiées en
dehors de la fonction.
Passage par référence : la fonction reçoit les adresses. Les valeurs pointées sont
réellement échangées, et le changement est visible en dehors de la fonction.
2.19. Le passage de paramètres
2.2. Passage par référence: un tableau
Le passage par référence d'un tableau est une manière de transmettre un tableau à une fonction en
lui fournissant directement une référence (ou un pointeur) vers le tableau d'origine,
plutôt que de faire une copie du tableau.
Lorsqu'un tableau est passé en tant qu'argument à une fonction, il est automatiquement converti en un
pointeur vers son premier élément. Cela signifie que charmessage[] est équivalent à char*message dans le contexte de cette fonction.
Remarque : L'erreur lors de la compilation est due à la déclaration d'une
fonction avec un tableau multidimensionnel sans spécifier la taille de la deuxième dimension.
$ gcc tableau.c
tableau.c:3:21: error: array type has incomplete element type ‘int[]’
3 | void affichage( int tableau[][] ) {
| ^~~~~~~
tableau.c:3:21: note: declaration of ‘tableau’ as multidimensional array must have bounds for all dimensions except the first
tableau.c: In function ‘main’:
tableau.c:15:13: error: type of formal parameter 1 is incomplete
2.19. Le passage de paramètres
2.2. Passage par référence: un tableau
Pour corriger l'erreur, nous avons précisé la taille de la deuxième dimension.
intmain()
{ int**tableau, lignes = 2, colonnes = 10; tableau = calloc(lignes, sizeof(int *)); for ( inti = 0;
i < lignes; i++) { tableau[i] = calloc(colonnes, sizeof(int));
} for ( inti = 0;
i < lignes; i++) { for ( intj = 0;
j < colonnes; j++) { tableau[i][j] = i+j;
}
}
...
2.19. Le passage de paramètres
Passage par référence: un tableau
... affichage(tableau,
lignes,
colonnes); for ( inti = 0;
i < lignes; i++) { free(tableau[i]);
} free(tableau); return 0; }
Remarque : Le tableau tableau a été passé
par référence à la fonction affichage, de sorte que toute modification apportée à tableau à l'intérieur
de cette fonction serait également répercutée sur le tableau d'origine.
scptr->bleu = 0x01 modifie
également le membre bleu, mais à travers le pointeur scptr. scptr->bleu et
(*scptr).bleu sont équivalents : la deuxième affectation
écrit 0x22 au même endroit.
L'opérateur -> combine les opérations de déréférencement et d'accès au membre en
une seule étape. Cela rend le code plus lisible et réduit les risques d'erreurs de déréférencement. Les
deux notations sont interchangeables.
Un membre d'une structure est modifié en utilisant une fonction qui prend un pointeur
vers la structure en argument. Cette approche permet de manipuler directement la structure à l'intérieur
de la fonction sans avoir besoin de renvoyer la structure modifiée, car les pointeurs permettent de
travailler avec la même instance de la structure.
La fonction nochange est censée modifier le membre bleu
de la structure c pour lui attribuer la valeur 0x03. Cependant, la fonction est déclarée pour prendre la structure c en tant que copie (par valeur) au lieu d'un pointeur. Par conséquent,
lorsqu'elle est appelée dans la fonction main, une copie de la structure c1 est passée à la fonction nochange. Toute modification apportée à cette
copie n'affecte pas la structure d'origine.
2.20. Les structures et les pointeurs
Une liste de couleurs simplement chaînée
Une liste simplement chaînée est une structure de données linéaire composée de nœuds, où chaque nœud
contient une valeur et une référence (pointeur) vers le nœud suivant. Elle est utilisée pour stocker des
éléments de manière séquentielle, offrant une manipulation efficace des données en insérant ou
supprimant des éléments en temps constant, mais avec un accès moins efficace aux éléments au milieu de
la liste.
2.20. Les structures et les pointeurs
Une liste de couleurs simplement chaînée
Chaque nœud de la liste contient également un pointeur vers le nœud suivant de la
liste, permettant de stocker et de naviguer à travers une séquence de couleurs. Cette structure est
souvent utilisée pour représenter une séquence de couleurs dans des applications graphiques ou de
traitement d'images.
Un pointeur cptr est utilisé pour parcourir la liste, et à chaque étape, la valeur du
composant "bleu" de la couleur actuelle est affichée à l'aide de printf. La boucle continue jusqu'à ce
que le pointeur atteigne la fin de la liste, ce qui permet de parcourir et d'afficher les composants
"bleu" de toutes les couleurs de la liste.
2.20. Les structures et les pointeurs
Une liste d'entiers simplement chaînée
2.20. Les structures et les pointeurs
Une liste d'entiers simplement chaînée
Chaque élément de la liste est représenté par la structure element, qui contient un numéro entier
(numero) et un pointeur vers l'élément suivant (suivant). Deux fonctions sont fournies : insertion pour
ajouter un nouvel élément à la liste et parcours pour parcourir et afficher les éléments de la liste.
Cela permet de construire et de manipuler une liste d'entiers simplement chaînée.
struct element{ unsigned int numero; struct element *suivant;
};
// insertion d'un élement dans une liste voidinsertion(structelement*, int);
// parcours de la liste voidparcours(struct element *);
Insertion d'un élément dans une liste simplement chaînée
L'objectif de cette fonction est d'insérer un nouvel élément dans une liste chaînée en créant un nouvel
élément, en assignant une valeur à ce nouvel élément, et en ajustant les pointeurs pour l'insérer
correctement dans la liste existante, ce qui permet de modifier la structure de la liste chaînée.
voidparcours(structelement *premier) { structelement *elem = premier->suivant; // premier est une sentinelle : // son numero n'est jamais initialisé while(elem != NULL) { printf("%u\n", elem->numero); elem = elem->suivant;
}
}
2.20. Les structures et les pointeurs
Une liste de couleurs doublement chaînée
Une liste doublement chaînée a pour objectif de permettre la navigation dans une structure de données
linéaire de manière bidirectionnelle, offrant un accès à la fois vers l'élément précédent et l'élément
suivant, ce qui facilite l'insertion, la suppression et la recherche efficace des éléments.
L'objectif de cette structure de données est de créer une liste doublement chaînée de couleurs, où chaque
élément conserve des informations sur la couleur, un compteur, ainsi que des pointeurs vers l'élément
précédent et l'élément suivant. Cela permet une navigation bidirectionnelle efficace et des opérations
telles que l'insertion et la suppression d'éléments au sein de la liste chaînée.
while (1) { charstrnum[50]; if (fgets(strnum, sizeof(strnum), stdin) == NULL) break; if(strcmp(strnum, "FIN\n") == 0) { break;
}
// alloué après le test : sinon fuite structelement *elem = malloc(sizeof(*elem)); if (elem == NULL) break; if (sscanf(strnum, "%d", &elem->num) != 1)
{ free(elem); continue; } insertion_fin(&liste, elem);
}
parcourir_debut(&liste); parcourir_fin(&liste);
}
2.21. C23, pointeurs et sûreté
Des outils pour expliciter l'intention
nullptr clarifie la notion de pointeur nul.
_BitInt(N) permet de choisir une précision entière adaptée au problème.
<stdckdint.h> ajoute des opérations d'arithmétique entière vérifiée.
[[nodiscard]] peut signaler qu'un résultat ne doit pas être ignoré.
Ces ajouts ne suppriment pas les erreurs de mémoire, mais ils aident le compilateur, les outils
d'analyse et le programmeur à mieux détecter certaines intentions ou anomalies.
2. Principes des Langages de Programmation : Références