Licence CC BY

Les bizarreries du langage C

Dernière mise à jour :
Auteur :
Catégories :
Temps de lecture estimé : 28 minutes

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 ! :pirate:

Pour comprendre ce billet, il est nécessaire d’avoir des bases dans un langage ayant une syntaxe et un fonctionnement proche du C.

Les opérateurs inusités

Il existe beaucoups d’opérateurs dans le langage C. Ici, on va parler particulièrement de deux d’entre eux qui ne sont pratiquement jamais utilisés.

L’opérateur virgule

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. Mais pas seulement ! C’est également un opérateur.

L’instruction suivante, bien qu’inutile est tout à fait valide :

1
printf("%d", (5,3) );

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 du for on peut faire :

1
2
3
for( ; i < j ; i++, j-- ) {
 // [...]
}

Ou encore, dans de petits if pour les simplifier :

1
2
if( argc > 2 && argv[2][0] == '0' )
    action = 4, color = false;

Ici, on assigne action et color. Normalement pour faire 2 assignations, on aurait du mettre des accolades au if.

On peut également s’en servir pour retirer des parenthèses.

1
2
3
4
5
6
7
while( c = getchar(), c != EOF && c != '\n' ) {
  // [...]
}
// Est strictement équivalent à :
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 !

L’opérateur ternaire

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 les ternaires, on ferait :

1
2
3
4
int min = a;
if( b < a)
  min = b;
printf("%d", min);

Alors qu’avec les ternaires on fait :

1
printf("%d", a<b ? a : b);

Grâce à la ternaire, on a économisé un nom de variable et quelques lignes. Mais surtout on a gagné en lisibilité. Quand on sait en lire une …

Pour lire une expression ternaire, il faut lire la découper en 3 parties :

1
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 courte expression. 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é peu nuire à la lisibilité du code. D’ailleurs, on peut très bien mettre une expression ternaire dans une expression ternaire :

1
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 :

1
2
3
#define MIN(a,b) a<b ?a : b

printf("%d", MIN(a,MIN(b,c)));

Et voilà ! Deux opérateurs qui vont désormais gagner un peu d’intérêt. Et surtout être mieux compris ! Puis d’ailleurs, même les opérateurs qu’on connait déjà, on n’en maîtrise pas forcément la syntaxe.

L'accès à un tableau

On nous a toujours appris que pour afficher le 3em élément d’un tableau, on faisait :

1
2
int tab[5] = {0, 1, 2, 3, 4, 5};
printf("%d", tab[2]);

2 et non 3, car un tableau en C commence à 0. Un tableau commence à 0, c’est une histoire d’adresse. L’adresse du premier élément est en fait l’adresse du tableau. Et par arithmétique des pointeurs, l’adresse du 3em élément est tab+2.

Donc on aurait très bien pu écrire :

1
printf("%d", *(tab+2));

Puis comme l’addition est commutative, on tab+2 ou 2+tab sont équivalents. En fait, on aurait même pu faire :

1
printf("%d", 2[tab]);

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.

Par exemple pour afficher un caractère ’=’ pour vrai, ’!’ pour faux et ’~’ pour aucun des deux. On pourrait faire :

1
2
3
4
5
6
if( is_good == 1 )
  printf("%c", '=');
else if( is_good == 0 )
  printf("%c", '!');
else
  printf("%c", '~');

Mais il existe plus simple :

1
2
3
4
5
6
printf("%c", "!=~"[is_good]);

// 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

En somme, tout le monde écrit tab[3] et pas 3[tab]. Du coup, il n’y a aucun intérêt à écrire 3[tab]. Mais c’est toujours bien de savoir que ça existe. ^^

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:

1
2
int tab[10] = {0};
tab[2] = 5;

{0} peut également être utilisé pour un nombre. Toute valeur initialisant une variable simple (pointeur nombre, …) peut optionnellement prendre des accolades.

1
2
3
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.

On initialise le tableau avec des 0 car, lors qu’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 une initialisation ; Puisque l’initialisation d’un tableau se faire suivant l’ordre de ses valeurs, on devrait écrire :

1
int tab[10] = {0, 0, 5};

Mais en réalité, il existe une autre syntaxe qui permet de faire plus simple :

1
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 ou les unions.

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.

