#include <stdio.h> void affiche_calcul(float,float); /* prototype */ float produit(float,float); int varglob; void main(void) { float a,b; /* déclaration locale */ varglob=0; puts("veuillez entrer 2 valeurs"); scanf("%f %f",&a,&b); affiche_calcul(a,b); printf("nombre d'appels à produit : %d\n",varglob); } float produit(float r, float s) { varglob++; return(r*s); } void affiche_calcul(float x,float y) { float varloc; varloc=produit(x,y); varloc=produit(varloc,varloc); printf("le carré du produit est %f\n",varloc); }Un programme C est composé :
* de directivess du pré-processeur, commençant par #, terminées par un retour à la ligne (pas de ;)
* de déclarations globales (terminées par un ;)
* d'une suite de fonctions, écrites les unes après les autres (sans imbrication comme on le ferait en Pascal)
Les fonctions sont écrites sous la forme : entête { corps }
L'entête est de la forme : type_résultat nom (arguments) . Le type_résultat n'était obligatoire (avant la norme ANSI) que s'il était différent de int (entier). Il doit désormais être void (rien) si la fonction ne renvoie rien (dans un autre langage on l'aurait alors appelé sous-programme, procédure, ou sous-routine). Les arguments, s'ils existent, sont passés par valeur. Si la fonction ne nécessite aucun argument, il faut indiquer (void) d'après la norme ANSI, ou du moins (),
Le corps est composé de déclarations de variables locales, et d'instructions, toutes terminées par un ;
Exemple : int a; a est une variable entière, le compilateur va lui réserver en mémoire la place nécessaire à un entier (2 octets en Turbo C). Le nom de cette variable est choisi par le programmeur. On préfère utiliser le terme identificateur plutôt que le nom, car il permet d'identifier tout objet que l'on voudra utiliser (pas seulement les variables).
Les identificateurs doivent suivre quelques règles de base : il peut être formé de lettres (A à Z), de chiffres et du caractère _ (souligné). Le premier caractère doit être une lettre (ou _ mais il vaut mieux le réserver au compilateur). Par exemple valeur1 ou prem_valeur sont possibles, mais pas 1ere_valeur. En C, les minuscules sont différentes des majuscules (SURFace et surFACE désignent deux objets différents). Le blanc est donc interdit dans un identificateur (utilisez _). Les lettres accentuées sont également interdites. La plupart des compilateurs acceptent n'importe quelle longueur d'identificateurs (tout en restant sur la même ligne) mais seuls les 32 premiers caractères sont significatifs.
On considère comme blanc : soit un blanc (espace), soit un retour à la ligne, soit une tabulation, soit un commentaire, soit plusieurs de ceux-ci. Les commentaires sont une portion de texte commençant par /* et finissant par le premier */ rencontré. les commentaires ne peuvent donc pas être imbriqués. mais un commentaire peut comporter n'importe quel autre texte, y compris sur plusieurs lignes.
Un identificateur se termine soit par un blanc, soit par un signe non autorisé dans les identificateurs (parenthèse, opérateur, ; ...). Le blanc est alors autorisé mais non obligatoire.
L'endroit où le compilateur a choisi de mettre la variable
est appelé adresse de la variable (c'est
en général un nombre, chaque mémoire d'un ordinateur
étant numérotée de 0 à ? ). Cette adresse ne
nous intéresse que rarement de manière explicite, mais souvent
de manière indirecte. Par exemple, dans un tableau, composé
d'éléments consécutifs en mémoire, en connaissant
son adresse (son début), on retrouve facilement l'adresse des différentes
composantes par une simple addition.
On appelle pointeur
une variable dans laquelle on place (mémorise) une adresse de variable
(où elle est) plutôt qu'une valeur (ce qu'elle vaut).
Les types de variables scalaires simples que l'on utilise le plus couramment sont le char (un caractère), l'int (entier) et le float (réel). Le char est en fait un cas particulier des int, chaque caractère étant représenté par son numéro de code ASCII.
Une expression de base peut donc être un appel
à une fonction (exemple sin(3.1416) ).
Une fonction est un bout de programme
(que vous avez écrit ou faisant partie d'une bibliothèque)
auquel on "donne" des valeurs (arguments), entre
parenthèses et séparés par des virgules. La fonction
fait un calcul sur ces arguments pour "retourner" un résultat. Ce
résultat pourra servir, si nécessaire, dans une autre expression,
voire comme argument d'une fonction exemple atan(tan(x)).
Les arguments donnés à l'appel de la fonction
(dits paramètres réels ou effectifs)
sont recopiés dans le même ordre dans des copies (paramètres
formels), qui elles ne pourront que modifier les copies (et pas les paramètres
réels).
Dans le cas de fonctions devant
modifier une variable, il faut fournir en argument l'adresse
(par l'opérateur &,
voir plus bas), comme par exemple pour scanf.
Pour former une expression, les opérateurs possibles sont assez nombreux, nous allons les détailler suivant les types de variables qu'ils gèrent.
Ces opérateurs nécessitent deux arguments, placés de part et d'autre de l'opérateur. Ce sont + (addition), - (soustraction), * (produit), / (division), % (reste de la division). % nécessite obligatoirement deux arguments entiers, les autres utilisent soit des entiers, soit des réels. Les opérandes doivent être du même type, le résultat sera toujours du type des opérandes. Lorsque les deux opérandes sont de type différent (mais numérique évidement), le compilateur prévoit une conversion implicite (vous ne l'avez pas demandée mais il la fait néanmoins) suivant l'ordre : { char -> int -> long -> float -> double } et { signed -> unsigned }. On remarque qu'il considère les char comme des entiers, les opérations sont en fait faites sur les numéros de code (ASCII). Les calculs arithmétiques sont faits uniquement soit en long soit en double, pour éviter des dépassements de capacité.
exemples :
int a=1,b=2,c=32000;
float x=1,y=2;
a=(c*2)/1000; /* que des int, le résultat est 64, bien que
l'on soit passé par un résultat intermédiaire (64000)
qui dépassait la capacité des entiers (mais pas celle des
long) */
b=7/b; /* signe = donc en premier calcul de l'argument à
droite : 7 (entier) / 2 (entier) donne 3 (entier, reste 1, que l'on obtient
par 5%2). donc b=3 */
x=7/b; /* 7 et b entiers => passage en réel inutile, calcul
de 7/3 donne 2 (entier, reste 1) puis opérateur = (transformation
du 2 en 2.0 puis transfert dans X qui vaut donc 2.0) */
x=7/y; /* un int et un float autour de / : transformation implicite
de 7 en réel (7.0), division des deux réel (3.5), puis transfert
dans x */
x=((float)(a+1))/b; /* calcul (entier) de a+1, puis transformation
explicite en float, et donc implicite de b en float, division 65.0/3.0
-> 21.666... */
Opérateur unaire : ! (non). !arg vaut 1 si arg vaut 0, et 0 sinon.
Opérateurs deuxaires : && (ET, vaut 1 si les 2 opérandes sont non nuls, 0 sinon) et || (OU, vaut 0 si les deux opérandes sont nuls, 1 sinon). Le deuxième opérande n'est évalué que si le premier n'a pas suffi pour conclure au résultat (ex (a= =0)&&(x++<0) incrémente x si a est nul, le laisse intact sinon).
exemples : 7&12 donne 4 (car 0111&1100 donne 0100); ~0 donne -1 (tous les bits à 1, y compris celui de signe); 8>>2 donne 32.
Exemples : a=5 (met la valeur 5 dans la variable a. Si a est float, il y a conversion implicite en float); b=(a*5)/2 (calcule d'abord la Rvalue, puis met le résultat dans b); a=5+(b=2) (Le compilateur lit l'expression de gauche à droite. la première affectation nécessite le calcul d'une Rvalue : 5+(b=2). Celle ci comporte une addition, dont il évalue le premier opérande (5) puis le second (b=2). Il met donc 2 dans b, le résultat de l'opération est 2, qui sera donc ajouté à 5 pour être mis dans a. A vaut donc 7 et b, 2. Le résultat de l'expression est 7 (si l'on veut s'en servir).
Remarque : il ne faut pas confondre = et = =. Le compilateur ne peut pas remarquer une erreur (contrairement au Pascal ou Fortran) car les deux sont possibles. Exemple : if (a=0) est toujours faux car quelle que soit la valeur initiale de a, on l'écrase par la valeur 0, le résultat de l'opération vaut 0 et est donc interprété par IF comme faux.
a++ : ajoute 1 à la variable a. Le résultat de l'expression est la valeur initiale de a (c'est à dire avant incrémentation). C'est l'incrémentation postfixée.
de même, la décrémentation --a et a-- soustrait 1 à a.
exemple : j=++i est équivalent à j=(i=i+1)
a+=5 est équivalent à a=(a+5). Il faut encore ici une Rvalue à droite et une Lvalue à gauche.
opérateurs | associativité | description |
() [] -> . | -> | |
! ~ ++ -- - + & * (cast) | <- | unaires (* pointeurs) |
* / % | -> | multiplicatifs |
+ - | -> | addition |
>> << | -> | décalages |
< <= > >= | -> | relations d'ordre |
= = != | -> | égalité |
& | -> | binaire |
^ | -> | binaire |
| | -> | binaire |
&& | -> | logique |
| | | -> | logique |
? : | -> | conditionnel (ternaire) |
= += -= *= etc. | <- | affectation |
, | <- | séquentiel |
Dans ce tableau, les opérateurs sont classés par priorité décroissante (même priorité pour les opérateurs d'une même ligne). Les opérateurs les plus prioritaires se verront évaluer en premier. L'associativité définit l'ordre d'évaluation des opérandes. La plupart se font de gauche à droite ( 4/2/2 donne (4/2)/2 donc 1 (et pas 4/(2/2))).
Les seules exceptions sont :
- soit une expression (pouvant comprendre
une affectation, un appel de fonction...), terminé par un ; qui
en fait signifie "on peut oublier le résultat de l'expression et
passer à la suite",
- soit une structure de contrôle (boucle,
branchement...),
- soit un bloc d'instructions : ensemble de déclarations
et instructions délimités par des accolades {}. Un bloc sera
utilisé à chaque fois que l'on désire mettre plusieurs
instructions là où on ne peut en mettre qu'une.
Seule la première forme est terminée par un ;. Un cas particulier est l'instruction vide, qui se compose uniquement d'un ; (utilisé là où une instruction est nécessaire d'après la syntaxe).
Tant que l'expression est vraie (!=0), on effectue l'instruction, qui peut être simple (terminée par ;), bloc (entre {}) ou vide (; seul). L'expression est au moins évaluée une fois. Tant qu'elle est vraie, on effectue l'instruction, dès qu'elle est fausse, on passe à l'instruction suivante (si elle est fausse dès le début, l'instruction n'est jamais effectuée).
exemple :
#include <stdio.h> void main(void) { float nombre,racine=0; puts("entrez un nombre réel entre 0 et 10"); scanf("%f",&nombre); while (racine*racine<nombre) racine+=0.01; printf("la racine de %f vaut %4.2f à 1%% près\n", nombre, racine); }Exercice (while_puiss) : faire un programme qui affiche toutes les puissances de 2, jusqu'à une valeur maximale donnée par l'utilisateur. On calculera la puissance par multiplications successives par 2. Cliquez ici pour une solution.
Exercice (while_err) : que fait ce programme ?
#include <stdio.h> #include <math.h> #define debut 100 #define pas 0.01 void main(void) { float nombre=debut; int compte=0,tous_les; puts("afficher les résultats intermédiaires tous les ? (333 par exemple) ?"); scanf("%d",&tous_les); while (fabs(nombre-(debut+(compte*pas)))<pas) { nombre+=pas; if (!(++compte%tous_les)) printf("valeur obtenue %12.8f, au lieu de %6.2f en %d calculs\n", nombre,(float)(debut+(compte*pas)), compte); } printf("erreur de 100%% en %d calculs\n",compte); }Cliquez ici pour une solution.
comme while, mais l'instruction est au moins faite une fois, avant la première évaluation de l'expression.
exemple :
#include <stdio.h> void main(void) { int a; do { puts("entrez le nombre 482"); scanf("%d",&a); } while (a!=482); puts("c'est gentil de m'avoir obéi"); }Exercice (do_while) : écrivez un programme de jeu demandant de deviner un nombre entre 0 et 10 choisi par l'ordinateur. On ne donnera pas d'indications avant la découverte de la solution, où l'on indiquera le nombre d'essais. La solution sera choisie par l'ordinateur par la fonction rand() qui rend un entier aléatoire (déclarée dans stdlib.h). Cliquez ici pour une solution.
Cette boucle est surtout utilisée lorsque l'on connaît à l'avance le nombre d'itération à effectuer. L'expr_initiale est effectuée une fois, en premier. Puis on teste la condition. On effectue l'instruction puis l'incrémentation tant que la condition est vraie. L'instruction et l'incrémentation peuvent ne jamais être effectuées. La boucle est équivalente à :
expr_initiale; while (expr_condition) { instruction expr_incrémentation; }Une ou plusieurs des trois expressions peuvent être omises, l'instruction peut être vide. for(;;); est donc une boucle infinie.
exemple :
Exercice (for) : faire un programme qui calcule la moyenne de N notes. N et les notes seront saisies par scanf. Le calcul de la moyenne s'effectue en initialisant une variable à 0, puis en y ajoutant progressivement les notes saisies puis division par N. Cliquez ici pour une solution.
ou : if (expression) instruction1 else instruction2
Si l'expression est vraie (!=0) on effectue l'instruction1, puis on passe à la suite. Sinon, on effectue l'instruction 2 puis on passe à la suite (dans le cas sans else on passe directement à la suite).
Exercice (jeu) : modifier le jeu de l'exercice (do_while) en précisant au joueur à chaque essai si sa proposition est trop grande ou trop petite. Cliquez ici pour une solution.
L'instruction d'un if peut être un autre if (imbriqué)
exemple :
if(c1) i1; else if (c2) i2; else if (c3) i3; else i4; i5;si c1 alors i1 puis i5, sinon mais si c2 alors i2 puis i5, ... Si ni c1 ni c2 ni c3 alors i4 puis i5.
Le else étant facultatif, il peut y avoir une ambiguïté s'il y a moins de else que de if. En fait, un else se rapporte toujours au if non terminé (c'est à dire à qui on n'a pas encore attribué de else) le plus proche. On peut aussi terminer un if sans else en l'entourant de {}.
exemple : if(c1) if(c2) i1; else i2; : si c1 et c2 alors i1, si c1 et pas c2 alors i2, si pas c1 alors (quel que soit c2) rien.
if (c1) {if (c2) i1;} else i2; : si c1 et c2 alors i1, si c1 et pas c2 alors rien, si pas c1 alors i2.
structure :
switch (expression_entière) { case cste1:instructions case cste2:instructions ........ case csteN:instructions default :instructions }L'expression ne peut être qu'entière (char, int, long). L'expression est évaluée, puis on passe directement au "case" correspondant à la valeur trouvée. Le cas default est facultatif, mais si il est prévu il doit être le dernier cas.
exemple : fonction vérifiant si son argument c est une voyelle.
int voyelle(char c) { switch(c) { case 'a': case 'e': case 'i': case 'o': case 'u': case 'y':return(1); /* 1=vrai */ default :return(0); } }Remarque : l'instruction break permet de passer directement à la fin d'un switch (au } ). Dans le cas de switch imbriqués on ne peut sortir que du switch intérieur.
Exemple :
switch (a) { case 1:inst1;inst2;....;break; case 2:....;break; default:..... } /*endroit où l'on arrive après un break*/Exercice (calcul) : faire un programme simulant une calculatrice 4 opérations. Cliquez ici pour une solution.
exemples :
do {if(i= =0)break;....}while (i!=0); /* un while aurait été
mieux */
for (i=0;i<10;i++){....;if (erreur) break;} /* à
remplacer par
for(i=0;(i<10)&&(!erreur);i++){...} */
exemple :
for (i=0;i<10;i++) {if (i= =j) continue; ...... }
peut être remplacé par :
for (i=0;i<10;i++) if (i!=j) { ...... }
Label est un identificateur (non déclaré, mais non utilisé pour autre chose), suivi de deux points (:), et indiquant la destination du saut. Un goto permet de sortir d'un bloc depuis n'importe quel endroit. Mais on ne peut entrer dans un bloc que par son { (qui créera proprement les variables locales du bloc).
{..... {..... goto truc; ..... } ..... truc: ..... }
structure : return ou return(valeur)
exemple :
int max(int a, int b) {if (a>b) return(a); else return(b);}
structure : exit(); ou exit(valeur);
Exemple :
#include <stdio.h> int doubl(int b) {int c; c=2*b; b=0; return(c); } void main(void) { int a=5; printf("%d %d\n",doubl(a),a); }A l'entrée du bloc main, création de a, à qui l'on donne la valeur 5. Puis appel de doubl : création de b au sommet de la pile, on lui donne la valeur de a. Puis entrée dans le bloc, création sur la pile de c, on lui donne la valeur b*2=10, on annule b (mais pas a), on rend 10 à la fonction appelante, et on libère le sommet de la pile (c et b n'existent plus) mais a reste (avec son ancienne valeur) jusqu'à la sortie de main. On affichera donc : 10 5.
Une variable locale est créée à l'entrée du bloc, et libérée à la sortie. Cette période est appelée sa durée de vie. Mais pendant sa durée de vie, une variable peut être visible ou non. Elle est visible : dans le texte source du bloc d'instruction à partir de sa déclaration jusqu'au }, mais tant qu'une autre variable locale de même nom ne la cache pas. Par contre elle n'est pas visible dans une fonction appelée par le bloc (puisque son code source est hors du bloc).
autre exemple :
void main(void); {int a=1; [1] {int b=2; [2] {int a=3; [3] fonction(a); [4] } [5] fonction(a); [6] } [7] } [8] int fonction (int b) [a] {int c=0; [b] c=b+8; [c] } [d]analysons progressivement l'évolution de la pile au cours du temps (en gras : variable visible) :
[1] a=1
[2] a=1 | b=2
[3] a=1 | b=2 | a=3 : seul le a le plus haut est visible
(a=3), l'autre vit encore (valeur 1 gardée) mais n'est plus visible.
[4a] a=1 | b=2 | a=3 | b=3 : entrée dans la fonction,
recopie de l'argument réel (a) dans l'argument formel (b). Mais
a n'est plus visible.
[4b] a=1 | b=2 | a=3 | b=3 | c=0
[4c] a=1 | b=2 | a=3 | b=3 | c=11 : quand le compilateur
cherche la valeur de b, il prend la plus haute de la pile donc 3 (c'est
la seule visible), met le résultat dans le c le plus haut. L'autre
b n'est pas modifié.
[4d] a=1 | b=2 | a=3 : suppression des variables locales
b et c du sommet de la pile
[5] a=1 | b=2 : sortie de bloc donc libération
de la pile
[6a] a=1 | b=2 | b=1 : l'argument réel (a) n'est plus
le même qu'en [4a]
[6b] a=1 | b=2 | b=1 | c=0
[6c] a=1 | b=2 | b=1 | c=9
[6d] a=1 | b=2 : suppression b et c
[7] a=1
[8] la pile est vide, on quitte le programme
Notez que la réservation et l'initialisation prennent un peu de temps à chaque entrée du bloc. Mais ne présumez jamais retrouver une valeur sur la pile, même si votre programme n'a pas utilisé la pile entre temps (surtout sur système multitâche ou avec mémoire virtuelle).
Une déclaration a toujours la structure suivante :
[classe] type liste_variables [initialisateur]; (entre [] facultatif)
Le type peut être simple (char, int, float,...) ou composé (tableaux, structures..., voir plus loin).
La liste_variables est la liste des noms des variables désirées, séparées par des virgules s'il y en a plusieurs. Chaque nom de la liste peut être précédés d'une *, ceci spécifiant un pointeur.
L'initialisateur est un signe =, suivi de la valeur à donner à la variable lors de sa création (à chaque entrée du bloc par exemple). La valeur peut être une constante, mais aussi une expression avec des constantes voire des variables (visibles, déjà initialisées).
Par défaut, la variable est publique, c'est à dire qu'elle pourra même être visible dans des fichiers compilés séparément (et reliés au link).
La classe static, par contre, rend la visibilité de la variable limitée au fichier actuel.
La classe extern permet de déclarer une variable d'un autre fichier (et donc ne pas lui réserver de mémoire ici, mais la rendre visible). Elle ne doit pas être initialisée ici. Une variable commune à plusieurs fichiers devra donc être déclarée sans classe dans un fichier (et y être initialisée), extern dans les autres.
Toute fonction, pour pouvoir être utilisée, doit également être déclarée. Une déclaration de fonction ne peut être que globale, et connue des autres fonctions. Une déclaration de fonction est appelée "prototype". Le prototype est de la forme :
Sans précision de classe, la fonction est publique. Sinon, la classe peut être extern (c'est ce que l'on trouve dans les fichiers .H) ou static (visibilité limitée au fichier). Le prototype peut être utilisé pour utiliser une fonction du même fichier, mais avant de l'avoir définie (par exemple si l'on veut main en début du fichier). En général, lorsque l'on crée une bibliothèque (groupe de fonctions et variables regroupées dans un fichier compilé séparément), on prépare un fichier regroupant toutes les déclarations extern, noté .H, qui pourra être inclus dans tout fichier utilisant la bibliothèque.
exemples de déclarations globales :
int i,j; /* publiques, initialisées à 0 */
static int k=1; /* privée, initialisée à 1
*/
extern int z; /* déclarée (et initialisée)
dans un autre fichier */
float produit(float,float); /* prototype d'une fonction définie
plus loin dans ce fichier */
extern void échange(int *a, int *b); /* prototype d'une
fonction définie dans un autre fichier */
Avant la norme ANSI, le prototype n'existait pas. Une fonction non définie auparavant était considérée comme rendant un int (il fallait utiliser un cast si ce n'était pas le cas).
structure : typedef type_de_base nouveau_nom;
Ceci permet de donner un nom à un type donné, mais ne crée aucune variable. Une déclaration typedef est normalement globale et publique.
exemple :
typedef long int entierlong; /* définition d'un nouveau
type */
entierlong i; /* création d'une variable i de type entierlong
*/
typedef entierlong *pointeur; /* nouveau type : pointeur = pointeur
d'entierlong */
pointeur p; /* création de p (qui contiendra une adresse), peut
être initialisé par =&i */
Remarque : Le premier typedef pouvait être remplacé par un #define mais pas le second.
entête : type_retourné nom_fonction(liste_arguments) (pas de ;)
Avant la norme ANSI, le type_retourné pouvait être omis si int. Désormais il est obligatoire, si la fonction ne retourne rien on indique : void.
La liste_argumentsdoit être typée
(ANSI), alors qu'auparavant les types étaient précisés
entre l'entête et le bloc :
ANSI: float truc(int a, float b) {bloc}
K&R: float truc(a,b) int a;float b; {bloc}
Si la fonction n'utilise pas d'arguments il faut la déclarer (ANSI) nom(void) ou (K&R) nom(). L'appel se fera dans les deux cas par nom() (parenthèses obligatoires).
Les arguments (formels) sont des variables locales à la fonction. Les valeurs fournies à l'appel de la fonction (arguments réels) y sont recopiés à l'entrée dans la fonction. Les instructions de la fonction s'exécutent du début du bloc ({) jusqu'à return(valeur) ou la sortie du bloc (}). La valeur retournée par la fonction est indiquée en argument de return.
exemple :
float produit(float a;float b) { float z; z=a*b; return(z); }
int factorielle(int i) { if (i>1) return(i*factorielle(i-1)); else return(1); }analysons la pile en appelant factorielle(3) :
i=1 | |||||
i=2 | i=2 | i=2 | |||
i=3 | i=3 | i=3 | i=3 | i=3 | |
(a) | (b) | (c) | (d) | (e) |
int factorielle(int i) { int result; for(result=1;i>1;i--) result*=i; return(result); }
void échange(int i;int j)
{int k;k=i;i=j;j=k;}
Lors d'un appel à cette fonction par échange(x,y), les variables locales i,j,k sont créées sur la pile, i vaut la valeur de x, j celle de y. Les contenus de i et j sont échangés puis la pile est libérée, sans modifier x et y. Pour résoudre ce problème, il faut passer par des pointeurs. On utilisera les opérateurs unaires : & (adresse de) et * (contenu de). Définissons donc la fonction ainsi :
void échange(int *i;int *j)
{int k;k=*i;*i=*j;*j=k;}
On appelle la fonction par échange(&x,&y); les deux arguments formels de la fonction (i et j) sont des pointeurs sur des int, c'est à dire qu'à l'appel, on crée sur la pile des variables i et j pouvant contenir une adresse, dans i on recopie l'argument réel qui et l'adresse de x, et l'adresse de y dans j. en entrant dans le bloc, on crée une variable locale k pouvant contenir un entier. Puis on met dans k non pas la valeur de i mais le contenu pointé par i (donc ce qui se trouve à l'adresse marquée dans i, qui est l'adresse de x), donc le contenu de x. On place la valeur pointée par j (donc y) à l'adresse pointée par j (donc x). Puis on place la valeur de k à l'adresse pointée par j (y). On a donc échangé x et y.
On a tout intérêt à essayer cet exemple en se fixant des adresses et des valeurs, et voir l'évolution des contenus des variables.
En conclusion, pour effectuer un passage d'argument par adresse, il suffit d'ajouter l'opérateur & devant l'argument réel (à l'appel de la fonction), et l'opérateur * devant chaque apparition de l'argument formel, aussi bien dans l'entête que le bloc de la fonction.
De même, le système d'exploitation peut transmettre
des arguments au programme. La déclaration complète de l'entête
de la fonction main est :
Le dernier argument est optionnel).
On peut aussi utiliser char **argv, mais cela peut paraître moins clair. argc indique le nombre de mots de la ligne de commande du système d'exploitation, argv est un tableau de pointeurs sur chaque mot de la ligne de commande, env pointe sur les variables de l'environnement (sous MSDOS obtenues par SET, mais aussi très utilisées sous UNIX par env).
Si votre programme s'appelle COPIER, et que sous MSDOS vous ayez entré la commande COPIER TRUC MACHIN alors argc vaut 3, argv[0] pointe sur "COPIER", argv[1] sur "TRUC" et argv[2] sur "MACHIN". argv[3] vaut le pointeur NULL. env est un tableau de pointeurs sur les variables d'environnement, on n'en connaît pas le nombre mais le dernier vaut le pointeur NULL.
exemple :
int *max(int tableau[], int taille) { int i,*grand; for(grand=tableau,i=1;i<taille;i++) if(tableau[i]>*grand) grand=tableau+i; return(grand); }Cette fonction rend l'adresse du plus grand entier du tableau.
type (*fonc)(arguments) est un pointeur sur une fonction
exemple :
int max(int,int); int min(int,int); void main(void); { int (*calcul)(int,int); /* calcul est un pointeur donc une variable qui peut être locale */ char c; puts("utiliser max (A) ou min (I) ?"); do c=getchar(); while ((c!='A')&&(c!='I')); calcul=(c= ='A')?&max:&min; printf("%d\n",(*calcul)(10,20)); } int max(int a,int b) {return(a>b?a:b);} int min(int a,int b) {return(a<b?a:b);}Cette fonctionnalité du C est assez peu utilisée, mais est nécessaire dans les langages orientés objets.
Désormais certains compilateurs considèrent short comme 16 bits, int comme 32 bits et long comme 64 bits.
type | taille (en bits) | plage de valeurs |
char | 8 | -128 à +127 |
unsigned char | 8 | 0 à 255 |
short (short int) | 16 | -32768 à 32767 |
unsigned short | 16 | 0 à 65535 |
long (long int) | 32 | -2.147.483.648 à 2.147.483.647 |
unsigned long | 32 | 0 à 4.294.967.295 |
float | 32 | -3.4e38 à 3.4e38 (7 chiffres significatifs) |
double (long float) | 64 | -1.7e308 à 1.7e308 (15 chiffres significatifs) |
long double (non standard) | 80 | 3.4E-4932 à 1.1E+4932 (19 chiffres signif.) |
Attention la transformation n'est effectuée que le plus tard possible, si nécessaire. 5/2+3.5 donnera donc 5.5. De plus les opérations arithmétiques sont toujours effectuées sur des long ou double, pour une précision maximale quels que soient les résultats intermédiaires (voir exemples au chapitre expressions arithmétiques).
On peut forcer une transformation en utilisant le cast, qui est un opérateur unaire. La syntaxe est : (type_résultat) valeur_à_transformer
exemple : {float x;int a=5; x=(float)a;}
Un cast transformant un réel en entier prendra la partie entière. Cette transformation doit être explicite, elle est impossible implicitement. Pour obtenir l'entier le plus proche , utiliser (int)(réel_positif+0.5).
Il faut bien noter que le cast n'est une opération de transformation que pour les types scalaires, pour tous les autres types, le cast ne permet que de faire croire au compilateur que la variable est d'un autre type que ce qu'il attendait, pour qu'il n'émette pas de message d'erreur (à utiliser avec grande prudence).
exemple : enum jour {lundi, mardi, mercredi, jeudi, vendredi, samedi, dimanche};
On a créé un nouveau type. toute variable de type jour
pourra valoir soit lundi, soit mardi, etc... On peut directement mettre
la liste des variables à créer (entre le "}" et le ";"),
ou indiquer :
enum nomdutype liste_variables;
exemple : enum jour aujourd_hui=mardi;)
En fait le type jour est un type int, avec lundi=0, mardi=1,... On peut donc faire toutes les opérations entières (aujourd_hui++ par exemple). Il n'y a aucun test de validité (dimanche+1 donne 7). Ceci permet de rendre les programmes plus clairs. On obtiendrait un résultat équivalent avec #define. Attention, printf affichera un entier, mais on peut faire:
char *nom[7]={"lundi", "mardi", "mercredi", "jeudi", "vendredi",
"samedi"," dimanche"};
puis printf("%s",nom[aujourd_hui]);
On peut aussi prévoir une codification non continue :
enum truc {a=4,b,c,d=2,e,f} : d=2,e=3,f=a=4,b=5,c=6
En utilisant typedef, on n'a pas besoin
de répéter enum dans la déclaration de variables :
typedef enum {coeur,carreau,pique,trèfle}couleurs;
couleurs i,j,k;
déclaration : [classe] type nom [nombre_d'éléments];
exemple : int tab[10];
Ceci réserve en mémoire un espace contigu pouvant
contenir 10 entiers. Le premier est tab[0], jusqu'à tab[9]. Attention,
en utilisant tab[10] ou plus, aucune erreur ne sera signalée et
vous utiliserez une partie de mémoire qui a certainement été
réservée pour autre chose. Il est possible de définir
un tableau de n'importe quel type de composantes (scalaires, pointeurs,
structures et même tableaux). Il est également possible de
définir un type tableau par typedef :
typedef float vecteur[3];
vecteur x,y,z;
On peut aussi initialiser un tableau. Dans ce cas la dimension
n'est pas nécessaire. Mais si elle est donnée, et est supérieure
au nombre de valeurs données, les suivantes seront initialisées
à 0 :
vecteur vect0={0,0,0};
int chiffres[]={0,1,2,3,4,5,6,7,8,9};
int tableau[20]={1,2,3}; /* les 17 autres à 0 */
On peut également déclarer un tableau sans en donner sa dimension. Dans ce cas là le compilateur ne lui réserve pas de place, elle aura du être réservée autre part (par exemple tableau externe ou argument formel d'une fonction).
Exercice (moyenne) : Ecrire le programme qui lit une liste de Nb nombres, calcule et affiche la moyenne puis l'écart entre chaque note et cette moyenne. Cliquez ici pour une solution.
Déclarons : int TAB[10],i,*ptr;
Ceci réserve en mémoire
- la place pour 10 entiers, l'adresse du début de cette zone
est TAB,
- la place pour l'entier i,
- la place pour un pointeur d'entier (le type pointé est important
pour définir l'addition).
Analysons les instructions suivantes :
ptr=TAB; /*met l'adresse du début du tableau dans ptr*/ for(i=0;i<10;i++) { printf("entrez la %dième valeur :\n",i+1); /* +1 pour commencer à 1*/ scanf("%d",ptr+i); /* ou &TAB[i] puisque scanf veut une adresse*/ } puts("affichage du tableau"); for(ptr=TAB;ptr<TAB+10 /* ou &TAB[10] */;ptr++) printf("%d ",*ptr); puts(" "); /* attention actuellement on pointe derrière le tableau ! */ ptr-=10; /* ou plutôt ptr=TAB qui lui n'a pas changé */ printf("%d",*ptr+1); /* affiche (TAB[0])+1 */ printf("%d",*(ptr+1)); /* affiche TAB[1] */ printf("%d",*ptr++); /* affiche TAB[0] puis pointe sur TAB[1] */ printf("%d",(*ptr)++); /* affiche TAB[1] puis ajoute 1 à TAB[1]*/TAB est une "constante pointeur", alors que ptr est une variable (donc TAB++ est impossible). La déclaration d'un tableau réserve la place qui lui est nécessaire, celle d'un pointeur uniquement la place d'une adresse.
Pour passer un tableau en argument d'une fonction, on ne peut que le passer par adresse (recopier le tableau prendrait de la place et du temps).
exemple utilisant ces deux écritures équivalentes :
#include <stdio.h> void annule_tableau(int *t,int max) { for(;max>0;max--)*(t++)=0; } void affiche_tableau(int t[], int max) { int i; for(i=0;i<max;i++) printf("%d : %d\n",i,t[i]); } void main(void) { int tableau[10]; annule_tableau(tableau,10); affiche_tableau(tableau,10); }Exercice (rotation) : Ecrire un programme qui lit une liste de Nb nombres, la décale d'un cran vers le haut (le premier doit se retrouver en dernier), l'affiche puis la décale vers le bas. On pourra décomposer le programme en fonctions. Cliquez ici pour une solution.
Exercice (classer) : Classer automatiquement un tableau de Nb entiers puis l'afficher dans l'ordre croissant puis décroissant. On pourra utiliser des fonctions de l'exercice précédent. On pourra créer un (ou plusieurs) tableau temporaire (donc local). Si vous vous en sentez la force, prévoyez le cas de valeurs égales. Cliquez ici pour une solution.
exemples :
puts("salut");
char mess[]="bonjour"; /* évite de mettre ={'b','o',..,'r',\0}
*/
puts (mess);
mess est un tableau de 8 caractères (\0 compris). On peut
au cours du programme modifier le contenu de mess, à condition de
ne pas dépasser 8 caractères (mais on peut en mettre moins,
le \0 indiquant la fin de la chaîne). Mais on peut également
initialiser un pointeur avec une chaîne de caractères :
char *strptr="bonjour";
Le compilateur crée la chaîne en mémoire de code (constante) et une variable strptr contenant l'adresse de la chaîne. Le programme pourra donc changer le contenu de strptr (et donc pointer sur une autre chaîne), mais pas changer le contenu de la chaîne initialement créée.
Exercice (chaînes) : écrire un programme qui détermine le nombre et la position d'une sous-chaîne dans une chaîne (exemple ON dans FONCTION : en position 1 et 6). Cliquez ici pour une solution.
- void *malloc(int taille) : réserve une zone mémoire contiguë de taille octets, et retourne un pointeur sur le début du bloc réservé. Retourne le pointeur NULL en cas d'erreur (en général car pas assez de mémoire).
- void *calloc(int nb, int taille) : équivalent à malloc(nb*taille).
exemple :
float *tab; int nb; puts("taille désirée ?"); scanf("%d",&nb); tab=(float*)calloc(nb,sizeof(float));malloc et calloc nécessitent un cast pour que le compilateur ne signale pas d'erreur.
- void free(void *pointeur) libère la place réservée auparavant par malloc ou calloc. Pointeur est l'adresse retournée lors de l'allocation. En quittant proprement le programme, la mémoire allouée est automatiquement restituée même si on omet d'appeler free.
- void *realloc(void *pointeur,int taille) essaie, si possible, de réajuster la taille d'un bloc de mémoire déjà alloué (augmentation ou diminution de taille). Si nécessaire, le bloc est déplacé et son contenu recopié. En retour, l'adresse du bloc modifié (pas nécessairement la même qu'avant) ou le pointeur NULL en cas d'erreur.
Ces fonctions sont définies dans stdlib.h ou alloc.h (suivant votre compilateur).
Une erreur fréquente consiste à "perdre" l'adresse du début de la zone allouée (par tab++ par exemple) et donc il est alors impossible d'accéder au début de la zone, ni de la libérer.
t correspond à l'adresse &t[0][0], mais t[1] est aussi
un tableau (une ligne), donc désigne l'adresse &t[1][0]. En
fait, une matrice est un
tableau de lignes. On peut expliciter cela par typedef
:
typedef int ligne[3];
typedef ligne matrice[2];
En utilisant pointeurs et allocation dynamique, pour gérer un tableau de NBLIG lignes de NBCOL éléments, , on peut :
Exercice (déterminant) : écrire un programme qui calcule le déterminant d'une matrice carrée (N,N), sachant qu'il vaut la somme (sur chaque ligne) de l'élément de la ligne en 1ère colonne par le déterminant de la sous-matrice obtenue en enlevant la ligne et la 1ère colonne (en changeant le signe à chaque fois). Le déterminant d'une matrice (1,1) est sont seul élément. On utilisera bien évidement la récursivité. Il existe (heureusement) d'autres méthodes plus rapides. Cliquez ici pour une solution.
exemple :
struct identite { char nom[30]; char prenom[20]; int age; }jean,jacques,groupe[20];Toute variable de type identité (jean, jacques, groupe[i]) comporte trois champs : nom, prenom et age. sizeof(jacques) retournera 52. Les champs peuvent être de n'importe quel type valable (scalaires, tableaux, pointeurs...), y compris une structure (à condition d'être déclaré plus haut). nom_type et liste_variables sont optionnels mais au moins l'un des deux doit être présent. Les noms de champs ont une portée limitée à la structure (c'est à dire qu'un autre objet peut avoir le même nom, s'il n'est pas cité dans cette structure). Nom_type (ici identite) est le nom d'un nouveau type, il peut être utilisé plus loin pour déclarer d'autres variables, voire d'autres types:
Il est également possible d'utiliser typedef, et (pas sur
tous les compilateurs) d'initialiser une structure :
typedef struct {int jour;int mois;int année;}date;
date aujourdhui={24,12,1992}; /*évite de répéter
struct*/
gets(jean.nom);
printf("initiales : %c %c\n",lui.nom[0],lui.prenom[0]);
printf("nom %s \n",groupe[10].nom);
scanf("%d",&moi.id.age);
Une composante d'enregistrement s'utilise comme une variable du même type (avec les mêmes possibilités mais aussi les mêmes limitations). Depuis la norme ANSI, on peut utiliser l'affectation pour des structures (recopie de tous les champs), ainsi que le passage des structures en arguments de fonction passés par valeur. Sur les compilateurs non ANSI, il faut utiliser des pointeurs.
On utilise des pointeurs de structures
comme des pointeurs sur n'importe quel autre type. L'opérateur ->
permet une simplification d'écriture (il signifie champ pointé)
:
date *ptr;
ptr=(struct date *)malloc(sizeof(date));
*ptr.jour=14;ptr->mois=7;
Les champs sont créés à partir des bits de poids faible. Le nom du champ est optionnel (dans le cas de champs réservés, non utilisés par le programme). Les champs n'ont alors pas d'adresse (impossible d'utiliser & sur un champ). On utilise ces structures comme les autres.
Les différents champs commenceront tous à la même adresse (permet d'utiliser des variables pouvant avoir des types différents au cours du temps, mais un seul à un instant donné). Les champs peuvent être de tout type, y compris structures. On les utilise comme les structures, avec les opérateurs "." et "->".
Exercice (tel) A l'aide d'un tableau de personnes (nom, prénom, numéro dans la rue, rue, code postal, ville, numéro de téléphone), faire un programme de recherche automatique de toutes les informations sur les personnes répondant à une valeur d'une rubrique donnée (tous les PATRICK , tous ceux d'Obernai, etc...). On suppose que le tableau est déjà initialisé. Cliquez ici pour une solution.
Appliquons cela , de manière informatique, à une liste d'entiers, avec pour chaque valeur l'adresse (numéro de mémoire) du suivant :
Si l'on veut insérer une valeur dans la liste, les modifications à apporter sont minimes :
En C on définira un type structure
regroupant une valeur entière et un pointeur
:
struct page {int val; struct page *suivant;
};
Un pointeur (souvent global) nous indiquera toujours le début
de la liste:
struct page *premier;
Au fur et à mesure des besoins on se crée une nouvelle
page :
nouveau=(struct page *)malloc(sizeof(struct page));
En n'oubliant pas de préciser le lien avec le précédent
:
precedent->suivant=nouveau;
le dernier élément ne doit pas pointer sur n'importe quoi, on choisit généralement soit le pointeur NULL, soit le premier (la liste est dite bouclée).
exemple :
#include <stdio.h> #include <conio.h> #include <ctype.h> #include <alloc.h> /*ou stdlib.h*/ struct page {int val; struct page *suivant; }; struct page *premier; int encore(void) /* demande si on en veut encore*/ { printf("encore (O/N) ? "); return(toupper(getche())= ='O'); } void lecture(void) { struct page *precedent,*nouveau; premier=(struct page *)malloc(sizeof(struct page)); puts("entrez votre premier entier"); scanf("%d",&premier->val); precedent=premier; while (encore()) { nouveau=(struct page *)malloc(sizeof(struct page)); precedent->suivant=nouveau; precedent=nouveau; puts("\nentrez votre entier"); scanf("%d",&nouveau->val); } precedent->suivant=NULL; } void affiche(struct page *debut) { printf("\nliste : "); while(debut!=NULL) { printf("%d ",debut->val); debut=debut->suivant; } printf("\n"); } void main(void) { lecture(); affiche(premier); }Exercice (liste) : modifier la fonction lecture ci-dessus pour que la liste soit stockée dans l'ordre inverse de son introduction (chaque nouvel élément est placé devant la liste déjà existante).
Les modifications sont aisées, une fois que l'on a repéré l'endroit de la modification. Exemple : suppression d'un élément :
void suppression(void) { struct page *actu,*prec; actu=premier; while (actu!=NULL) { printf("\nvaleur : %d - supprimer celui_ci (O/N) ? ", actu->val); if (toupper(getche())= ='O') { if(actu= =premier)premier=actu->suivant; else prec->suivant=actu->suivant; free(actu); break; } else { prec=actu; actu=prec->suivant; } } }Exercice (insertion) : ajouter au programme précédent une procédure d'insertion d'une valeur dans la liste.
Ce type de données (structure pointant
sur un même type) est utilisé dans d'autres cas. Par exemple,
pour représenter un arbre, il suffit pour
chaque élément de connaître l'adresse de chaque fils
:
Remarque : si le nombre de fils n'est pas constant, on a intérêt
à stocker uniquement le fils aîné, ainsi que le frère
suivant(voir partie algorithmique et structures de données).