cours c impécable.pdf

68
1 Université de Savoie Module ETRS-505 Cours Initiation à la programmation en C Sylvain MONTAGNY [email protected] Bâtiment chablais, bureau 13 04 79 75 86 86 Source du cours : Yann Laurillau [email protected] Retrouver tous les documents de Cours/TD/TP sur le site www.master-electronique.com

Transcript of cours c impécable.pdf

Page 1: cours c impécable.pdf

1

Université de Savoie Module ETRS-505

Cours Initiation à la programmation en C

Sylvain MONTAGNY [email protected]

Bâtiment chablais, bureau 13 04 79 75 86 86

Source du cours : Yann Laurillau [email protected]

Retrouver tous les documents de Cours/TD/TP sur le site www.master-electronique.com

Page 2: cours c impécable.pdf

2

1. Introduction .......................................................................................................... 4

1.1. Petite histoire du C............................................................................................................ 4 1.2. Outils pour programmer en C ........................................................................................... 4 1.3. Quel environnement de travail ? ....................................................................................... 4

1.3.1 Comment faire sous Linux (version Redhat 9 en français)....................................... 4 1.3.2 Comment faire sous Mac OS X ? ............................................................................. 6

1.4. Programmer en C : comment ça marche ? ........................................................................ 6 1.5. Programmer en C : à quoi ça ressemble ?......................................................................... 6 1.6. La fonction printf() ........................................................................................................... 8 1.7. Lecture formatée de données ............................................................................................ 9 1.8. Systèmes de numération ................................................................................................. 10

2. Variables et opérateurs arithmétiques ............................................................. 12 2.1. La déclaration de variables ............................................................................................. 12 2.2. L’affectation d’une valeur constante .............................................................................. 12 2.3. Manipuler une variable (II) : les opérateurs arithmétiques............................................. 13 2.4. Priorité des opérateurs..................................................................................................... 14 2.5. Déclaration et affectation simultanées ............................................................................ 15 2.6. Les opérateurs de manipulation de bits........................................................................... 15

3. Types élémentaires et conversion de type ........................................................ 17 3.1. Les types élémentaires .................................................................................................... 17 3.2. Représentation des nombres ........................................................................................... 17

3.2.1 Les nombres entiers ................................................................................................ 17 3.2.2 Les nombres réels ................................................................................................... 18 3.2.3 Le type caractère ..................................................................................................... 19

3.3. La conversion de type ..................................................................................................... 20 4. Expressions conditionnelles et opérateurs de comparaison ........................... 21

4.1. Si-Alors-Sinon ................................................................................................................ 21 4.2. Opérateurs de comparaison et logiques .......................................................................... 21

4.2.1 Opérateurs de comparaison..................................................................................... 21 4.2.2 Opérateurs logiques ................................................................................................ 23

4.3. Switch ............................................................................................................................. 24 5. Boucles et itérations............................................................................................ 26

5.1. While et do-while............................................................................................................ 26 5.1.1 La boucle While...................................................................................................... 26 5.1.2 La boucle Do-While................................................................................................ 26

5.2. Boucle for........................................................................................................................ 27 5.3. Imbrication de boucles.................................................................................................... 27 5.4. Mots-clé break ................................................................................................................ 28

6. Fonctions et procédures ..................................................................................... 29 6.1. Les fonctions................................................................................................................... 29 6.2. Les prototypes et les appels inter-fonctions.................................................................... 30 6.3. Les variables globales ..................................................................................................... 30 6.4. Le type void et les procédures ........................................................................................ 31 6.5. Variables globales et portée des identificateurs.............................................................. 31

7. Tableaux et chaînes de caractères..................................................................... 33 7.1. Les tableaux .................................................................................................................... 33 7.2. Les chaînes de caractères ................................................................................................ 34

8. Pointeurs.............................................................................................................. 35

Page 3: cours c impécable.pdf

3

8.1. Généralités sur les pointeurs ........................................................................................... 35 8.2. Opérateur & : adresse de................................................................................................. 36 8.3. Opérateur * : valeur de.................................................................................................... 36 8.4. Une valeur de pointeur particulière : NULL................................................................... 37 8.5. Pointeur de pointeur........................................................................................................ 38 8.6. Pointeur et paramètres de fonctions............................................................................... 39

8.6.1 Passage par référence.............................................................................................. 39 8.7. Pointeurs et tableaux ....................................................................................................... 40

8.7.1 Tableau de pointeurs ............................................................................................... 40 8.7.2 Analogie entre tableau et pointeur .......................................................................... 41 8.7.3 Passage par référence d’un tableau en paramètre d’une fonction........................... 42 8.7.4 Arithmétique de pointeurs et notion de décalage (offset) ....................................... 44 8.7.5 Opérateurs de comparaison..................................................................................... 44

9. Définition de types, types énumérés.................................................................. 45 9.1. Typedef ........................................................................................................................... 45 9.2. type énuméré : définition de constantes symboliques..................................................... 45

10. Structures ........................................................................................................ 47 10.1. Généralités .................................................................................................................. 47 10.2. Structure et typedef ..................................................................................................... 48 10.3. Structures imbriquées.................................................................................................. 49 10.4. Structures et tableaux.................................................................................................. 50 10.5. Structures et pointeurs................................................................................................. 51 10.6. Structures récursives ................................................................................................... 53

11. Gestion dynamique de la mémoire ............................................................... 55 11.1. Fonctions d’attribution et de libération dynamique de la mémoire ............................ 55 11.2. Demande d'allocation mémoire (malloc) :.................................................................. 55 11.3. Libération mémoire (free) :......................................................................................... 56 11.4. Demande d'allocation mémoire pour tableau (calloc) : .............................................. 57 11.5. Tableaux multiples : allocation et libération............................................................... 57 11.6. Erreurs fréquentes ....................................................................................................... 58

12. Manipulation des chaînes de caractères....................................................... 59 12.1. Erreurs fréquentes ....................................................................................................... 60

13. Fichiers et E/S ................................................................................................. 61 13.1. Les fichiers.................................................................................................................. 61

14. Préprocesseur, headers et programmation multi-fichiers.......................... 67 14.1. Préprocesseur .............................................................................................................. 67

14.1.1 Définition de constante ....................................................................................... 67 14.1.2 Usage des macros................................................................................................ 67 14.1.3 Inclusion de fichiers en-tête (headers)................................................................ 68 14.1.4 Erreurs fréquentes ............................................................................................... 68

Page 4: cours c impécable.pdf

4

1. Introduction Le langage C est un langage de programmation qui peut, comme son nom l’indique, produire des programmes, c’est-à-dire des logiciels. Une grande partie de ceux que nous utilisons, comme Linux, sont écrits dans le langage C.

1.1. Petite histoire du C Ce langage a été inventé par Kernighan et Ritchie au début des années 70 (1972) dans les laboratoires Bell Labs et a été utilisé pour développer le système UNIX. Il a été créé pour être efficace et offrir une programmation plus évoluée que la programmation dans le langage assembleur (plus fastidieuse). Pour la petite histoire, le langage C est une évolution du langage B. Depuis, le langage C lui- même a connu une évolution vers la programmation par objet avec le C++. On trouve également des variations avec le langage Java et le langage C# (prononcer si-charp) imaginé par Microsoft. C’est le langage de prédilection d’un grand nombre de développeurs et il est loin d’être abandonné.

1.2. Outils pour programmer en C Les outils à utiliser pour programmer en C sont : un éditeur de texte (qui peut être aussi simple que le bloc-note) et un compilateur. Il existe de nombreux compilateurs, payants pour la plupart. Sous Linux, on dispose de gcc, compilateur C gratuit installé en standard par la majorité des distributions. C’est également ce compilateur qui est utilisé sur les ordinateurs Apple sous MacOS X. Il existe également des logiciels qui intègrent deux logiciels et que l’on nomme «environnement de développement» (IDE). C’est le cas de Visual C/C++ de Microsoft ou de Borland C (plus anciennement, Turbo C).

1.3. Quel environnement de travail ?

1.3.1 Comment faire sous Linux (version Redhat 9 en français) Pour éditer un programme C, il est possible d'utiliser un éditeur de texte tel que XEmacs. Pour compiler, on utilise le compilateur gcc en ligne de commande à partir d’un terminal, accessible depuis le menu principal de l’ environnement de travail. Toutes les compilations seront effectuées depuis la ligne de commande (shell). Il est cependant nécessaire de connaître les commandes élémentaires pour manipuler les fichiers dans un shell Unix dont voici la liste et des exemples d’utilisation : Format Action Exemple ls Affiche la liste des fichiers et répertoires ls -l idem mais avec plus de détails cd Change le répertoire courant cd src/ cp Copie un fichier cp exercice.c programme.c rm Supprime un fichier rm programme.c mkdir Crée un répertoire mkdir cours rmdir Supprime un répertoire vide rmdir cours rm -rf Supprime un répertoire et son contenu rm –rf cours mv Déplace un fichier ou un répertoire mv exercice.c programme.c pwd Indique le chemin pour accéder au répertoire courant pwd

Exemple : sortie de la commande ls –l

Page 5: cours c impécable.pdf

5

1. $ ls -l 2. drwxr-xr-x 1 yalau yalau 128 sep 8 15:06 . 3. drwxr-xr-x 1 yalau yalau 128 sep 8 15:06 .. 4. drwxr-xr-x 1 yalau yalau 128 sep 8 15:06 code 5. -rw-r--r-- 1 yalau yalau 208 sep 8 15:06 exercice_I_3.c 6. -rw-r--r-- 1 yalau yalau 1790 sep 8 15:06 linux-conf.tar 7. -rw------- 1 yalau yalau 53760 sep 8 15:06 Uds-T0-9-1.doc 8. $ Signification des informations du ls –l :

• Première colonne : la première lettre o Le d indique qu’il s’agit d’un répertoire (directory). o Le – indique qu’il s’agit d’un fichier. o Le l indique qu’il s’agit d’un raccourci (link).

• Première colonne : trois blocs de trois lettres o Indique les droits d’accès pour le propriétaire du fichier/répertoire (premier bloc),

pour les membres du groupe (second bloc), pour tous les autres (dernier bloc). Si un droit n’est pas accordé, un tiret – apparaît.

o r : droit en lecture (Read). o w: droit en écriture (Write). o x : droit en exécution (eXecute).

• La troisième colonne indique le nom du propriétaire du fichier/répertoire • La quatrième colonne indique le groupe associé au fichier/répertoire • La cinquième colonne indique la taille en octets du fichier

Comme on peut le constater, il existe deux répertoires spéciaux . et .. qui représentent respectivement le répertoire courant et le répertoire parent dans la hiérarchie. Il existe un autre répertoire spécial noté ~ qui symbolise le répertoire personnel (home directory). Ces notations ., .. et ~ peuvent être utilisées avec les autres commandes de manipulation de fichiers et répertoires tels que cd ou mv. Enfin, un répertoire est identifié par le chemin qui permet d’y accéder dans la hiérarchie. Ce chemin est constitué des noms des répertoires parents séparés par un / (que l’on nomme séparateur). Un / seul désigne la racine de cette hiérarchie (root). Par exemple, la commande pwd produit dans ce format le chemin du répertoire courant. Exemple : utilisation de la commande pwd 1. $ pwd 2. /home/syscom/yalau 3. $ cd / 4. $ pwd 5. / 6. $ cd home 7. $ pwd 8. /home Sous windows, il est possible d'installer l'environnement cygwin qui reproduit l’environnement UNIX avec les terminaux et les différents shell pour l'accès à la ligne de commande. Au sein de cet environnement, il est également possible d'installer gcc et ainsi reproduire à « l'identique » l'environnement dont vous disposez sous Linux (http://www.mingw.org ) Il existe également les environnements de développement du type Visual C++, Borland C ou autre qui vous permettent de compiler un programme. Toutefois, il faut faire attention car ces environnements peuvent prendre en compte une version non normalisée du langage.

Page 6: cours c impécable.pdf

6

1.3.2 Comment faire sous Mac OS X ? Il s'agit d'un UNIX et ce qui est relatif à Linux s’applique à Mac OS X. Par contre il est nécessaire d'installer les paquets de développement (logiciel XCode) qui contiennent une version de gcc pour Mac OS X. Normalement, les manipulations en ligne de commande sont identiques à Linux. Il est également possible de travailler directement avec qui est un environnement de développement complet.

1.4. Programmer en C : comment ça marche ? Programmer en C, c’est d’abord écrire un document textuel (le code du programme) conforme à un ensemble de règles, défini sous forme de grammaire. Par convention, le code du programme est enregistré dans un fichier avec l’extension .c. Une fois le programme écrit, il doit être compilé à l’aide d’un outil : le compilateur. Le rôle d’un compilateur est d’analyser le code et de le transformer en instructions binaires qui pourront être ensuite exécutées par le processeur. Si le code que vous avez écrit contient des erreurs d’écriture, c’est-à-dire qui n’est pas conforme aux règles du langage C, le compilateur ne produira pas de binaire et vous affichera des messages d’erreur vous permettant ensuite de localiser les erreurs et de les corriger. Voici le principe général de la compilation : Utilisation de gcc : Pour compiler sous Linux avec gcc, voici la forme générale : gcc –Wall –o <nom de l’exécutable> <fichier à compiler> Il existe bien sûr d’autres options dont certaines seront détaillées ultérieurement. Exemple : gcc –Wall –o mon_programme exemple.c Pour lancer l’exécutable « mon_programme » : ./mon_programme

1.5. Programmer en C : à quoi ça ressemble ? Etudions un exemple très simple :

Compilateur

fichier.c

01010101 10101010 exécutable

01010101 10101010 bibliothèque

Page 7: cours c impécable.pdf

7

1. #include <stdio.h> 2. 3. /* Calcule et affiche le cube des nombres de 1 a 5 */ 4. 5. int main(void) 6. { 7. int nombre, cube; 8. 9. nombre = 1; 10. while (nombre <= 5) 11. { 12. cube = nombre * nombre * nombre; 13. printf("Le cube de %d est %d\n", nombre, cube); 14. nombre = nombre + 1; 15. } 16. 17. return 0; 18. } 19. Anatomie du programme : Ce programme calcule et affiche le cube des nombres compris entre 1 et 5.

1) Il s'agit d'une série d'instructions, ou commandes (on parle de bloc d'instructions), délimitées deux accolades. En pascal, cela correspond à Begin/End

