Click here to print

Structures de controle, Pointeurs et tableaux

Structures de contrôle

if then else

On l'a vu dans les exemples précédents, il est possible de définir des blocs conditionnel, c'est à dire dont l'entrée est conditionné par la valeur d'une expression.

... if(cond){bloc1;}else{bloc2;} ...

La condition est évaluée, si elle vaut 1, les instruction du bloc 1 sont évaluées, sinon ce sont celles du bloc2.

Le else n'est pas obligatoire :

... if(cond){bloc1;} ...

et les bloc 1 et 2 peuvent eux même contenir des branchements conditionnels :

... if(cond){bloc1}else if(cond2){bloc2} else {bloc3} ...

les accolades ne sont obligatoires que si les blocs contiennent plusieurs instructions;

Pour la lisibilité du code, il est très fortement conseillé (cad obligatoire) d'indenter le code, c'est à dire d'ajouter les espace et retours à la ligne nécessaires pour faire ressortir à l'œil nu la logique du code(contrairement à ce que j'ai fait juste au dessus).

Remarquons enfin que si le bloc d'instructions correspondant à un if contient un return ou un exit, le else devient superflu :

if(cond){ blabla1; return;}else{blabla2;}
équivaut exactement à
if(cond){ blabla1; return;} blabla2;
sauf que la deuxième version est beaucoup plus lisible.

Switch

Le switch est une facon d'écrire une imbrication de if(expr==val1)...else if(expr==val2)...else if(expr==val3)... etc. plus performante et plus concise. Plus performante car l'expression n'est évaluée qu'une seule fois.

switch(expr){
case val1 : ... break;
case val2 : ... break;
case val3 : ... break;
default ...
}
break sert à sortir du bloc switch. Si on l'omet, et que par exemple l'expression vaut val2, alors le case val1 sera sauté, par contre toutes les autres instructions du switch seront évaluées (ça peut être intentionnelle, par exemple quand plusieurs valeurs de l'expression doivent être traité de la même façon).

Boucles

Il existe trois façons de répéter du code en C : while(), for(...) et do...while(). Pour les débutants, il est conseillé de n'apprendre que la boucle for, car sa syntaxe force à bien réfléchir au conditions d'entrée dans la boucle, de continuation et à ce qui est fait systématiquement à chaque passage. Et ces trois informations sont visibles sur une seule lignes au début de la boucle, alors que dans le cas du while, par exemple, il faut parfois lire plusieurs lignes de code attentivement pour comprendre le fonctionnement de la boucle.

for( instruction1,instruction2,... ; condition ; instructionA, instructionB, instructionC,...){
corps de la boucle;
}
les instructions numérotées ne sont exécutées qu'une seule fois, avant d'entrer dans la boucle. Ensuite la condition est évaluée, si elle vaut 1, on exécute les instructions du corps de la boucle, puis les instructions lettrées (A,B...), puis seulement on réévalue la condition. Si elle vaut 1 on réexecute le corps de la boucle etc.

Aller plus loin...

Les cours de C en ligne sont très nombreux sur internet. N'hésitez pas à chercher, par exemple : Ici une page bien mieux faites que celle-ci sur les instructions conditionnelles.

Tableaux

Déclaration

Si on a besoin de plusieurs variables de mêmes type, on peut les déclaré en une seule instruction :

int tab[10]; /* declare 10 entiers */
Les dix entiers sont déclarés sur la pile, ils sont consécutifs en mémoire. Un seul nom est réservé : tab.

  • Pour accéder au premier entier, on utilise la notation tab[0].
  • Pour accéder au 9e, on utilise la notation tab[8].
  • etc.

Pour initialiser un tableau, on peut utiliser la notation :

int tab[3] = {1,2,3};
qui peut en fait également s'écrire :
int tab[] = {1,2,3};
la taille est alors déduite directement du nombre de valeurs dans les accolades à droite.

En fait les tableaux, c'est des pointeurs

En réalité, ce qui est masqué par l'écriture ci-dessus, c'est qu'un tableau n'est en fait qu'une variable particulière, qui contient l'adresse en mémoire du premier élément. Les éléments suivants sont retrouvés car on sait qu'ils sont consécutifs, et on connait la taille en mémoire d'un élément (ce sont tous les même).

Une variable qui contient une adresse en mémoire associée à un type (donc à une taille), s'appelle en C un pointeur.

On déclare un pointeur comme ceci : int * pointeur;

On réserve sur la pile la taille nécessaire pour stocker une adresse (32bits ou 64bits). On réserve un nom (ici "pointeur") associé à cette adresse sur la pile. Et on informe le compilateur, que la zone pointée, c'est à dire les données qui se trouvent à l'adresse codée dans la variable pointeur, est censé contenir un entier (donc au moins être de taille sizeof(int)).

Le compilateur comprend les opérations arithmétiques sur les pointeurs. pointeur+1, est compris car le compilateur sait que pointeur pointe sur un entier. Donc pointeur+1 veut dire : ajouter à l'adresse contenue dans pointeur sizeof(int).

Il existe deux opérateurs spéciaux pour manipuler les pointeurs : * et &

  • & s'utilise sur n'importe quelle variable et permet de récupérer son adresse.
  • * s'utilise sur un pointeur et permet de désigner la variable qui se trouve à l'adresse codée par le pointeur.

Enfin la dernière chose à savoir est qu'un pointeur peut pointer... sur un pointeur ! int ** tab; par exemple. tab est un pointeur, qui contient l'adresse d'un pointeur, qui contient l'adresse d'un entier.

Revenons à notre tableaux :

int tab[] = {1,2,3};
est en fait plus ou moins équivalent à :
int var1, var2, var3;
int * tab = &var1;
*tab=1;
*(tab+1)=2;
*(tab+2)=3;
à la seule différence qu'ici on est obligé de réserver des noms 'var1' 'var2', 'var3' alors que dans le premier cas ce n'est pas nécessaire.

Donc, à retenir :
  • tab[0]; équivaut à *tab;
  • tab[4]; équivaut à *(tab+4); (sauf pour la déclaration où le 4 veut dire 4 cases)

Parcours d'un tableau

Il est tout à fait possible d'écrire le code suivant :

int tab[2];
int a = tab[14];
Le résultat dépendra de l'état de la mémoire de la machine. Ce code réserve la place pour deux entiers et réserve le nom tab pour un pointeur qui contient l'adresse du premier entier. Il est tout a fait possible du coup de réserver ensuite un emplacement pour un entier, de l'appeler a, et de lui assigner ce qui se trouve à l'adresse pointé par tab[0] plus 14 fois la taille d'un entier. Ce qui risque de se passer, c'est que 14 fois la taille d'un entier plus loin, on ne soit plus dans l'espace pré-reserver pour la pile, et qu'on arrive donc à une adresse mémoire déjà réservé par un autre processus : le segmentation fault n'est pas loin !

Voila pourquoi lorsqu'on veut parcourir un tableau, il faut savoir quand s'arrêter ! Un tableau intrinsèquement n'a pas de fin, puisqu'il s'agit simplement de l'adresse du premier élément. Il existe trois stratégies pour savoir quand s'arrêter. Je vous renvois au cours (ca vous fera une bonne occasion de le lire).

Tableaux et fonctions

Comme les tableaux sont en fait des pointeurs déguisés, si une fonction prend en paramètre un tableau, elle prend en réalité l'adresse de la première case. Si elle effectue des changements sur le tableaux, ceux-ci seront alors répercutés sur le tableau de la fonction appelante !

exemple :
Warning: file_get_contents(passage_addresse_1.c): failed to open stream: No such file or directory in /mnt/tanenbaum/sd3/thesards/masson/W3/v2/Teachings/PR-IT-4205/C/5Structures, Dynamic Allocation.../content.php on line 264

  1.  
Ce code affiche 11,12,13. Remarquez les notations int * tab et int tab[] qui sont utilisées indifféremment, de même que tab[x] ou *(tab+x).

Étudions ce qui se passe sur la pile :

Tableaux à deux dimensions

On peut également utiliser une notation avec deux crochets. Ce n'est pas du tout équivalent à un pointeur vers un pointeur.
int tab[2][3];
est en réalité équivalent à :
int tab[6];
La notation tab[x][y] sera alors convertie en tab[x*y]. Voir le cours.

Passage de paramètres par adresse

Ce que l'on peut faire avec les tableaux est en fait généralisable à une seule variable (tableau à une seule case) et s'écrit alors plus lisiblement directement avec la syntaxe classique des pointeurs :
  1.  
  2. #include <stdio.h>
  3. void fonction(int * a){
  4. *a=2;
  5. }
  6.  
  7. int main(){
  8. int a=1;
  9. fonction(&a);
  10. printf("%d\n",a);/* affiche 2 */
  11. return 0;
  12. }

Notez que dans le main la variable a est de type int, alors que dans la fonction la variable a est de type int* (pointeur sur entier).

Les chaînes de caractères

Il n'existe pas en C de type pour représenter les mots (String dans d'autres langages. On utilise simplement des tableaux de char.

De plus, pour manipuler ces tableaux sans gaspiller de mémoire, on dissocie la taille de la chaine de caractère, de la taille du tableau (qui sert de contenant).

On utilise un tableau d'une taille N, et si la chaine fait comme taille n, alors les n premières cases contiennent les caractères de la chaine et la n+1 case contient le caractère spéciale \0, pour marquer la fin de cette chaine.

C'est tout ce qu'il y a a savoir sur les chaine de caractères !

Exercices

Pour chaque fonction, vous pouvez essayez de donner la version récurssive et la version itérative.
  1. Ecrire une fonction qui affiche un tableau d'entier. La taille est passée en paramètre.
  2. Ecrire une fonction qui prend un tableau de réels et renvoit le plus grand.
  3. Ecrire une fonction qui prend deux tableaux d'entiers et recopie le premier dans le second. Réfléchissez au paramètres pour gérer correctement les cas où les deux tableaux ne font pas la même taille.
  4. Ecrire une fonction qui affiche une chaine de caractère à l'envers (utiliser la programmation récurssive pour ne parcourir la chaine qu'une seule fois.
  5. Ecrire une fonction qui prend deux chaines de caractère et renvoit la taille du plus long prefixe commun
  6. Ecrire une fonction qui prend deux chaines de caractère et renvoit la taille du plus long suffixe commun
  7. Ecrire une fonction swap qui échange la valeur de deux variables entières.