summaryrefslogtreecommitdiff
path: root/articles/bizarreries-du-langage-c.md
diff options
context:
space:
mode:
Diffstat (limited to 'articles/bizarreries-du-langage-c.md')
-rw-r--r--articles/bizarreries-du-langage-c.md61
1 files changed, 32 insertions, 29 deletions
diff --git a/articles/bizarreries-du-langage-c.md b/articles/bizarreries-du-langage-c.md
index 45dc394..890eccd 100644
--- a/articles/bizarreries-du-langage-c.md
+++ b/articles/bizarreries-du-langage-c.md
@@ -1,14 +1,17 @@
---
pubDate = 2018-11-18
-tags = ['C', 'bizarrerie', 'syntaxe', 'obfusquation', 'programmation']
+tags = ['langage', 'obfusquation', 'programmation']
[author]
name = "ache"
email = "ache@ache.one"
----
+[[alt_lang]]
+lang = "en"
+url = "/articles/c-language-quirks"
+---
Les bizarreries du langage C
@@ -18,7 +21,7 @@ Les bizarreries du langage C
Le C est un langage à la syntaxe simple. Les seules complexités de ce langage viennent du fait qu'il agit de manière proche de la machine.
Pourtant, une partie des syntaxes autorisées par le C n'est pratiquement jamais enseignée. Attaquons-nous à ces cas mystérieux ! 🧞
-:::attention
+:::note
Pour comprendre ce billet, il est nécessaire d'avoir des bases dans un langage ayant une syntaxe et un fonctionnement proche du C.
:::
@@ -30,14 +33,14 @@ Sommaire
Les opérateurs inusités
----------------------
-Il existe deux opérateurs dans le langage C qui ne sont pratiquement jamais utilisés. Le premier est l'opérateur virgule.
-En C la virgule sert à séparer factoriser les éléments d'une définition ou séparer les éléments d'une fonction. En bref, c'est un élément de ponctuation.
+Il existe deux opérateurs dans le langage C qui ne sont presque jamais utilisés. Le premier est l'opérateur virgule.
+En C la virgule sert à séparer ou factoriser les éléments d'une définition ou séparer les éléments d'une fonction. En bref, c'est un élément de ponctuation.
Mais pas seulement ! C'est également un opérateur.
### L'opérateur virgule
-L'instruction suivante, bien qu'inutile est tout à fait valide :
+L'instruction suivante, bien qu'inutile, est tout à fait valide :
~~~c
printf("%d", (5,3) );
@@ -63,7 +66,7 @@ if( argc > 2 && argv[2][0] == '0' )
Ici, on assigne `action` et `color`. Normalement pour faire 2 assignations, on aurait dû mettre des accolades au `if`.
-On peut également s'en servir pour retirer des parenthèses.
+On peut aussi s'en servir pour retirer des parenthèses.
~~~c
while( c = getchar(), c != EOF && c != '\n' ) {
@@ -75,8 +78,8 @@ while( (c = getchar()) != EOF && c != '\n' ) {
}
~~~
-Mais surtout, ne pas abuser de cet opérateur ! On peut, de manière assez rapide, obtenir des choses illisibles.
-Cette remarque est d'ailleurs également valide pour le prochain opérateur !
+Surtout, n'abusez pas de cet opérateur ! On peut, de manière assez rapide, obtenir des choses illisibles.
+Cette remarque est d'ailleurs valide pour le prochain opérateur.
### L'opérateur ternaire
@@ -101,14 +104,14 @@ if( b < a)
printf("%d", min);
~~~
-Alors qu'avec les ternaires on fait simplement :
+Alors qu'avec les ternaires, on fait simplement :
~~~c
printf("%d", a<b ? a : b);
~~~
Grâce au ternaire, on a économisé une répétition ainsi que quelques lignes.
-Mais surtout on a gagné en lisibilité. Quand on sait en lire une...
+On a gagné en lisibilité... Quand on sait en lire une...
Pour lire une expression ternaire, il faut la découper en 3 parties :
@@ -137,7 +140,7 @@ Le plus lisible est d'utiliser une macro :
printf("%d", MIN(a,MIN(b,c)));
~~~
-Et voilà ! Deux opérateurs qui vont désormais gagner un peu d'intérêt.
+Et voilà ! Deux opérateurs qui vont gagner un peu d'intérêt.
Et être mieux compris !
Puis d'ailleurs, même les opérateurs que l'on connait déjà, on n'en maîtrise pas forcément la syntaxe.
@@ -153,7 +156,7 @@ printf("%d", tab[2]);
2 et non 3, car un tableau en C commence à 0. Si un tableau commence à 0, c'est une histoire d'adresse[^adresse]. L'adresse du tableau est en fait l'adresse du premier élément du tableau. Et par arithmétique des pointeurs, l'adresse du 3ème élément est `tab+2`.
-Donc on aurait très bien pu écrire :
+Donc, on aurait très bien pu écrire :
~~~c
printf("%d", *(tab+2));
@@ -274,7 +277,7 @@ message to_send = { .dst="23:12:23", .msg="Code 10"};
~~~
-Avec ces syntaxes on peut également alourdir le code, mais généralement, on gagne en lisibilité.
+Avec ces syntaxes, on peut légèrement alourdir le code, mais généralement, on gagne en lisibilité.
Parfois, ces syntaxes sont très judicieusement utilisées !
Comme ici, dans ce décodeur de base64 :
>~~~c
@@ -298,7 +301,7 @@ Source: [Taurre](https://openclassrooms.com/forum/sujet/defis-8-tout-en-base64-1
[^tableau]:
- `{0}` peut également être utilisé pour un nombre. Toute valeur initialisant une variable simple (pointeur, nombre, ...) peut optionnellement prendre des accolades.
+ `{0}` peut également être utilisé pour un nombre. Toute valeur initialisant une variable simple (pointeur, nombre...) peut optionnellement prendre des accolades.
~~~c
int a = {11};
@@ -319,10 +322,10 @@ int tab[5] = {5, 4, 5, 2, 1};
printf("%d", tab[i]); // Avec i égale à quelque chose >=0 et <5
~~~
-Cependant, je ne l'utilise qu'une seule fois ce tableau ...
+Cependant, je ne l'utilise qu'une seule fois ce tableau...
C'est un peu dérangeant d'avoir à utiliser un identificateur juste pour ça.
-*Eh* bien je peux faire ceci :
+*Eh* bien, je peux faire ceci :
~~~c
printf("%d", ((int[]){5,4,5,2,1}) [i] ); // Avec i égale à quelque chose >=0 et <5
~~~
@@ -358,7 +361,7 @@ for(int i = 0 ; i < n ; i++)
~~~
Ce code, bien que valide, a dû être réprimandé par de nombreux professeurs. En effet, on nous apprend qu'un tableau doit avoir une taille connue à la compilation.
-*Eh* bien, les VLAs constituent l'exception. Apparu avec la norme C99, les VLAs jouissent d'une mauvaise réputation. À cela, il existe plusieurs raisons que je ne détaillerais pas ici[^raisons]. Je vais simplement parler des comportements non-intuitifs introduits avec les VLAs. Mais tout d'abord, voyons ce qu'est et comment se servir d'un tableau à taille variable.
+*Eh* bien, les VLAs constituent l'exception. Apparu avec la norme C99, les VLAs jouissent d'une mauvaise réputation. À cela, il existe plusieurs raisons que je ne détaillerai pas ici[^raisons]. Je vais simplement parler des comportements non-intuitifs introduits avec les VLAs. Mais tout d'abord, voyons ce qu'est et comment se servir d'un tableau à taille variable.
Les VLAs se définissent avec la même syntaxe qu'un tableau classique. La seule différence est que la taille du tableau est une expression entière non constante.
@@ -388,7 +391,7 @@ void bar(int n, int tab[n]) {
}
~~~
-Cependant, la taille de la première dimension d'un tableau n'a pas beaucoup d'importance, un tableau étant implicitement convertit en un pointeur vers son premier élément. En revanche, pour un tableau à deux dimensions, la taille de la deuxième dimension *doit* être spécifiée :
+Cependant, la taille de la première dimension d'un tableau n'a pas beaucoup d'importance, un tableau étant implicitement converti en un pointeur vers son premier élément. En revanche, pour un tableau à deux dimensions, la taille de la deuxième dimension *doit* être spécifiée :
~~~c
void foo( int n, int m, int tab[][m]) {
@@ -402,7 +405,7 @@ void foo( int n, int m, int tab[][m]) {
void foo(int, int, int[][*]);
~~~
-Bien, après cette courte introduction aux VLAs, passons aux cas qui nous intéressent. Pour être précis, les bizarreries et excentricités que les VLAs ont introduits.
+Bien, après cette courte introduction aux VLAs, passons aux cas qui nous intéressent. Pour être précis, les bizarreries et excentricités que les VLAs ont introduites.
[^raisons]: Je peux néanmoins vous donnez deux références [“Is it safe to use variable-length arrays?”](https://stackoverflow.com/questions/7326547/is-it-safe-to-use-variable-length-arrays) de stack overflow et cet article : [“The Linux Kernel Is Now VLA-Free”](https://www.phoronix.com/scan.php?page=news_item&px=Linux-Kills-The-VLA).
@@ -427,7 +430,7 @@ printf("%zu", sizeof b++); // Affiche 8
Le premier résultat n'est pas très surprenant, la taille d'un `char` est défini à 1 *byte* et `sizeof(char)` doit retourner 1. Le deuxième résultat est la taille d'un `int`[^machine]. Le troisième résultat est la taille d'un `float`[^machine]. Le quatrième est la taille d'un `double`[^machine] qui est le type de l'expression `a*2.`[^float]. Le dernier résultat est la taille d'un `size_t`[^machine], `size_t` étant le type de l'expression `b++`.
-Ici, je ne me suis pas intéressé à la valeur des expressions et pour cause, `sizeof` ne s'y intéresse pas non plus. Ces valeurs sont déterminées à la compilation. Les opérations que l'on retrouve dans l'expression passé à `sizeof` ne sont pas effectuées. Puisque l'expression doit être valide, son type doit être déterminé à la compilation. Le résultat de `sizeof` étant alors connu à la compilation, il n'y avait aucune raison d'exécuter l'expression.
+Ici, je ne me suis pas intéressé à la valeur des expressions et pour cause, `sizeof` ne s'y intéresse pas non plus. Ces valeurs sont déterminées à la compilation. Les opérations que l'on retrouve dans l'expression passée à `sizeof` ne sont pas effectuées. Puisque l'expression doit être valide, son type doit être déterminé à la compilation. Le résultat de `sizeof` étant alors connu à la compilation, il n'y avait aucune raison d'exécuter l'expression.
~~~c
int n = 5;
@@ -452,14 +455,14 @@ En supposant que les affichages ne causent pas d'erreur, appeler cette fonction
Il est à noter qu'il existe d'autres exceptions induites pas la standardisation des VLAs comme l'impossibilité d'allouer les VLAs de manière statique (assez logique), ou l'impossibilité d'utiliser des VLAs dans une structure (GNU GCC le support tout de même). Et même certains branchements conditionnels sont interdits lorsque l'on utilise un VLA.
[^machine]: Sur ma machine
-[^float]: Ici, l'implémentation des nombres flottants suit la norme IEE 754, où la taille d'un nombre flottant simple est de 4 octets et double précision est de 8 octets. `2.` est de type `double`, l'expression `2.*a` est donc du même type que le plus grand des deux types de ses deux opérandes, ici `double`.
+[^float]: Ici, l'implémentation des nombres flottants suit la norme IEEE 754, où la taille d'un nombre flottant simple est de 4 octets et double précision est de 8 octets. `2.` est de type `double`, l'expression `2.*a` est donc du même type que le plus grand des deux types de ses deux opérandes, ici `double`.
Un tableau flexible
-------------------
-Vous n'avez peut-être jamais entendu parler des « *flexible arrays member* ». C'est normal, ceux-ci répondent à une problématique très précise et peu courante.
+Vous n'avez peut-être jamais entendu parler des « *flexible array members* ». C'est normal, ceux-ci répondent à une problématique très précise et peu courante.
-L'objectif est d'allouer une structure mais dont un champ (un tableau) est de taille inconnue à la compilation et le tout sur un espace contiguë[^pourquoi].
+L'objectif est d'allouer une structure, mais dont un champ (un tableau) est de taille inconnue à la compilation et le tout sur un espace contiguë[^pourquoi].
Ici, pas de VLAs, car comme on l'a vu, ceux-ci sont interdits en tant que champs de structure. L'allocation dynamique s'impose.
On pourrait vouloir faire :
@@ -503,8 +506,8 @@ Ici, le champ `flexiTab` est un tableau membre flexible. Un tel tableau doit êt
Cette syntaxe répond autant à un besoin de portabilité sur les architectures imposant un alignement particulier (le tableau est contigu à la structure) qu'au besoin de faire apparaître un lien sémantique entre le tableau et la structure (le tableau appartient à la structure).
-[^pourquoi]: On peut vouloir que cet espace soit contiguë pour plusieurs raisons. Une de ces raisons est pour optimiser l'utilisation du cache processeur. Également, la gestion des couches réseaux se portent assez bien à l'utilisation de tableaux flexibles.
-[^zero]: Avant la spécification des « flexible array member » en C99, il était courant d'utiliser des tableaux de taille un pour reproduire le concept.
+[^pourquoi]: On peut vouloir que cet espace soit contiguë pour plusieurs raisons. Une de ces raisons est pour optimiser l'utilisation du cache processeur. Également, la gestion des couches réseaux se porte assez bien à l'utilisation de tableaux flexibles.
+[^zero]: Avant la spécification des « flexible array members » en C99, il était courant d'utiliser des tableaux de taille un pour reproduire le concept.
Une histoire d'étiquettes
@@ -530,7 +533,7 @@ end: return 0;
En gros, une étiquette est un nom que l'on donne à une instruction.
-*Eh* bien on en utilise des étiquettes ! Dans les `switch` !
+*Eh* bien, on en utilise des étiquettes ! Dans les `switch` !
~~~c
@@ -615,7 +618,7 @@ Mais comme je voulais vous parler de syntaxe, il était nécessaire de parler de
Les nombres complexes
--------------------
-Encore une fois nous allons voir une syntaxe introduite en C99. Plus exactement, ce sont 3 types qui ont été introduits, ceux-ci correspondent aux nombres complexes. Le type d'un nombre complexe est `double _Complex` (les deux autres types suivent le même schéma, afin de ne pas me répéter, je vais me concentrer sur le type `double`).
+Encore une fois, nous allons voir une syntaxe introduite en C99. Plus exactement, ce sont 3 types qui ont été introduits, ceux-ci correspondent aux nombres complexes. Le type d'un nombre complexe est `double _Complex` (les deux autres types suivent le même schéma, afin de ne pas me répéter, je vais me concentrer sur le type `double`).
Ainsi en C, il est possible de déclarer un nombre complexe comme ceci :
@@ -625,7 +628,7 @@ double complex point = 2 + 3 * I;
Ici, on retrouve les macros spéciales `complex` et `I` (définie dans l'en-tête `<complex.h>`). Le premier sert à créer un type complexe alors que le second sert à définir la partie imaginaire d'un nombre complexe.
-En mémoire une variable complexe occupe autant d'espace que 2 fois le type réel sur lequel elle est basée. On se sert d'une variable complexe comme d'une variable normale. L'arithmétique y est intuitive puisque basées sur celle des réels. Il est à noter qu'il est recommandé d'utiliser le macro `CMPLX` pour initialiser un nombre complexe :
+En mémoire une variable complexe occupe autant d'espace que 2 fois le type réel sur lequel elle est basée. On se sert d'une variable complexe comme d'une variable normale. L'arithmétique y est intuitive puisque basées sur celle des réels. Il est à noter qu'il est recommandé d'utiliser la macro `CMPLX` pour initialiser un nombre complexe :
~~~c
double complex cplx = CMPLX(2, 3);