2) Une directive de précompilation : il s'agit d’une commande pour le préprocesseur. Sans entrer dans le détail, il s'agit d'indiquer que le code repose sur l’utilisation de fonctions standards d'entrée et de sortie, à savoir, ici, afficher du texte dans le terminal (STD = standard, IO = Input/Output). C'est ce qui est fait avec l'appel de fonction printf dans la suite du programme.

3) Un commentaire : ça ne produit pas de code binaire mais c'est très utile (et indispensable) pour documenter le code du programme. Un commentaire commence toujours par un /* et se termine par un */. Entre les deux, on peut y mettre n'importe quoi (y compris du code qui ne sera pas compilé). Par exemple: /* commentaire */ /* commen /* taire */ /* ** commentaire */

4) Une fonction : Une fonction est un bloc d’instruction qui est désignée par un nom, une valeur de retour et une déclaration d’arguments entre parenthèse. Le mot-clé void indique qu’il n’y a pas d’argument. L’intérêt des fonctions est de pouvoir exécuter plusieurs fois un même bloc d’instruction sans avoir à le réécrire dans le programme. Cette fonction se nomme main et est particulière car elle est indispensable pour que le programme compile correctement et puisse être exécuté : il s’agit du point d’entrée du programme.

5) Une déclaration de variables : une variable est une zone mémoire qui permet de sauvegarder une valeur. Pour chaque variable, il est nécessaire de préciser la nature de cette valeur, qu’on appelle type. Dans notre, exemple, il s’agit de deux variables, nombre et cube, qui nous

Page 8: cours c impécable.pdf

8

permettent de manipuler deux nombres entiers (int pour integer). Il existe d’autres types comme les nombres réels.

6) Une affectation : on mémorise dans la variable nombre la valeur 1. Cette variable est utilisée dans la suite pour calculer son cube.

7) Une boucle avec condition : une boucle permet de répéter plusieurs fois un bloc d’instruction. Dans notre exemple, le bloc est répété autant de fois que la valeur mémorisée par la variable nombre n’est pas supérieure à 5. Ceci nous permet de calculer donc le cube des nombres de 1 à 5.

8) Une expression et une affectation : dans cette instruction on calcule le cube en multipliant trois fois le contenu de la variable nombre à l’aide de l’opérateur arithmétique *. Le résultat de cette expression est mémorisé dans la variable cube.

9) Une commande d’affichage : cette instruction affiche le message « le cube de X est Y ». le %d indique que l’on va imprimer un nombre entier. A chaque %d, on doit associer une variable contenant un entier et, à l’affichage, le %d est substitué par la valeur entière contenue dans cette variable. Dans notre exemple, nous avons deux %d et nous utilisons les variables nombre et cube. Le \n indique que le message doit se terminer par un retour à la ligne.

10) Une expression et une affection : cette instruction permet de passer au nombre suivant en ajoutant 1 et en mémorisant la nouvelle valeur dans la variable nombre.

11) Un retour : il s’agit d’une instruction particulière retournant une valeur pour la fonction main. Pour plus d’information, se reporter à la partie sur les fonctions.

Point important : toutes les instructions et déclarations se terminent systématiquement par un point-virgule.

1.6. La fonction printf() La fonction printf est l’instruction indispensable de tout programmeur en C et permet l'écriture formatée sur le flux standard de sortie stdout (l'écran par défaut). Le premier paramètre de cette fonction est toujours une chaîne de caractères délimitées par deux guillemets ". Cette fonction peut prendre ensuite un nombre de paramètre qui est égale au nombre de variable qu’on a demander à afficher. printf("chaîne de caractère", <expression1>, […], <expressionN>);

Le caractère \n indique le retour à ligne. D’une manière générale, tous les caractères spéciaux sont précédés systématiquement d’un \ comme le \" qui désigne un guillement (pour ne pas confondre avec le guillemet utilisé en fin et début de chaîne dans les paramètres du printf). Exemple : utilisation de la fonction printf

Page 9: cours c impécable.pdf

9

1. #include <stdio.h> 2. 3. int main(void) { 4. int nbre = 5; 5. long prix = 12; 6. 7. printf("Bonjour\n"); 8. printf("Nombre : %d Prix : %ld Total : %ld \n",nbre, prix, prix *

nbre); 9. printf("\n"); 10. return 0; 11. } Point important : pour utiliser la fonction printf, le programme doit systématiquement comporter la directive de compilation #include <stdio.h> au tout début du programme comme cela figure dans l’exemple commenté. L’ensemble des spécificateurs de format sont rassemblés dans le tableau suivant : Format Type Détail %d int entier signée %ld long int entier signée %u unsigned int entier non signée %lu unsigned long int entier non signée %o unsigned int octale non signée %lo unsigned long int octale non signée %x unsigned int hexadécimale non signée %lx unsigned long int hexadécimale non signée %f double décimale virgule fixe %lf long double décimale virgule fixe %e double décimale notation exponentielle %le long double décimale notation exponentielle %g double décimale, représentation la plus courte parmi %f et %e %lg long double décimale, représentation la plus courte parmi %lf et %le %c unsigned char caractère %s char* chaîne de caractères

1.7. Lecture formatée de données La fonction scanf est la fonction symétrique à printf; elle nous offre pratiquement les mêmes conversions que printf, mais en sens inverse. scanf("<format>",<AdrVar1>,<AdrVar2>, ...)

"<format>" : format de lecture des données <AdrVar1>,... : adresses des variables auxquelles les données seront attribuées La fonction scanf reçoit ses données à partir du fichier d'entrée standard stdin (par défaut le clavier). Les données reçues correctement sont mémorisées successivement aux adresses indiquées par <AdrVar1>,... .L'adresse d'une variable est indiquée par le nom de la variable précédé du signe &.

Page 10: cours c impécable.pdf

10

Exemple : utilisation de la fonction scanf 1. #include <stdio.h> 2. 3. int main(void) { 4. int jour, mois, annee; 5. 6. scanf("%d %d %d", &jour, &mois, &anne); 7. return 0; 8. } 9. Cet exemple lit trois entiers, séparés par des espaces, tabulations ou interlignes. Les valeurs sont attribuées respectivement aux trois variables : jour, mois et année.

1.8. Systèmes de numération Sur les ordinateurs, l’information est stockée et traitée sous forme d’une suite constituée de 0 et 1, c’est-à-dire en binaire. Aussi, les informaticiens et les programmeurs ont l’habitude de manipuler les nombres en base 2 ou dans toutes autres bases d’une puissance de 2 comme l’octal (base 8) ou l’hexadécimal (base 16). Très souvent, ceci nous oblige à faire des conversions entre le système décimal (base 10) et la base 8 ou la base 16 par exemple.

• Pour représenter un nombre en base 10, on utilise les chiffres de 0 à 9. • Pour représenter un nombre en base 2, on utilise les chiffres 0 et 1. • Pour représenter un nombre en base 16, on utilise les chiffres de 0 à 9 et les lettres de A à F

(avec A = 10, B = 11, C = 12, D = 13, E = 14 et F = 15) Exemples : écriture du nombre 237 dans les différentes bases En base 10 : 237 = 2 * 102 + 3 * 101 + 7 * 100 = 200 + 30 + 7 En base 2 : 11101101 = 1 * 27 + 1 * 26 + 1 * 25 + 0 * 24 + 1 * 23 + 1 * 22 + 0 * 21 + 1 * 20 = 128 + 64 + 32 + 0 + 8 + 4 + 0 + 1 En base 16 : 0xED = E * 161 + D * 160

= 14 * 16 + 13 * 1 = 224 + 13 Méthode pour convertir un nombre décimal en binaire : A partir du nombre décimal, il faut soustraire les puissances de 2 dans l’ordre décroissant jusqu’à ce que l’on obtienne 0. On note 1 si on utilise une certaine puissance de 2 et 0 dans le cas contraire. Au final, on doit obtenir dans sa représentation binaire. Exemple : convertir 163 en binaire

128 1 163 – 128 = 35 64 0 32 1 35 – 32 = 3 16 0

8 0 4 0 2 1 3 – 2 = 1 1 1 1 – 1 = 0

= 10100011

Page 11: cours c impécable.pdf

11

Méthode pour convertir un nombre binaire en hexadécimal : on regroupe, à partir du dernier chiffre, les nombres binaires en bloc de 4 chiffres. S’il manque des chiffres pour former le dernier bloc, le bloc est complèté avec des 0. Pour chaque bloc on regarde la valeur entre 0 et 16 pour déterminer la représentation hexadécimale. Exemple : convertir 11010100011

0 1 1 0 1 0 1 0 0 0 1 1 = 0 * 8 + 1 * 4 + 1 * 2 + 0 * 1 = 1 * 8 + 0 * 4 + 1 * 2 + 0 * 1 = 0 * 8 + 0 * 4 + 1 * 2 + 1 * 1

= 6 = 10 = 3 6 A 3

= 0x6A3

Page 12: cours c impécable.pdf

12

