|
|||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
Le mois dernier, nous parlions des types de base. Ce mois-ci, voici les types construits. Ce sont les tableaux, les structures et les unions. Il reste un type particulier, les pointeurs mais je vous les réserve pour la rentrée parce que ça ne fait pas bon ménage avec les coups de soleil. Les tableaux Comme tout langage qui se respecte, le C permet de déclarer des tableaux. Ça n'est pas très compliqué ; la syntaxe est la suivante :
Par exemple : int table[100]; permet de déclarer une table (ou plus exactement un vecteur) de 100 entiers. Attention, le C ne vous laisse pas le choix de l'intervalle des indices (à la différence de Pascal). Ces entiers vont obligatoirement de table[0] à table[99]. Ceux qui ont conçu le langage ne sont pas pour autant idiots, c'est simplement dans un but d'optimisation, car, lors du parcours d'une table, on compare souvent la valeur de l'indice avec les extrémités de la table. Et chacun sait que les comparaisons les plus rapides sont les comparaisons à zéro. Vous pouvez ensuite utiliser les valeurs avec : x = table[33]; Autre différence avec Pascal, les indices ne sont jamais vérifiés. C'est une règle, le C ne fait aucune vérification à l'exécution. C'est ce qui fait sa rapidité mais aussi son manque de rigueur face à Pascal ou Ada. Par conséquent, si vous écrivez table[200], un bon compilateur doit vous retourner une erreur car il voit à la compilation que vous tapez hors des bornes. Pour sa part, le Lattice se contente de vous signaler une alerte (Reference beyond object size) mais pas d'erreur. En plus, il vous retourne quand même. à l'exécution une valeur complètement fantaisiste ! Par contre, si vous écrivez table[i] et que "i" vaut 200 à l'exécution, il n'y aura pas d'alerte à la compilation et table[i] retournera une valeur tout aussi fausse à l'exécution. Attention à ce genre de piège ! Naturellement, vous pouvez déclarer des tableaux de n'importe quel type, y compris des types construits, et par conséquent, d'autres tableaux. Les matrices sont donc déclarées de la manière suivante (par exemple) :
...définit une matrice de 100 lignes sur 10 colonnes. Et vous nommez un élément de la même manière : x matrice[5][10]; Ici, c'est une matrice (dimension 2) mais vos tableaux peuvent être d'une autre dimension : int toto[2][2][2][2][2]; est une déclaration correcte correspondant à un tableau de dimension 5, comprenant 32 éléments. L'initialisation des tableaux C'est un point important. Si votre tableau doit contenir des valeurs initiales, vous pouvez les donner au moment de la déclaration. Le tableau sera alors réservé à la compilation, avec toutes les valeurs qu'il contient. Si vous ne lui donnez pas de valeurs initiales, la compilation ne réserve qu'une étiquette indiquant sa taille. Il ne sera alloué et initialisé avec des zéros que juste au début de l'exécution. C'est ce qui fait la différence entre les "datas" (données initialisées) et le BSS (données non initialisées). Autre point très important : vous ne pouvez déclarer de tableau avec des valeurs initiales qu'à l'extérieur d'une fonction. Ceci parce que, dans le cas d'une initialisation à l'intérieur d'une fonction, le compilateur devrait générer un code qui ferait ce travail chaque fois que la fonction est appelée et ça peut être compliqué. Contrairement à Ada qui le fait, le C, conforme à sa philosophie (flemmard), ne le fait pas. En résumé, les tableaux dynamiques ne peuvent pas être initialisés à leur déclaration (c'est à vous de le gérer après). Et il faut d'autant plus le gérer que l'exécutable les alloue sans aucune initialisation. Chaque fois que vous entrez dans une fonction qui contient des tableaux dynamiques, ils sont alloués sur la pile mais sans initialisation (mise à zéro) préalable. C'est donc un autre point important : les tableaux statiques sont initialisés à zéro par défaut au début de l'exécution ; les tableaux dynamiques ne sont pas initialisés lorsqu'ils sont alloués. Je rappelle, pour les deux cancres qui dorment au fond, que les variables statiques sont celles qui sont déclarées hors de toute fonction (réservées une fois pour toutes) et que les variables dynamiques sont celles qui sont déclarées à l'intérieur d'une fonction (allouées à chaque appel de la fonction). C'est un principe important, bien qu'on puisse un peu le contourner mais on y reviendra plus tard. Comment déclarer un tableau avec un échantillon de valeurs initiales ? Il suffit d'énumérer ces valeurs de la façon suivante :
Cette déclaration réserve un tableau de deux entiers qui contiennent les valeurs 1 et 2. Je vous rappelle que ça ne marche pas à l'intérieur d'une fonction. Vous pouvez aussi initialiser une matrice avec une expression de la forme suivante :
Vous pouvez aussi n'initialiser qu'une partie de la table : les termes non initialisés sont mis à zéro. Mais je vous le déconseille car les règles ici ne sont pas claires et certains termes doivent absolument être cités même si vous ne souhaitez pas les initialiser. Donc je passe. Enfin, il est possible de réserver une table caractérisée par son contenu et non pas par sa taille :
Ceci ne réserve pas à proprement parler un tableau mais quatre entiers contigus, accessibles comme s'ils appartenaient à un tableau, par liste[2] ou liste[i]. Mais bien sûr, il n'y a dans ce cas-là absolument aucun contrôle sur les indices, ni à la compilation, ni à l'exécution. Affectation de tableaux Dernier point important, il n'est pas possible en C de recopier un tableau dans un autre avec une simple affectation : il est nécessaire de parcourir l'ensemble de la table et de recopier les éléments un par un. Par exemple, si "a" et "b" sont deux vecteurs de 100 entiers, vous ne pouvez pas écrire : a = b; mais for (i=0; i<100; i++) a[i] = b[i]; (on reviendra plus tard sur l'expression de la boucle for). C'est toujours le même principe, le compilateur devrait générer du code pour recopier un tableau dans l'autre. Or, cette opération peut être compliquée dans le cas d'un tableau de structures par exemple (mais pas impossible puisqu'Ada le fait bien lui). Alors, toujours dans l'optique du C qui est d'en faire le moins possible, cette question de l'affectation des tableaux est laissée à l'initiative de l'utilisateur (mais vous allez voir comme c'est drôle...). Les structures Pour ceux qui connaissent Pascal ou Ada, vous pouvez passer : struct=record. Que les autres se rassurent, ça n'est pas très dur. C'est un principe clef des langages structurés et c'est très puissant. Une structure définit un nouveau type issu de la juxtaposition de plusieurs variables ; ça s'appelle aussi un enregistrement (d'où "record"). Imaginons un instant que Bruce Lepper demande à Chorizo Kid, spécialiste ès-algorithmes de lui écrire un programme pour faire la liste des abonnés à A-News (en fait, il a Superbase et Chorizo Kid est pénard). Notre artiste du logiciel ne va pas faire un tableau de 250 000 cellules pour les noms (si si A-News a beaucoup d'abonnés), un autre aussi grand pour les numéros de rue et un troisième pour les codes postaux. La bonne méthode, celle du Chorizo des grands jours, consiste à créer une structure comprenant le nom du Gentil Abonné, son numéro de rue et son code postal. Et c'est cette structure-là qui sera reproduite à 250 000 exemplaires (voire 250 005 car A-News sera bientôt distribué au Liechtenstein). Résumons : cela s'écrit :
Chaque fois que l'on manipulera une structure "Abonné", le compilateur comprendra : un paquet contenant un tableau de 12 caractères, un "short" et un "long". Il vous garantit que ces données sont stockées dans cet ordre-là mais par contre, vous n'avez aucune certitude sur la contiguïté des composantes : il peut y avoir des trous si ça l'avantage (principalement dans le cas où il y a un nombre impair de caractères). L'exemple ci-dessus est une déclaration du type "Abonné". Pour déclarer une variable de ce type, il faut écrire :
Vous avez aussi la possibilité de déclarer une variable en même temps que le type en remplaçant ces deux étapes par :
Mais le fait de séparer les deux opérations est préférable afin de mettre en valeur chacune d'entre elles. A signaler aussi dans la série baroque, cette construction possible mais peu intéressante :
Elle permet de déclarer la variable "UnAbonne" sans déclarer de type. Autrement dit, toutes les variables de ce type doivent être déclarées ici car il n'est pas connu du compilateur (puisqu'il n'a pas de nom). A éviter par conséquent. Accès aux composantes d'un enregistrement C'est bien joli d'avoir un paquet de données mais on aimerait bien pouvoir les utiliser. Les concepteurs du C, qui ne sont pas des niais, ont judicieusement prévu cette possibilité. Pour accéder à l'un des champs (c'est le nom des variables qui composent l'enregistrement), il suffit d'écrire :
"UnAbonne" représente pour sa part la structure entière. Affectation de structures Contrairement aux tableaux, le langage C gère l'affectation d'une structure à une autre, en une seule opération.
...est tout à fait légal. Ne me demandez pas pourquoi ça marche pour les structures et pas pour les tableaux : c'est comme ça. Lors de l'affectation d'une structure à une autre, tous les champs de l'une sont recopiés dans l'autre. Évidemment, ces deux variables doivent être du même type. Pourquoi les concepteurs du C ont-ils eu cette idée bizarre qui consiste à permettre la recopie de structures mais pas de tableaux ? J'avoue que la question reste posée à l'heure où nous abordons l'Europe de 1992 et le troisième millénaire... Il faut dire que la possibilité d'affecter une structure à une autre était interdite dans la définition du langage C, mais qu'elle est finalement autorisée dans ses versions les plus récentes. En fait, tout dépend du "degré de modernisme" de votre compilateur. Peut-être que dans un avenir proche, les compilateurs C permettront aussi bien l'affectation des tableaux que celle des structures. En tout cas, ceci vous permet de contourner l'interdiction que vous fait le compilateur de recopier un tableau dans un autre : en considérant qu'un tableau est le seul champ d'une structure, vous pouvez affecter une structure à l'autre et donc, un tableau à l'autre. Trêve de plaisanteries, il reste un point important : L'initialisation des structures Cela fonctionne exactement de la même façon que pour les tableaux : vous ne pouvez pas initialiser de structure à l'intérieur d'une fonction. Les structures globales non initialisées sont mises à zéro au début de l'exécution ; les structures dynamiques ne sont pas initialisées lorsqu'elles sont allouées. Voici un exemple :
Il est aussi possible d'initialiser des structures imbriquées, suivant le même principe que les tableaux imbriqués (les matrices). Les unions Notre Uncle Ben's des algorithmes (on ne colle jamais Chorizo Kid) doit affronter avec ses petits neurones un nouveau défi. Il serait en effet souhaitable, pour les abonnées, de conserver leur numéro de téléphone, et pour les abonnés, disons, leur meilleur score à Deluxe Paint. Celui-ci peut être stocké sous la forme d'un entier court, alors qu'un numéro de téléphone peut, par exemple, être rangé sous la forme d'un entier long. Il nous faut donc stocker suivant les cas, soit un entier court, soit un entier long. Nous aurons donc un type hétérogène, ce que permettent les unions. On écrira donc :
Le compilateur réserve l'objet qui prend le plus de place entre l'entier court et l'entier long. Et le programme interprète ce champ "Renseignement", suivant les cas, comme un entier court ou comme un entier long. Mais, comme à son habitude, le C ne fait aucune vérification à l'exécution. Par conséquent, c'est à votre programme de bien gérer l'accès à ce champ. Lorsque vous avez écrit un entier court, il ne faudra essayer de lire qu'un entier court. Si vous essayez de lire un entier long, il n'y aura pas d'erreur déclenchée mais la valeur retournée sera complètement farfelue. En résumé, vous ne pouvez lire qu'une valeur du type utilisé pour la dernière affectation de ce champ. Si vous passez outre, le programme ne s'en apercevra pas mais la valeur retournée sera mauvaise. Il est un fait que Pascal et Ada gèrent mieux ce problème puisqu'ils vérifient à l'exécution que l'on accède bien à la bonne valeur. Mais, comme vous l'avez remarqué depuis un moment, c'est un choix délibéré : le C ne fait pas de vérifications à l'exécution pour aller plus vite ; c'est important pour un langage dans lequel sont écrits tous les systèmes d'exploitation récents. Au niveau des affectations et des initialisations, les unions se comportent comme les structures. Par exemple, vous pouvez écrire :
...mais après ça, vous ne devez pas essayer de lire les valeurs de UneAbonnee.Renseignement.Hiscore et de UnAbonne.Renseignement.Telephone. Car, d'abord, ça ne vous regarde pas, et puis la valeur retournée serait fausse bien qu'aucune erreur ne survienne. Voilà trois notions importantes du C qui, je l'espère, se graveront dans votre tête. Le mois prochain, nous parlerons des pointeurs, un aspect primordial du C mais, hélas pas très simple à aborder. N'oubliez donc pas votre aspirine pour le prochain article.
|