Les Structures (struct)
Une structure permet de grouper plusieurs variables de types différents sous un même nom. C’est le mécanisme fondamental en C pour créer des types composés. Les structures sont fondamentalement des tableaux d’octet, toutes les données sont contigue en mémoire !
struct Personne {
char nom[50]; // 50 octets
int age; // 4 octets (supposé)
double salaire; // 8 octets (supposé)
};
Cette déclaration définit un modèle de structure. Pour créer des variables de ce type :
struct Personne p1;
struct Personne p2 = {"Alice", 30, 2500.50};
struct Personne p3 = {.nom="Merlin", .age=999, .salaire=-0.50};
On accède aux membres d’une structure via l’opérateur . (point) :
strcpy(p1.nom, "Bob");
p1.age = 25;
p1.salaire = 2000.0;
printf("%s a %d ans et gagne %.2f€\n", p1.nom, p1.age, p1.salaire);
Alias de type avec typedef
Pour éviter de répéter struct à chaque fois, on peut créer un alias de type. Deux solutions :
struct Personne {
char nom[50];
int age;
double salaire;
};
typedef struct Personne Personne;
typedef struct {
char nom[50];
int age;
double salaire;
} Personne;
On pourra alors écrire :
Personne p1;
Personne p2 = {"Alice", 30, 2500.50};
Le mot cléf typedef fonctionne avec n’importe quelle type ex: typedef int bool;
Structures imbriquées
Les structures peuvent contenir d’autres structures :
struct Adresse {
char rue[100];
int code_postal;
char ville[50];
};
typedef struct {
char nom[50];
int age;
struct Adresse adresse;
} Personne;
Personne p;
strcpy(p.adresse.rue, "123 Rue de la Paix");
p.adresse.code_postal = 75001;
La taille réservé pour Personne seras alors la somme des tailles des deux structures.
Copie de structure
En C on peut copier le contenue d’une structure dans une autre. Au moment de la compilation, l’affectation sera éttendue a tous les champs. (Attention aux effects de bords sur les pointeurs). C’est néanmoin une solution idéal pour le passage de structure en argument de fonction.
typedef struct {
int x, y;
} Vec2;
int main()
{
Vec2 a = {.x = 5, .y = 8 };
Vec2 b = a;
printf("%d %d", b.x, b.y); // Affiche 5, 8
return 0;
}
Taille des structures
La fonction sizeof retourne la taille en octets d’une structure. Attention : il peut y avoir du remplissage (padding) à cause de l’alignement en mémoire.
typedef struct {
char c; // 1 octet
int x; // 4 octets (mais possiblement 3 octets de padding avant)
double d; // 8 octets
} Exemple;
printf("%lu\n", sizeof(Exemple)); // possiblement 16 ou 24 octets selon l'alignement
La taille réelle de la structure est probablement plus grande et le mot clés sizeof retournera la tailel réel. Le padding est un mécanisme par lequel le compilateur insère automatiquement des octets vides à l’intérieur des structures, afin d’aligner correctement chaque membre selon les contraintes de l’architecture cible. Ce comportement vise à optimiser l’accès mémoire pour le processeur.
- Les processeurs lisent plus rapidement des données alignées sur certaines puissances de 2. Par exemple, un int (4 octets) devrait commencer à une adresse multiple de 4 pour des raisons de performance.
- Si un membre d’une structure ne respecte pas cet alignement naturellement (par exemple un tableau de char suivi d’un int), le compilateur insère des octets de padding pour garantir l’alignement du champ suivant.
La structure précédente sera en réalité :
struct Personne {
char nom[50]; // 50 octets
char padding[2]; // 2 octets de padding pour que age commence à une adresse multiple de 4
int age; // 4 octets, alignement sur 4
char padding[4]; // 4 octets de padding pour que salaire commence à une adresse multiple de 8
double salaire; // 8 octets, alignement sur 8
};
Les compilateurs utilisent automatiquement le padding nécessaire, mais on peut forcer un alignement avec #pragma pack (non-portable) ou en réorganisant les membres par ordre décroissant de taille.
Tableaux de structures
On peut créer des tableaux de structures :
Personne employes[100];
employes[0].age = 25;
employes[1].salaire = 3000.0;
Dans ce cas la taille total sera de 100 * sizeof(Personne).
Fonctions et paramètres
Une fonction est un bloc de code réutilisable qui effectue une tâche précise. Elle peut accepter des paramètres et retourner une seule et unique valeur. Pour en retourner plusieurs, pas le choix, il faut utiliser une structure. Une fonction se compose d’une signature (type de retour, nom, paramètres) et d’un corps :
int ajouter(int a, int b) {
return a + b;
}
int main(void) {
int resultat = ajouter(5, 3); // résultat = 8
return 0;
}
En C, quelle que soit la norme, la surcharge de fonctions (avoir plusieurs définitions avec le même nom et des signatures différentes) n’est pas autorisée. Cette fonctionnalité est propre au C++ et implique une gestion différente du nom des fonctions (« name mangling ») qui est absente en C standard.
Organisation de la mémoire
Les fonctions ont une zone mémoire ! On parle de stack frame. Lorsqu’une fonction est appelée, le système réserve de la mémoire sur la stack pour ses variables locales et ses paramètres.
Chaque appel de fonction crée une nouvelle frame sur la stack :
- L’adresse de retour est empilée pour que le programme sache où reprendre l’exécution après le
return. - Les variables locales sont créées dans cette zone.
- Les paramètres y sont stockés (toujours copiés depuis les registres ou la pile de l’appelante).
Quand la fonction se termine, sa frame est libérée (on dépile) et toutes ses variables locales disparaissent.
Passage par valeur
Par défaut, les paramètres sont passés par valeur. La fonction reçoit une copie de la valeur, pas la variable originale :
void incrementer(int x) {
x++; // modifie la copie locale, pas la variable originale
}
int main(void) {
int a = 5;
incrementer(a);
printf("%d\n", a); // affiche 5, pas 6
return 0;
}
Passage par pointeur
Pour modifier la variable originale, on passe son adresse (un pointeur, c’est une position dans la mémoire) :
void incrementer(int *x) { // x est un pointeur
(*x)++; // déréférence et incrément
}
int main(void) {
int a = 5;
incrementer(&a); // passe l'adresse de a
printf("%d\n", a); // affiche 6
return 0;
}
Dans ce contexte, la fonction incrémentée reçoit une copie de l’adresse. Ce n’est donc plus un souci. Pour faire court, incrementer connaît le numéro de téléphone de x. La syntaxe *x permet d’appeler la valeur, ensuite on fait ce que l’on veut. Notez que sizeof(a) est probablement 4, par contre sizeof(&a) = sizeof(x) = 8 si l’on est sur un système 64 bits ! Donc la fonction incrementer a réservé 8 octets pour l’adresse de a. Dans ce cas précisément, la quantité de mémoire utilisée est légèrement supérieure, mais c’est nécessaire si l’on veut modifier a par l’intermédiaire de la fonction incrementer.
Avec les structures, c’est exactement la même chose ! Supposons la structure suivante :
typedef struct {
int x;
int y;
} Point;
Le code suivant ne fonctionnera pas :
void deplacer(Point p, int dx, int dy) {
p.x += dx; // aie, modification locale
p.y += dy; // aie, modification locale
}
int main(void) {
Point origin = {0, 0};
deplacer(origin, 10, 20);
printf("(%d, %d)\n", origin.x, origin.y); // affiche (0, 0)
return 0;
}
Il conviendra alors d’utiliser les pointeurs :
void deplacer(Point *p, int dx, int dy) {
(*p).x += dx; // modification de l'original
(*p).y += dy; // modification de l'original
}
int main(void) {
Point origin = {0, 0};
deplacer(&origin, 10, 20);
printf("(%d, %d)\n", origin.x, origin.y); // affiche (10, 20)
return 0;
}
La notation : dans le code ci-dessus, le type de p est Point*, c’est-à-dire un pointeur (adresse) vers une structure de type Point. La syntaxe *p permet de déréférencer p. On a la vue au-dessus, c’est-à-dire que l’on appelle le numéro de téléphone qui permet de contacter p. Donc le type de *p est Point. Ensuite, vous pouvez accéder aux membres de façon classique avec l’opérateur .. Il existe toutefois un raccourci syntaxique : (*p).x peut aussi s’écrire p->x.
Attention le code suivant ne veux strictement rien dire
p->*x
ou encore*p->xest équivalent à*(p->x),xétant de typeint, déréférencéxn’a pas de sens !
Passage de tableaux
Quand on passe un tableau à une fonction, il est converti en pointeur vers son premier élément. La fonction ne connaît donc pas la taille réelle du tableau et doit la recevoir en paramètre.
void afficher_tableau(int tab[], int taille) {
// PAS BIEN
int n = sizeof(nombres) / sizeof(nombres[0]);
// Le famaux sizeof(tab) / sizeof(*tab) QUI NE MARCHE PAS !
// Dans ce context le type de tab n'est pas `int[]` MAIS `int*` !!!!
// On aurans donc sizeof(tab) = 8 et sizeof(*tab) = 4
for (int i = 0; i < taille; i++) {
printf("%d ", tab[i]);
}
printf("\n");
}
int main(void) {
int nombres[] = {1, 2, 3, 4, 5};
// BIEN
int n = sizeof(nombres) / sizeof(nombres[0]);
afficher_tableau(nombres, n);
return 0;
}