2. Variables et opérateurs arithmétiques 2.1. La déclaration de variables Une variable est une zone mémoire qui permet de stocker des valeurs. Avant d’utiliser une variable, il est nécessaire de la déclarer, c’est-à-dire choisir un nom et un type. Le type est la nature de la variable. Exemple : 1. int main(void) 2. { 3. int x; 4. int y, nombre; 5. 6. […] 7. 8. return 0; 9. } Dans notre exemple, les variables sont de type entier (int), c’est-à-dire que les valeurs de x, y et nombre seront des nombres entiers. La syntaxe pour déclarer une ou plusieurs variables de même type est (notez le point-virgule en fin de ligne) : <type> <variable1>, <variable2>, […], <variableN>; En C, les déclarations de variable se font systématiquement en début de bloc d’instruction, c’est-à-dire juste après une accolade ouvrante {. Il est possible de déclarer des variables sur plusieurs lignes en début de bloc, même si elles sont du même type. Remarque : Le mot-clé const signifie que la variable est constante donc non modifiable sauf à l’initialisation.

2.2. L’affectation d’une valeur constante Une variable ne se contente pas de stocker une valeur, il est également possible d’en modifier son contenu à l’aide d’une affectation. Pour cela, on utilise un opérateur particulier, l’opérateur d’affectation représenté par un = et la syntaxe est : <variable> = <valeur>; Il est bien sûr possible de réutiliser une variable et d’en changer successivement le contenu. Exemple :

Page 13: cours c impécable.pdf

13

1. int main(void) 2. { 3. int nombre; 4. 5. nombre = 28; 6. nombre = 2; 7. 8. return 0; 9. }

2.3. Manipuler une variable (II) : les opérateurs arithmétiques L’affectation d’une variable ne se limite pas aux valeurs constantes. Il est possible d’affecter le résultat d’une expression arithmétique à l’aide d’opérateurs mathématiques. La syntaxe est identique : <variable> = <expression>; Pour cela, on dispose de plusieurs opérateurs arithmétiques :

Op. Fonction Exemple + Addition calcul = nombre + 5; - Soustraction calcul = 10 – nombre; * Multiplication calcul = 3 * nombre; / Division calcul = nombre / 2; % Modulo calcul = nombre % 3; ++ Incrémentation nombre++; équivalent à nombre = nombre + 1; -- Décrémentation nombre--; équivalent à nombre = nombre - 1; += Addition et affectation nombre += 5; -= Soustraction et affectation nombre -= 6; *= Multiplication et affection nombre *= 3; /= Division et affectation nombre /= 2; %= Modulo et affectation nombre %= 4; <variable> op= <expr>; est équivalent à <variable> = <variable>op<expr>; Par exemple :

nombre += 5; est équivalent à nombre = nombre + 5; Point important : dans une expression, il est possible d’utiliser en même temps des valeurs constantes et plusieurs variables. De plus, comme en mathématique, il est possible d’utiliser des parenthèses. Règles de priorité : il existe des règles de priorité entre ces opérateurs. Dans une expression, la multiplication, la division et le modulo sont prioritaires sur l’addition et la soustraction. Ces priorités sont identiques au modèle mathématique. Exemple :

Page 14: cours c impécable.pdf

14

1. #include <stdio.h> 2. 3. int main(void) 4. { 5. int nombre1, nombre2, calcul; 6. 7. nombre1 = 10; 8. nombre2 = 4; 9. nombre2++; 10. calcul = 3 * (nombre1 + nombre2) - 50; 11. nombre1 *= nombre2; 12. nombre2 += nombre2; 13. 14. printf("%d %d %d \n", nombre1, nombre2, calcul); 15. 16. return 0; 17. }

2.4. Priorité des opérateurs Les différents opérateurs qui ont été vus jusqu'à présent doivent agir selon des règles de priorité. Lorsqu'une instruction combine plusieurs opérateurs de même priorité, le langage C définit alors un ordre de priorité de droite à gauche ou, le plus souvent, de gauche à droite. Le tableau ci-dessous donne la liste des opérateurs par ordre décroissant de priorité.

Priorité Opérateurs Sens 1 () [] . -> gauche à droite 2 ! ~ ++ -- - + (<type>) sizeof

* (pointeur) & (pointeur) droite à gauche

3 * / % gauche à droite 4 + - gauche à droite 5 << >> gauche à droite 6 < <= > >= gauche à droite 7 == != gauche à droite 8 & gauche à droite 9 ^ gauche à droite 10 | gauche à droite 11 && gauche à droite 12 || gauche à droite 13 ? : droite à gauche 14 = += -= etc droite à gauche 15 , gauche à droite

Exemples :

o a = b + c * d est équivalent à a = (b + (c * d)) o c - d + e est équivalent à (c - d) + e o a = b = c = 0 est équivalent à a = (b = (c = 0)) o x = y++; est équivalent à x = y; y++; o x = ++y est équivalent à ++y; x = y; o x >= 0 && y >= 0 est équivalent à (x >= 0) && (y >= 0) o (x == 0) || (y == 1) && (x == 2)

est équivalent à (x == 0) || ((y == 1) && (x == 2))

o (y == 1) && (x == 2) || (x == 0)

est équivalent à ((y == 1) && (x == 2)) || (x == 0)

Page 15: cours c impécable.pdf

15

Par contre, il existe des combinaisons qui produisent des effets de bords mais dont l'ordre est tout de même déterminable :

o a || b++ o b n'est pas incrémenté si a est vraie

o a && b++ o b n'est pas incrémenté si a est faux

Il existe des cas non prévus par la grammaire du langage C :

o a[i] = i++; o a = i++ * i++; o b = (f() + g()) * h()

o On ne sait pas dans quel ordre sont appelées f() et g(). Voici une solution: tf = f(); tg = g(); th = h(); b = (tf + tg) / th;

2.5. Déclaration et affectation simultanées Le langage C autorise la déclaration et l’affectation simultanées des variables. La syntaxe devient alors : <type> <variable1> = <expression>, […], <variableN> = <expression>; Exemples : 1. #include <stdio.h> 2. 3. int main(void) 4. { 5. int n = 10; 6. int m = 5 * n, l = m + n + 5; 7. 8. printf("%d %d %d\n", n, m, l); 9. 10. return 0; 11. } 12. 13.

2.6. Les opérateurs de manipulation de bits Il existe des opérateurs permettant de faire des opérations arithmétiques directements sur les bits. Ces opérateurs disposent d’une version combinée avec l’affectation.

Page 16: cours c impécable.pdf

16

Opérateur Signification Exemples << Décalage à gauche (0x15<<3) équivalent à 10101000 (0xA8) >> Décalage à droite (0x15>>2) équivalent à 101 (0x5) & ET bit à bit (0x15 & 0x1C) équivalent à 10100 (0x14) | OU bit à bit (0x15 | 0x1C) équivalent à 11101 (0x1D) ^ OU Exclusif bit à bit (0x15 ^ 0x1C) équivalent à 1001 (0x9) ~ Complément (dépendent du type) ~0x15 équivalent à 11101010 (0xEA) n = 0x15;

<<= Décalage à gauche et affectation n <<= 3; >>= Décalage à droite et affectation n >>= 2; &= ET bit à bit et affectation n &= 0x1C; |= OU bit à bit et affectation n |= 0x1C; ^= OU Exclusif bit à bit et affectation n ^= 0x1C;

0x15 = 00010101 et 0x1C = 00011100 Les règles pour les opérateurs &, | et ^ sont : 0 & 0 = 0 1 & 0 = 0 0 & 1 = 0 1 & 1 = 1 0 | 0 = 0 1 | 0 = 1 0 | 1 = 1 1 & 1 = 1 0 ^ 0 = 0 1 ^ 0 = 1 0 ^ 1 = 1 1 ^ 1 = 0

Page 17: cours c impécable.pdf

17

3. Types élémentaires et conversion de type 3.1. Les types élémentaires Il existe trois types élémentaires qui se décomposent en sous catégories :

Types Déclaration C Nature Codage Valeurs Caractère char Caractère 1 octet -128 à 127 unsigned char Caractère non signé 1 octet 0 à 255 Nombres entiers short int (ou short) Entier positif ou négatif 2 octets [-32768, 32767] unsigned short int Entier positif uniquement 2 octets [0, 65535] int Entier positif ou négatif 2 (sur processeur 16 bits)

4 (sur processeur 32 bits [-231, 231-1] si 4 octets

unsigned int Entier positif uniquement 2 (sur processeur 16 bits) 4 (sur processeur 32 bits

[0, 232-1] si 4 octets

long int (ou long) Entier positif ou négatif 4 ou 8 octets [-231, 231-1] unsigned long int Entier positif uniquement 4 ou 8 octets [0, 232-1] Nombres réels float 6 chiffres significatifs 4 octets [-3.4 1038, 3.4 1038] double 16 chiffres significatifs 8 octets [-1.7 10308, 1.7

10308] long double (ça dépend) 10/16 octets

231 = 2 147 483 648232 = 4 294 967 296

3.2. Représentation des nombres

3.2.1 Les nombres entiers Toute valeur entière est représentée par une série de chiffres de 0 à 9. Cependant, il existe d’autres façons de représenter une valeur : en octal (base 8) et en hexadécimal (base 16). De plus, il est possible d’utiliser les suffixes L et U lorsque l’on veut indiquer que cette valeur correspond à un entier long et à un entier non signé. Exemple : représentation de la valeur 58

Catégorie d’entier normal long non signé long et non

signé Décimal 58 58L 58U 58UL Octal 072 072L 072U 072UL Base Hexadécimal 0x3A 0x3AL 0x3AU 0x3AUL

Page 18: cours c impécable.pdf

18

1. int main(void) 2. { 3. int entier; 4. long int entierLong; 5. unsigned int entierPositif; 6. unsigned long int entierLongPositif; 7. 8. entier = 0x3A; 9. entierLong = 58L; 10. entierPositif = 072U; 11. entierLongPositif = 0x3AUL; 12. 13. return 0; 14. } Pour afficher une valeur entière à l’écran à l’aide la fonction printf, nous avons vu qu’il fallait utiliser la chaîne de formatage %d. En fait cela varie selon le type d’entier.

Chaîne de formatage Catégorie d’entier %d int ou short int %u unsigned int ou unsigned short int %ld long int %lu unsigned long int %X (en hexadecimal) int ou unsigned int %lX (en hexadecimal) long int ou unsigned long int

Exemple : On veut afficher les valeurs de l’exemple précédent 1. #include <stdio.h> 2. 3. int main(void) 4. { 5. printf("entier : %d\n", entier); 6. printf("entier positif : %u\n", entierPositif); 7. printf("entier long : %ld\n", entierLong); 8. printf("entier long positif : %lu\n", entierLongPositif); 9. printf("entier en hexadecimal : %X\n", entier); 10. 11. return 0; 12. } Attention : il est possible d’affecter une valeur négative à un entier non signé. La valeur sera convertie en une valeur positive.

3.2.2 Les nombres réels Les nombres réels sont constitués d’une partie avec virgule et d’une partie exposant. La partie exposant est toujours préfixée par la lettre E ou e. Si l’on veut préciser qu’il s’agit d’un nombre réel de type flottant (float) ou un nombre réel double précision (long double), on ajoute le suffixe F (ou f) ou L (ou l). Pour afficher un nombre réel avec la fonction printf, on utilise principalement les chaînes de formatage %f. Il existe bien sûr d’autres façons d’afficher des nombres réels avec la fonction printf.

Page 19: cours c impécable.pdf

19

Exemple : le nombre réel 0.01234 peut s’écrire de différentes façons : 1. #include <stdio.h> 2. 3. int main(void) 4. { 5. float f1, f2, f3; 6. 7. f1 = 0.01234; 8. f2 = .01234; 9. f3 = 12.34e-3; 10. printf("affichage d’un flottant : %f\n", f1); 11. 12. return 0; 13. }

3.2.3 Le type caractère Le type caractère (char) est un nombre entier un peu particulier car il permet de représenter une lettre ou des symboles comme les chiffres. Du point de vue de l’entier, une variable de type char représente une valeur entière comprise entre –128 et 127 ou entre 0 et 255 s’il est non signé. Du point de vue des caractères, il se trouve que les lettres affichées à l’écran sont désignées par des codes (une valeur entière) définies dans une table, la table ASCII (American Standard Code for Information Interchange) et que celles-ci ont pour taille 1 octet (la taille du type char). Pour afficher une lettre ou un symbole à l’aide de la fonction printf, il est possible d’utiliser soit %d (type entien : code de la lettre) ou %c (type caractère : le caractère lui-même). Exemple : la lettre A majuscule est désignée par le code 65 En C, il n’est pas toujours nécessaire de connaître la valeur ASCII d’un caractère en utilisant directement celui-ci entre deux guillemets simples. Exemple : deux exemples différents pour désigner la lettre A dans une affectation et pour l’afficher à l’écran. On doit observer que le résultat est identique pour les deux formes d’affectation. 1. #include <stdio.h> 2. 3. int main(void) 4. { 5. char c1, c2; 6. 7. c1 = 65; /* code de la lettre A */ 8. c2 = 'A'; /* accès direct au caractère */ 9. printf("affichage du code : %d %d\n", c1, c2); 10. printf("affichage du caractere : %c %c\n", c1, c2); 11. 12. return 0; 13. } Il existe également des codes pour désigner les caractères non imprimables comme le retour à la ligne, désigné par le code de valeur 13 ou par le caractère '\n', très utilisé en programmation.

Page 20: cours c impécable.pdf

20

3.3. La conversion de type La conversion de type consiste à transformer un type de valeur en un autre. Dans certains cas, il est nécessaire de rendre explicite cette conversion, notamment pour les affectations, en respectant la syntaxe suivante : (<type>) <expression> Les opérateurs (addition), - (soustraction), * (produit), / (division), néccésite deux arguments 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 } Les calculs arithmétiques sont faits uniquement soit en long soit en double, pour éviter des dépassements de capacité.

Exemples : 1. int main(void) 2. { 3. int n, m, l; 4. double d; 5. 6. d = 5; // conversion implicite d’un entier en un réel 7. n = (int) 3.4; // conversion explicite d’un réel en entier avec

arrondi 8. 9. n = 1; 10. m = 5; 11. d = 2; 12. l = (int) (m / d);// l = 2 car 5/2.0 = 2.5 mais arrondi à 2 13. d = n / m; // d = 0.0 car division entière 1 / 5 14. d = n / ((double) m); // d = 0.2 car division réelle 1 / 5.0 15. d = 'A' * 6.0 – m + 0xA2CL; 16. 17. return 0; 18. }

Page 21: cours c impécable.pdf

21

4. Expressions conditionnelles et opérateurs de comparaison

4.1. Si-Alors-Sinon La construction Si-Alors-Sinon est l’instruction conditionnelle la plus élémentaire en C et dans tous les langages en général. La syntaxe est : if (<expression conditionnelle>) { <instructions si condition vraie> } else { <instructions si condition fausse> } Si la condition est vraie alors c’est le premier bloc d’instruction qui est exécuté, sinon (else) c’est le second bloc. Cependant, le second bloc est optionnel et la syntaxe devient : if (<expression conditionnelle>) { <instructions si condition vraie> } La valeur d’une expression conditionnelle est de type entier qui vaut soit 1 si la condition est vraie et qui vaut 0 dans le cas contraire. Cette expression repose sur l’utilisation d’opérateurs de comparaison et logiques.

4.2. Opérateurs de comparaison et logiques Il existe deux catégories d’opérateurs pour les expressions conditionnelles : les opérateurs de comparaison qui permettent la comparaison de valeurs ; les opérateurs logiques qui permettent la combinaison de plusieurs expressions logiques.

4.2.1 Opérateurs de comparaison Ces opérateurs sont des opérateurs binaires. Leur syntaxe est : <expression1> <opérateur> <expression2> Opérateur Signification Détails

< Inférieur stricte Vaut 1 si le membre de gauche est inférieur au membre de droite > Supérieur stricte Vaut 1 si le membre de gauche est supérieur au membre de droite == Egalité Vaut 1 si les deux membres sont égaux != Différent Vaut 1 si les deux membres sont différents <= Inférieur ou égal Vaut 1 si le membre de gauche est inférieur au membre de droite >= Supérieur ou égal Vaut 1 si le membre de gauche est supérieur au membre de droite

Exemple :

Page 22: cours c impécable.pdf

22

1. #include <stdio.h> 2. 3. int main(void) 4. { 5. int a = 5; 6. float b = 3.6; 7. 8. if (a < 2) { printf("a est inferieur a 2\n"); } 9. else { printf("a est superieur ou egal a 2\n"); } 10. 11. if (b >= 1.5f) { printf("b est plus superieur ou egal a 1.5\n"); } 12. else { printf("b est inferieur a 1.5\n"); } 13. 14. if (a < b) { printf("a est plus petit que b\n"); } 15. 16. b = a; 17. if (a == b) { printf("a et b sont egaux\n"); } 18. if (a != b) { printf("a et b sont differents\n"); } 19. 20. return 0 ; 21. } A savoir : Il est possible d’enchaîner les instructions if-then-else. Par exemple : if (<condition>) { <instructions> } else if (<condition>) { <instructions> } else if (<condition>) { <instructions> } else { <instructions> } Il existe une version sous forme d’opérateur ternaire du if-then-else que l’on rencontre souvent combiné avec une affectation. La valeur de cette opérateur varie selon la valeur de l’expression conditionnelle. Par contre, contrairement à la forme classique if-then-else, le second et troisième membres de l’opérateur ne sont pas des blocs d’instructions mais des expressions qui doivent impérativement retourner une valeur. La syntaxe est :

(<expression conditionnelle>) ? <valeur si vrai> : <valeur si faux> Exemple : utilisation de l’opérateur ternaire et son équivalent avec l’instruction if-then-else int main(void) int main(void) { { int a, b = 6; int a, b = 6; a = ((b == 3) ? (b – 2) : (b + 5)); if (b == 3) { a = (b – 2); } else { a = (b + 5); } return 0; return 0; } } Dans cet exemple, si la condition est :

• Vraie, c’est-à-dire que b est égal à 3, alors on affecte le résultat de l’expression (b - 2) à la variable a. En d’autre terme, par substitution, l’expression est équivalente à a = (b – 2);

• Fausse, c’est-à-dire que b n’est pas égal à 3, alors on affecte le résultat de l’expression (b + 5) à la variable a. En d’autre terme, par substitution, l’expression est équivalente à a = (b + 5);

Page 23: cours c impécable.pdf

23

4.2.2 Opérateurs logiques Ces opérateurs sont binaires sauf l’opérateur de complément qui est unaire. La syntaxe est :

<expression conditionnelle 1> && <expression conditionnelle 2> <expression conditionnelle 1> || <expression conditionnelle 2> <expression conditionnelle 1> ^ <expression conditionnelle 2>

! <expression conditionnelle>

Opérateur Signification Détail && ET logique Vaut 1 si les deux conditions sont vraies (i.e. égales à 1) && Vrai Faux Vrai Vrai Faux Faux Faux Faux || OU logique Vaut 1 si l’une des deux conditions est vraie || Vrai Faux Vrai Vrai Vrai Faux Vrai Faux ^ OU exclusif Vaut 1 si uniquement une seule des deux conditions est vraie ^ Vrai Vrai Vrai Faux Vrai Faux Vrai Faux ! Complémentaire Vaut 1 si la condition est fausse et inversement

Exemple :

1. #include <stdio.h> 2. 3. int main(void) 4. { 5. int a; 6. 7. a = 2; 8. if ((a >= -5) && (a <= 5)) { printf("a est dans l’intervalle [-5,5]\n"); } 9. 10. if ((a < -5) || (a > 5)) 11. { 12. printf("a est dans l’intervalle ]-inf , 5[ ou ]5, + inf[\n"); 13. } 14. 15. if (! (a < -5)) { printf("a n’est pas dans l’intervalle [-5, +inf[\n") ; } 16. } 17.

A savoir (i) : ! (a < 5) est équivalent à (a >= 5) ! (a != 0) est équivalent à (a == 0) ! (<c1> && <c2>) est équivalent à ((! <c1>) || (! <c2>)) ! (<c1> || <c2>) est équivalent à ((! <c1>) && (! <c2>)) A savoir (ii) : Il est possible d’enchaîner plusieurs expressions conditionnelles avec ces opérateurs. Par exemple :

Page 24: cours c impécable.pdf

24

1. int main(void) 2. { 3. int x = 50, y = -2, z = 0; 4. 5. if ((x > 0) && (x < 100) && (y > 0) && (y < 100) && (z == 0)) { […] } 6. 7. return 0; 8. } A savoir (iii) : Cependant, ces opérateurs logiques binaires sont dits « paresseux » puisque si une condition permet de déterminer le résultat de l’expression conditionnelle, le reste de l’expression n’est pas évalué. Dans notre exemple, on sait que la troisième condition (y > 0) est fausse car y vaut –2. Aussi, à l’exécution, les expressions (y < 100) et (z == 0) ne seront pas évaluées.

4.3. Switch L’instruction switch est une variante des enchaînements de if-then-else mais à la différence que les expressions conditionnelles se réduisent à une comparaison d’entiers. La syntaxe est la suivante :

switch(<expression entière>) { case <valeur entière>: <bloc d’instructions> break;

case <valeur entière>: <bloc d’instructions> break;

[…]

case <valeur entière>: <bloc d’instructions> break;

default: <bloc d’instructions> break; }

Exemple :

Page 25: cours c impécable.pdf

25

1. int main(void) 2. { 3. int n = 8; 4. 5. switch(n) 6. { 7. case 2: 8. printf("n est egal a 2\n"); 9. break; 10. 11. case 8: 12. printf("n est egal a 8\n"); 13. break; 14. 15. default: 16. printf("n n’est egal ni a 2, ni a 8\n"); 17. break; 18. } 19. 20. return 0; 21. } A chaque case, correspond une clause possible (case) que peut prendre l’expression entière. L’instruction break indique la fin de la clause sachant qu’elle peut être omise (à éviter dans un premier temps). A l’aide de l’instruction if-then-else, l’instruction switch peut s’écrire : 1. int main(void) 2. { 3. int n = 8; 4. 5. if (n == 2) { printf("n est egal a 2\n"); } 6. else if (n == 8) { printf("n est egal a 8\n"); } 7. else { printf("n n’est egal ni a 2, ni a 8\n"); } 8. 9. return 0; 10. } 11.

Page 26: cours c impécable.pdf

26

5. Boucles et itérations Les boucles et itérations offrent la possibilité de répéter et d’exécuter en série un bloc d’instructions sans avoir à le réécrire. Ces boucles et itérations sont exécutées autant fois que la condition d’arrêt n’a pas été vérifiée. Et parfois, les boucles tournent … en rond !!!

5.1. While et do-while

5.1.1 La boucle While La première instruction de boucle est le while qui signifie tant que. La syntaxe est :

while (<expression conditionnelle>) { <instructions> }

Le principe est le suivant : tant que la condition est vraie le bloc d’instruction est exécuté. Exemple : La table de multiplication par 7. 1. #include <stdio.h> 2. 3. int main(){ 4. int i=0; 5. 6. printf("Apprenons la table de 7\n\n"); 7. 8. while(i < 10){/* TANT QUE i < 10 */ 9. i++;/* repeter les instructions */ 10. printf("%d x 7 = %d\n", i, i * 7);/* suivantes */ 11. } 12. 13. return 0; 14. } 15.

5.1.2 La boucle Do-While Il existe une autre forme de la boucle qui est de la forme faire-tant-que. Dans cette forme, l’expression conditionnelle est évaluée après l’exécution du bloc d’intruction. La syntaxe devient alors :

do { <instructions> } while(<expression conditionnelle>);

Dans certains cas, il peut être utile d’exécuter une fois le bloc d’instruction avant de réaliser l’évaluation de l’expression conditionnelle. Exemple : boucle do-while utilisée lorsque l’on ne peut pas prédire le nombre d’itérations

Page 27: cours c impécable.pdf

27

1. #include <stdio.h> 2. 3. int main(){ 4. int age=32; 5. 6. do{ 7. printf("Quel est votre age ? "); 8. scanf("%d", &age); 9. }while(age < 18); 10. 11. return 0; 12. }

5.2. Boucle for La boucle for est une variante de la boucle while intégrant un compteur de boucle, fort utile lorsque l’on connaît par avance le nombre d’itération nécessaire. La syntaxe est la suivante :

for(<instructions>; <expression conditionnelle>; <instructions>) { <instructions>

}

Le premier membre du for offre un espace pour l’initialisation de variables (en général le compteur de boucle). Le second membre est réservé à l’expression conditionnelle qui permet d’arrêter la boucle. Le dernier membre peut contenir n’importe quelle instruction. Il est en général réservé pour l’incrémentation de la boucle for. Les différents membres peuvent être vides. Point important : l’évaluation de l’expression conditionnelle est réalisée avant l’exécution du corps de la boucle et le dernier membre est exécuté après l’exécution corps de la boucle. Exemple : calcul et affichage de la somme des nombres de 1 à 10 1. #include <stdio.h> 2. 3. /* Calcule et affiche la somme des nombres de 1 a 10 */ 4. 5. int main(void) 6. { 7. int i, somme = 0; 8. 9. for(i = 1; i <= 10; i++) { somme += i; } 10. 11. printf("la somme des nombres de 1 a 10 est %d\n", somme); 12. 13. return 0; 14. }

5.3. Imbrication de boucles Il est possible d’imbriquer les boucles et d’utiliser les variables de compteur au sein de ses boucles. Le programme ci-dessous réalise l’addition des indices de deux matrices 10x10.

Page 28: cours c impécable.pdf

28

1. #include <stdio.h> 2. 3. int main(void) 4. { 5. int i, j, somme = 0; 6. 7. for(i = 1; i <= 10; i++) 8. { 9. for(j = 1; j <= 10; j++) 10. { 11. somme += i + j; 12. } 13. } 14. 15. printf("Somme des coefficients egale a %d\n", somme); 16. 17. return 0; 18. } 19.

5.4. Mots-clé break Le mot-clé break, utilisé dans une boucle, permet d’interrompre une boucle et d’en sortir même si l’expression conditionnelle d’arrêt de boucle n’a pas été vérifiée. Ce mot-clé est a utiliser avec prudence. Exemple : 1. #include <stdio.h> 2. int main(void) 3. { 4. int i = 1; 5. 6. while(i < 10) 7. { 8. if (((i + 3) % 5) == 0) { break; } 9. i++; 10. } 11. printf("Arret a la valeur %d\n", i); 12. return 0; 13. } 14.

Page 29: cours c impécable.pdf

29

6. Fonctions et procédures 6.1. Les fonctions Les fonctions sont un moyen pour fragmenter le code d’un programme afin de le rendre plus lisible. Cette méthode consiste à séparer des blocs d’instructions du reste du code et à les remplacer par un appel de fonction. A l’image de la fonction en mathématique, les fonctions en C peuvent prendre des paramètres nécessaires à l’exécution du bloc d’instruction et doivent retourner une valeur. La syntaxe est la suivante :

<type de retour> <nom de la fonction>([<type> <variable1>,]*) { <instructions>

return <expression du type de retour>; }

On nomme en général prototype ou signature la première ligne d’une déclaration de fonction et de corps de fonction pour désigner le bloc d’instruction. Il convient d’ajouter que les paramètres sont des copies et que l’on ne travaille pas directement sur les variables utilisées pour le passage de paramètre. Ceci peut être contourner avec l’utilisation des pointeurs.

Exemple : Voici par analogie une fonction mathématique, 12),(

:3

2

++→

yxyxRRf

, et sa traduction dans le

langage C. 1. #include <stdio.h> 2. 3. float f(float x, int y) 4. { 5. return (2 * x * x * x + y + 1); 6. } 7. 8. int main(void) 9. { 10. float u, valeur; 11. 12. valeur = f(5.3, 2); 13. printf("la valeur de f(5.3, 2) est %f\n", valeur); 14. 15. u = 2.2; 16. valeur = f(u, 5); /* appel de procédure */ 17. printf("la valeur de f(%f, 5) est %f\n", u, valeur); 18. 19. return 0; 20. } Point important : il est autorisé d’utiliser plusieurs fois l’instruction return pour interrompre prématurément l’exécution de la fonction. Ceci est à utiliser avec précaution.

Page 30: cours c impécable.pdf

30

6.2. Les prototypes et les appels inter-fonctions Dans certains cas, il est possible qu’une fonction appelle une fonction dont la déclaration intervient à la suite, dans le code du programme. Dans ce cas, il est nécessaire de déclarer le prototype de la fonction avant de pouvoir l’utiliser. Exemple : 1. #include <stdio.h> 2. 3. /* Déclaration du prototype (noter le point-virgule) */ 4. float f(int y); 5. 6. int main(void) 7. { 8. float valeur; 9. 10. valeur = f(11); 11. printf("le resultat de f(11) est %f\n", valeur) ; 12. 13. return 0; 14. } 15. 16. /* Déclaration complète de la fonction f */ 17. float f(int y) 18. { 19. return (y * y + 2); 20. } 21.

6.3. Les variables globales Jusqu’à présent, les variables étaient considérées comme étant locales à un bloc d’instructions. Il est cependant possible de déclarer des variables en dehors de ces blocs et qui peuvent être manipulées par différentes fonctions. En général, la déclaration de ces variables globales est réalisée avant la déclaration des fonctions. Cependant, les variables globales sont à éviter autant que possible. Exemple :

Page 31: cours c impécable.pdf

31

1. #include <stdio.h> 2. 3. int compteur = 11; 4. 5. int incrementer(int increment) 6. { 7. compteur += increment; 8. return compteur; 9. } 10. 11. int main(void) 12. { 13. printf("le compteur vaut %d\n", compteur); 14. compteur = incrementer(3) + 2; 15. printf("le compteur vaut %d\n", compteur); 16. 17. return 0; 18. }

6.4. Le type void et les procédures En C, il existe un type particulier qui est le type void qui signifie « rien » ou « néant ». C’est type qu’on utilise lorsqu’une fonction ne nécessite pas d’arguments. Ce type peut également être utilisé pour indiquer qu’il n’y a pas de valeur de retour. Dans ce dernier cas, cette fonction se nomme procédure et l’instruction return n’a plus a être utilisée. Le type void est également utilisé dans d’autres circonstances avec notamment les pointeurs. Lors de l’appel d’une fonction ou d’une procédure ne prenant aucun paramètre, il convient de ne pas oublier les parenthèses (c.f. exemple). Exemples : voici quelques exemples d’utilisation du type void avec les fonctions et procédures 1. #include <stdio.h> 2. 3. float indice = 2.5; 4. 5. float PI(void) { return 3.14159; } 6. 7. void ajoutIndice(int x) { indice += x ; } 8. 9. void incrementIndice(void) { indice += 3; } 10. 11. int main(void) 12. { 13. indice = PI(); 14. ajoutIndice(6); 15. incrementIndice(); 16. 17. return 0; 18. }

6.5. Variables globales et portée des identificateurs Il est autorisée de déclarer deux variables portant le même nom avec un type identique ou différent dans deux blocs imbriqués. Les variables suivent les règles suivantes :

• Toute variable définie dans un bloc est exploitable dans tout le bloc et sous-blocs, • Une variable n’existe plus en dehors du bloc,

Page 32: cours c impécable.pdf

32

• Entre deux variables identiques, c’est la déclaration « la plus proche » qui est utilisée. Exemple : #include <stdio.h> float a, b; int main(void) { float a, b; if(a == 0) { float a, b; } } Exemple : 1. #include <stdio.h> 2. 3. int i = 10; 4. int main(void) 5. { 6. int i = 20, j = 3; 7. 8. if (j == 3) 9. { 10. int i = 30; 11. printf("valeur de i %d\n", i); 12. } 13. 14. printf("valeur de i %d\n", i); 15. return 0; 16. }

Page 33: cours c impécable.pdf

33

7. Tableaux et chaînes de caractères 7.1. Les tableaux Un tableau est un regroupement consécutif de données de même type et de taille fixe. A chaque entrée du tableau correspond un indice pouvant prendre une valeur dans l’intervalle [0, taille – 1]. Il est possible de déclarer des tableaux avec de multiples indices (tableaux à plusieurs dimensions) comme les matrices. La syntaxe est : Syntaxe (tableau à une dimension) :

<type> <nom du tableau>[<expression entière constante>]; Syntaxe (tableau à plusieurs dimensions) :

<type> <nom du tableau>[<expr entière>]…[<expr entière>]; Point important : l’expression entre crochet indique le nombre d’éléments que peut contenir le tableau (taille du tableau selon sa dimension). Cependant, cette expression doit être impérativement constante, entière et positive car le compilateur doit connaître à l’avance cette taille pour produire l’exécutable. Exemple : 1. #include <stdio.h> 2. int main(void) 3. { 4. int i, j; 5. int t1[3]; /* tableau à une dimension */ 6. float t2[5][5]; /* tableau à deux dimensions */ 7. 8. /* 9. ** Utilisation des opérateurs usuels avec les éléments du tableau 10. */ 11. t1[0] = 10; 12. t1[1] = 20; 13. t1[2] = t1[0] + t1[1]; 14. 15. for(i = 0; i < 5 ; i++) 16. { 17. for(j = 0; j < 5; j++) 18. { 19. t2[i][j] = i + j; 20. } 21. } 22. 23. return 0; 24. } Points importants :

• L’indice entre crochet pour accéder à un élément du tableau doit être impérativement entre [0] et [taille – 1].

• Le compilateur ne contrôle pas le débordement d’indice, ni à la compilation, ni à l’exécution.

Page 34: cours c impécable.pdf

34

7.2. Les chaînes de caractères Une chaîne de caractères est un cas particulier de tableau contenant des éléments de type char uniquement et contenant systématiquement 0 comme dernière valeur que l’on nomme zéro terminal ou caractère nul noté '\0'. En tenant compte du zéro terminal, la taille exacte du chaîne de caractère est égale à nb de caractères + 1. De plus, un tableau peut être initialisé à l’aide d’une chaîne de caractères entre guillemets : le zéro terminal est ajouté automatiquement par le compilateur. Enfin, une chaîne caractère est affichable à l’aide de la fonction printf en utilisant la chaîne de formatage %s. Il existe des fonctions particulières pour manipuler les chaînes de caractères, comme la concaténation, qui seront abordées avec l’étude sur les pointeurs. Exemple : 1. #include <stdio.h> 2. 3. int main(void) 4. { 5. char chaine[7] = "Coucou"; 6. /* sous cette forme, le 0 terminal est ajouté automatiquement */ 7. 8. printf("le tableau contient la chaine \"%s\"\n", chaine); 9. 10. chaine[0] = 'H'; 11. chaine[1] = 101; /* Code ascii de la lettre e */ 12. chaine[2] = 'l'; 13. chaine[3] = 'l'; 14. chaine[4] = 'o'; 15. chaine[5] = '\0'; 16. 17. printf("la nouvelle chaine est \"%s\"\n", chaine) ; 18. 19. return 0; 20. } 21.

Page 35: cours c impécable.pdf

35

8. Pointeurs 8.1. Généralités sur les pointeurs Le type pointeur fait partie de la famille des types élémentaires et la valeur contenue dans une variable de type pointeur est l’adresse d’une donnée stockée en mémoire (on parle également de référence). Aussi, pour connaître la nature de cette donnée, il convient d’en préciser son type lors de la déclaration du pointeur. Par abus de langage, le terme pointeur est à la fois utilisé pour désigner le type et la variable. Syntaxe (déclaration d’une variable de type pointeur) :

<type> * <variable>

Exemple : 1. int main(void) 2. { 3. int i; /* une variable de type entier */ 4. float f; /* une variable de type flottant */ 5. int * iPtr; /* une variable pointeur sur un entier */ 6. float * fPtr; /* une variable pointeur sur un flottant */ 7. 8. […] 9. } Les pointeurs sont très utiles et relativement indispensables. Cela permet de :

• Modifier les paramètres en retour d’une fonction (passage par référence par opposition au passage par copie),

• Passer un tableau d’une taille quelconque en paramètre d’une fonction, • Gérer dynamiquement la mémoire, • Accéder à n’importe quelle donnée en mémoire.

Représentation en mémoire d’un pointeur « sachant pointer » : un pointeur p sur un entier i

0x00000000

(int) i = 50 0x0A143FC2

(int *) p = 0x0A143FC2 0x3F25187F

0xFFFFFFFF Dans cette illustration, nous considérons une variable p de type pointeur sur un entier. Cette variable p contient comme valeur l’adresse d’une variable i de type entier. Aussi, comme le symbolise la flèche, on dit que p pointe sur la variable i. De plus, à l’aide de ce pointeur, comme nous le verrons dans la suite, il est possible de modifier indirectement la valeur de la variable i à l’aide des deux opérateurs & (“adresse de”) et * (“valeur de”).

« pointe sur »

Page 36: cours c impécable.pdf

36

Autre représentation : p

8.2. Opérateur & : adresse de L’opérateur & permet d’obtenir l’adresse de n’importe quelle variable (on parle également de référence) et retourne ainsi un pointeur sur la variable manipulée. Syntaxe :

&<variable> Exemple : dans cet exemple, nous réutilisons le schéma précédent et montrons comment faire en sorte que la variable pointeur p pointe sur la variable i. 1. int main(void) 2. { 3. int i = 50; 4. int * p; /* déclaration d’un pointeur non initialisé */ 5. /* (pointe sur une zone quelconque de la mémoire */ 6. 7. p = &i; /* p pointe désormais sur la variable i */ 8. 9. return 0; 10. } Attention : il convient de vérifier que la variable et le pointeur sont de type compatible. De plus, lorsque l’on déclare une variable de type pointeur, la valeur de cette variable est quelconque ce qui revient à dire que le pointeur déclaré pointe n’importe où dans la mémoire. Ceci est source de nombreuses erreurs dûes à ce problème de non-initialisation que l’on rencontre avec tout type de variable.

8.3. Opérateur * : valeur de L’opérateur * réalise l’inverse de l’opérateur &. Connaissant un pointeur sur une donnée, il est possible de lire et de modifier la valeur de celle-ci en utilisant cet opérateur. Syntaxe :

*<pointeur> Exemple : 1. #include <stdio.h> 2. int main(void) 3. { 4. int i = 50; 5. int * p; // déclaration d’un pointeur non initialisé 6. 7. p = &i // p pointe désormais sur la variable i 8. (*p) = 18; // a l’aide de l’opérateur *, la valeur de i est modifiée 9. // et cette instruction est équivalente à i = 18; 10. printf("%d\n", i); // cette instruction doit afficher en théorie 18 11. 12. return 0; 13. }

i = 50« pointe sur »

Page 37: cours c impécable.pdf

37

Combiné avec cet opérateur, le pointeur se comporte alors comme la variable pointée. Il est alors possible d’utiliser tous les opérateurs compatibles avec le type de la variable. Exemple : 1. int main(void) 2. { 3. int i = 18, *iPtr; 4. float f = 3.6f, *fPtr; 5. double d = 0.5, *dPtr; 6. 7. iPtr = &i; 8. fPtr = &f; 9. dPtr = &d; 10. 11. (*dPtr) = d + 3.0 * (*iPtr) + i / (*fPtr) – (*dPtr); 12. 13. return 0; 14. } Attention : ne jamais accéder à une donnée pointée avec un pointeur qui n’a pas été correctement initialisé. Ceci est source de bug et provoque en général l’arrêt involontaire du programme. Exemple : mauvais exemple dû à l’utilisation d’un pointeur non initialisé. 1. int main(void) 2. { 3. int i = 50; 4. int * p; 5. 6. (*p) = 12; /* ERREUR : le pointeur n’est pas initialisé */ 7. 8. return 0 ; 9. }

8.4. Une valeur de pointeur particulière : NULL Il existe un pointeur de valeur particulière, notée NULL, de valeur égale à 0 et pointant sur la zone mémoire d’adresse 0x00000000. Cette valeur est utilisée pour initialiser des variables de type pointeur et pour détecter plus facilement les erreurs de pointage. En particulier, lorsqu’on ne connaît pas encore la valeur du pointeur lors de sa déclaration, il fortement recommandé de l’initialiser systématiquement avec le pointeur NULL. Exemple :

Page 38: cours c impécable.pdf

38

1. #include <sdtio.h> 2. 3. int main(void) 4. { 5. int i = 50 ; 6. int * p = NULL ; /* déclaration d’un pointeur initialisé */ 7. /* mais qui n’est pas utilisable */ 8. 9. p = &i; // le pointeur pointe désormais sur une zone valide 10. (*p) = 12; 11. 12. return 0; 13. } Attention : ne jamais accéder à une donnée pointée par NULL à l’aide de l’opérateur *. Avant de manipuler un pointeur, il faut systématiquement vérifier que celui-ci n’est pas équivalent à NULL. Exemple : mauvais exemple dû à l’utilisation d’un pointeur nul. 1. int main(void) 2. { 3. int i = 50; 4. int * p = NULL; 5. 6. (*p) = 12; /* ERREUR : le pointeur pointe sur une zone non valide */ 7. /* (i.e. NULL) */ 8. 9. return 0 ; 10. } A savoir : la constante symbolique NULL est définie à l’aide d’une directive de précompilation #define NULL ((void *) 0)

8.5. Pointeur de pointeur Par définition, une variable pointeur peut pointer sur n’importe quel type, y compris un pointeur. La déclaration et l’utilisation d’un pointeur de pointeur suit les mêmes règles qu’un pointeur sur un type élémentaire. Syntaxe :

<type> ** <variable> Exemple : ptr2 est un pointeur qui pointe sur ptr1 et ptr1 pointe sur la variable i.

Page 39: cours c impécable.pdf

39

1. int main(void) 2. { 3. int i, * ptr1; 4. int ** ptr2; /* déclararation d’un pointeur de pointeur */ 5. 6. ptr1 = &i; /* ptr1 pointe sur i */ 7. ptr2 = &ptr1; /* ptr2 pointe sur ptr1 */ 8. (*(*ptr2)) = 12; /* par le jeu d’un double « déréférencement » 9. /* la valeur de i est modifiée. L’instruction est */ 10. /* équivalente à i = 12; */ 11. printf("%d\n", i); 12. 13. return 0; 14. }

8.6. Pointeur et paramètres de fonctions

8.6.1 Passage par référence Une limitation liée aux fonctions est l’impossibilité de modifier les paramètres autrement que localement. En effet, les valeurs des paramètres sont passés par copie et les modifications apportées sont appliquées uniquement à ces copies. Le problème se complique lorsqu’il s’agit de tableaux de grande dimension car c’est une méthode très gourmande en ressources : il est peu recommandable de passer par copie un tableau de plusieurs Mo. Exemple de passage par copie des paramètres : Les variables a et b ne sont pas modifiées lors de l’appel de la fonction. Les modifications apportées aux variables x et y sont locales à la fonction et n’ont aucun impact sur les variables a et b. 1. void formule(int x, int y) 2. /* x et y contiennent la copie des valeurs de a et b lors de l’appel

de la fonction */ 3. { 4. /* x et y sont modifiées localement et ces opérations*/ 5. x = x + y + 12; y++; 6. /* n’ont aucun effet en dehors de la fonction */ 7. } 8. 9. int main(void) 10. { 11. int a = 12, b = 55; 12. 13. formule(a, b); 14. /* a et b ne sont pas affectés pas l’appel de la fonction : */ 15. /* leur valeur reste la même avant et après la fonction */ 16. 17. return 0; 18. } Aussi, un moyen pour contourner cette limitation consiste à exploiter les pointeurs. Le passage de paramètres par le biais des pointeurs est qualifié de passage par référence. Cette autre méthode consiste à passer l’adresse de la variable (on parle de référence) en guise de paramètre à l’aide de l’opérateur & au lieu de passer sa valeur.

Page 40: cours c impécable.pdf

40

Exemple : Version passage par référence de l’exemple précédent. 1. void formule(int * x, int * y) 2. /* Lors de l’appel de la fonction, x et y sont deux pointeurs

contenant les adresses des variables a et b */ 3. { 4. (*x) = (*x) + (*y) + 12; 5. /* les modifications apportées via x et y sont répercutées sur a et

b */ 6. (*y) ++; 7. } 8. 9. int main(void) 10. { 11. int a = 12, b = 55; 12. 13. formule(&a, &b); 14. // la fonction est appelée en donnant les références 15. // sur les variables a et b. A l’issue, les valeurs 16. // de a et b sont modifiées 17. return 0; 18. } Utilisation du pointeur NULL pour la programmation défensive : L’utilisation du passage par référence est également soumis au problème de validité des pointeurs. En d’autre terme, avant toute manipulation des paramètres, il est nécessaire de vérifier que les pointeurs sont valides. Cependant, il est pratiquement impossible de déterminer si un pointeur pointe sur une zone valide ou non. La seule vérification possible consiste à s’assurer que le pointeur n’est pas le pointeur NULL. Exemple : 1. void fonction_non_defensive(int * x) 2. { 3. (*x)++; /* source d’erreur si x n’est pas un pointeur valide */ 4. } 5. 6. void fonction_ defensive(int * x) { if (x != NULL) { (*x)++; } } 7. 8. int main(void) 9. { 10. fonction_non_defensive(NULL); /* appel de fonction source d’erreur */ 11. fonction_defensive(NULL); /* fonction "protégée" */ 12. 13. return 0; 14. }

8.7. Pointeurs et tableaux

8.7.1 Tableau de pointeurs En programmation C, un tableau peut contenir des éléments de n’importe quel type. En particulier, il est possible de définir un tableau de pointeurs. La syntaxe est inchangée en dehors du fait que l’on utilise le type pointeur pour définir la nature des éléments.

Page 41: cours c impécable.pdf

41

Exemple : 1. #include <stdio.h> 2. 3. int main(void) 4. { 5. int i, j, k, l; /* quatre entiers */ 6. int* t1[4]; /* un tableau de 4 pointeurs sur des entiers */ 7. int* t2[2][2]; /* un tableau 2D de pointeurs sur des entiers */ 8. 9. /* 10. ** le premier élément de t1 pointe sur i 11. ** le second élément de t1 pointe sur j 12. ** le troisième élément de t1 pointe sur k 13. ** le quatrième élément de t1 pointe sur l 14. */ 15. t1[0] = &i; t1[1] = &j; t1[2] = &k; t1[3] = &l; 16. 17. (*t1[0]) = 12 /* équivalent à i = 12; */ 18. (*t1[1]) = 99 /* équivalent à j = 99; */ 19. (*t1[2]) = 25 /* équivalent à k = 25; */ 20. (*t1[3]) = 57 /* équivalent à l = 57; */ 21. /* 22. ** l’élément (0,0) pointe sur i 23. ** l’élément (0,1) pointe sur j 24. ** l’élément (1,0) pointe sur k 25. ** l’élément (1,1) pointe sur l 26. */ 27. t2[0][0] = &i; t2[0][1] = &j; 28. t2[1][0] = &k; t2[1][1] = &l; 29. 30. (*t2[0][0]) = 255 /* équivalent à i = 255; */ 31. (*t2[0][1]) = 374 /* équivalent à j = 374; */ 32. (*t2[1][0]) = 199 /* équivalent à k = 199; */ 33. (*t2[1][1]) = 736 /* équivalent à l = 736; */ 34. 35. return 0; 36. }

8.7.2 Analogie entre tableau et pointeur o Il y a une analogie entre les tableaux et pointeurs dans le sens où un pointeur peut être

utilisé avec l’opérateur [] destiné aux tableaux. De plus, le nom d’un tableau peut être considéré comme étant un pointeur sur son premier élément, c’est à dire équivalent à &(t[0]). Par contre, contrairement au pointeur, il est impossible de le faire pointer sur une autre zone. Si l’opérateur [] peut également être utilisé avec les pointeurs pour les tableaux à plusieurs dimensions (cf chapitre sur la gestion dynamique de la mémoire). Par contre, l’analogie entre pointeurs et tableaux n’est valide que pour les tableaux à une seule dimension.

Page 42: cours c impécable.pdf

42

Exemple : 0 1 2 3 4 5 6 7 8 9 t 255 4 127 8 10 12 14 16 18 20

t1 x

1. int main(void) 2. { 3. int t[10] = { 2, 4, 6, 8, 10, 12, 14, 16, 18, 20 }; 4. int * t1, * x; 5. 6. t1 = t; /* équivalent à t1 = &(t[0]); */ 7. t1[3] = 255; /* équivalent à t[3] = 255; */ 8. x = &(t[2]); /* x pointe sur le troisième élément de t */ 9. (*x) = 127; /* équivalent à t[2] = 127; */ 10. x[0] = 127; /* équivalent à (*x) = 127; donc à t[2]=127 */ 11. 12. return 0; 13. } Remarque : Cette analogie est largement utilisée dans le cadre des chaînes de caractères qu’elles soient déclarées de façon statique ou dynamique. Aussi, la notation pointeur pour les chaîne de caractères est également très utilisée avec la notation tableau. Exemple : char ch1[7] = "Coucou"; char ch2[] = "Hello"; char * ch3 = "Bonjour"; /* Utilisation de la notation pointeur */

8.7.3 Passage par référence d’un tableau en paramètre d’une fonction Du fait de la conversion d'un identificateur de type tableau en l'adresse du premier élément, lorsqu'un tableau est passé en paramètre effectif, c'est cette adresse qui est passée en paramètre. Le paramètre formel correspondant devra donc être déclaré comme étant de type pointeur. Voyons sur un exemple. Soit à écrire une procédure imp_tab qui est chargée d'imprimer un tableau d'entiers qui lui est passé en paramètre. On peut procéder de la manière suivante : 1. void imp_tab(int *t, int nb_elem) /* définition de imp_tab */ 2. { 3. int i; 4. for (i = 0; i < nb_elem; i++) printf("%d ",*(t + i)); 5. } Cependant, cette méthode a un gros inconvénient. En effet, lorsqu'on lit l'en-tête de cette procédure, c'est à dire la ligne : void imp_tab(int *t, int nb_elem) il n'est pas possible de savoir si le programmeur a voulu passer en paramètre un pointeur vers un int (c'est à dire un pointeur vers un seul int), ou au contraire si il a voulu passer un tableau, c'est à dire un pointeur vers une zone de n int. De façon à ce que le programmeur puisse exprimer cette différence dans l'en-tête de la procédure, le langage C admet que l'on puisse déclarer un paramètre formel de la manière suivante :

Page 43: cours c impécable.pdf

43

1. void proc(int t[]) 2. { 3. ... /* corps de la procédure proc */ 4. } car le langage assure que lorsqu'un paramètre formel de procédure ou de fonction est déclaré comme étant de type tableau de X, il est considéré comme étant de type pointeur vers X. Si d'autre part, on se souvient que la notation *(t + i) est équivalente à la notation t[i], la définition de imp_tab peut s'écrire : 1. void imp_tab(int t[], int nb_elem) /* définition de imp_tab */ 2. { 3. int i; 4. for (i = 0; i < nb_elem; i++) printf("%d ",t[i]); 5. } Cette façon d'exprimer les choses est beaucoup plus claire, et sera donc préférée. L'appel se fera de la manière suivante : 1. #define NB_ELEM 10 2. int tab[NB_ELEM]; 3. 4. int main(void) 5. { 6. imp_tab(tab,NB_ELEM); 7. } 8. Remarque : Quand une fonction admet un paramètre de type tableau, il y a deux cas possibles :

- soit les différents tableaux qui lui sont passés en paramètre effectif ont des tailles différentes, et dans ce cas la taille doit être un paramètre supplémentaire de la fonction, comme dans l'exemple précédent ;

- soit les différents tableaux qui lui sont passés en paramètre effectif ont tous la même taille, et dans ce cas la taille peut apparaître dans le type du paramètre effectif :

9. 10. #define NB_ELEM 10 11. void imp_tab(int t[NB_ELEM]) 12. { 13. ... 14. } 15. 16. 17. Cette analogie entre pointeur et tableau permet ainsi le passage par référence d’un tableau en paramètre d’une fonction. Ceci évite de recopier tout le contenu d’un tableau lors de l’appel d’une fonction. Exemple :

Page 44: cours c impécable.pdf

44

1. #include <stdio.h> 2. 3. /* Fonction qui calcule la somme des termes contenus dans le tableau t */ 4. int sommeTableau(int * t, int n) 5. { 6. int i, somme = 0; 7. 8. if (t != NULL) { for(i = 0 ; i < n ; i++) { somme += t[i]; } } 9. return somme; 10. } 11. 12. int main(void) 13. { 14. int t[5] = { 1, 3, 5, 7, 9 }; 15. 16. printf("%d\n", sommeTableau(t, 5)); 17. 18. return 0; 19. }

8.7.4 Arithmétique de pointeurs et notion de décalage (offset) Les opérateurs arithmétiques +, -, ++ et -- peuvent être également utilisés pour réaliser des décalages en mémoire. Cette méthode est très utilisées pour se repérer dans les tableaux. Exemple :

0 1 2 3 4 5 6 7 8 9 t 2 4 6 127 34 23 14 16 18 20

y y+3

x1 x2

1. int main(void) 2. { 3. int * x, * y; 4. int t[10] = { 2, 4, 6, 8, 10, 12, 14, 16, 18, 20 }; 5. 6. y = &(t[0]); /* y pointe sur le 1er élément */ 7. x = y + 5; /* x pointe sur le 5ème élément */ 8. (*x) = 34; /* équivalent à t[5] = 34; */ 9. x++; 10. (*x) = 23; /* équivalent à t[6] = 23; */ 11. *(y + 3) = 127; /* équivalent à t[3] = 127; */ 12. 13. return 0; 14. }

8.7.5 Opérateurs de comparaison Les opérateurs de comparaison <=, >=, <, >, != et == peuvent également être utilisés pour comparer des pointeurs, c’est-à-dire des adresses. En particulier, il peut être parfois utile de savoir si un pointeur est situé avant ou après un autre pointeur.

+5 +3

+1

Page 45: cours c impécable.pdf

45

9. Définition de types, types énumérés 9.1. Typedef Le mot-clé typedef permet de définir des synonymes (alias) sur des types de données élémentaires ou composés. Cela revient à créer de nouveaux types et à rendre plus lisible le code. Syntaxe :

typedef <type> <nom du synonyme>; Exemple :

1. typedef int MonTypeEntier; /* Nouveau type équivalent à int */ 2. typedef int * IntPtr; /* Nouveau type équivalent à int* */ 3. int main(void) 4. { 5. MonTypeEntier n = 0; /* Déclaration avec le nouveau type */ 6. IntPtr p = &n; 7. 8. p = &n; 9. (*p) = 12; /* instruction équivalent à n = 12; */ 10. 11. return 0; 12. }

9.2. type énuméré : définition de constantes symboliques Le mot-clé enum est ce que l’on appelle un type énuméré permettant de définir des constantes symboliques équivalentes à des valeurs entières. Par la suite, la constante symbolique peut être utilisée telle quelle dans le code. Cette approche est très utile pour rendre plus lisible le code et pour le faire évoluer plus facilement (changer les valeurs). Lors de la compilation, la constante symbolique est remplacée par la valeur entière qu’elle représente. Syntaxe :

enum {<nom de constante> [=<expr>]1[,<nom de constante> [=<expr>]1]+ }; Par défaut, la première constante est équivalente à la valeur 0, la suivante à la valeur 1, etc. Exemple : enum { NORD, /* NORD est équivalent à la valeur 0 */ SUD, /* SUD est équivalent à la valeur 1 */ EST, /* EST est équivalent à la valeur 2 */ OUEST /* OUEST est équivalent à la valeur 3 */ }; Cependant, il est possible de préciser la valeur d’une constante à l’aide de l’opérateur =. La constante suivante est tout simplement égale à la valeur de la constante précédente plus 1. Exemple :

Page 46: cours c impécable.pdf

46

1. enum 2. { 3. PIQUE, /* PIQUE est équivalent à la valeur 0 */ 4. COEUR = 12, /* COEUR est équivalent à la valeur 12 */ 5. CARREAU, /* CARREAU est équivalent à la valeur 13 */ 6. TREFLE /* TREFLE est équivalent à la valeur 14 */ 7. }; 8. 9. int main(void) 10. { 11. int carte = CARREAU; /* équivalent à carte = 13; */ 12. int direction = SUD; /* équivalent à direction = 1; */ 13. 14. if (carte == TREFLE) { […] } /* équivalent à if (carte == 14) { […] }*/ 15. 16. switch(direction) 17. { 18. case NORD: /* équivalent à case 0: */ 19. […]; 20. break; 21. 22. case SUD: /* équivalent à case 1: */ 23. […]; 24. break; 25. 26. case EST: /* équivalent à case 2: */ 27. […]; 28. break; 29. 30. case OUEST: /* équivalent à case 3: */ 31. […]; 32. break; 33. 34. default: 35. break; 36. } 37. 38. […] 39. 40. return 0; 41. }

Page 47: cours c impécable.pdf

47

10. Structures 10.1. Généralités Si les tableaux permettent le regroupement de plusieurs données de même type, il souvent fort utile de pouvoir regrouper des données de type différent. C’est ce que permettent les structures (struct), selon la terminologie employée par le langage C. Pour désigner les différentes données composant la structure, on parle alors de champs. A chaque champ est associé un type. Définir une structure, revient à créer un nouveau type. Dans la syntaxe proposée ci-dessous, le mot-clé typedef n’est pas utilisé. Dans la suite, nous montrons quel est l’effet de l’utilisation du mot-clé typedef combiné avec les structures. Syntaxe : définition d’une structure

struct <nom de la structure> { [<type> <nom du champ>;]+ }; Exemple : 1. /* Définition du type Personne identifié par son age, ses nom et

prénom */ 2. struct Personne 3. { 4. int age; 5. char * nom; 6. char * prenom; 7. }; 8. 9. int main(void) 10. { 11. […] 12. } Point important : les déclarations de structure sont généralement situées en dehors du corps des fonctions. Par la suite, il devient alors possible de déclarer et d’exploiter des variables de type structure. Aussi, pour déclarer une variable de ce type, le mot-clé struct est indipensable. Dans la suite, nous verrons qu’avec le mot-clé typedef, il est possible de faire disparaître le mot-clé struct. Syntaxe : déclaration de variables de type structure

struct <nom de la structure> <nom de variable>[,<nom de variable>]+;

Tout comme les tableaux, il est également possible d’initialiser tous les champs d’une structure en donnant la liste des valeurs séparées par des virgules, le tout entre accolades. L’ordre des valeurs est déterminé par l’ordre dans lequel sont déclarés les champs. Pour accéder et modifier les différents champs, qui s’utilisent de la même façon que n’importe quelle variable, on utilise la notation pointée ".". Syntaxe :

<nom de la variable de type structure>.<nom du champ>

Page 48: cours c impécable.pdf

48

Exemple : 1. /* Création du type Personne */ 2. struct Personne 3. { 4. int age; 5. char * nom; 6. char * prenom; 7. }; 8. 9. int main(void) 10. { 11. struct Personne p1 = { 25, "Martin", "Pierre" }; 12. struct Personne p2; 13. 14. p2.age = p1.age; 15. p2.nom = "Dupont"; 16. p2.prenom = "Hubert"; 17. 18. p2.age = p2.age+50; 19. 20. return 0; 21. } 22. A savoir : le nom de la structure n’est pas indispensable lorsque l’on combine la déclaration de la structure et déclaration de variables de ce type. Le nom de la structure est nécessaire lorsque l’on désire déclarer plusieurs instances d’une même structure à divers endroits du programme. Exemple : version sans déclaration de structure de l’exemple précédent. 1. int main(void) 2. { 3. struct 4. { 5. int age; 6. char * nom; 7. char * prenom; 8. } p1 = { 25, "Martin", "Pierre" }, p2; 9. 10. p2.age = p1.age; 11. p2.nom = "Dupont"; 12. p2.prenom = "Hubert"; 13. 14. p2.age += 5; 15. 16. return 0; 17. }

10.2. Structure et typedef Les structures appartiennent à la catégorie des types composés. A ce titre, il est possible de créer de nouveaux types avec le mot-clé typedef. Un des effets de ce genre d’instruction est de faire disparaître le mot-clé struct. De plus, le nom de la structure, à ne pas confondre avec le nom du nouveau type, peut être omis. Par contre, l’utilisation de la structure et l’accès aux différents champs reste inchangée.

Page 49: cours c impécable.pdf

49

Syntaxe :

typedef struct [<nom de la structure>]1 {<liste des champs>} <nom du type>; Exemple : 1. struct Date /* Exemple sans typedef */ 2. { 3. int jour; 4. int mois; 5. int annee; 6. }; 7. 8. typedef struct /* Exemple avec typedef */ 9. { 10. int heures; 11. int minutes; 12. int secondes; 13. } Heure; 14. 15. int main(void) 16. { 17. struct Date jour; /* déclaration sans typedef : struct est conservé

*/ 18. Heure horloge; /* déclaration avec typedef : struct est supprimé

*/ 19. 20. dateDuJour.jour = 17; 21. horloge.heures = 10; 22. 23. […]; 24. }

10.3. Structures imbriquées Les structures appartiennent à la famille des types composés. Aussi, il est autorisé de déclarer des champs de type structure au sein même d’une autre structure (s’il s’agit de la même structure, on parle de structure récurrente dont le sujet est abordé dans la suite). Pour accéder aux différents champs de ce champ de type structure, il est alors nécessaire d’utiliser la notation pointée autant de fois que cela est nécessaire. Exemple :

Page 50: cours c impécable.pdf

50

1. struct Date { int jour; int mois; int annee; }; 2. typedef struct { int heures; int minutes; int secondes; } Heure; 3. 4. /* Déclaration d’une structure composée de deux champs de type

structure */ 5. struct RendezVous 6. { 7. struct Date date; 8. Heure horaire; 9. }; 10. 11. int main(void) 12. { 13. struct RendezVous rdv; 14. /* Accès aux champs du champ date */ 15. rdv.date.jour = 14; 16. rdv.date.mois = 2; 17. rdv.date.annee = 2004; 18. /* Accès aux champs du champ horaire */ 19. rdv.horaire.heures = 14; 20. rdv.horaire.minutes = 30; 21. rdv.horaire.secondes = 17; 22. 23. return 0; 24. }

10.4. Structures et tableaux Par extension, il possible de déclarer des tableaux de structures ainsi que des champs de type tableau au sein d’une structure. Suivant la déclaration, l’utilisation de l’opérateur [] doit être effectuée dans le bon ordre. Exemple :

Page 51: cours c impécable.pdf

51

1. struct Date { int jour; int mois; int annee; }; 2. typedef struct { int heures; int minutes; int secondes; } Heure; 3. struct RendezVous { struct Date date; Heure horaire; }; 4. 5. /* structure Agenda contenant un champ de type tableau de structures

*/ 6. struct Agenda 7. { 8. struct Date calendrier[365]; 9. }; 10. 11. int main(void) 12. { 13. struct RendezVous rdv[5]; /* tableau de 5 structures de type

RendezVous */ 14. struct Agenda agenda; 15. 16. rdv[0].date.jour = 14; 17. agenda.calendrier[38].jour = 29; 18. }

10.5. Structures et pointeurs Par généralisation, les pointeurs peuvent être également exploités avec les structures. Il est alors possible d’avoir des pointeurs sur des structures. Toutefois, le langage définis l’opérateur flèche "->" pour accéder aux différents champs d’une structure à partir d’un pointeur, autrement que par l’opérateur de déférencement "*". Cet opérateur a l’avantage d’indiquer explicitement que le code manipule un pointeur sur une structure. L’exemple suivant montre comment ces deux opérateurs sont utilisés. Syntaxe :

<pointeur> -> <champ> Exemple :

Page 52: cours c impécable.pdf

52

1. struct Date { int jour; int mois; int annee; }; 2. 3. int main(void) 4. { 5. struct Date * p; /* pointeur sur une structure Date */ 6. struct Date today; /* variable de type structure Date */ 7. 8. today.jour = 17; 9. today.mois = 9; 10. today.annee = 2004; 11. p = &today; /* initialisation du pointeur : celui-ci pointe

sur */ 12. /* la variable today */ 13. 14. (*p).annee = 2012; 15. /* utilisation de l’opérateur *, l’instruction est équivalente à

today.annee = 2012; */ 16. 17. p -> jour = 24; /* utilisation de l’opérateur ->, l’instruction est équivalente à

(*p).jour = 24; qui est équivalente à today.jour = 24;*/ 18. return 0; 19. } Plus généralement, il est possible de définir des structures contenant des champs de type pointeur, des tableaux de pointeurs sur des structures, une structure contenant un tableau de pointeur, etc. Toutes les combinaisons possibles et imaginables (ou presque) peuvent être alors mises en œuvre. Exemple : exemple plus complet avec diverses combinaisons

Page 53: cours c impécable.pdf

53

1. struct Date { int jour; int mois; int annee; }; 2. struct Heure { int heures; int minutes; int secondes; }; 3. struct RendezVous { struct Date date; Heure horaire; }; 4. 5. struct RendezVous 6. { 7. struct Heure horaire; 8. struct Date * date; 9. struct Date calendrier[365]; 10. struct Heure * crenaux[10]; 11. }; 12. 13. int main(void) 14. { 15. struct Date today; 16. struct RendezVous rdv; 17. struct RendezVous * ptr; 18. 19. rdv.date = &today; 20. rdv.date -> jour = 14; 21. rdv.date -> annee = 2004; 22. 23. ptr = &rdv; 24. ptr -> horaire.heures = 10; 25. ptr -> horaire.minutes = 30; 26. 27. ptr -> date = &today; 28. ptr -> date -> jour = 28; 29. ptr -> date -> annee = 2004; 30. ptr -> calendrier[3].jour = 15; 31. ptr -> crenaux[3] -> heure = 18; return 0; }

10.6. Structures récursives Jusqu’à présent, nous avons pu observer que les structures peuvent contenir des champs de n’importe quel type, y compris de type structure. Cependant, une structure ne peut pas contenir un champ qui soit une instance d’elle-même. Par contre, il est possible pour une structure d’avoir un champ de type pointeur et pointant sur cette dernière. Ce cas est largement utilisé pour définir des listes chaînées. On parle alors de structure récurrente. Pour définir une structure récurrente, une méthode simple consiste à définir un type pointeur sur la structure avant de définir la structure elle-même. Cette méthode repose sur l’utilisation du mot-clé typedef selon deux étapes. L’exemple suivant montre comment déclarer une telle structure pour définir une liste chaînée d’entiers. Exemple : déclaration d’une structure récurrente

Page 54: cours c impécable.pdf

54

1. /* 1ère étape : déclaration d’un type pointeur sur la structure */ 2. typedef struct Liste * ListePtr; 3. 4. /* 2nde étape : déclaration de la structure */ 5. struct Liste 6. { 7. int valeur; 8. ListePtr suivant; /* la structure contient un champ pointant */ 9. /* sur ce type de structure */ 10. }; 11. 12. int main(void) 13. { 14. struct Liste element1; 15. struct Liste element2; 16. 17. element1.valeur = 5; 18. element1.suivant = &element2; 19. element1.suivant -> valeur = 10; 20. element1.suivant -> suivant = &element1; 21. 22. return 0; 23. } Le schéma suivant représente les relations entre les deux structures à l’issue du programme :

elément1 element2 valeur = 5 valeur = 10suivant suivant

Page 55: cours c impécable.pdf

55

11. Gestion dynamique de la mémoire Allocation de la mémoire : Contrairement aux tableaux définis statiquement ou aux déclarations de variables dont l’espace mémoire est réservé par le compilateur, il peut arriver que l’on ne connaisse pas à l’avance la taille d’un tableau ou d’une manière générale de l’espace mémoire dont on a besoin à l’exécution. Bien qu’avec les pointeurs on puisse accéder à n’importe quelle zone de la mémoire, il convient de réserver un espace mémoire pour être sûr de ne pas avoir un comportement aléatoire de notre programme (en théorie). Cette procédure d’allocation consiste à indiquer au système la taille de l’espace dont on a besoin et celui-ci nous retourne un pointeur sur le début de cette zone.

11.1. Fonctions d’attribution et de libération dynamique de la mémoire

Nécessite la directive de précompilation #include <stdlib.h> Type size_t : défini dans type.h et de type unsigned long Type utilisé pour les tailles de chaînes de caractères et de bloc mémoire, défini par l’ANSI-C void * malloc(<expression entière (i.e nb d’octets)>); /* allocation */ void * calloc(<taille du tableau>, <taille d’une cellule>); /* alloc tab */ void free(void * pointeur); /* libération */

11.2. Demande d'allocation mémoire (malloc) : malloc demande l'allocation d'un bloc de mémoire de size octets consécutifs dans la zone de mémoire du tas. Valeur retournée :

• Si l'allocation réussit, malloc retourne un pointeur sur le début du bloc alloué. • Si la place disponible est insuffisante ou si size vaut 0, malloc retourne NULL.

Remarque : Les fonctions d'allocation dynamique retournent des pointeurs sur des void. Il faut donc opérer des conversions de types explicites pour utiliser ces zones mémoire en fonction du type des données qui y seront mémorisées. Exemple :

Page 56: cours c impécable.pdf

56

1. #include <stdio.h> 2. #include <stdlib.h> 3. 4. main() { 5. char *ptr; 6. struct s_fiche { char nom[30]; 7. int numero; 8. struct s_fiche *suiv; 9. } *fiche; 10. 11. ptr = (char *) malloc(80); /* demande d'allocation de 80 octets */ 12. 13. if ( ptr == NULL) { 14. fprintf(stderr,"Allocation mémoire impossible\n"); 15. exit(1); 16. } 17. 18. if (fiche = (struct s_fiche *) malloc(sizeof(struct s_fiche)) ==

NULL) { 19. fprintf(stderr, "Allocation mémoire impossible\n"); 20. exit(1); 21. } 22. 23. free(fiche); /* libération de la mémoire */ 24. free(ptr); 25. }

11.3. Libération mémoire (free) : La fonction free libère un bloc mémoire d'adresse de début ptr. Ce bloc mémoire a été précédemment alloué par une des fonctions malloc, calloc, ou realloc. Attention :

• Il n'y a pas de vérification de la validité de ptr. • Ne pas utiliser le pointeur ptr après free, puisque la zone n'est plus réservée. • A tout appel de la fonction malloc ( ou calloc ) doit correspondre un et un seul appel à la

fonction free. • Les appels pour libérer des blocs de mémoire peuvent être effectués dans n'importe quel

ordre, sans tenir compte de celui dans lequel les blocs ont été obtenus à l'allocation. Valeur retournée : aucune Exemple 1 : 1. #include <stdio.h> 2. #include <stdlib.h> 3. 4. int main() { 5. int *str = NULL, i; 6. 7. str = (int *) malloc(10 * sizeof(int)); 8. for (i=0; i < 10; i++) 9. str[i] = i * 10; 10. printf("%d\n", i[9]); 11. free(str); 12. return 0; 13. }

Page 57: cours c impécable.pdf

57

Exemple 2 : illustration d'une erreur commise fréquemment 1. #include <stdio.h> 2. #include <stdlib.h> 3. 4. int main() { 5. char *str; 6. 7. str = (char *) malloc(100 * sizeof(char)); 8. gets(str); /* saisie d'une chaine de caracteres */ 9. 10. /* suppression des espaces en tete de chaine */ 11. while ( *str == ' ') 12. str++; 13. 14. /* free ne libere pas toute la zone allouee car str ne designe 15. plus le debut de la zone memoire allouee par malloc */ 16. free(str); 17. 18. return 0; 19. }

11.4. Demande d'allocation mémoire pour tableau (calloc) : La fonction calloc réserve un bloc de taille [nbr_element] x [element_size] octets consécutifs. Le bloc alloué est initialisé à 0. Valeur retournée :

• Si succès, calloc retourne un pointeur sur le début du bloc alloué. • Si échec, calloc retourne NULL s'il n'y a plus assez de place ou si nelem ou elsize valent 0.

Exemple : 1. #include <stdio.h> 2. #include <stdlib.h> 3. 4. int main() { 5. int *str = NULL; 6. 7. str = (char *) calloc(10, sizeof(int)); 8. printf("%d\n", str[9]); 9. free(str); 10. return 0; 11. }

11.5. Tableaux multiples : allocation et libération tableaux à 2 dimensions (10x20) int i; int ** tab; tab = (int **) malloc(10 * sizeof(int *)); if (tab != NULL) { for(i = 0; i < 10; i++) { tab[i] = (int *) malloc(20 * sizeof(int)); } }

Page 58: cours c impécable.pdf

58

for(i = 0; i < 10; i++) { free(tab[i]); } free(tab); tableaux à 3 dimensions (10x20x30) int i, j ; int *** tab; tab = (int ***) malloc(10 * sizeof(int **)); if (tab != NULL) { for(i = 0; i < 10; i++) { tab[i] = (int **) malloc(20 * sizeof(int *)); if (tab[i] != NULL) { for(j = 0; j < 20; j++) { tab[i][j] = (int *) malloc(30 * sizeof(int)); } } } }

11.6. Erreurs fréquentes indirection illégale char * ptr; (*ptr) = (char *) malloc(100 * sizeof(char)); (*ptr) = 'y'; débordement de tableau ou dépassement de zone mémoire int * tab ; tab = (int *) malloc(10 * sizeof(int)) ; tab[20] = 12; libération d’une zone invalide ou d’un tableau statique int * p ; int t[50]; free(p) ; free(t); accès mémoire interdit (cf init) int * p ; (*p) = 12 ; fuites mémoire void swap(int * ptr1, int * ptr2, int n) { int * tmp = (int *) malloc(n * sizeof(int)); memcpy(tmp, ptr1, n * sizeof(int)); memcpy(ptr1, ptr2, n * sizeof(int)); memcpy(ptr2, tmp, n * sizeof(int)); } utilisation d’un pointeur d’un espace libéré int * p = (int *) malloc(10 * sizeof(int)); free(p); p[0] = 10;

Page 59: cours c impécable.pdf

59

12. Manipulation des chaînes de caractères Type size_t : défini dans type.h et de type unsigned long Type utilisé pour les tailles de chaînes de caractères et de bloc mémoire, défini par l’ANSI-C Existence de fonctions standards pour la manipulation des chaînes de caractères Nécessite la direction de précompilation #include <string.h> size_t strlen (const char *s) /* taille d’une chaine */ char *strcat (char *dest, const char *src); char *strncat (char *dest, const char *src, size_t n) ; /* concatenation de deux chaînes, à savoir src copié à la suite de dest (avec arrêt au bout de n caractères pour strncat) */ int strcmp (const char *s1, const char *s2); int strncmp (const char *s1, const char *s2, size_t n); /* comparaison de deux chaînes (avec arrêt au bout de n caractères de s1 pour strncmp) */ Regles de comparaison : Si (s1 < s2) strcmp retourne –1 Si (s1 == s2) strcmp retourne 0 Si (s1 > s2) strcmp retourne 1 La comparaison est effectué caractère par caractère sur la base de leur code ASCII Ainsi : "abcd" < "abz" car 'c' < 'z' c’est-à-dire 99 < 122 Par contre "abcd" > "abZ" car 'c' > 'Z' c’est-à-dire 99 > 90 Et "abc" > "ab" car 'c' > '\0' c’est-à-dire 99 > 0 char *strcpy (char *dest, const char *src); char *strncpy (char *dest, const char *src, size_t n); /* copie de src dans dest (avec arrêt au bout de n caractères pour strncmp) */ char *strchr (const char *s, int c); char *strrchr (const char *s, int c); /* recherche d’un caractère dans une chaîne et retour d’un pointeur sur le caractère correspondant (NULL si non trouvé)*/ La fonction strchr() renvoie un pointeur sur la première occurence du caractère c dans la chaîne s. La fonction strrchr() renvoie un pointeur sur la dernière occurence du caractère c dans la chaîne s. char *strstr (const char *meule_de_foin, const char *aiguille); /* recherche la première occurrence de aiguille dans meule_de_foin, retourne le pointeur sur la première occurrence, NULL sinon */

Page 60: cours c impécable.pdf

60

Exemples : 1. int main(void) 2. { 3. int n; 4. char * p; 5. char chaine[100]; 6. char texte[] = "Ceci est un texte"; 7. 8. n = strlen(texte); 9. strncpy(chaine, texte, 6); 10. strncat(chaine, texte, n); 11. n = strchr(texte, 't'); 12. n = strrchr(texte, 't'); 13. n = strcmp(chaine + 3, texte, 5); 14. }

12.1. Erreurs fréquentes Oubli du 0 terminateur ds les chaînes (ex : copie) char * s = (char *) malloc(2 * sizeof(char)); s[0] = 'A'; s[1] = 'B'; printf("%s\n", s); Egalité de chaînes de caractère vs égalité de pointeurs char * texte1 = "Coucou"; char * texte2 = "Hello"; if (texte1 == texte2) { […] } Débordements de buffer strcpy et strcat autorisent le débordement de buffer sans vérifier la taille de la destination : char dest[10] ; char * src = "abcdefghijklmnopq"; strcpy(dest, src); Il vaut mieux utiliser les fonctions strncpy et strcat mais ces fonctions ne permettent pas d’éviter complètement les débordements de buffer (tronques les chaînes sans insérer un 0 terminateur). Il vaut mieux utiliser les fonctions non-standard strlcpy et strlcat qui insère systématiquement un 0 terminateur. Modification de mémoire constante char * s = "Coucou"; s[0] = 'H'; libération de mémoire constante char * texte = "coucou"; free(texte);

Page 61: cours c impécable.pdf

61

13. Fichiers et E/S 13.1. Les fichiers 2 types de fichiers :

• Les fichiers textes : chaques octets du fichier sont des caractères imprimables : exemple, les fichiers .c

• Les fichiers binaires : + compacts mais non lisible par un humain car succession d’octets qui ne sont pas nécessairement des caractères imprimables : exemple, les fichiers .pdf

Un fichier peut être vu comme étant un énorme tableau d’octets avec un indicateur de position égal au décalage depuis le début du fichier.

Créer/ouvrir un ficher : FILE * fopen(const char * restrict path, const char * restrict mode); retourne un pointeur de type FILE * : descripteur de fichier nécessaire pour identifier l'accès au fichier lors de toutes les opérations. Le pointeur est NULL si l'ouverture a échoué. En général on désigne l'accès au fichier comme un flux de données (stream). C'est également le terme employé pour communiquer à travers le réseau. premier argument : nom du fichier second argument : mode d'ouverture du fichier (chaîne de caractère) Les différents modes sont : "r" (read) : ouverture du fichier en lecture depuis le début du fichier "r+" : ouverture du fichier en lecture et écriture depuis le début du fichier "w" : ouverture du fichier en écriture (fichier créé s'il n'existe pas sinon de taille 0) avec positionnement en début de fichier "w+" : ouverture du fichier en lecture et écriture (fichier créé s'il n'existe pas sinon de taille 0) "a" : ouverture en écriture avec positionnement à la fin du fichier "a+" : ouverture en lecture et écriture Possibilité d'ajouter la lettre b pour indiquer que l'on fonctionne en mode binaire (sinon, il y a conversion des retours à la ligne selon les plates-formes). Normalement le "b" n'est plus nécessaire mais ça peut qd même varier selon les libc. Exemple : FILE * fichier = NULL; fichier = fopen("test.txt", "w+"); Fermer un fichier : le corollaire de la fonction fopen est la fonction fclose. Une valeur de retour de 0 indique une erreur. int fclose(FILE *stream); Une fois le fichier fermé, le flux ne peut plus être utilisé.

Page 62: cours c impécable.pdf

62

Exemple: 1. FILE * fichier = NULL; 2. fichier = fopen("test.txt", "rb"); 3. if (fichier != NULL) 4. { 5. fclose(fichier); 6. fichier = NULL; 7. } Dans le cas où on lit des octets, l’index de fichier avance d’autant d’octets. Lire des données dans un fichier : size_t fread(void * restrict ptr, size_t size, size_t n, FILE * stream); Arguments a) pointeur sur le buffer contenant les données à copier du fichier vers une zone mémoire b) taille des objets à copier c) nombre d'objet à copier d) descripteur de flux Retour : nb d’objets réellement lus Exemple : 1. char buffer[50]; 2. FILE * fichier = NULL; 3. 4. fichier = fopen("test.zip", "rb"); 5. if (fichier != NULL) 6. { 7. fread(buffer, sizeof(char), 50, fichier); 8. fclose(fichier); 9. fichier = NULL; 10. } Ecrire dans un fichier : size_t fwrite(void * restrict ptr, size_t size, size_t n, FILE * stream); Arguments a) pointeur sur le buffer contenant les données à copier dans le fichier b) taille des objets à copier c) nombre d'objet à copier d) descripteur de flux Retour : nb d’objets réellement écrits Exemple : recopier un fichier

