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 :