Fonctions sans retour
Une fonction peut ne retourner aucune valeur. On utilise alors le type void :
void afficher_message() {
printf("Bonjour!\n");
}
int main(void) {
afficher_message();
return 0;
}
Déclarations et prototypes
Pour utiliser une fonction avant sa définition complète, on la déclare avec son prototype :
int ajouter(int a, int b); // prototype
int main(void) {
int resultat = ajouter(5, 3);
return 0;
}
int ajouter(int a, int b) { // définition
return a + b;
}
Le prototype informe le compilateur de la signature de la fonction, permettant une vérification des types au moment de l’appel. Cela permet aussi de déplacé l’implémentation de la fonction dans un autre fichier. Il faut donc déclarer qu’elle existe et la résolution des liens feras la suite. On pourra alors avec un fichier intémédiaire qui contiendra les déclarations :
utils.h
int ajouter(int a, int b); // prototype
utils.c
int ajouter(int a, int b) { // définition
return a + b;
}
main.c
#include "utils.h"
int main(void) {
int resultat = ajouter(5, 3);
return 0;
}
Variables locales statiques en fonction
Une variable locale déclarée comme static persiste entre les appels de la fonction :
int compteur_appels(void) {
static int compteur = 0;
compteur++;
return compteur;
}
int main(void) {
printf("%d\n", compteur_appels()); // 1
printf("%d\n", compteur_appels()); // 2
printf("%d\n", compteur_appels()); // 3
return 0;
}