Page 63: cours c impécable.pdf

63

1. FILE * fileIn = NULL, * fileOut = NULL; 2. 3. fileIn = fopen("Input.txt", "rb"); 4. fileOut = fopen("Output.txt", "wb"); 5. 6. if ((fileIn != NULL) && (fileOut != NULL)) 7. { 8. int n; 9. char buffer[1024]; 10. 11. n = fread(buffer, 1, 1024, fileIn); 12. while (n != 0) 13. { 14. fwrite(buffer, 1, n, fileOut); 15. n = fread(buffer, 1, 1024, fileIn); 16. } 17. 18. fclose(fileIn); fclose(fileOut); 19. } Exemple : écrire et line un entier, un tableau, une structure

Page 64: cours c impécable.pdf

64

1. #include <stdio.h> 2. 3. int main(void) 4. { 5. FILE * fichier; 6. int n = 129; 7. double tab[3] = { 0.5, 3.14, 1.44 }; 8. struct 9. { 10. int age; 11. char nom[50]; 12. } personne = { 25, "Martin" }; 13. 14. fichier = fopen("test.dat", "wb"); 15. if (fichier != NULL) 16. { 17. fwrite(&n, sizeof(n), 1, fichier); 18. fwrite(tab, sizeof(tab), 1, fichier); 19. fwrite(&personne, sizeof(personne), 1, fichier); 20. fclose(fichier); 21. } 22. 23. n = 99; 24. tab[0] = 0; tab[1] = 0; tab[2] = 0; 25. personne.age = 99; 26. strncpy(personne.nom, "Dupont", 6); 27. personne.nom[6] = '\0'; 28. 29. fichier = fopen("test.dat", "rb"); 30. if (fichier != NULL) 31. { 32. fread(&n, sizeof(n), 1, fichier); 33. fread(tab, sizeof(tab), 1, fichier); 34. fread(&personne, sizeof(personne), 1, fichier); 35. 36. printf("n = %d\n", n); 37. printf("tab = { %lf, %lf, %lf }\n", tab[0], tab[1], tab[2]); 38. printf("personne = { %d, %s }\n", personne.age, personne.nom); 39. 40. fclose(fichier); 41. } 42. 43. return 0; 44. } Problème de portabilité : écriture d’entiers, de structures (little endian vs big endian) Se déplacer dans un fichier : int fseek (FILE *stream, long offset, int whence); /* 0 si ok */ long ftell (FILE *stream); /* position dans le fichier depuis le début */ Arguments de fseek : 1) descripteur de fichier 2) décalage (peut être négatif)