1
2
3
4
5
6
typedef struct point {
  int x,y;
} point;


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 :

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
typedef struct message {
   char src[20], dst[20], msg[200];
} message;

// [...]

message to_send = {.src="", .dst="23:12:23", .msg="Code 10"};

// Est bien plus claire que :

message to_send = {"", "23:12:23", "Code 10"};

// D'ailleurs, je ne l'ai pas fait mais avec cette syntaxe pas besoin de se souvenir de l'ordre
// des champs de la structure

message to_send = { .msg="Code 10", .dst="23:12:23", .src=""};

// Et aussi puisque tout champ d'une structure est inialisé à sa valeur nulle s'il n'est pas initialisé explicitement.
// On peut également omètre src.

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 :

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
static int b64_d[] = {
   ['A'] =  0, ['B'] =  1, ['C'] =  2, ['D'] =  3, ['E'] =  4,
   ['F'] =  5, ['G'] =  6, ['H'] =  7, ['I'] =  8, ['J'] =  9,
   ['K'] = 10, ['L'] = 11, ['M'] = 12, ['N'] = 13, ['O'] = 14,
   ['P'] = 15, ['Q'] = 16, ['R'] = 17, ['S'] = 18, ['T'] = 19,
   ['U'] = 20, ['V'] = 21, ['W'] = 22, ['X'] = 23, ['Y'] = 24,
   ['Z'] = 25, ['a'] = 26, ['b'] = 27, ['c'] = 28, ['d'] = 29,
   ['e'] = 30, ['f'] = 31, ['g'] = 32, ['h'] = 33, ['i'] = 34,
   ['j'] = 35, ['k'] = 36, ['l'] = 37, ['m'] = 38, ['n'] = 39,
   ['o'] = 40, ['p'] = 41, ['q'] = 42, ['r'] = 43, ['s'] = 44,
   ['t'] = 45, ['u'] = 46, ['v'] = 47, ['w'] = 48, ['x'] = 49,
   ['y'] = 50, ['z'] = 51, ['0'] = 52, ['1'] = 53, ['2'] = 54,
   ['3'] = 55, ['4'] = 56, ['5'] = 57, ['6'] = 58, ['7'] = 59,
   ['8'] = 60, ['9'] = 61, ['+'] = 62, ['/'] = 63, ['='] = 64
};
Taurre

Les expressions littéralement composées

Puisque nous parlons des tableaux. Il existe une syntaxe simple pour utiliser des tableaux constants.

Je voudrais utiliser ce tableau :

1
2
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 … C’est pas cool d’avoir à utiliser un identificateur juste pour ça.

Et bien je peux faire ceci :

1
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 pleins de cas où cette syntaxe est très utile :

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
// Pour envoyer notre message :
send_msg( (message){ .dst="192.168.11.1", .msg="Code 11"} );

// Pour afficher la distance entre deux points
printf("%d", distance( (point){1, 2}, (point){2, 3} )  );

// Ou encore sous Linux, en programmation système
execvp( "bash" , ((char*[]){"bash", "-c", "ls", NULL}) );

