You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Copy file name to clipboardexpand all lines: content/20-datatype.rst
+6-1
Original file line number
Diff line number
Diff line change
@@ -733,10 +733,15 @@ L'utilisation d'un type énuméré peut être la suivante :
733
733
}
734
734
}
735
735
736
+
Type incomplet
737
+
==============
738
+
739
+
Un type incomplet est un qualificatif de type de donnée décrivant un objet dont sa taille en mémoire n'est pas connue.
740
+
736
741
Type vide (*void*)
737
742
==================
738
743
739
-
Le type ``void`` est particulier car c'est un type qui ne vaut rien. Il est utilisé comme type de retour pour les fonctions qui ne retournent rien :
744
+
Le type ``void`` est particulier. Il s'agit d'un type dit **incomplet** car la taille de l'objet qu'il représente en mémoire n'est pas connue. Il est utilisé comme type de retour pour les fonctions qui ne retournent rien :
Copy file name to clipboardexpand all lines: content/40-functions.rst
+140
Original file line number
Diff line number
Diff line change
@@ -279,6 +279,146 @@ En des termes plus corrects, mais nous verrons cela au chapitre sur les pointeur
279
279
280
280
Retenez simplement que lors d'un passage par référence, on cherche à rendre la valeur passée en paramètre modifiable par le *caller*.
281
281
282
+
Récursion
283
+
=========
284
+
285
+
La récursion, caractère d'un processus, d'un mécanisme récursif, c'est à dire qui peut être répété un nombre indéfini de fois par l'application de la même règle, est une méthode d'écriture dans laquelle une fonction s'appelle elle même.
286
+
287
+
Au chapitre sur les fonctions, nous avions donné l'exemple du calcul de la somme de la suite de fibonacci jusqu'à ``n`` :
288
+
289
+
.. code-block:: c
290
+
291
+
int fib(int n)
292
+
{
293
+
int sum = 0;
294
+
int t1 = 0, t2 = 1;
295
+
int next_term;
296
+
for (int i = 1; i <= n; i++)
297
+
{
298
+
sum += t1;
299
+
next_term = t1 + t2;
300
+
t1 = t2;
301
+
t2 = next_term;
302
+
}
303
+
return sum;
304
+
}
305
+
306
+
Il peut sembler plus logique de raisonner de façon récursive. Quelque soit l'itération à laquelle l'on soit, l'assertion suivante est valable :
307
+
308
+
fib(n) == fib(n - 1) + fib(n - 2)
309
+
310
+
Donc pourquoi ne pas réécrire cette fonction en employant ce caractère récursif ?
311
+
312
+
.. code-block:: c
313
+
314
+
int fib(int n)
315
+
{
316
+
if (n < 2) return 1;
317
+
return fib(n - 1) + fib(n - 2);
318
+
}
319
+
320
+
Le code est beaucoup plus simple a écrire, et même à lire. Néanmoins cet algorithme est notoirement connu pour être mauvais en terme de performance. Calculer ``fib(5)`` revient à la chaîne d'appel suivant.
321
+
322
+
Cette chaîne d'appel représente le nombre de fois que ``fib`` est appelé et à quel niveau elle est appelée. Par exemple ``fib(4)`` est appelé dans ``fib(5)`` :
323
+
324
+
.. code-block:: text
325
+
326
+
fib(5)
327
+
fib(4)
328
+
fib(3)
329
+
fib(2)
330
+
fib(1)
331
+
fib(2)
332
+
fib(1)
333
+
fib(0)
334
+
fib(3)
335
+
fib(2)
336
+
fib(1)
337
+
fib(0)
338
+
fib(1)
339
+
340
+
Si l'on somme le nombre de fois que chacune de ces fonctions est appelée :
341
+
342
+
.. code-block:: text
343
+
344
+
fib(5) 1x
345
+
fib(4) 1x
346
+
fib(3) 2x
347
+
fib(2) 3x
348
+
fib(1) 4x
349
+
fib(0) 2x
350
+
-----------
351
+
fib(x) 13x
352
+
353
+
Pour calculer la somme de fibonacci, il faut appeler 13 fois la fonction. On le verra plus tard mais la complexité algoritmique de cette fonction est dite :math:`O(2^n)`. C'est à dire que le nombre d'appels suit une relation exponentielle. La réelle complexité est donnée par la relation :
Ce terme 1.6180 est appelé `le nombre d'or <https://fr.wikipedia.org/wiki/Nombre_d%27or>`__.
360
+
361
+
Ainsi pour calculer fib(100) il faudra sept cent quatre-vingt-douze trillions soixante-dix mille huit cent trente-neuf billions huit cent quarante-huit milliards trois cent soixante-quatorze millions neuf cent douze mille deux cent quatre-vingt-douze appels à la fonction `fib` (792'070'839'848'374'912'292). Pour un processeur Core i7 (2020) capable de calculer environ 100 GFLOPS (milliards d'opérations par seconde), il lui faudra tout de même 251 ans.
362
+
363
+
En revanche, dans l'approche itérative, on constate qu'une seule boucle ``for``. C'est à dire qu'il faudra seulement 100 itérations pour calculer la somme.
364
+
365
+
Généralement les algorithmes récursifs (s'appelant eux même) sont moins performants que les algorithmes itératifs (utilisant des boucles). Néanmoins il est parfois plus facile d'écrire un algorithme récursif.
366
+
367
+
Notons que tout algorithme récursif peut être écrit en un algorithme itératif, mais ce n'est pas toujours facile.
368
+
369
+
Mémoïsation
370
+
===========
371
+
372
+
En informatique la `mémoïsation <https://fr.wikipedia.org/wiki/M%C3%A9mo%C3%AFsation>`__ est une technique d'optimisation du code souvent utilisée conjointement avec des algorithmes récursifs. Cette technique est largement utilisée en `programmation dynamique <https://fr.wikipedia.org/wiki/Programmation_dynamique>`__.
373
+
374
+
Nous l'avons vu précédemment, l'algorithme récursif du calcul de la somme de la suite de Fibonacci n'est pas efficace du fait que les mêmes appels sont répétés un nombre inutiles de fois. La parade est de mémoriser pour chaque appel de ``fib``, la sortie correspondante à l'entrée.
375
+
376
+
Dans cet exemple nous utiliserons un mécanisme composé de trois fonctions :
377
+
378
+
- ``int memoize(Cache *cache, int input, int output)``
379
+
- ``bool memoize_has(Cache *cache, int input)``
380
+
- ``int memoize_get(Cache *cache, int input)``
381
+
382
+
La première fonction mémorise la valeur de sortie ``output`` liée à la valeur d'entrée ``input``. Pour des raisons de simplicité d'utilsation, la fonction retourne la valeur de sortie ``output``.
383
+
384
+
La seconde fonction ``memoize_has`` vérifie si une valeur de correspondance existe pour l'entrée ``input``. Elle retourne ``true`` en cas de correspondance et ``false`` sinon.
385
+
386
+
La troisième fonction ``memoize_get`` retourne la valeur de sortie correspondante à la valeur d'entrée ``input``.
387
+
388
+
Notre fonction récursive sera ainsi modifiée comme suit :
389
+
390
+
.. code-block:: c
391
+
392
+
int fib(int n)
393
+
{
394
+
if (memoize_has(n)) return memoize_get(n);
395
+
if (n < 2) return 1;
396
+
return memoize(n, fib(n - 1) + fib(n - 2));
397
+
}
398
+
399
+
Quant aux trois fonctions utilitaires, voici une proposition d'implémentation. Notons que cette implémentation est très élémentaire et n'est valable que pour des entrées inférieures à 1000. Il sera possible ultérieurement de perfectionner ces fonctions, mais nous aurons pour cela besoin de concepts qui n'ont pas encore été abordés, tels que les structures de données complexes.
Copy file name to clipboardexpand all lines: content/55-memory-management.rst
+51
Original file line number
Diff line number
Diff line change
@@ -59,6 +59,7 @@ Lorsqu'un programme à besoin de mémoire, il peut générer un appel système p
59
59
60
60
L'allocation se fait sur le `tas` (*heap*) qui est de taille variable. À chaque fois qu'un espace mémoire est demandé, ``malloc`` recherche dans le segment un espace vide de taille suffisante, s'il ne parvient pas, il exécute l'appel système `sbrk <https://en.wikipedia.org/wiki/Sbrk>`__ qui permet de déplacer la frontière du segment mémoire et donc d'agrandir le segment.
@@ -265,3 +266,53 @@ allez piétiner les zones mémoires voisines sans en avoir la permission.
265
266
Le compilateur (en réalité l'éditeur de liens - le *linker*) vous permet
266
267
de spécifier la taille de la pile ; c'est une de ses nombreuses options.
267
268
269
+
Variables automatiques
270
+
======================
271
+
272
+
Une variable est dite *automatique* lorsque sa déclaration est faite au sein d'une fonction. La variable d'itération ``int i`` dans une boucle ``for`` est dite automatique. C'est à dire que le compilateur à le choix de placer cette variable :
273
+
274
+
- sur la pile ;
275
+
- dans un registre mémoire processeur.
276
+
277
+
Jadis, le mot clé ``register`` était utiliser pour forcer le compilateur à placer une variable locale dans un registre processeur pour obtenir de meilleures performances. Aujourd'hui, les compilateurs sont assez malins pour déterminer automatiquement les variables souvent utilisées.
278
+
279
+
Fragmentation mémoire
280
+
=====================
281
+
282
+
On peut observer à la figure :numref:`fig_allocation` qu'après un appel successif de ``malloc`` et de ``free`` des espaces mémoires non utilisés peuvent apparaître entre des régions utilisées. Ces *trous* sont appelés fragmentation mémoire.
283
+
284
+
Dans la figure suivante, on suit l'évolution de l'utilisation du *heap* au cours de la vie d'un programme. Au début ➀, la mémoire est libre. Tant que de la mémoire est allouée sans libération (``free``), aucun problème de fragmentation ➁. Néanmoins, après un certain temps la mémoire devient fragmentée ➂ ; il reste dans cet exemple 2 emplacements de taille 2, un emplacement de taille 5 et un emplacement de taille 8. Il est donc impossible de réserver un espace de taille 9 malgré que l'espace cumulé libre est suffisant.
Dans une petite architecture, l'allocation et la libération fréquente d'espaces mémoire de taille arbitraire est malvenue. Une fois que la fragmentation mémoire est installée, il n'existe aucun moyen de soigner le mal si ce n'est au travers de l'ultime solution de l'informatique : `éteindre puis redémarrer <https://www.youtube.com/watch?v=nn2FB1P_Mn8>`__.
289
+
290
+
MMU
291
+
---
292
+
293
+
Les systèmes d'exploitations modernes (Windows, Linux, macOS...) utilisent tous un dispositif matériel nommé `MMU <https://en.wikipedia.org/wiki/Memory_management_unit>`__ pour *Memory Management Unit*. La MMU est en charge de créer un espace mémoire **virtuel** entre l'espace physique. Cela crée une indirection supplémentaire mais permet de réorganiser la mémoire physique sans compromettre le système.
294
+
295
+
En pratique l'espace de mémoire virtuelle est toujours beaucoup plus grand que l'espace physique. Cela permet de s'affranchir dans une large mesure de problèmes de fragmentation car si l'espace virtuel est suffisament grand, il y aura statistiquement plus de chance d'y trouver un emplacement non utilisé.
296
+
297
+
La programmation sur de petites architectures matérielles (microcontrôleurs, DSP) ne possèdent pas de MMU et dès lors l'allocation dynamique est généralement à proscrire à moins qu'elle soit faite en connaissance de cause et en utilisant des mécanisme comme les *memory pool*.
298
+
299
+
Dans la figure ci-dessous. La mémoire physique est représentée à droite en termes de pages mémoires physiques (*Physical Pages* ou **PP**). Il s'agit de blocs mémoires contigus d'une taille fixe, par exemple 64 kB. Chaque page physique est mappée dans une table propre à chaque processus (programme exécutable). On y retrouve quelques proriétés utiles à savoir est-ce que la page mémoire est accessible en écriture, est-ce qu'elle peut contenir du code exécutable ? Une propriété peut indiquer par exemple si la page mémoire est valide. Chacune de ces entrées est considérée comme une page mémoire virtuelle (*virtual page* **VP**).
300
+
301
+
.. figure:: ../assets/figures/dist/memory/mmu.*
302
+
303
+
Erreurs de segmentation (*segmentation fault*)
304
+
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
305
+
306
+
Lorsqu'un programme tente d'accéder à un espace mémoire qui n'est pas mappé dans la MMU, ou que cet espace mémoire ne permet pas le type d'accès souhaité : par exemple une écriture dans une page en lecture seule. Le système d'exploitation tue le processus avec une erreur *Segmentation Fault*. C'est la raison pour laquelle, il n'est pas systématique d'avoir une erreur de segmentation en cas de jarinage mémoire. Tant que les valeurs modifées sont localisées au sein d'un bloc mémoire autorisé, il n'y aura pas d'erreur.
307
+
308
+
L'erreur de segmentation est donc générée par le système d'exploitation en lèvant le signal **SIGSEGV** (Violation d'accès à un segment mémoire, où erreur de segmentation).
309
+
310
+
Memory Pool
311
+
-----------
312
+
313
+
Un *memory pool* est une méthode faisant appel à de l'allocation dynamique de blocs de taille fixe. Lorsqu'un programme doit très régulièrement allouer et désalouer de la mémoire, il est préférable que les blocs mémoire ait une taille fixe. De cette façon, après un ``free``, la mémoire libérée est assez grande pour une allocation ultérieure.
314
+
315
+
Lorsqu'un programme est exécuté sous Windows, macOS ou Linux, l'allocation dynamique standard ``malloc``, ``calloc``, ``realloc`` et ``free`` sont performants et le risque de crash dû à une fragmentation mémoire est rare.
316
+
317
+
En revanche lors de l'utilisation sur de petites architectures (microcontrôleurs) qui n'ont pas de système sophistiqués pour gérer la mémoire, il est parfois nécessaire d'écrire son propre système de gestion de mémoire.
Copy file name to clipboardexpand all lines: content/78-translation-units.rst
+33-31
Original file line number
Diff line number
Diff line change
@@ -1,4 +1,6 @@
1
1
2
+
.. _TranslationUnits:
3
+
2
4
===================
3
5
Compilation séparée
4
6
===================
@@ -31,7 +33,7 @@ Ainsi, lorsque le programme commence à être volumineux, sa lecture, sa compré
31
33
├── complex.h
32
34
└── main.c
33
35
34
-
Le programme principal et la fonction ``main`` est contenu dans ``main.c`` quant au module *complex* il est composé de deux fichiers : ``complex.h`` l'en-tête et ``complex.c``, l'implémentation du module.
36
+
Le programme principal et la fonction ``main`` est contenu dans ``main.c`` quant au module *complex* il est composé de deux fichiers : ``complex.h`` l'en-tête et ``complex.c``, l'implémentation du module.
35
37
36
38
Le fichier ``main.c`` devra inclure le fichier ``complex.h`` afin de
37
39
pourvoir utiliser correctement les fonctions du module de gestion des
@@ -44,7 +46,7 @@ nombres complexes. Exemple :
44
46
45
47
int main() {
46
48
Complex c1 = { .real = 1., .imag = -3. };
47
-
complex_fprint(stdout, c1);
49
+
complex_fprint(stdout, c1);
48
50
}
49
51
50
52
.. code-block:: c
@@ -86,43 +88,43 @@ Cet exemple sera compilé dans un environnement POSIX de la facon suivante :
86
88
gcc -c main.c -o main.o
87
89
gcc complex.o main.o -oprogram -lm
88
90
89
-
Nous verrons plus bas les éléments théoriques vous permettant de mieux comprendre ces lignes.
91
+
Nous verrons plus bas les éléments théoriques vous permettant de mieux comprendre ces lignes.
90
92
91
93
Module logiciel
92
94
===============
93
95
94
-
Les applications modernes dépendent souvent de nombreux modules logiciels externes aussi utilisés dans d'autres projets. C'est avantageux à plus d'un titre :
96
+
Les applications modernes dépendent souvent de nombreux modules logiciels externes aussi utilisés dans d'autres projets. C'est avantageux à plus d'un titre :
95
97
96
98
- les modules externes sont sous la responsabilité d'autres développeurs et le programme a développer comporte moins de code ;
97
99
- les modules externes sont souvent bien documentés et testés et il est facile de les utiliser ;
98
100
- la lisibilité du programme est accrue car il est bien découpé en des ensembles fonctionnels ;
99
101
- les modules externes sont réutilisables et indépendants, ils peuvent donc être réutilisés sur plusieurs projets.
100
102
101
-
Lorsque vous utiliser la fonction ``printf``, vous dépendez d'un module externe nommé ``stdio``. En réalité l'ensemble des modules ``stdio``, ``stdlib``, ``stdint``, ``ctype``... sont tous groupé dans une seule bibliothèque logicielle nommée ``libc`` disponible sur tous les systèmes compatibles POSIX. Sous Linux, le pendant libre ``glibc`` est utilisée. Il s'agit de la biblothèque `GNU C Library <https://fr.wikipedia.org/wiki/GNU_C_Library>`__.
103
+
Lorsque vous utiliser la fonction ``printf``, vous dépendez d'un module externe nommé ``stdio``. En réalité l'ensemble des modules ``stdio``, ``stdlib``, ``stdint``, ``ctype``... sont tous groupé dans une seule bibliothèque logicielle nommée ``libc`` disponible sur tous les systèmes compatibles POSIX. Sous Linux, le pendant libre ``glibc`` est utilisée. Il s'agit de la biblothèque `GNU C Library <https://fr.wikipedia.org/wiki/GNU_C_Library>`__.
102
104
103
-
Un module logiciel peut se composer de fichiers sources, c'est à dire un ensemble de fichiers ``.c`` et ``.h`` ainsi qu'une documentation et un script de compilation (``Makefile``). Alternativement, un module logiciel peut se composer de bibliothèques déjà compilées sous la forme de fichiers ``.h``, ``.a`` et ``.so``. Sous Windows on rencontre fréquemment l'extension ``.dll``. Ces fichiers compilés ne donnent pas accès au code source mais permettent d'utiliser les fonctionnalités quelles offrent dans des programmes C en mettant à disposition un ensemble de fonctions documentées.
105
+
Un module logiciel peut se composer de fichiers sources, c'est à dire un ensemble de fichiers ``.c`` et ``.h`` ainsi qu'une documentation et un script de compilation (``Makefile``). Alternativement, un module logiciel peut se composer de bibliothèques déjà compilées sous la forme de fichiers ``.h``, ``.a`` et ``.so``. Sous Windows on rencontre fréquemment l'extension ``.dll``. Ces fichiers compilés ne donnent pas accès au code source mais permettent d'utiliser les fonctionnalités quelles offrent dans des programmes C en mettant à disposition un ensemble de fonctions documentées.
104
106
105
107
Compilation avec assemblage différé
106
108
===================================
107
109
108
-
Lorsque nous avions compilé notre premier exemple `Hello World <hello>`__ nous avions simplement appelé ``gcc`` avec le fichier source ``hello.c`` qui nous avait créé un exécutable ``a.out``. En réalité, GCC est passé par plusieurs sous étapes de compilation :
110
+
Lorsque nous avions compilé notre premier exemple `Hello World <hello>`__ nous avions simplement appelé ``gcc`` avec le fichier source ``hello.c`` qui nous avait créé un exécutable ``a.out``. En réalité, GCC est passé par plusieurs sous étapes de compilation :
109
111
110
112
1. **Préprocessing** : les commentaires sont retirés, les directives pré-processeur sont remplacées par leur équivalent C.
111
113
2. **Compilation** : le code C d'une seule *translation unit* est converti en langage machine en un fichier objet ``.o``.
112
114
3. **Édition des liens** : aussi nommé *link*, les différents fichiers objets sont réunis en un seul exécutable.
113
115
114
-
Lorsqu'un seul fichier est fourni à GCC, les trois opérations sont effectuées en même temps mais ce n'est plus possible aussitôt que le programme est composé de plusieurs unités de translation (plusieurs fichiers C). Il est alors nécessaire de compiler manuellement chaque fichier source et d'en créer.
116
+
Lorsqu'un seul fichier est fourni à GCC, les trois opérations sont effectuées en même temps mais ce n'est plus possible aussitôt que le programme est composé de plusieurs unités de translation (plusieurs fichiers C). Il est alors nécessaire de compiler manuellement chaque fichier source et d'en créer.
115
117
116
-
La figure suivante résume les différentes étapes de GCC. Les pointillés indiquent à quel niveau les opérations peuvent s'arrêter. Il est dès lors possible de passer par des fichiers intermédiaires assembleur (``.s``) ou objets (``.o``) en utilisant la bonne commande.
118
+
La figure suivante résume les différentes étapes de GCC. Les pointillés indiquent à quel niveau les opérations peuvent s'arrêter. Il est dès lors possible de passer par des fichiers intermédiaires assembleur (``.s``) ou objets (``.o``) en utilisant la bonne commande.
Notons que ces étapes existent quelque soit le compilateur ou le système d'exploitation. Nous retrouverons ces exactes mêmes étapes avec Microsoft Visual Studio mais le nom des commandes et les extensions des fichiers peuvent varier s'ils ne respectent pas la norme POSIX (et GNU).
122
+
Notons que ces étapes existent quelque soit le compilateur ou le système d'exploitation. Nous retrouverons ces exactes mêmes étapes avec Microsoft Visual Studio mais le nom des commandes et les extensions des fichiers peuvent varier s'ils ne respectent pas la norme POSIX (et GNU).
121
123
122
-
Notons que généralement, seul deux étapes de GCC sont utilisées :
124
+
Notons que généralement, seul deux étapes de GCC sont utilisées :
123
125
124
126
1. Compilation avec ``gcc -c <fichier.c>``, ceci génère automatiquement un fichier ``.o`` du même nom que le fichier d'entrée.
125
-
2. Édition des liens avec ``gcc <fichier1.o> <fichier2.o> ...``, ceci génère automatiquement un fichier exécutable ``a.out``.
127
+
2. Édition des liens avec ``gcc <fichier1.o> <fichier2.o> ...``, ceci génère automatiquement un fichier exécutable ``a.out``.
126
128
127
129
Fichiers d'en-tête (*header*)
128
130
=============================
@@ -133,7 +135,7 @@ Les fichiers d'en-tête (``.h``) sont des fichiers écrits en langage C mais qui
133
135
- Des déclaration de types (``typedef``, ``struct``).
134
136
- Des définitions pré-processeur (``#include``, ``#define``).
135
137
136
-
Nous l'avons vu dans le chapitre sur le pré-processeur, la directive ``#include`` ne fait qu'inclure le contenu du fichier cible à l'emplacement de la directive. Il est donc possible (mais fort déconseillé), d'avoir la situation suivante :
138
+
Nous l'avons vu dans le chapitre sur le pré-processeur, la directive ``#include`` ne fait qu'inclure le contenu du fichier cible à l'emplacement de la directive. Il est donc possible (mais fort déconseillé), d'avoir la situation suivante :
137
139
138
140
.. code-block:: c
139
141
@@ -142,32 +144,32 @@ Nous l'avons vu dans le chapitre sur le pré-processeur, la directive ``#include
142
144
#include "foobar.def"
143
145
}
144
146
145
-
Et le fichier ``foobar.def`` pourrait cotenir :
147
+
Et le fichier ``foobar.def`` pourrait cotenir :
146
148
147
149
.. code-block:: c
148
150
149
-
// foobar.def
150
-
#ifdef FOO
151
+
// foobar.def
152
+
#ifdef FOO
151
153
printf("hello foo!\n");
152
154
#else
153
155
printf("hello bar!\n");
154
156
#endif
155
157
156
-
Vous noterez que l'extension de ``foobar`` n'est pas ``.h`` puisque le contenu n'est pas un fichier d'en-tête. ``.def`` ou n'importe quelle autre extension pourrait donc faire l'affaire ici.
158
+
Vous noterez que l'extension de ``foobar`` n'est pas ``.h`` puisque le contenu n'est pas un fichier d'en-tête. ``.def`` ou n'importe quelle autre extension pourrait donc faire l'affaire ici.
157
159
158
-
Dans cet exemple, le pré-processeur ne fait qu'inclure le contenu du fichier ``foobar.def`` à l'emplacement de la définition ``#include "foobar.def"``. Voyons le en détail :
160
+
Dans cet exemple, le pré-processeur ne fait qu'inclure le contenu du fichier ``foobar.def`` à l'emplacement de la définition ``#include "foobar.def"``. Voyons le en détail :
159
161
160
-
.. code-block:: c
162
+
.. code-block:: console
161
163
162
164
$ cat << EOF > main.c
163
165
→ int main() {
164
166
→ #include "foobar.def"
165
-
→ #include "foobar.def"
167
+
→ #include "foobar.def"
166
168
→ }
167
169
→ EOF
168
170
169
171
$ cat << EOF > foobar.def
170
-
→ #ifdef FOO
172
+
→ #ifdef FOO
171
173
→ printf("hello foo!\n");
172
174
→ #else
173
175
→ printf("hello bar!\n");
@@ -177,12 +179,12 @@ Dans cet exemple, le pré-processeur ne fait qu'inclure le contenu du fichier ``
177
179
$ gcc -E main.c | sed '/^#/ d'
178
180
int main() {
179
181
printf("hello bar\n");
180
-
printf("hello bar\n");
182
+
printf("hello bar\n");
181
183
}
182
184
183
185
Lorsque l'on observe le résultat du pré-processeur, on s'aperçois que toutes les directives préprocesseur ont disparues et que la directive ``#include`` a été remplacée par de contenu de ``foobar.def``. Remarquons que le fichier est inclus deux fois, nous verrons plus loin comme éviter cela.
184
186
185
-
Nous avons vu au chapitre sur les `prototypes de fonctions <function_prototype>`__ qu'il est possible de ne déclarer que la première ligne d'une fonction. Ce prototype permet au compilateur de savoir combien d'arguments est composé une fonction sans nécessairement disposer de l'implémentation de cette fonction. Aussi on trouve dans tous les fichiers d'en-tête des déclaration en amont (*forward declaration*). Dans le fichier d'en-tête ``stdio.h`` on trouvera la ligne : ``int printf( const char *restrict format, ... );``.
187
+
Nous avons vu au chapitre sur les `prototypes de fonctions <function_prototype>`__ qu'il est possible de ne déclarer que la première ligne d'une fonction. Ce prototype permet au compilateur de savoir combien d'arguments est composé une fonction sans nécessairement disposer de l'implémentation de cette fonction. Aussi on trouve dans tous les fichiers d'en-tête des déclaration en amont (*forward declaration*). Dans le fichier d'en-tête ``stdio.h`` on trouvera la ligne : ``int printf( const char *restrict format, ... );``.
186
188
187
189
.. code-block::c
188
190
@@ -201,7 +203,7 @@ Un fichier d'en-tête contiendra donc tout le nécessaire utile à pouvoir utili
201
203
Protection de réentrance
202
204
------------------------
203
205
204
-
La protection de réentrence aussi nommée *header guards* est une solution au problème d'inclusion multiple. Si par exemple on défini dans un fichier d'en-tête un nouveau type et que l'on inclus ce fichier, mais que ce dernier est déjà inclu par une autre bibliothèque une erreur de compilation apparaîtera :
206
+
La protection de réentrence aussi nommée *header guards* est une solution au problème d'inclusion multiple. Si par exemple on défini dans un fichier d'en-tête un nouveau type et que l'on inclus ce fichier, mais que ce dernier est déjà inclu par une autre bibliothèque une erreur de compilation apparaîtera :
205
207
206
208
.. code-block:: console
207
209
@@ -242,7 +244,7 @@ La protection de réentrence aussi nommée *header guards* est une solution au p
242
244
^~~
243
245
...
244
246
245
-
Dans cet exemple l'utilisateur ne sait pas forcément que ``bar.h`` est déjà inclus avec ``foo.h`` et le résultat après pré-processing est le suivant :
247
+
Dans cet exemple l'utilisateur ne sait pas forcément que ``bar.h`` est déjà inclus avec ``foo.h`` et le résultat après pré-processing est le suivant :
246
248
247
249
.. code-block:: console
248
250
@@ -260,11 +262,11 @@ Dans cet exemple l'utilisateur ne sait pas forcément que ``bar.h`` est déjà i
260
262
foo(bar);
261
263
}
262
264
263
-
On y retrouve la définition de ``Bar`` deux fois et donc, le compilateur génère une erreur.
265
+
On y retrouve la définition de ``Bar`` deux fois et donc, le compilateur génère une erreur.
264
266
265
-
Une solution à ce problème est d'ajouter des gardes d'inclusion multiple par exemple avec ceci:
267
+
Une solution à ce problème est d'ajouter des gardes d'inclusion multiple par exemple avec ceci:
266
268
267
-
.. code-block:: c
269
+
.. code-block:: c
268
270
269
271
#ifndef BAR_H
270
272
#define BAR_H
@@ -275,16 +277,16 @@ Une solution à ce problème est d'ajouter des gardes d'inclusion multiple par e
275
277
276
278
#endif // BAR_H
277
279
278
-
Si aucune définition du type ``#define BAR_H`` n'existe, alors le fichier ``bar.h`` n'a jamais été inclus auparavant et le contenu de la directive ``#ifndef BAR_H`` dans lequel on commence par définir ``BAR_H`` est exécuté. Lors d'une future inclusion de ``bar.h``, la valeur de ``BAR_H`` aura déjà été définie et le contenu de la directive ``#ifndef BAR_H`` ne sera jamais exécuté.
280
+
Si aucune définition du type ``#define BAR_H`` n'existe, alors le fichier ``bar.h`` n'a jamais été inclus auparavant et le contenu de la directive ``#ifndef BAR_H`` dans lequel on commence par définir ``BAR_H`` est exécuté. Lors d'une future inclusion de ``bar.h``, la valeur de ``BAR_H`` aura déjà été définie et le contenu de la directive ``#ifndef BAR_H`` ne sera jamais exécuté.
279
281
280
282
Alternativement, il existe une solution **non standard** mais supportée par la plupart des compilateurs. Elle fait intervenir un pragma :
281
283
282
-
.. code-block:: c
284
+
.. code-block:: c
283
285
284
286
#pragma once
285
287
286
288
typedef struct Bar {
287
289
int b, a, r;
288
290
} Bar;
289
291
290
-
Cette solution est équivalente à la méthode traditionnelle et présente plusieurs avantages. C'est tout d'abord une solution atomique qui ne nécessite pas un ``#endif`` à la fin du fichier. Il n'y a ensuite pas de conflit avec la règle SSOT car le nom du fichier ``bar.h`` n'apparaît pas dans le fichier ``BAR_H``.
292
+
Cette solution est équivalente à la méthode traditionnelle et présente plusieurs avantages. C'est tout d'abord une solution atomique qui ne nécessite pas un ``#endif`` à la fin du fichier. Il n'y a ensuite pas de conflit avec la règle SSOT car le nom du fichier ``bar.h`` n'apparaît pas dans le fichier ``BAR_H``.
Copy file name to clipboardexpand all lines: content/82-testing.rst
+2-2
Original file line number
Diff line number
Diff line change
@@ -61,7 +61,7 @@ Considérons le programme suivant :
61
61
62
62
A priori, c'est un programme tout à fait correct. Si l'utilisateur entre le bon mot de passe, il se voit octroyé des privilèges administrateurs. Testons ce programme :
63
63
64
-
.. code-block:: c
64
+
.. code-block:: console
65
65
66
66
$ gcc u.c -fno-stack-protector
67
67
$ ./a.out
@@ -71,7 +71,7 @@ A priori, c'est un programme tout à fait correct. Si l'utilisateur entre le bon
71
71
72
72
Très bien, maintenant testons avec un mauvais mot de passe :
Copy file name to clipboardexpand all lines: content/90-advanced-topics.rst
+175-1
Original file line number
Diff line number
Diff line change
@@ -228,4 +228,178 @@ On voit immédiatement que la partie entière vaut 2, donc 90% de 3.14 donnera u
228
228
inline int16_t q12_mul(int16_t a, int16_t b)
229
229
{
230
230
return q_mul(a, b, 12);
231
-
}
231
+
}
232
+
233
+
Mémoire partagée
234
+
================
235
+
236
+
Nous le verrons plus loin au chapitre sur la MMU, mais la mémoire d'un processus mémoire (programme) ne peut pas être accedée par un autre programme. Le système d'exploitation l'en empêche.
237
+
238
+
Lorsque l'on souhaite communiquer entre plusieurs programmes, il est possible d'utiliser différentes méthodes :
239
+
240
+
- les flux (fichiers, stdin, stdout...)
241
+
- la mémoire partagée
242
+
- les sockets
243
+
244
+
Vous avez déjà vu les flux au chapitre précédant, et les sockets ne font pas partie de ce cours d'introduction.
245
+
246
+
Notons que la mémoire partagée est un mécanisme propre à chaque système d'exploitation. Sous POSIX elle est normalisée et donc un programme compatible POSIX et utilisant la mémoire partagée pourra fonctionner sous Linux, WSL ou macOS, mais pas sous Windows.
247
+
248
+
C'est principalement l'appel système ``mmap`` qui est utilisé. Il permet de mapper ou démapper des fichiers ou des périphériques dans la mémoire.
249
+
250
+
.. code-block:: c
251
+
252
+
void *mmap(
253
+
void *addr,
254
+
size_t length, // Taille en byte de l'espace mémoire
255
+
int prot, // Protection d'accès (lecture, écriture, exécution)
256
+
int flags, // Attributs (partagé, privé, anonyme...)
257
+
int fd,
258
+
off_t offset
259
+
);
260
+
261
+
262
+
Voici un exemple permettant de réserver un espace partagé en écriture et en lecture entre deux processus :
263
+
264
+
.. code-block:: c
265
+
266
+
#include <stdio.h>
267
+
#include <stdlib.h>
268
+
#include <sys/mman.h>
269
+
270
+
void* create_shared_memory(size_t size) {
271
+
// Accessible en lecture et écriture
272
+
int protection = PROT_READ | PROT_WRITE;
273
+
274
+
// D'autres processus peuvent accéder à cet espace
275
+
// lequel est anonyme
276
+
// so only this process and its children will be able to use it:
277
+
int visibility = MAP_SHARED | MAP_ANONYMOUS;
278
+
279
+
// The remaining parameters to `mmap()` are not important for this use case,
280
+
// but the manpage for `mmap` explains their purpose.
Traditionnellement lorsque l'on souhaite travailler sur un fichier, il convient de l'ouvrir avec ``fopen`` et de lire son contenu. Lorsque cela est nécessaire, ce fichier est copié en mémoire :
288
+
289
+
.. code-block:: c
290
+
291
+
FILE *fp = fopen("foo", "r");
292
+
fseek(fp, 0, SEEK_END);
293
+
int filesize = ftell(fp);
294
+
fseek(fp, 0, SEEK_SET);
295
+
char *file = malloc(filesize);
296
+
fread(file, filesize, sizeof(char), fp);
297
+
fclose(fp);
298
+
299
+
Cette copie n'est pas nécessairement nécessaire. Une approche **POSIX**, qui n'est donc pas couverte par le standard **C99** consiste à lier le fichier dans un espace mémoire partagé.
300
+
301
+
Ceci nécessite l'utilisation de fonctions bas niveau.
printf("Premiers caractères du fichiers : %.*s...\n", 20, addr);
314
+
}
315
+
316
+
Les avantages de cette méthode sont :
317
+
318
+
- pas nécessaire de copier l'intégralité du fichier en mémoire ;
319
+
- possibilité de partager le même fichier ouvert entre plusieurs processus ;
320
+
- possibilité laissée au système d'exploitation d'utiliser la RAM ou non si les ressources mémoires deviennent tendues.
321
+
322
+
Collecteur de déchets (*garbage collector*)
323
+
===========================================
324
+
325
+
Le C est un langage primitif qui ne gère pas automatiquement la libération des ressources allouées dynamiquement. L'exemple suivant est évocateur :
326
+
327
+
.. code-block:: c
328
+
329
+
int* get_number() {
330
+
int *num = malloc(sizeof(int));
331
+
*num = rand();
332
+
}
333
+
334
+
int main() {
335
+
for (int i = 0; i < 100; i++) {
336
+
printf("%d\n", *get_number());
337
+
}
338
+
}
339
+
340
+
La fonction ``get_number`` alloue dynamiquement un espace de la taille d'un entier et lui assigne une valeur aléatoire. Dans le programme principal, l'adresse retournée est déréférencée pour être affichée sur la sortie standard.
341
+
342
+
A la fin de l'exécution de la boucle for, une centaine d'espaces mémoire sont maintenant dans les `limbes <https://fr.wikipedia.org/wiki/Limbes>`__. Comme le pointeur retourné n'a jamais été mémorisé, il n'est plus possible de libérer cet espace mémoire avec ``free``.
343
+
344
+
On dit que le programme à une `fuite mémoire <https://fr.wikipedia.org/wiki/Fuite_de_m%C3%A9moire>`__. En admettant que ce programme reste résidant en mémoire, il peut arriver un moment où le programme peut aller jusqu'à utiliser toute la RAM disponible. Dans ce cas, il est probable que ``malloc`` retourne ``NULL`` et qu'une erreur de segmentaiton apparaisse lors du ``printf``.
345
+
346
+
Allons plus loin dans notre exemple et considérons le code suivant :
La fonction ``get_number`` alloue dynamiquement un espace mémoire et assigne un nombre aléatoire. Les fonctions ``foo`` et ``bar`` reçoivent en paramètre un pointeur sur un entier. Chacune à le choix de mémoriser ce pointeur et de clamer sur ``stdout`` qu'elle aime un des nombre mémorisés.
396
+
397
+
Au niveau du ``#if 0`` dans la fonction ``main``, il est impossible de savoir si l'adresse pointée par ``num`` est encore utilisée ou non. Il se peut que ``foo`` et ``bar`` utilisent cet espace mémoire, comme il se peut qu'aucun des deux ne l'utilise.
398
+
399
+
Comment peut-on savoir si il est possible de libérer ou non ``num`` ?
400
+
401
+
Une solution courament utilsée en C++ s'appelle un *smart pointer*. Il s'agit d'un pointeur qui contient en plus de l'adresse de la valeur, le nombre de références utilisées. De cette manière il est possible en tout temps de savoir si le pointeur est référencé quelque part. Dans le cas ou le nombre de référence tombe à zéro, il est possible de libérer la ressource.
402
+
403
+
Dans un certain nombre de langage de programmation comme Python ou Java, il existe un mécanisme automatique nommé *Garbage Collector* et qui, périodiquement, fait un tour de toutes les allocations dynamique pour savoir si elle sont encore référencées ou non. Le cas échéant, le *gc* décide libérer la ressource mémoire. De cette manière il n'est plus nécessaire de faire la chasse aux ressources allouées.
404
+
405
+
En revanche en C, il n'existe aucun mécanisme aussi sophistiqués alors prenez garde à bien libérer les ressources utilisée et à éviter d'écrire des fonctions qui allouent du contenu mémoire dynamiquement.
0 commit comments