Page 65: cours c impécable.pdf

65

3) position de référence pour effectuer le décalage • SEEK_SET : par rapport au début du fichier • SEEK_CUR : par rapport à la position actuelle • SEEK_END : par rapport à la fin du fichier

Exemple : FILE * fichier; fichier = fopen("donnees.dat", "rb"); if (fichier != NULL) { fseek(fichier, -50, SEEK_SET); /* 50 octets en arrière depuis la fin */ fseek(fichier, 10, SEEK_CUR); /* 10 octets en avant / pos° courante */ fseek(fichier, 20, SEEK_SET); /* position 20 octets apres le debut */ fclose(fichier); } Lire et écrire des caractères et chaînes de caractères : int fprintf (FILE *stream, const char format, ...); /* comme printf */ int fscanf (FILE *stream, const char *format, ...); /* comme scanf */ int fgetc (FILE *stream); /* retourne EOF si fin de fichier */ int fputc (int c, FILE *stream); /* copie un caractère */ Les fonctions fprintf et fscanf permettent de faire du filtrage mais ce sont des fonctions dangereuses. Exemple :

Page 66: cours c impécable.pdf

66

1. #include <stdio.h> 2. 3. int main(void) 4. { 5. FILE * fichier; 6. int n = 35, ip[4] = {0, 0, 0, 0}; 7. float pi = 3.14; 8. 9. fichier = fopen("config.txt", "wb"); 10. if (fichier != NULL) 11. { 12. fprintf(fichier, "IP 193.48.120.32\n"); 13. fprintf(fichier, "n = %d\n", n); 14. fprintf(fichier, "pi = %f\n", pi); 15. fclose(fichier); 16. } 17. 18. n = 99; 19. pi = 1.44; 20. 21. fichier = fopen("config.txt", "rb"); 22. if (fichier != NULL) 23. { 24. fscanf(fichier, "IP %d.%d.%d.%d\n",

