__ ___ _ _ _ ____ / / ___ / _ \__ _ _ _ ___ __| |_ _ | || ||___ \ / / / _ \ / /_)/ _` | | | / __| / _` | | | | | || |_ __) | / /__| __/ / ___/ (_| | |_| \__ \ | (_| | |_| | |__ _/ __/ \____/\___| \/ \__,_|\__, |___/ \__,_|\__,_| |_||_____| |___/
$> ls -l
-rwrw-rw- 1 db0 db0 62.1 Ko 2015-03-30 19:37 Impatient C
-rwrw-rw- 1 db0 db0 3.5 Ko 2015-03-30 19:37 Impatient SCM
-rwrw-rw- 1 db0 db0 3.0 Ko 2015-03-30 19:37 Impatient Emacs
-rwrw-rw- 1 db0 db0 2.4 Ko 2015-03-30 19:37 Impatient Man Pages
-rwrw-rw- 1 db0 db0 10.5 Ko 2015-03-30 19:37 Impatient Perl
-rwrw-rw- 1 db0 db0 17.3 Ko 2015-03-30 19:37 Impatient Python
-rwrw-rw- 1 db0 db0 13.6 Ko 2015-03-30 19:37 Impatient Shell Debutant
-rwrw-rw- 1 db0 db0 16.4 Ko 2015-03-30 19:37 Impatient Shell Script
-rwrw-rw- 1 db0 db0 3.4 Ko 2015-03-30 19:37 Impatient Shell
-rwrw-rw- 1 db0 db0 1.8 Ko 2015-03-30 19:37 Impatient Vim
Impatient - C
Ce cours se veut être utilisable comme une fiche de référence (des "rappels" ou un "support de cours") mais aussi permettre aux débutants d'apprendre.
C'est pour cette raison que l'on y trouve des informations explicatives aux débutants mélangées à des indications plus avancées (cachées, il faut cliquer sur "details" pour les voir).
J'ai conscience qu'il est difficile de satisfaire deux publics différents avec un même texte, c'est pourquoi j'écouterai volontiers vos remarques et commentaires permettant de l'améliorer. Particulièrement celles d'un debutant.
(Le systeme de commentaires en bas de page est là pour ca)
Sommaire
- Structure d'un code en C
- Les variables
- Un exemple...
- La Compilation
- Conditions et boucles
- Opérateurs de calcul et incrémentations
- Afficher des variables
- Les tableaux
- Les pointeurs
- Les chaînes de caractères
- La récursivité
- Manipulation de fichiers
- Les fichiers headers, les includes, les defines et les macros
Chapitre I
Chapitre II
Chapitre III
Chapitre I
Structure d'un code en C
- Du code C s'écrit dans un fichier ayant pour extension .c. On le modifie avec n'importe quel éditeur de texte (emacs, vim, etc).
- Un code C s'exécute en suivant des instructions lignes par lignes dans des fonctions.
- Une fonction se compose de plusieurs partie :
- Déclaration des variables utilisées
- Instructions
- Valeur retournée en fin de fonction (facultatif)
- La première fonction appelée est la fonction main. Il doit toujours y en avoir une dans un programme en C.
- Une fonction se déclare de la manière suivante :
type_de_variable_retournee nom_fonction(type_argument1 nom_argument1, type_argument2 nom_argument2) { declaration1; declaration2; instruction1; instruction2; return (valeur_retournee); }
- Une fonction peut appeler d'autres fonctions dans une instruction :
valeur_retournee = ma_fonction(argument1, argument2);
- L'instruction "return" renvoie une valeur et cesse l'exécution de la fonction. Si on met des instructions après un return, celle-ci ne seront jamais exécutées.
- Une fonction doit être déclarée avant d'être appelée. Donc le main doit être tout en bas du fichier.
Les variables
Les instructions utilisent des variables.
Ces variables ont des types permettant de définir leur taille (en octets).
Dans une variable, on ne peut que stocker un nombre, dont la limite inférieure et supérieure depend du type.
Type | Nombre d'octets en mémoire | Nombre de bits en mémoire | Limite inférieure | Limite supérieure |
---|---|---|---|---|
char | 1 octet | 8 bits | - 128 | 127 |
unsigned char | 1 octet | 8 bits | 0 | 255 |
short | 2 octets | 8 x 2 = 16 bits | - 32 768 | 32 767 |
unsigned short | 2 octets | 8 x 2 = 16 bits | 0 | 65 535 |
int | 4 octets | 8 x 4 = 32 bits | - 2 147 483 648 | 2 147 483 647 |
unsigned int | 4 octets | 8 x 4 = 32 bits | 0 | 4 294 967 295 |
long | 4 octets | 8 x 4 = 32 bits | -2 147 483 648 | 2 147 483 647 |
unsigned long | 4 octets | 8 x 4 = 32 bits | 0 | 4 294 967 295 |
float | 4 octets | 8 x 4 = 32 bits | -3.4 x 10-38 | 3.4 x 1038 |
double | 8 octets | 8 x 8 = 64 bits | -1.7 x 10-308 | 1.7 x 10308 |
long double | 12 octets | 8 x 12 = 96 bits | -3.4 x 10-4932 | 3.4 x 104932 |
- Les char, short, int, long et leurs unsigned respectifs ne peuvent contenir que des entiers (2, 6, 42, 254, ...).
- Les float, double et long double peuvent contenir des nombres à virgule flottante (2.0, 6.21, 42.5694, -3.457, ...).
Le mot-clé void signifie l'absence de variable, par exemple, si une fonction ne renvoie rien ou si elle ne prend pas de paramètre.
Il existe des types pré-definis qui portent un nom permettant de connaitre leurs rôles.
Par exemple, size_t contient la taille d'un tableau (voir plus tard ce qu'est un tableau.)
ssize_t servira à parcourir un tableau.Pour utiliser une variable, on doit la déclarer en lui donnant un nom :
Dans une fonction, on déclare toutes les variables avant des les utiliser.type nom;
Les noms de fonctions ne doivent comporter que des lettres minuscules, des chiffres et des underscore ("_").
Ils doivent être en anglais et explicites. Grâce au nom de la variable, on doit comprendre ce qu'elle contient, à quoi elle sert dans le programme.
Le nom de variable i est souvent utilisé pour un compteur.- Les variables sont internes aux fonctions. Une variable déclarée dans une fonction n'existe nulle part ailleurs que dans celle-ci.
- Il est possible de déclarer des variables dites globales en les déclarant en dehors de toute fonction. Ce n'est pas recommandé, il vaut mieux les éviter autant que possible.
Un exemple...
... vaut mieux qu'un long discours !
int subtraction(int a, int b) { int result; result = a - b; return (result); } int main(void) { int i; i = 3; i = i + 5; i = subtraction(i, 2); return (0); }
- Le programme commence son execution par la fonction main (elle ne prend aucun paramètre).
- Dans celle-ci, on déclare une variable de type int et de nom "i".
- On assigne ensuite une valeur à la variable i grace au "=".
--> i = 3. - L'instruction suivante utilise la valeur de i (3) et lui ajoute 5. Elle met ensuite ce résultat dans i, ce qui écrase son ancienne valeur.
--> i = 8. - L'instruction suivante appelle une fonction à laquelle elle donne deux arguments : i (8) et 2.
- On entre alors dans la fonction substraction. On voit qu'elle prend en paramètre 2 arguments de type int (a et b). Ca tombe bien, c'est ce qu'on lui a envoyé lors de l'appel !
--> a = 8 et b = 2. - On déclare ensuite une nouvelle variable de type int nomme "result".
--> a = 8, b = 2 et result = valeur inconnue (/!\ pas forcement = 0) - On effectue le calcul de a - b, c'est a dire 8 - 2, et on met cette valeur dans "result".
--> a = 8, b = 2 et result = 6 - L'instruction return renvoie la valeur de result et on retourne dans la fonction main.
- La valeur que la fonction a renvoyé est placee dans i.
--> i = 6. (a, b et result n'existe plus : ils étaient propres à la fonction substraction). - La fonction main a alors terminé ses instructions et fait donc la dernière instruction qui retourne une valeur.
La valeur 0 signifie que l'exécution du programme s'est bien passée. Une autre valeur signifie qu'il y a eu une erreur.
UP
La Compilation
On compile avec gcc (alias cc).
Le compilateur génère alors un fichier (executable) a.out.
Il est possible de modifier le nom du fichier grace a l'option -o
gcc -o nom_executable mon_code.c
S'il y a une ou plusieurs erreurs, elles seront affichées et la compilation s'arrêtera.
Cependant, certaines "erreurs" peuvent ne pas empêcher la compilation, on les appelle les Warnings.
Il est malgré tout très important de corriger ces petites erreurs car la plupart du temps, elle perturberont le fonctionnement normal du programme.
Pour afficher plus de Warnings, il est possible (et conseillé) d'ajouter des options (dites flags de compilation) :
- -w : Désactive tout les warnings (déconseillé)
- -Wextra : Affiche encore plus de warnings (détails)
- -Wall : Affiche plus de warnings (détails)
- -Werror : Considère les warnings comme des erreurs et cesse la compilation
- -ansi : Affiche des warnings en cas de non respect de la norme ISO C90
Il est aussi possible de demander au compilateur d'effectuer (ou pas) des optimisations :
- -O0 : Désactive toutes les optimisations
- -O1 : Optimisation de niveau 1 (détails)
- -O2 : Optimisation de niveau 2 (détails)
- -O3 : Optimisation de niveau 3 (details)
On peut compiler plusieurs fichiers C pour un meme programme.
Exemple de compilation :
gcc -O0 -Wall -Wextra -Werror -ansi -o mon_executable mon_code.c
Une fois que le programme est compilé, on peut le lancer pour voir ce qu'il fait :
./mon_executable
Conditions et boucles
-
On peut demander à un programme de n'effectuer une action que dans un cas précis.
Pour cela, on utilise la construction if.
On peut aussi lui demander de faire autre chose dans un autre cas avec else if. (facultatif)
Puis, faire autre chose si aucun cas propose précédemment ne correspond grace à else. (facultatif)if ( condition 1 ) { action; } else if ( condition 2 ) { action; } else { action; }
-
On peut demander a notre programme d'éxecuter une action tant qu'une condition est respectée grace à la construction while.
Il faudra alors faire attention à ce que la condition ne soit pas toujours respectée, car si c'est le cas, la boucle ne s'arrêtera jamais. On appelle ca une boucle infinie.
while ( condition ) { action; action; }
-
Une condition se construit de la manière suivante :
Francais Symbole Exemple égal == (n == 2) différent de != (i != 0) supérieur à > (j > 5) supérieur ou égal à >= (k >= 4) inférieur à < (count < 2) inférieur ou égal à <= (plop <= 3) -
On peut demander à ce que plusieurs conditions soient prises en compte.
Francais Symbole Exemple ET
Les deux conditions doivent être vérifiées&& ((n == 3) && (j != 5)) OU
Au moins une condition doit être vérifée|| ((substraction(5, 3) < 1) || (n <= 7)) OU exclusif
Une condition des deux conditions doit être respectée, mais pas les deux^ ((i < 3) ^ (function(d) == 6)) condition ne doit pas etre vérifiée ! (!(k == 5)) - S'il n'y a qu'une seule action, on peut se passer des accolades :
if (i == 5) i = 6; else i = 5;
- Si on a besoin d'un if et d'un else (pas de else if), on peut utiliser un ternaire :
Cet exemple aura le meme comportement que celui ci-dessus :
(condition ? action-si-condition-respectee : action-si-condition-non-respectee);
(i == 5 ? i = 6 : i = 5);
- Il est possible d'utiliser la valeur de retour d'une fonction directement dans une condition :
if (subtraction(3, 5) != -2) return (-1);
- On peut même faire des calculs directement dans la condition :
if ((n + 1) == 3)
- En fait, le système de condition en C fonctionne comme un booléen. Le resultat final sera soit faux (= 0), soit vrai (toute autre valeur).
Ainsi, il est possible de remplacer (a == 0) par (!a) et remplacer (a != 0) par (a). - Exemple :
a = 1; b = 3; ((((a > 3) || (b > a)) && (a)) ^ (b == 3))
- Comme pour un calcul de maths habituel, on commence par regarder ce qui se trouve à l'intérieur des parentheses les plus profondes.
On commence donc par (a > 3).
a = 1 et 1 n'est pas supérieur a 3.
La condition est donc fausse.((((FAUX) || (b > a)) && (a)) ^ (b == 3))
- Le séparateur est un || (OU).
Donc si l'instruction de droite est vraie, alors le tout est vrai.
Si l'instruction de gauche avait été vraie, alors on aurait pas eu besoin de regarder l'instruction de droite, puisqu'il faut qu'au moins une soit vraie pour que le tout soit vrai.
b = 3 et a = 1. (3 > 1) est bien vrai. On peut donc remplacer :
((((FAUX) || (VRAI)) && (a)) ^ (b == 3)) ((( VRAI ) && (a)) ^ (b == 3))
- Le prochain séparateur est un && (ET).
Les deux conditions doivent être vraies, donc on doit aussi regarder celle de droite.
a n'est pas égal a 0, donc a existe.
La condition est donc vraie.
(((VRAI) && (VRAI)) ^ (b == 3)) (( VRAI ) ^ (b == 3))
- Le séparateur est un ^ (Ou exclusif).
Il faut donc que l'une des deux conditions soit vérifiée, mais pas l'autre.
La condition de gauche est vraie, donc la condition de droite doit être fausse pour que le tout soit vrai.
b = 3 donc (3 == 3) est vrai.
Les deux sont vraies, donc le tout est faux.((VRAI) ^ (VRAI)) ( FAUX )
- Comme pour un calcul de maths habituel, on commence par regarder ce qui se trouve à l'intérieur des parentheses les plus profondes.
Opérateurs de calcul et incrémentations
- Les opérateurs de calculs sont ceux habituels :
- + : addition
- - : soustraction
- * : multiplication
- / : division
- % : modulo (qu'est-ce que c'est ?)
- Une instruction de calcul peut donner une nouvelle valeur à une variable en utilisant la valeur de celle-ci.
Dans ces cas-là , il est possible d'utiliser des opérateurs d'affectations (=) effectuant un calcul sur la variable.
i = i + 3;
donnera le même résultat que la ligne précédente.i += 3;
-
Le calcul sur la variable est effectué en dernier, donc ici, on calculera (2 * 6) + i.
i += 2 * 6;
- Ces opérateurs d'affectations spéciaux fonctionnent aussi avec les autres opérateurs de calculs :
- += : addition
- -= : soustraction
- *= : multiplication
- /= : division
- %= : modulo
- Si l'on souhaite ajouter 1 a notre variable, il existe une syntaxe spéciale d'incrémentation :
- La post-incrementation : variable++
On fait l'action, puis on incrémente.
- La pre-incrementation : ++variable
On incrémente, puis on fait l'action.
- Exemple :
i = 1; if ((i++ == 2) || (++i == 3)) i -= 2;
- (i++ == 2).
C'est une post-incrémentation.
On va donc utiliser la valeur actuelle de i (1) pour faire la vérification.
---> i = 1;
1 n'est pas égal a 2. C'est donc FAUX.
Une fois la vérification faite, on incrémente.
---> i = 2; - (++i == 2).
C'est une pré-incrementation.
On va donc incrémenter i avant de faire la verification.
---> i = 3;
On fait maintenant la vérification.
i (3) est bien egal à 3. C'est donc VRAI.
- La condition est donc vraie, et i vaut 3 a la fin de la vérification de la condition.
- Comme la condition est vraie, on fait le calcul. On enlève 2 à i (3), ce qui donne 1.
- (i++ == 2).
- Il existe une syntaxe identique au même comportement pour la décrémentation de 1.
i-- est une post-decrementation
--i est une pre-decrementation.
- La post-incrementation : variable++
- On peut utiliser ces syntaxes d'[in-de]crementation pour remplacer une ligne de calcul.
peut alors etre remplacé par :i = i + 1;
(La pré-incrementation est recommandé dans ces cas-la) (Pourquoi ?)++i;
Afficher des variables
Pour donner à une variable comme valeur une lettre, on a deux solutions :
- Lui donner sa valeur de manière claire en utilisant des quotes :
char c; c = 'a';
- Donner sa valeur ASCII :
char c; c = 97;
- 48 - 57 | '0' - '9'
- 65 - 90 | 'A' - 'Z'
- 97 - 122 | 'a' - 'z'
D'autres caractères non visibles mais qui peuvent etre utiles :
- 00 | '\0' | NULL, 0
- 07 | '\a' | bell, bip sonore
- 08 | '\b' | backspace
- 09 | '\t' | Tabulation
- 10 | '\n' | Retour a la ligne
- 11 | '\v' | Tabulation verticale
- 13 | '\r' | Revient au debut de la ligne
Pour afficher une lettre, on va utiliser cette petite fonction :
#include <unistd.h> void print_char(char c) { write(STDOUT_FILENO, &c, sizeof(c)); }
Ce petit programme va afficher la lettre 'p' suivie d'un retour à la ligne et finir le programme en indiquant qu'il est réussi :
int main(void) { print_char('p'); print_char('\n'); return (0); }
Chapitre II
Les tableaux
- Si l'on a besoin de stocker plusieurs variables, on peut utiliser un tableau.
Un tableau, c'est plusieurs variables d'un même type les unes à la suite des autres.
On peut déclarer un tableau ainsi :
Les cases du tableau commencent a 0.type nom[nombre_de_cases];
Ici, la case tab[3] n'existe pas, ni toutes celles d'après.int tab[3]; tab[0] = 1; tab[1] = 76; tab[2] = 5;
- Pour écrire plus qu'une lettre, mais écrire un mot, les tableaux sont très pratiques :
Par convention, on termine un mot par le caractère '\0' qui sert a repérer la fin du mot.
char str[4]; str[0] = 'L'; str[1] = 'o'; str[2] = 'L'; str[3] = '\0';
Les pointeurs
- Chaque variable, pour être utilisée, doit être stockée quelque part dans la mémoire.
On dit que cette variable se trouve a une adresse en mémoire.
Cette adresse est un numero que l'on exprime en général en hexadecimal (type 0x7f6a8d). - Un pointeur est une variable qui contient l'adresse d'une autre variable.
- Sur le schema, les petites cases représentent les variables stockées dans la mémoire.
Notre tableau de tout a l'heure y est représenté.
En dessous, le contenu des cases du tableau, et au dessus, l'adresse ou elles sont stockées.
A un autre endroit dans la mémoire, une autre variable est stockée.
Elle a pour contenu l'adresse de la première case du tableau.
-
Dans la partie déclarations d'une fonction :
- On déclare un pointeur en indiquant comme type celui vers lequel il pointe et en ajoutant une * devant le nom.
est un pointeur dont le contenu sera l'adresse d'une variable de type int.
int *pointer;
- On déclare un pointeur en indiquant comme type celui vers lequel il pointe et en ajoutant une * devant le nom.
-
Dans la partie instructions d'une fonction :
- On récupère l'adresse d'une variable en ajoutant un & devant le nom de la variable.
- On récupère ce qui se trouve à l'adresse contenue dans le pointeur en ajoutant une * devant le nom du pointeur.
On dit alors qu'on déréference un pointeur.
- Exemple :
char c; char *p; c = 'a'; print_char(c); p = &c; *p = 'z'; print_char(c); print_char(*p); print_char('\n');
- On déclare une variable de type char que l'on appelle c.
- On déclare un pointeur vers un char que l'on appelle p.
- On met la lettre 'a' (valeur ASCII 97) dans la variable c.
--> c = 97 ; p = valeur inconnue. - On affiche le contenu de la variable c.
- On récupère l'adresse de la variable c et on la met dans le pointeur p.
--> c = 97 ; p = adresse de c (de type 0x75f5a). - On déréference le pointeur p pour modifier son contenu et y mettre la lettre z (valeur ASCII 122).
--> c = 122 ; p = adresse de c. - On affiche c. On voit qu'il contient bien la nouvelle valeur : 'z'.
- On déréference le pointeur p pour afficher le contenu de la variable vers laquelle il pointe. C'est le contenu de c, donc ca affiche la meme chose que précédemment.
- On affiche un retour a la ligne, c'est toujours plus joli.
- Quand on déclare un tableau, on déclare en fait un pointeur vers la premiere case du tableau.
- Concrètement, ces deux lignes sont équivalantes :
tab[0] = 3; *tab = 3;
- On peut demander à un pointeur d'être vide. On lui donne alors la valeur NULL.
Les chaînes de caractères
On appelle une chaîne de caractères un tableau de type char qui contient des lettres (leurs valeurs ASCII) et qui se termine par un '\0'.
On peut envoyer une chaine de caractere à une fonction en utilisant des double quotes (") "ma chaine de caracteres\n".Les simples quotes (‘) sont utilisées pour les caractères seuls.
La fonction à laquelle on aura envoye la chaîne de caracteres grâce à des doubles quotes prendra alors en paramètre un pointeur vers un char. C'est le pointeur vers la première case du tableau qui contient une chaîne de caractères.
void print_string(char *string) { /* fonction qui affiche */ } int main(void) { print_string("Le pays du 42"); return (0); }
Chapitre III
La récursivité
- Une fonction récursive est une fonction qui s'appelle elle-même.
- La récursivité peut s'utiliser en remplacement d'une boucle.
- Exemple :
Ces deux fonctions ont le même comportement, mais l'une utilise une boucle et l'autre est récursive.void print_c_while(int n) { int i; i = 0; while (i < n) { print_char('c'); ++i; } } void print_c_recursive(int n, int i) { if (i < n) { print_char('c'); print_c_recursive(n, ++i); } } int main(void) { print_c_while(10); print_char('\n'); print_c_recursive(10, 0); print_char('\n'); return (0); }
Manipulation de fichiers
- Pour lire et écrire dans des fichiers, on utilise des filedescriptor (ou fd).
Ce sont des int qui contiennent un numéro.
Ces "numéros" indiquent quel est le fichier sur lequel on écrit.
On n'attribue pas soi-même ces nombres, ce sont des appels système qui les gèrent.
Certains nombres (fd) sont réservés :
- L'entrée standard (ou STDIN_FILENO) : en général égal à zero, on l'associe souvent au clavier, sur lequel on va lire des informations. Quand j'entre une commande dans mon terminal, il va lire ce que j'écris sur l'entrée standard.
- La sortie standard (ou STDOUT_FILENO) : en général égal à un, c'est ce qui nous permet de lire des informations à l'ecran. Quand une commande ou un programme est exécuté, il affiche du contenu sur la sortie standard. C'est ce que faisait notre fonction print_char.
- La sortie d'erreur (ou STDERR_FILENO) : en général égal à deux, elle a souvent le même comportement que la sortie standard mais permet de différencier les messages de bon fonctionnement du programme à ceux qui apparaissent en cas d'erreur(s). Les erreurs et warnings de compilations sont par exemple affichés sur la sortie d'erreur.
- Les appels système sont des fonctions qui renvoient une valeur.
Celle-ci doit absolument être vérifiée.
En cas d'échec, il se peut que la suite de l'exécution du programme soit perturbée, et il vaudra mieux, alors, en cas d'échec, arrêter le programme (en affichant un message d'erreur, par exemple). - Pour écrire dans un fd, on utilise la fonction write.
Elle prend en paramètre le fd, une chaine de caractères et une taille.
Elle retourne le nombre de caractères écrits (et -1 en cas d'échec).
Lire le man 2 de write ! - Pour lire dans un fd, on utilise la fonction read.
Elle prend en paramètre le fd, un buffer (un tableau qui sert a stocker) et une taille.
Elle retourne le nombre de caractères lus.
Lire le man 2 de read ! - Pour ouvrir un fichier, c'est-à -dire associer un fd a un fichier dans lequel on pourra lire et écrire, on utilise la fonction open.
Elle prend en paramètre le chemin du fichier et le type d'ouverture.
Les types d'ouvertures :- O_RDONLY : Lecture uniquement
- O_WRONLY : Ecriture uniquement
- O_RDWR : Lecture et écriture
- O_CREAT : Créer un fichier qui n'existe pas
- O_TRUNC : Ecrase le fichier dans le cas ou on essaierai de créer un fichier qui existe déjÃ
- O_APPEND : Ecrit a la fin du fichier
Lire le man 2 de open ! - Le nombre de filedescriptor est limité, donc il vaut mieux les économiser.
Il est donc extrêmement important de "libérer" le numero filedescriptor correspondant à un fichier aussitôt que l'on n'a plus besoin de celui-ci.
Pour cela, on utilise l'appel système close qui ferme un filedescriptor.
Elle prend en paramètre un filedescriptor et renvoie 0 en cas de réussite, -1 sinon. - Exemple : Lire sur l'entree standard, écrire sur la sortie standard.
#include <unistd.h> #include <stdlib.h> void print_string_fd(int fd, char *string) { ssize_t i; int w; i = 0; while (string[i]) ++i; w = write(fd, string, i); if (w == -1) exit(EXIT_FAILURE); } void print_username(void) { char buffer[12]; int r; print_string_fd(STDOUT_FILENO, "Bonjour ! Quel est ton nom ? "); if ((r = read(STDIN_FILENO, buffer, 12)) == -1) { print_string_fd(STDERR_FILENO, "Il y a eu une erreur avec la fonction read.\n"); exit (EXIT_FAILURE); } buffer[r - 1] = '\0'; print_string_fd(STDOUT_FILENO, "Tu t'appelles "); print_string_fd(STDOUT_FILENO, buffer); print_string_fd(STDOUT_FILENO, ". Quel joli nom !\n"); }
- La fonction print_string_fd a le même comportement que la fonction print_string.
Elle prend en plus en parametre un fd, ce qui lui permet d'ecrire sur le fd que l'on souhaite.
Elle calcule aussi la taille de la chaîne de caractères et n'utilise qu'une fois l'appel système write.
Si l'appel systeme write échoue, le programme est quitté grace à la fonction exit. - On lit sur l'entrée standard ce que l'utilisateur va écrire.
Ce qui est lu est stocké dans un buffer.
Ce buffer est un tableau de 12 cases.
On va donc lire une taille de 12 pour éviter de dépasser la place qui nous est allouée.
On récupère la valeur de retour de read pour deux raisons :- Pour vérifier que l'appel système n'a pas échoué.
- Pour connaître la taille du mot.
S'il est plus court, le tableau buffer ne sera pas complètement rempli !
- Si l'appel système échoue, on affiche un message d'erreur sur la sortie d'erreur et on quitte le programme.
EXIT_FAILURE indique que le programme n'a pas réussi, EXIT_SUCCESS (= 0) indiquerai que le programme a quitté en ayant son comportement normal.
Rappel : Retourner la valeur 0 en fin de programme signifie que l'execution du programme s'est bien passé. Une autre valeur signifie qu'il y a eu une erreur. - L'appel système read ne met pas de '\0' a la fin du mot qu'il a lu (puisqu'il ne lit pas que des mots, en fait).
Il faut donc le mettre soi-même, car si on ne le fait pas, notre fonction print_string_fd ne saura quand s'arrêter.
- La fonction print_string_fd a le même comportement que la fonction print_string.
- Exemple : Lire dans un fichier, écrire dans un nouveau fichier
void readfile(void) { int fd; int new_file; int r; char buff[20]; if ((fd = open("file.txt", O_RDONLY)) == -1) { print_string_fd(STDERR_FILENO, "Open n'a pas reussi. \ Sans doute parce que le fichier n'existe pas ou n'a pas les \ droits en lecture.\n"); exit(EXIT_FAILURE); } if ((new_file = open("new_file.txt", O_WRONLY|O_CREAT|O_TRUNC)) == -1) { print_string_fd(STDERR_FILENO, "Erreur lors de la creation du nouveau fichier.\n"); exit(EXIT_FAILURE); } while ((r = read(fd, buff, 20)) > 0) write(new_file, buff, r); close(fd); close(new_file); }
- On commence par ouvrir le fichier de départ, en lecture seule. En cas d'échec, on quitte.
- On créé ensuite un nouveau fichier. Si le fichier existe déjà , on l'écrase. On compte écrire seulement dedans.
- On lit dans le fichier puis on écrit dans le nouveau fichier, et ce, tant qu'il y a des choses a lire (tant que read n'a pas renvoyé 0 ou une erreur.)
- On a termine avec les deux fichiers, on ferme les filedescriptors.
- En lançant ce programme, on se rend compte que s'il n'y a pas eu d'erreur, rien ne s'affiche. C'est normal : On n'a rien écrit sur la sortie standard, on a écrit dans un fichier. En ouvrant le fichier new_file.txt, on voit qu'il contient la même chose que file.txt.
- Pourquoi n'a't-on pas ajoute de '\0' a la fin du buffer lu ? On ne lit pas forcement des chaînes de caractères, on peut aussi lire du binaire. On utilise directement write en lui donnant un nombre de caractères à afficher. A aucun moment on ne parcoure le buffer à la recherche d'un '\0' indiquant la fin. Et pour cause : si on lit du binaire, par exemple, un '\0' pourra se trouver au milieu mais ne voudra pas dire que le fichier est terminé !
Les fichiers headers, les includes, les defines et les macros
- Il existe des fichiers a l'extension .h que l'on appelle les fichiers headers qui contiennent diverses informations pour utiliser des fonctions en C.
- On a vu par exemple que l'on ne pouvait pas utiliser la fonction write sans ajouter cette ligne en début de fichier :
#include <unistd.h>
- La présence des < > indique que le fichier se trouve dans le dossier des headers standard. En général, c'est /usr/include. On peut donc voir le contenu du fichier pour mieux comprendre.
- Si on met des " ", cela signifie que le fichier header est dans le dossier courant (ou celui spécifié à la compilation grace a -I).
- En général, on aura un fichier .h par fichier .c.
- Cela permet de compiler avec plusieurs fichiers séparés.
- Le mieux est d'avoir un fichier .c par fonction.
- Les fichiers headers contiennent des prototypes de fonction.
Un prototype de fonction :Ne pas oublier le ; a la fin !type_de_variable_retournee nom_fonction(type_argument1, type_argument2);
- Les fichiers headers contiennent aussi des defines.
Ce sont des valeurs qui sont remplacées dans le code avant la compilation.#define NOM VALEUR
On met leurs noms en majuscule de façon à la différencier des noms de variables.
Très utile lorsque que l'on utilise plusieurs fois une valeur : si on a besoin de la changer, on ne le fera qu'une seule fois.
C'est toujours bien d'en utiliser, même si la valeur n'est utilisée qu'une fois, dans le cas où elle peut être modifiée.
Ca marche aussi avec des chaînes de caractères ! - Exemple :
Le fichier test_read.c :Le fichier test_read.h :#include "test_read.h" int test_read(int fd) { char buffer[BUFF_SIZE]; if (read(fd, buffer, BUFF_SIZE) == -1) { print_string_fd(STDERR_FILENO, MSG_READ_ERR); return (FALSE); } return (TRUE); } int main(void) { return (test_read(STDIN_FILENO) ? EXIT_SUCCESS : EXIT_FAILURE); }
#ifndef TEST_READ_H_ # define TEST_READ_H_ # include <stdlib.h> # include <unistd.h> # define FALSE 0 # define TRUE !FALSE # define BUFF_SIZE 1024 # define MSG_READ_ERR "Error: read\n" int test_read(int); #endif /* !TEST_READ_H_ */
- Vous vous en serez douté : STDOUT_FILENO, EXIT_SUCCESS, etc sont des defines !
Vous pouvez aller voir les déclarations de STDOUT_FILENO anc co dans /usr/include/unistd.h.
Vous pouvez aller voir les déclarations de EXIT_SUCCESS anc co dans /usr/include/stdlib.h. - Le fichier .c inclut son fichier .h associe, et rien d'autre.
Les defines, includes, macros, prototypes se trouvent dans le fichier .h. - On peut faire des if et des else dans les .h !
- #if EXPRESSION = si EXPRESSION est vrai (les expressions sont a peu près équivalante au C)
- #elif EXPRESSION = equivalent au else if en C
- #else = equivalent au else en C
- #ifdef NOM_DEFINE = si NOM_DEFINE a été déclaré
- #ifndef NOM_DEFINE = si NOM_DEFINE n'a pas été déclaré
- #endif = à la fin des vérifications
En fait, on définit toutes nos déclarations dans une définition appelée TEST_READ_H_.
Mais avant de le faire, on vérifie si cette grosse définition n'a pas été faite auparavant, si, par exemple, j'avais inclus 2 fois le fichier test_read_h.
Il est extrêmement important de protéger tout les fichiers headers contre la multi-inclusion !
- Vous vous en serez douté : STDOUT_FILENO, EXIT_SUCCESS, etc sont des defines !
- Les macros sont tout simplement des defines qui ont pour particularité de contenir des morceaux de code et de prendre des valeurs (ou variables) en paramètre.
En général, on y met des ternaires.
Cette macro prend en paramètre une valeur (peu importe le type) et renvoie TRUE si elle est négative, FALSE sinon. (TRUE et FALSE définie plus tot)#define IS_NEGATIVE(value) (value < 0 ? TRUE : FALSE)
UP