`

Ce type d’expression est une expression littérale, c’est-à-dire qu’elle est immuable. Comme pour les chaînes de caractères littérales.

Ainsi ceci n’est pas valide :

1
"lache"[0] = 'v' // Est une expression invalide puisque "lache" est une expression littérale

De même ceci n’est pas valide :

1
((int[]){1,2,3})[0] = 0; 

On appelle donc ces expressions des compound literals. Ou une expression littérale composée si l’on s’essaye à traduire.

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 :

1
2
3
4
5
6
int n = 11;

int tab[n];

for(int i = 0 ; i < n ; i)
  tab[i] = 0;

Ce code, bien que valide, à dût être réprimandé par de nombreux professeurs. En effet, on nous apprend qu’un tableau doit avoir une taille connue à la compilation. Et bien, les VLA 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.

Je vais simplement parler des comportements non-intuitifs introduits avec les VLAs. Comme par exemple :

1
printf("%zu", sizeof(int[puts("Je retourne :")]) );

En temps normal, l’expression entre crochet serait constante. Il n’y aurait pas eu besoin de l’exécuter. Cependant, avec les VLAs, on est obligé d’exécuter ce code afin de connaitre la taille du tableau.

Du coup, ce code affiche bien :

1
2
Je retourne :
56

56 étant la taille du tableau (14*sizeof(int)). 14 étant le nombre de caractères affichés par puts.

Ceci est d’autant plus étrange que sizeof a pour habitude de ne pas interpréter l’expression que l’on lui en paramètre. Afin de faciliter l’allocation dynamique d’un tableau, il est courant d’utiliser cette syntaxe :

1
char* str = malloc( n * sizeof *str);

Ici, l’expression *str n’a pour l’instant pas de comportement définit (str n’étant pas encore initialiser) mais peu importe car elle n’est pas exécutée. Son type est déterminé à la compilation et sizeof là remplace par la taille de son type.

Le type de *str étant char, sa taille est donc de 1 byte. Et le code précédant est traduit pas le compilateur par :

1
char* str = malloc( n );

On peut même faire plus bizarre encore :

1
2
3
4
5
6
7
8
void foo(int a[][puts("Avant foo")]) {
    puts("Début de foo");

}
int main(void) {
    int b[3][3];
    f(b);
}

Humm, curieux effet de bord. Il y a deux choses assez intéressantes dans ce code. La première, assez évidente, c’est le fait d’avoir un appel de fonction dans la définition du type. La deuxième chose intéressante est le fait que foo prenne en argument un tableau à taille variable, mais qu’on lui passe en paramètre un tableau de taille fixe ! En fait, on aurait pu écrire n’importe quelle expression qui retourne un entier. Ces deux points sont causés par les VLAs. Voyons ça plus doucement.

La définition d'un type VLA

Pour définir une fonction qui prend un tableau d’entiers de taille N en paramètre, on peut simplement faire :

1
void foo(int tab[N]);

Mais en fait, peu importe la taille, la fonction ne fait pas vraiment la différence. Pour elle, c’est simplement un pointeur sur un entier. Du coup, on peut très bien écrire :

1
2
3
4
5
// Si on veut gardé l'idée que c'est un tableau (au moins à la base)
void foo(int tab[]);

// Ou si on préfère traduire l'idée qu'on manipule un pointeur.
void foo(int* tab);

Tout ceci est possible car par arithmétique des pointeurs, tab[9] ne dépends pas de la taille du tableau, mais plutôt de la taille d’un int. Mais du coup, ça se complexifie si on a des types un peu plus compliqués, par exemple … Des types dont on ne connait pas la taille à la compilation.
Oui je parle de VLAs. Pour un double tableau, on doit spécifier une taille à la compilation la déclaration ressemble à :

1
void foo(int tab[][5]);

On a donc au final un pointeur sur un tableau d’entier de taille 5, soit de taille complète 5*sizeof int. Et donc pour un tableau de taille n (VLAs). On doit faire :

1
void foo(int tab[][*]);

Ouais … Car ici, c’est une déclaration et pas une définition. n n’est pas censé avoir de valeur pour le moment du coup, ça n’aurait pas vraiment de sens. Sauf si n était global, mais du coup, elle aurait une valeur fixe pour le moment. Bref, on a donc introduit cette syntaxe. Puis pour la définition on utilise bien évidemment n :

1
2
3
void foo(int tab[][n]) { // Où n est une variable globale

}

Personne n’aime les variables globales. Du coup on préfèrerait que n soit passé en argument :

1
2
3
4
5
void foo(int n, int tab[][n]);

void foo(int n, int tab[][n]) {

}

Puis en fait, pour être tout à fait précis, dans les crochets, un VLA prend en paramètre une expression qui retourne une variable entière. Donc ici, si le tableau faisait $2n$ on aurait pût écrire :

1
2
3
void foo(int n, int tab[][*]);

void foo(int n, int tab[][2*n]);

Cool. Non en fait, c’est même vraiment méga cool. Car sans ça, on aurait du bidouiller avec des adresses pour arriver à la même chose. On aurait pût cacher la misère avec des fonctions. Une autre solution, très courante, était d’utiliser le tableau à deux dimensions comme un tableau à une dimension.
Comme ceci :

1
2
3
int tab[3][3];

tab[0][8] == tab[2][2]; // 8 = (2*3+2)

Mais ça reste bien moins pratique que l’utilisation de VLAs.

Mais alors une dernière question reste en suspens. Peut-on utiliser cette astuce pour des tableaux 2D qui ne sont pas des VLAs. À cela il existe une réponse simple. Un tableau reste un tableau, son agencement en mémoire est le même. Du coup, pas de problème à utiliser un tableau à deux dimensions fixes dans une fonction qui prend en paramètre un VLA.

Une histoire d'étiquettes

En C, s’il y a un truc dont on ne doit pas parler, ce sont bien les étiquettes. On les utilses avec la structure de contrôle goto ! L’interdite !

Pour les cacher, on remplace les goto par des structures de contrôles adapées et plus claires comme break ou continue. 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 :

1
2
3
4
5
goto end;


end: return 0;
}

En gros, une étiquette est un nom que l’on donne à une instruction.

Et bien on en utilise des étiquettes ! Dans les switch !

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
switch( action ) {
    case 0 :
        do_action0();
    case 1:
        do_action1();
        break;
    case 2:
        do_action2();
    break;
    default:
        do_action3();
}

Ici, les case et le default sont en fait des étiquettes ! Comme pour les goto. Sauf qu’elles sont pour les switch, et inutilisables par les goto

Pourquoi tu nous parles de ça ?

Déjà, c’est bien de savoir que ça s’appele 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 tests de vérification de fin de boucle (ainsi que le nombre de décrémentation).

Voici la version historique écrite par Tom Duff :

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
{
    register n = (count + 7) / 8;
    switch (count % 8) {
    case 0: do { *to = *from++;
    case 7:      *to = *from++;
    case 6:      *to = *from++;
    case 5:      *to = *from++;
    case 4:      *to = *from++;
    case 3:      *to = *from++;
    case 2:      *to = *from++;
    case 1:      *to = *from++;
            } while (--n > 0);
    }
}

Peu importe ce que signifit register. Aussi, to est un pointeur particulié mais ce n’est pas vraiment important.

Ici, ce dont je veux vous parlez, c’est cette boucle do-while en plein milieu d’un switch.

Le test que l’on cherche à effectuer le moins possible est --n > 0.

En temps normal, n serrait en fait count. Et on devrait faire le test count fois. De même pour sa décrémentation.

C’est-à-dire :

1
2
while( count-- > 0 )
    *to = *from++;

En divisant par 8 (nombre arbitraire) on divise également par 8 le nombre de tests et de décrémentations. Cependant, si count n’est pas divisible par 8, on a un problème, on ne fait pas toutes les instructions. Se serrait bien de pouvoir sauter directement à la 2em instruction, si on a seulement 6 instructions restantes.

Et c’est là que les étiquettes peuvent nous aider ! Grâce au switch on peut sauter directement à la bonne instruction.

Il suffit d’étiqueter chaque instruction avec le nombre d’instructions qu’il reste à faire dans la boucle. Puis de sauter dans la boucle avec la structure de contrôle switch sur le reste d’instructions à réaliser.

Ensuite, on execute nos paquets de 8 instructions normalement.

Il est très rare d’avoir à utiliser ce type d’astuces. D’ailleurs, c’est une optimisation d’un autre temps. Mais comme je voulais vous parler de syntaxe alors, il était nécessaire de parlez des étiquettes.

Les caractères trop spéciaux

On va remonter encore dans le temps. Je vais vous citer une dernière chose. Un temps où tous les caractères n’étaient pas aussi accessibles que de nos jours sur autant de type de claviers.

Les claviers n’avaient pas forcément de touches de composition. Ainsi, il était impossible de taper le caractère ’#’.

On pouvait alors remplacer la caractère ’#’ par la séquence "??=". Et pour chaque caractère composé, il existait une séquence à base de "??". Voici une image des séquences de trigraphe et de leur représentation en caractère.

Liste des trigraphes

Il existe également des digraphes. C’est à peu près la même chose mais en deux caractères :

Liste des digraphes

La différence principale est dans une chaîne de caractère :

1
2
puts("??= est un croisillon");
puts("%:");

Ces mécanismes du moyen age sont encore valides de nos jours en C. Du coup, cette ligne de code est tout à fait valide :

1
??=define FIRST tab<:0]

La seule utilité de cette syntaxe de nos jours est d’obfusquer un code source très facilement. Une combinaison d’un ternaire avec un trigraphe et un digraphe et vous avez un code absolument illisible. ;)

1
printf("%d", a ?5??((tab):>:0);

À ne jamais utiliser dans un code sérieux donc.


Voilà, c’est fini, j’espère que vous avez appris quelque chose de ce billet. Surtout n’oubliez pas d’utiliser ces syntaxes avec parcimonie;

Vous pouvez (re)découvrir de nombreuses codes abusants de la syntaxe du langage C aux IOCCC. :diable:

7 commentaires

\o

Ceci est mon premier billet. Même si j’ai fais attention, je sais qu’il reste encore des fautes. Désolé :honte:

Signalez les moi avec le bouton ad-hoc.

ache.one                 🦹         👾                                🦊

+5 -0

Salut,

Merci pour ce petit billet. :)
Il est peut-être juste à préciser que les initialisations précisant un champ ou un élément et les aggrégats littéraux ne sont disponibles que depuis la norme C99.

Un tableau commence à 0, c’est une histoire d’adresse

Eh bien, j’ai appris récemment que non, en fait. Il faudrait d’ailleurs que je mette le cours sur le C à jour par rapport à cela. ;)

+0 -0

@Ache, si tu étoffe et que tu corrige les fautes (à la marge), pourquoi pas en faire un article ?

écolo-utopiste altermondialiste radicalisé sur Internet | La tero estas nur unu lando | Géographe de service | Cliquez 👍 pour dire merci

+4 -0

@Society: Tu m’en vois ravi ! :D
C’est exactement le but de ce billet.

@Taurre: Je l’avais mis au début. Puis je me suis dis que C99 avait bientôt 20ans alors bon. Mais du coup, je vais rajouter une simple note pour bien préciser. ^^

Intéressant ton lien. Du coup, si les tableaux en C commencent à 0, c’est surtout car le BCPL faisait ça. Et si le BCPL faisait ça, c’était pour gagner du temps de … compilation ?!

Ça parait tellement ridicule x)

@qwerty: Je vais suivre vos commentaires je pense. J’étoffe certaines parties, je corrige les fautes, je précise des points et je rajoute une petite section. Puis ensuite, là je pense que je le republierais en article.

ache.one                 🦹         👾                                🦊

+1 -0

Je l’avais mis au début. Puis je me suis dis que C99 avait bientôt 20ans alors bon.

ache

C’est sûr que, logiquement, on devrait de nos jours partir du principe que l’on utilise au moins le C99 et de préférence le C11, mais bon, l’emploie des nouvelles normes étant ce qu’il est… :-°

Intéressant ton lien. Du coup, si les tableaux en C commencent à 0, c’est surtout car le BCPL faisait ça. Et si le BCPL faisait ça, c’était pour gagner du temps de … compilation ?!

ache

Yep, parce que translater les index de un, ça prend du temps. x)

Ça parait tellement ridicule x)

ache

De nos jours, c’est sûr. :D

@qwerty: Je vais suivre vos commentaires je pense. J’étoffe certaines parties, je corrige les fautes, je précise des points et je rajoute une petite section. Puis ensuite, là je pense que je le republierais en article.

ache

Note, n’hésite pas à parler d’autres points moins usiter comme la possibilité d’employer les tableaux de longueur variable pour écrire des fonctions comme ceci.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
#include <stddef.h>
#include <stdio.h>
#include <stdlib.h>


void print_tab(int x, int y, int tab[x][y])
{
        for (int i = 0; i < x; i++)
                for (int j = 0; j < y; j++)
                        printf("[%d][%d] = %d\n", i, j, tab[i][j]);
}


void init_tab(int x, int y, int tab[x][y])
{
        for (int i = 0; i < x; i++)
                for (int j = 0; j < y; j++)
                        tab[i][j] = x * i + j;
}


int
main(void)
{
        int (*tab)[10] = malloc(sizeof(int[10][10]));

        if (tab == NULL)
                exit(EXIT_FAILURE);

        init_tab(10, 10, tab);
        print_tab(10, 10, tab);
        return 0;
}

Ce qui évite par exemple les calculs d’adresse à la main dans le cas de l’allocation dynamique (ou non) d’un tableau « plat ».

Il y a sûrement encore l’un ou l’autre point à présenter. ;)

Édité par Taurre

+3 -0
Vous devez être connecté pour pouvoir poster un message.
Connexion

Pas encore inscrit ?

Créez un compte en une minute pour profiter pleinement de toutes les fonctionnalités de Zeste de Savoir. Ici, tout est gratuit et sans publicité.
Créer un compte