&(ip[0]),&(ip[1]),&(ip[2]),&(ip[3])); 25. fscanf(fichier, "n = %d\n", &n); 26. fscanf(fichier, "pi = %f\n", &pi); 27. fclose(fichier); 28. } 29. 30. printf("IP %d.%d.%d.%d\n", ip[0], ip[1], ip[2], ip[3]); 31. printf("n = %d\n", n); 32. printf("pi = %f\n", pi); 33. 34. return 0; 35. } 36. Autre fonctions pour l’accès au fichiers : int open(const char *pathname, int flags); ssize_t read(int fd, void *buf, size_t count); ssize_t write(int fd, const char *buf, size_t count); int close(int fd);

• Plus bas niveau mais plus complet • Spécifiques à Unix et ne faisant pas partie des bibliothèques standard • Utilise des entiers pour définir les descripteurs de fichiers : des

descripteurs de fichier particuliers stdin (0) stdout (1) stderr (2) Erreurs fréquentes : Pb conversion d’une machine à une autre pour les fichiers textes et les caractères de retour à la ligne : - Unix : '\n' - Windows : '\r\n' - MacOS : '\r'

Page 67: cours c impécable.pdf

67

14. Préprocesseur, headers et programmation multi-fichiers

14.1. Préprocesseur Phase intervenant avant la compilation du programme qui apporte plusieurs modifications sur le code du programme : - inclusion de fichiers en-tête comme #include <stdio.h> - définition de constantes - substitution par des macros Ces directives peuvent intervernir n’importe ou dans le code du programme.

