diff options
Diffstat (limited to 'articles')
-rw-r--r-- | articles/bizarreries-du-langage-c.md (renamed from articles/bizarrerie-du-langage-c.md) | 151 |
1 files changed, 71 insertions, 80 deletions
diff --git a/articles/bizarrerie-du-langage-c.md b/articles/bizarreries-du-langage-c.md index e90829f..7ad6aae 100644 --- a/articles/bizarrerie-du-langage-c.md +++ b/articles/bizarreries-du-langage-c.md @@ -5,8 +5,9 @@ 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 -> Pour comprendre ce billet, il est nécessaire d'avoir des bases dans un langage ayant une syntaxe et un fonctionnement proche du C. +:::attention +Pour comprendre ce billet, il est nécessaire d'avoir des bases dans un langage ayant une syntaxe et un fonctionnement proche du C. +::: Sommaire @@ -23,7 +24,7 @@ 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) ); @@ -33,14 +34,14 @@ Elle affiche 3. L'opérateur `,` sert à juxtaposer des expressions. La valeur de l'expression complète est égale à la valeur de la dernière expression. Cet opérateur se révèle très utile dans une boucle `for` pour multiplier les itérations. -Par exemple pour incrémenter `i` et décrémenter `j` dans la même itération d'une boucle `for` on peut faire : +Par exemple pour incrémenter `i` et décrémenter `j` dans la même itération d'une boucle `for` on peut faire : ~~~c for( ; i < j ; i++, j-- ) { // [...] } ~~~ -Ou encore, dans de petits `if` pour les simplifier : +Ou encore, dans de petits `if` pour les simplifier : ~~~c if( argc > 2 && argv[2][0] == '0' ) @@ -55,7 +56,7 @@ On peut également s'en servir pour retirer des parenthèses. while( c = getchar(), c != EOF && c != '\n' ) { // [...] } -// Est strictement équivalent à : +// Est strictement équivalent à : while( (c = getchar()) != EOF && c != '\n' ) { // [...] } @@ -69,7 +70,7 @@ Cette remarque est d'ailleurs également valide pour le prochain opérateur ! Le ternaire pour les intimes. Le seul opérateur du langage C qui prenne 3 opérandes. Il sert à simplifier des expressions conditionnelles. -Par exemple pour afficher le minimum de deux nombres, sans le ternaire, on ferait : +Par exemple pour afficher le minimum de deux nombres, sans le ternaire, on ferait : ~~~c if (a < b) @@ -78,7 +79,7 @@ else printf("%d", b); ~~~ -Ou avec une variable temporaire : +Ou avec une variable temporaire : ~~~c int min = a; @@ -87,38 +88,38 @@ 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); +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... -Pour lire une expression ternaire, il faut la découper en 3 parties : +Pour lire une expression ternaire, il faut la découper en 3 parties : ~~~c -expression_1 ? expression_2 : expression_3 +expression_1 ? expression_2 : expression_3 ~~~ La valeur d'une expression ternaire est `expression_2` si la valeur de `expression_1` est évaluée à vrai et `expression_3` sinon. En somme, cela simplifie un peu la lecture pour de courtes expressions. -L'expression `a<b ? a : b` se lit « Si `a` est inférieur à `b` alors `a` sinon `b` ». +L'expression `a<b ? a : b` se lit « Si `a` est inférieur à `b` alors `a` sinon `b` ». J'insiste encore sur le fait que cet opérateur, s'il est mal utilisé, peut nuire à la lisibilité du code. -Notez que l'on peut très bien utiliser une expression ternaire comme opérande d'une autre expression ternaire : +Notez que l'on peut très bien utiliser une expression ternaire comme opérande d'une autre expression ternaire : ~~~c -printf("%d", a<b ? a<c ? a : b<c ? b : c : b < c ? b : c); +printf("%d", a<b ? a<c ? a : b<c ? b : c : b < c ? b : c); ~~~ Désormais, on prend le minimum de trois nombres. C'est aéré, mais impossible à suivre. -Le plus lisible est d'utiliser une macro : +Le plus lisible est d'utiliser une macro : ~~~c -#define MIN(a,b) ((a) < (b) ? (a) : (b)) +#define MIN(a,b) ((a) < (b) ? (a) : (b)) printf("%d", MIN(a,MIN(b,c))); ~~~ @@ -130,7 +131,7 @@ Puis d'ailleurs, même les opérateurs que l'on connait déjà, on n'en maîtris L'accès à un tableau ------------------ -On nous a toujours appris que pour afficher le 3ème élément d'un tableau, on faisait : +On nous a toujours appris que pour afficher le 3ème élément d'un tableau, on faisait : ~~~c int tab[5] = {0, 1, 2, 3, 4, 5}; @@ -139,25 +140,25 @@ 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)); ~~~ Puis comme l'addition est commutative, `tab+2` ou `2+tab` sont équivalents. -En fait, on aurait même pu faire : +En fait, on aurait même pu faire : ~~~c printf("%d", 2[tab]); ~~~ -C'est tout à fait valide. Et pour cause : la syntaxe `E[F]` est strictement équivalente à `*((E)+(F))`. +C'est tout à fait valide. Et pour cause : la syntaxe `E[F]` est strictement équivalente à `*((E)+(F))`. Du coup, le titre de cette section est un peu trompeur. Cet opérateur n'a pas grand-chose à voir avec les tableaux en fait. C'est du sucre syntaxique pour cacher l'arithmétique des pointeurs.[^sucre] Par exemple pour afficher le caractère `=` pour vrai, `!` pour faux et `~` pour aucun des deux. -On pourrait faire : +On pourrait faire : ~~~c if( is_good == 1 ) printf("%c", '='); @@ -167,11 +168,11 @@ else printf("%c", '~'); ~~~ -Mais il existe plus simple : +Mais il existe plus simple : ~~~c printf("%c", "!=~"[is_good]); -// Ou comme on l'a vu : +// Ou comme on l'a vu : printf("%c", is_good["!=~"] ); // Affiche '!' si is_good vaut 0 // '=' si is_good vaut 1 // '~' si is_good vaut 2 @@ -190,7 +191,7 @@ L'initialisation L'initialisation, c'est quelque chose que l'on maîtrise en C. C'est le fait de donner une valeur à une variable lors de sa déclaration. En gros, on définit sa valeur. -Pour un tableau[^tableau] : +Pour un tableau[^tableau] : ~~~c int tab[10] = {0}; tab[2] = 5; @@ -200,13 +201,13 @@ tab[2] = 5; À la première ligne, on initialise le tableau avec des 0 car, lorsqu'on initialise un tableau toute valeur non spécifiée est par défaut 0. La ligne suivante est une affectation et non une initialisation. -Si on veut seulement initialiser le troisième élément, puisque l'initialisation d'un tableau se fait suivant l'ordre de ses valeurs, on devrait écrire : +Si on veut seulement initialiser le troisième élément, puisque l'initialisation d'un tableau se fait suivant l'ordre de ses valeurs, on devrait écrire : ~~~c int tab[10] = {0, 0, 5}; ~~~ -Mais en réalité, il existe une autre syntaxe qui permet de faire plus simple : +Mais en réalité, il existe une autre syntaxe qui permet de faire plus simple : ~~~c int tab[10] = {[2] = 5}; @@ -215,8 +216,9 @@ int tab[10] = {[2] = 5}; On dit simplement que la troisième case vaut 5. Le reste est par défaut 0. Une syntaxe équivalente existe d'ailleurs pour les structures et les unions. ->!information -> Pour l'exemple, on va prendre une structure `point` que je vais utiliser plusieurs fois dans ce billet, idem pour la structure `message`. +:::information + Pour l'exemple, on va prendre une structure `point` que je vais utiliser plusieurs fois dans ce billet, idem pour la structure `message`. +::: On peut ainsi initialiser un point en utilisant ses composantes. @@ -232,7 +234,7 @@ point A = {.x = 1, .y = 2}; Ici, il n'y avait pas d'ambigüité. Mais pour une structure plus complexe, cette syntaxe est vraiment avantageuse. -Tenez : +Tenez : ~~~c typedef struct message { @@ -243,7 +245,7 @@ typedef struct message { message to_send = {.src="", .dst="23:12:23", .msg="Code 10"}; -// Est bien plus clair que : +// Est bien plus clair que : message to_send = {"", "23:12:23", "Code 10"}; @@ -261,7 +263,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é. Parfois, ces syntaxes sont très judicieusement utilisées ! -Comme ici, dans ce décodeur de base64 : +Comme ici, dans ce décodeur de base64 : >~~~c >static int b64_d[] = { > ['A'] = 0, ['B'] = 1, ['C'] = 2, ['D'] = 3, ['E'] = 4, @@ -283,22 +285,22 @@ 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}; - float pi = {3.1415926}; - char* s = {"unicorn"}; - ~~~ + ~~~c + int a = {11}; + float pi = {3.1415926}; + char* s = {"unicorn"}; + ~~~ - Ce qui caractérise le tableau du coup est surtout le fait d'utiliser des virgules entre les accolades. + Ce qui caractérise le tableau du coup est surtout le fait d'utiliser des virgules entre les accolades. Les expressions littéralement composées -------------------------------------- Puisque nous parlons des tableaux. Il existe une syntaxe simple pour utiliser des tableaux à usage unique. -Je voudrais utiliser ce tableau : +Je voudrais utiliser ce tableau : ~~~c int tab[5] = {5, 4, 5, 2, 1}; printf("%d", tab[i]); // Avec i égale à quelque chose >=0 et <5 @@ -307,16 +309,16 @@ printf("%d", tab[i]); // Avec i égale à quelque chose >=0 et <5 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 ~~~ -Ce n'est pas super lisible pour le coup. Mais il existe plein de cas où cette syntaxe est très utile. Par exemple avec une structure : +Ce n'est pas super lisible pour le coup. Mais il existe plein de cas où cette syntaxe est très utile. Par exemple avec une structure : ~~~c -// Pour envoyer notre message : +// Pour envoyer notre message : send_msg( (message){ .dst="192.168.11.1", .msg="Code 11"} ); // Pour afficher la distance entre deux points @@ -331,7 +333,7 @@ On appelle ces expressions des *compound literals* (ou littéraux agrégats si l Introduction aux VLAs -------- -Les tableaux à taille variable (ou *Variable Length Arrays* en anglais, soit VLAs) sont des tableaux dont la taille n'est connue qu'à l'exécution. Si vous n'avez jamais entendu parler des VLAs, ceci devrait vous choquer : +Les tableaux à taille variable (ou *Variable Length Arrays* en anglais, soit VLAs) sont des tableaux dont la taille n'est connue qu'à l'exécution. Si vous n'avez jamais entendu parler des VLAs, ceci devrait vous choquer : ~~~c int n = 11; @@ -356,7 +358,7 @@ double tab2[2*n]; unsigned int tab[foo()]; // avec foo une fonction définie ailleurs ~~~ -Un VLA ne peut pas être initialisé, de plus, il ne peut être déclaré `static`. C'est-à-dire que ces deux déclarations sont incorrectes : +Un VLA ne peut pas être initialisé, de plus, il ne peut être déclaré `static`. C'est-à-dire que ces deux déclarations sont incorrectes : ~~~c int n = 30; @@ -365,7 +367,7 @@ int tab[n] = {0}; static tab2[n]; ~~~ -Dans une fonction, on pourrait utiliser : +Dans une fonction, on pourrait utiliser : ~~~c void bar(int n, int tab[n]) { @@ -373,7 +375,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 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 : ~~~c void foo( int n, int m, int tab[][m]) { @@ -389,7 +391,7 @@ 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. -[^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). +[^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). L'exception des VLAs --------------------------- @@ -424,7 +426,7 @@ printf("%zu", sizeof(char[++n])); // Affiche 6 Ce n'est que peu intuitif puisque les VLAs ont ici introduit une *exception à la règle* qui est de ne pas exécuter l'expression passée à `sizeof`. -Un autre comportement bizarre introduit par les VLAs est l'exécution des expressions liées à la taille d'un VLA dans la définition d'une fonction. Ainsi : +Un autre comportement bizarre introduit par les VLAs est l'exécution des expressions liées à la taille d'un VLA dans la définition d'une fonction. Ainsi : ~~~c int foo( char tab[printf("bar")] ) { @@ -447,7 +449,7 @@ Vous n'avez peut-être jamais entendu parler des « *flexible arrays member* ». 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 : +On pourrait vouloir faire : ~~~c struct foo { @@ -455,7 +457,7 @@ struct foo { }; ~~~ -Et l'utiliser comme ceci : +Et l'utiliser comme ceci : ~~~c struct foo* contigue = malloc( sizeof(struct foo) ); @@ -476,7 +478,7 @@ struct foo { }; ~~~ -Ici, le champ `flexiTab` est un tableau membre flexible. Un tel tableau doit être le dernier élément de la structure et ne pas spécifier de taille[^zero]. On l'utilise comme ceci : +Ici, le champ `flexiTab` est un tableau membre flexible. Un tel tableau doit être le dernier élément de la structure et ne pas spécifier de taille[^zero]. On l'utilise comme ceci : ~~~c struct foo* contigue = malloc( sizeof(struct foo) + N * sizeof *flexiTab ); @@ -503,7 +505,7 @@ De manière à ne jamais avoir à utiliser `goto`. Du coup, on n'apprend jamais ce qu'est une étiquette... -Voici comment on utilise `goto` et une étiquette : +Voici comment on utilise `goto` et une étiquette : ~~~c goto end; @@ -537,15 +539,16 @@ Ici, les `case` et le `default` sont en fait des étiquettes ! Comme pour les `g Sauf qu'elles sont pour les `switch`, et inutilisables par les `goto`... ->!question -> Pourquoi tu nous parles de ça ? +:::question +Pourquoi tu nous parles de ça ? +::: Déjà, c'est bien de savoir que ça s'appelle une étiquette. Ensuite, parce que je vais vous parlez d'un classique. Le dispositif de *Duff*. C'est une sorte de boucle déroulée optimisée. Le but est de réduire le nombre de vérifications de fin de boucle (ainsi que le nombre de décrémentations). -Voici la version historique écrite par Tom Duff : +Voici la version historique écrite par Tom Duff : ~~~c { @@ -573,7 +576,7 @@ Le test que l'on cherche à effectuer le moins possible est `--n > 0`. En temps normal, `n` serait en fait `count`. Et on devrait faire le test `count` fois. De même pour sa décrémentation. -C'est-à-dire : +C'est-à-dire : ~~~c while( count-- > 0 ) @@ -601,7 +604,7 @@ 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`). -Ainsi en C, il est possible de déclarer un nombre complexe comme ceci : +Ainsi en C, il est possible de déclarer un nombre complexe comme ceci : ~~~c double complex point = 2 + 3 * I; @@ -609,7 +612,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 le macro `CMPLX` pour initialiser un nombre complexe : ~~~c double complex cplx = CMPLX(2, 3); @@ -626,7 +629,7 @@ Il existe un moyen en C d'avoir des macros qui soient définies différemment en Cette généricité s'obtient avec les sélections génériques basées sur la syntaxe ` _Generic ( /* ... */ )` -Pour comprendre la syntaxe, voyons un exemple simpliste : +Pour comprendre la syntaxe, voyons un exemple simpliste : ~~~c #include <stdio.h> @@ -651,10 +654,10 @@ int main(int argc, char* argv[]) { ~~~ Ici, on affiche le maximum que peut stocker chacun des types que nous utilisons. C'est quelque chose qui n'aurait pas été possible sans l'utilisation de ce nouveau mot-clé `_Generic`. -Pour utiliser cette syntaxe, on utilise le mot clé `_Generic` auquel on passe 2 paramètres. Le premier est une expression dont le type va influencer l'expression finalement exécutée. Le deuxième est une suite d'association de type et d'expression (type : expression) dont les associations sont séparées par des virgules. Au final, seule l'expression désignée par le type de la première expression est finalement évaluée. +Pour utiliser cette syntaxe, on utilise le mot clé `_Generic` auquel on passe 2 paramètres. Le premier est une expression dont le type va influencer l'expression finalement exécutée. Le deuxième est une suite d'association de type et d'expression (type : expression) dont les associations sont séparées par des virgules. Au final, seule l'expression désignée par le type de la première expression est finalement évaluée. -Un exemple réel d'utilisation pourrait être : +Un exemple réel d'utilisation pourrait être : ~~~c int powInt(int,int); @@ -662,7 +665,7 @@ int powInt(int,int); ~~~ -Il n'y a plus grand-chose à dire si ce n'est qu'il est possible d'avoir dans la liste des types le mot `default` qui correspondra alors à tous les types non-cités. Ainsi une définition plus propre de la macro `POW` de tout à l'heure pourrait être : +Il n'y a plus grand-chose à dire si ce n'est qu'il est possible d'avoir dans la liste des types le mot `default` qui correspondra alors à tous les types non-cités. Ainsi une définition plus propre de la macro `POW` de tout à l'heure pourrait être : ~~~c int powIntuInt(int a, unsigned int b); @@ -673,7 +676,7 @@ double powlFltInt(long double a,int b) { return powl(a,b); } #define POWINT(x) _Generic((x), double: powFltInt, \ - float : powfFltInt, \ + float : powfFltInt, \ long double: powlFltInt, \ unsigned int: powIntuInt, \ default: powIntInt) @@ -695,33 +698,22 @@ Et pour chaque caractère n'étant pas sur le clavier et utiliser en langage C, Voici un tableau récapitulatif des séquences de trigraphes, de digraphes et de leur représentation en caractère. -+-----------+----------+-----------+ | Caractère | Digraphe | Trigraphe | -+===========+==========+===========+ +| --------- | -------- | --------- | | `#` | `%:` | `??=` | -+-----------+----------+-----------+ | `[` | `<:` | `??(` | -+-----------+----------+-----------+ | `]` | `:>` | `??)` | -+-----------+----------+-----------+ | `{` | `<%` | `??<` | -+-----------+----------+-----------+ | `}` | `%>` | `??>` | -+-----------+----------+-----------+ | `\` | | `??/` | -+-----------+ +-----------+ | `^` | | `??'` | -+-----------+ +-----------+ -| `|` | | `??!` | -+-----------+ +-----------+ +| `\|` | | `??!` | | `~` | | `??-` | -+-----------+----------+-----------+ | `##` | `%:%:` | | -+-----------+----------+-----------+ -La différence **principale** entre les digraphes et les trigraphes est dans une chaîne de caractère : +La différence **principale** entre les digraphes et les trigraphes est dans une chaîne de caractère : ~~~c puts("??= est un croisillon"); @@ -729,8 +721,7 @@ puts("%:"); ~~~ Ces mécanismes du Moyen Âge sont encore valides de nos jours en C. -Du coup, cette ligne de code est tout à fait valide : - +Du coup, cette ligne de code est tout à fait valide : ~~~c ??=define FIRST tab<:0] ~~~ |