14.1.1 Définition de constante Syntaxe :

#define <nom de constante> <expression quelconque> A chaque fois que le precompilateur rencontre le nom de la constante, celle-ci est substitué par l’expression qui peut contenir n’importe quoi. Exemple : avant et après précompilation 1. #define PI 3.1415 2. #define TEXTE "bonjour" 3. 4. int main(void) int main(void) 5. { { 6. float x; float x; 7. 8. x = PI; x = 3.1415 9. printf("%s", TEXTE); printf("%s", "bonjour"); 10. } }

14.1.2 Usage des macros Une macro est une simili fonction mais qui repose uniquement sur la substitution et ne fait aucune vérification de type, ni d’arguments. Syntaxe :

#define <nom_macro>(<arguments>) <expression> Exemple :

Page 68: cours c impécable.pdf

68

1. #define SOMME(a,b,c) ((a) + (b) + (c)) 2. 3. int main(void) int main(void) 4. { { 5. int x; int x; 6. int n = 6; int n = 6; 7. 8. x = SOMME(n, 10, 23); x = ((n) + (10) + (23)); 9. } }

14.1.3 Inclusion de fichiers en-tête (headers) Le préprocesseur dispose d’une directive permettant d’inclure complètement le code contenu dans un autre fichier que l’on nomme en général fichier en-tête (header). Ces fichiers contiennent principalement des déclarations de constantes, de types et de fonctions dont le code est généralement stocké dans des bibliothèques de fonctions. C’est le cas de la fonction printf dont la déclaration est contenu dans le fichier stdio.h. Syntaxe :

#include <nom_de_fichier> #include "nom_de_fichier"

La première forme permet d’inclure les déclarations appartenant à un fichier dit « système » comme stdio.h. La seconde forme permet d’inclure un fichier « local » à notre programme. Exemple : programme.h Résultat de la précompilation : programme.c est modifié #define AGE_MIN 18 typedef struct #define TAILLE 40 { int age; typedef struct char nom[40]; { } Personne; int age; char nom[TAILLE]; int main(void) } Personne; {

Personne p; programme.c #include "programme.h" p.age = 5 + 18; p.nom = "Martin"; int main(void) return 0; { } Personne p; p.age = 5 + AGE_MIN; p.nom = "Martin"; return 0; } Les directives d’inclusion et les fichiers en-tête sont très utilisés pour la compilation séparée et la création de bibliothèques. Ce point est abordé dans la suite.

14.1.4 Erreurs fréquentes • Ajout d’un ; dans les macros • Ajout d’un espace entre le nom de la macro et de la liste des arguments • Absence de parenthèses