Le langage C a été créé dans les années 1970 par B.W. Kernighan et K. Thomson qui est l'auteur du langage B. Ce langage a fait l'objet d'un livre intitulé "The C Programming Language" écrit par B.W. Kernighan et D.M. Ritchie qui disposent de plusieurs versions en Anglais et éditions en Français.
Le langage a fait également l'objet de plusieurs normalisations de 1988 à nos jours. Ces normes sont essentiellement :
Dans certains cas, il peut s'avérer nécessaire de préciser la norme au compilateur, comme avec le compilateur gnu gcc avec l'option -std= suivi de la norme.
Avertissement : tous les exemples de programmes C ont été compilés avec gcc et make et testés sous linux.
Il existe une grande diversité d'outils de développement pour le langage C, cela va du compilateur à l'IDE qui intègre un éditeur de texte, un gestionnaire de projet, le compilateur et d'autres outils d'aide au développement.
Parmi ces compilateurs, on trouve les logiciels libres :
Parmi les IDE, on trouve les environnements :
Un compilateur transforme un code source (C) en un langage objet compréhensible par le micro-processeur. Pour tous les exemples de ce chapitre, on utilisera le compilateur GNU gcc sous Linux et l'éditeur de texte Geany qui est un logiciel libre disponible pour tous les systèmes d'exploitation. Il est fourni avec les distributions linux et téléchargeable pour le système Windows.
Les différentes étapes de développement sont principalement :
Les principales options de la commande make
La syntaxe générale d'une ligne de commande dans le fichier est de la forme :
cible: dependances [tablulation]commande
cible correspond à ce qui doit être réalisé, par défaut c'est la cible all qui est utilisée, ce qui signifie que cette cible doit toujours être présente.
dependances est la liste des cibles nécessaires à l'exécution des commandes relatives à cette cible
[tabulation] représente une tabulation pour décaler la commande, cette tabulation est obligatoire et ne peut pas être remplacée par des espaces.
Un fichier Makefile, est une suite de cibles et de commandes associées à cette cible.
Cibles et dépendances génériques :
%.o: %.c
correspond à chaque fichier.o en fonction du fichier .c de même nom
Les variables sont définies par une chaîne de caractères minuscules ou majuscules sans accent ni ponctuation. L'affectation se fait avec le symbole = suivi de la valeur. L'utilisation se fait avec le nom entre parenthèses précédé du symbole $ (ex : $(NOM)).
Il existe des variables prédéfinies
Création d'une liste des fichiers sources
SRC=$(willcard *.c)
Etablir une liste des fichiers objets à partir de la liste des fichiers sources
OBJ=$(wilcard .c=.o)
Exemple pour un seul fichier source
CC=gcc
PROG=hello
OBJ=$(PROG).o
SRC=$(PROG).c
CFLAGS=-Wall
LDFLAGS=
all: $(EXEC)
$(EXEC): $(OBJS)
$(CC) $(LDFLAGS) -o $(PROG) $^
$(OBJ):
$(CC) $(CFLAGS) -c -o $@ $(SRC)
clean:
rm -f $(OBJ) $(PROG)
Ce fichier va créer le fichier exécutable hello en fonction du fichier source hello.c, en passant par la création du fichier hello.o
L'option de compilation -Wall permet d'afficher toutes les alertes (warning) en plus des erreurs, ce qui est vivement conseillé, car un warning peut se transformer en erreur lors de l'exécution.
Exemple pour plusieurs fichiers sources
CC=gcc
CFLAGS= -Wall
LDFLAGS=-lm
EXEC=hello
SRC= $(wildcard *.c)
OBJS= $(SRC:.c=.o)
all: $(EXEC)
$(EXEC): $(OBJS)
$(CC) -o $(EXEC) $^ $(LDFLAGS)
%.o: %.c
$(CC) $(CFLAGS) -c -o $@ $<
clean:
rm *.o $(EXEC)
Ce fichier va créer le fichier exécutable hello à partir de tous les fichiers sources .c présents dans le répertoire.
L'option d'édition de liens -lm permet d'utiliser les librairies partagées de calcul mathématique.
/* hello.c */
#include <stdio.h>
int main(int argc, char **argv) {
printf("Bonjour le monde\n");
return EXIT_SUCCESS;
}
Le texte compris entre /* et */ est du commentaire qui n'est pas compilé. On peut également utiliser un commentaire ligne (valable pour une seule ligne) en commençant par //
Le fichier de prototypes stdio.h contient entre autres la déclaration de la fonction printf.
La fonction main(argc,argv) doit toujours être présente une seule fois dans un projet C. Cette fonction est appelée au démarrage du programme. Le paramètre argc correspond au nombre de paramètres présents sur la ligne de commande incluant le nom du programme. Le paramètre argv est un tableau de chaînes de caractères qui contiennent chaque valeur de la ligne de commande, argv[0] correspond au nom du programme.
La valeur de retour EXIT_SUCCESS fournit la valeur de retour au système d'exploitation qui correspond au fait qu'il n'y a pas d'erreur, en général 0.
Contenu du fichier hello.o obtenu avec la commande objdump -d hello.o, il fournit le code assembleur de la fonction main.
hello.o: format de fichier elf64-x86-64 Déassemblage de la section .text : 0000000000000000: 0: f3 0f 1e fa endbr64 4: 55 push %rbp 5: 48 89 e5 mov %rsp,%rbp 8: 48 83 ec 10 sub $0x10,%rsp c: 89 7d fc mov %edi,-0x4(%rbp) f: 48 89 75 f0 mov %rsi,-0x10(%rbp) 13: 48 8d 3d 00 00 00 00 lea 0x0(%rip),%rdi # 1a 1a: e8 00 00 00 00 callq 1f 1f: b8 00 00 00 00 mov $0x0,%eax 24: c9 leaveq 25: c3 retq
La ligne 1a représente l'appel à la fonction printf. Ce code sera complété à l'édition de lien pour construire le programme exécutable.
L'exemple ci-dessus utilise une librairie dynamique, le programme a donc besoin des librairies partagées lors de l'exécution. La liste des librairies partagées nécessaires peut être obtenue avec la commande ldd.
ldd hello linux-vdso.so.1 (0x00007ffe2b1d5000) libc.so.6 => /lib/x86_64-linux-gnu/libc.so.6 (0x00007f9f858b9000) /lib64/ld-linux-x86-64.so.2 (0x00007f9f85add000)
Ce programme a besoin de la libraire libc.so qui contient le code de printf, la librairie linux-vdso.so qui contient les liens vers les espaces mémoires dédiés au programme, la librairie ld-linux-x86-64.so qui est le chargeur de librairie.
Le programme hello a une taille d'environ 16ko, car il ne contient pas le code de la fonction printf.
Si on ajoute l'option -static aux options de l'édition de liens (LDFLAGS), on crée un fichier qui inclut le code de la fonction printf. Sa taille est maintenant d'environ 86ko, et la commande ldd précise qu'il ne s'agit pas d'un exécutable dynamique
En C, on distingue les entiers signés et non signés, les réels en simple, double et quadruple précision. Le booléen est un entier qui représente la valeur fausse lorsqu'il est à 0 et la valeur vraie lorsqu'il est différent de 0.
Il existe également un type non typé : void
Les entiers signés
Type | Limites |
---|---|
char | -128 à +127 |
short | -32768 à +32767 |
int1 | -2147483648 à 2147483647 |
long | -263 à 263-1 |
Les entiers non signés
Type | Limites |
---|---|
unsigned char | 0 à 255 |
unsigned short | 0 à 65535 |
unsigned int1 | 0 à 4294967295 |
unsigned long | 0 à 264-1 |
1 : pour les types int et unsigned int, la taille dépend du processeur, elle peut être de 16 ou 32 bits suivant le processeur.
Afin d'éviter le problème de la taille de ces types de données, il existe de nouvelles définitions de données déclarées dans le fichier stdint.h qu'il faudra inclure.
Les entiers signés
Type | Limites |
---|---|
int8_t | -128 à +127 |
int16_t | -32768 à +32768 |
int32_t1 | -2147483648 à 2147483647 |
int64_t | -263 à 263-1 |
Les entiers non signés
Type | Limites |
---|---|
uint8_t | 0 à 255 |
uint16_t | 0 à 65535 |
uint32_t1 | 0 à 4294967295 |
uint64_t | 0 à 264-1 |
Les nombres réels respectent la norme IEEE 754 comme cela est expliqué dans ce chapitre sur la représentation binaire.
Type | Limites | Plus petite valeur | Précision relative |
---|---|---|---|
float | ≈±3.4×1038 | ≈±2-126 | ≈10-6 |
double | ≈±1.8×10308 | ≈±2-1022 | ≈10-15 |
long double | ≈±1.19×104932 | ≈±2-16382 | ≈10-33 |
La déclaration d'une variable se fait avec le type suivi du nom de la variable et se termine par une point-virgule. Il est possible de déclarer plusieurs variables de même type en les séparant par des virgules.
type nomvariable; type nomvariable1,nomvariable2;
Exemple de déclarations de variables :
int nb1,nb2,nb3;
float nb4;
Une variable peut être initialisée lors de la déclaration en utilisant le symbole =, comme par exemple
int a= 3;
float reel = 2.5;
L'affectation consiste à transmettre le résultat d'une expression à une variable, il utilise le symbole = qui n'est pas une égalité au sens mathématique. L'affectation se termine également par un point-virgule.
resultat = expression de calcul;
Exemple de calcul :
nb3 = nb1 + nb2;
On réalise l'addition de nb1 avec nb2, puis on affecte la somme à la variable resultat. Ces variables doivent être déclarées au préalable.
L'affectation peut être mise en cascade :
a = b = 5;
La valeur 5 est transférée à b puis à a. Après cette ligne les deux variables ont la même valeur 5.
La conversion implicite de type fonctionne de la plus faible précision vers la plus forte :
Dans ce cas il n'y a pas de perte de précision
Dans une affectation, le type du résultat du calcul de l'expression ne dépend pas du tout du type de la variable situé à gauche du symbole =. L'expression est évaluée en effectuant les conversions de type implicites de la plus faible précision vers la plus élevée. Ensuite la conversion de type est effectuée entre le type du résultat et le type de la variable qui contient le résultat. Exemple :
float r; int x,p; r = x / p;
On aura une division entière avec le résultat entier converti en réel, ce qui donne une valeur entière au format réel.
Dans certains cas, on peut forcer la conversion de type en précédant la variable du nouveau type entre parenthèses, on parle de transtypage ou "cast".
On modifie l'exemple précédent afin d'obtenir une division réelle.
float r; int x,p; r = (double)x / p;
x est convertit en réel double précision, la division est réelle, le résultat réel est affecté à la variable r.
Il faut faire attention au placement de l'opérateur cast.
float r; int x,p; r = (double)(x / p);
Dans ce cas on effectue une division entière qui donne une résultat entier qui est ensuite converti en réel.
La conversion d'entier vers réel peut induire des pertes de précision, comme par exemple lors de la conversion d'un entier 32 bits vers un réel simple précision. Cela se produit lorsque l'entier N exprimé sur 32 bits est supérieur à 224, comme par exemple, avec N=268499633 qui prend la valeur réelle : R=268499648.00000. En utilisant la représentation mémoire des entiers et réels, on a :
31 | 30 | 29 | 28 | 27 | 26 | 25 | 24 | 23 | 22 | 21 | 20 | 19 | 18 | 17 | 16 | 15 | 14 | 13 | 12 | 11 | 10 | 9 | 8 | 7 | 6 | 5 | 4 | 3 | 2 | 1 | 0 | |
N | 0 | 0 | 0 | 1 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 1 | 1 | 1 | 1 | 1 | 0 | 1 | 0 | 1 | 0 | 1 | 1 | 0 | 0 | 0 | 1 |
s | ed7 | ed6 | ed5 | ed4 | ed3 | ed2 | ed1 | ed0 | a1 | a2 | a3 | a4 | a5 | a6 | a7 | a8 | a9 | a10 | a11 | a12 | a13 | a14 | a15 | a16 | a17 | a18 | a19 | a20 | a21 | a22 | a23 | |
R | 0 | 1 | 0 | 0 | 1 | 1 | 0 | 1 | 1 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 1 | 1 | 1 | 1 | 1 | 0 | 1 | 0 | 1 | 1 | 0 |
La valeur réelle est (1.00023913383483886718750) × 228=268499648.000000. L'erreur relative est environ 5.587e-08
Cas du réel en double précision
double reel = 8.62;
printf("%lf\n",reel);
int entier = (int)(reel *100) ;
printf("%d\n",entier);
L'affichage est
8.620000 861
Cas du réel en simple précision
float reel = 8.62;
printf("%f\n",reel);
int entier = (int)(reel *100) ;
printf("%d\n",entier);
L'affichage est
8.620000 862
La représentation binaire de 8.62 utilise une infinité de bits, elle donc obligatoirement arrondie. On a une erreur de calcul en double précision mais pas en simple précision, cela vient du fait qu'en double précision la valeur réelle enregistrée n'est pas arrondie sur le même nombre de bits.
Cette erreur n'apparaît pas en seulement en C mais dans tous les langages qui utilisent la double précision ainsi que les logiciels de calcul comme octave et scilab.
Par défaut, une valeur entière est écrite en base 10, pour écrire une valeur en hexadécimal, il faut utiliser le préfixe 0x, pour affecter un entier avec le code ascii d'un caractère, il suffit d'utiliser le caractère entre simples apostrophes comme par exemple 'a'.
Dans un fichier C les variables sont généralement déclarées à l'intérieur des fonctions, ce sont des variables locales qui ne sont connues de la fonction et stockées dans la pile du programme.
Quelques fois elles peuvent déclarées en début du fichier en dehors de toute fonction, dans ce cas ce sont des variables globales accessibles et modifiables par toutes les fonctions. Cela doit rester un cas particulier.
On a bien sûr les opérateurs arithmétiques : +,-,*,/,%. Le type de division dépend des opérandes, si les deux opérandes sont de type entier, la division est entière (euclidienne), si une des opérandes est réelle, la division est réelle. Le symbole % représente le modulo ou reste de la division entière.
Il existe également des opérateurs simplifiés comme += , -=, *=, /= qui effectue l'opération entre la variable qui contient le résultat et une vautre variable ou valeur.
Il existe également des opérateurs de post-incrémentation en suffixant la variable des symboles ++, pré-incrémentation en préfixant la variable des symboles ++, post-décrémentation en suffixant la variable des symboles -- et enfin la pré-décrémentation en préfixant la variable des symboles --. La post-incrémentation permet d'utiliser la valeur de la variable puis de lui ajouter 1, la pré-incrémentation permet d'ajouter 1 à la variable avant d'utiliser sa valeur. Il est est de même pour la post-décrémentation et la pré-décrémentation avec la soustraction de 1.
Le C fournit également des opérateurs qui permettent de traiter les bits des entiers : | pour l'opération OU logique bit par bit, & pour l'opération ET bit par bit, ~ pour l'inversion de chaque bit de l'entier, ou encore << pour décaler les bits d'un entier à gauche (bit de poids faible vers bit de poids fort), >> pour décaler à droite les bits d'une entier (bits de poids fort vers bit de poids faible).
Il fournit également des opérateurs booléens qui traite les entiers du point de vue booléen : || pour l'opération OU, && pour l'opération ET, ou encore ! pour l'inversion booléenne.
Lors d'écriture d'expressions booléennes, il ne faut pas oublier la signification des opérateurs bit par bit et booléen, car le résultat n'est pas le même.
L'entier a vaut 38 en base 10, et l'entier b 72. Le résultat de l'opération booléenne && est 1.
Du point de vue booléen a et b sont différents de 0, donc vrai, l'opération booléenne VRAI et VRAI donne VRAI qui vaut 1.
L'entier a vaut 38 en base 10, et l'entier b 72. Le résultat de l'opération booléenne & est 0.
L'opération bit par bit & effectue un ET logique entre chaque bit des entiers a et b, ce qui donne le résultat 0.
Le résultat n'est pas correct du point de vue booléen car il donne FAUX pour VRAI et VRAI
En conséquence il faut être très vigilant sur l'utilisation des opérateurs logiques, car cela pourrait entraîner des erreurs importantes.
Le résultat des opérations de comparaisons est un booléen qui vaut 0 (faux) ou 1 (vrai)
Les opérateurs sont :
Il est possible de combiner des expressions de comparaisons avec les opérateurs booléens || &&, à condition d'encadrer chaque expression de comparaison avec des parenthèses.
Les structures de contrôles permettent d'effectuer un traitement en fonction du résultat d'une expression booléenne
if (expression booleenne) { // traitement si expression booleenne VRAIE } else { // traitement si expression booleenne FAUSSE }
Il est possible d'imbriquer ces structures
if (premiere expression booleenne) { // traitement si premiere expression booleenne VRAIE } else if (deuxième expression boolenne) { // traitement si premiere expression boolenne FAUSSE // et deuxieme expression booleenne VRAIE } else { // traitement si premiere expression boolenne FAUSSE // et deuxieme expression booleenne FAUSSE }
switch (variable) { case val1 : // traitement pour variable=val1 break; case val2 : // traitement pour variable=val2 break; // ... case valn : // traitement pour variable=valn break; default : // traitement pour tout autre valeur }
Très important : variable ne peut être qu'un entier, car cette structure effectue uniquement une comparaison d'égalité.
C'est la commande break qui permet de sortir de cette structure, si elle est omise, le cas suivant sera traité.
for(initialisation;expression booléenne;calcul) { // bloc instructions ; }
Il peut y avoir plusieurs initialisations séparées par des virgules, et plusieurs calculs séparés par des virgules
Exemple avec la somme des 10 premiers entiers :
int s=0;
for(int n=1;n<=10;n+=1) {
s += n ;
}
while (expression booleenne) { // bloc instructions }
do { // bloc instructions } while (expression booleenne);
L'entrée et la sortie standard correspondent respectivement au clavier et à l'écran.
L'affichage de message et de résultat sur la sortie standard fait appel à la fonction printf
La syntaxe est :
printf(chaineformat,liste variables);
avec
La lecture de données depuis l'entrée standard fait appel à la fonction scanf
La syntaxe est :
scanf(format,adresse variable);
avec
Code source C
#include <stdlib.h>
#include <stdio.h>
int main(int argc, char **argv) {
double a,b, resultat ;
char operateur, operation[5] ;
scanf("%lf",&a);
scanf("%s",operation);
scanf("%lf",&b);
operateur = operation[0] ;
switch (operateur) {
case '+' : resultat = a + b;
break;
case '-' : resultat = a - b;
break;
case '*' : resultat = a * b;
break;
case '/' : resultat = a / b;
break;
}
printf("%lf\n",resultat);
return EXIT_SUCCESS;
}
Ce programme "calculatrice" permet d'effectuer une opération arithmétique sur deux nombres réels. On a donc trois variables réelles a,b,resultat ainsi qu'une variable qui contient le caractère représentant l'opération. La variable opération est une chaîne de 4 caractères. Ce type de données sera présenté dans les paragraphes qui suivent. La ligne : operateur=operation[0] permet de transférer le premier caractère de la chaîne operation dans la variable operateur.
Les trois lignes scanf permettent de saisir les valeurs a,b et l'opération à partir de l'entrée standard. Les variables a et b sont précédées de & pour signifier qu'il s'agit de l'adresse de la variable. Ce n'est pas le cas pour la variable operation qui est une chaîne où operation représente l'adresse.
La structure switch permet de choisir l'opération à effectuer en fonction de la valeur du caractère qui représente l'opération.
Enfin printf affiche le résultat de l'opération sur la sortie standard.
Ce programme peut être testé en ligne de commande, à condition d'être dans le répertoire du programme, avec la syntaxe suivante :
echo "3 + 5" | ./calculatrice 8.000000
Si on lance le programme ./calculatrice seul, il faudra saisir au clavier la valeur de a, l'opération puis la valeur de b.
Généralement, quelques tests manuels ne sont généralement pas suffisant pour permettre d'affirmer que le programme fonctionne parfaitement. Même si cela peut être suffisant dans le cas présent, on va montrer comment ou pourrait effectuer un test plus complet de ce programme à l'aide d'un script shell et de la calculatrice bc.
#!/bin/sh
EQ=0
N=100
LISTE=$(seq 1 $N)
operateur="/"
for i in $LISTE
do
INTA=$(shuf -i 1-100000 -n 1)
INTB=$(shuf -i 1-100000 -n 1)
a=$( echo "scale=3;$INTA / 1000" | bc -l)
b=$( echo "scale=3;$INTB / 1000" | bc -l)
RBC=$(echo "scale=6;$a $operateur $b" | bc -l )
RCAL=$(echo "$a $operateur $b" | ./calculatrice )
EQ=$( echo "scale=20;$EQ + ($RBC - $RCAL)^2" | bc -l)
done
EQM=$( echo "scale=20;sqrt($EQ / $N)" | bc -l)
printf "EQM=%e\n" $EQM
Ce script va effectuer 100 divisions avec des valeurs aléatoires, calculer l'erreur quadratique moyenne sur l'ensemble de ces valeurs.
La commande shuf permet de générer un entier aléatoire dans l'intervalle 1-100000. Ensuite on utilise la calculatrice bc pour construire des réels en divisant par 1000 ces entiers.
On réalise la division avec la calculatrice pour fournir la valeur dans la variable RBC, puis avec le programme calculatrice pour fournir la valeur dans la variable RCAL
On utilise la calculatrice bc pour effectuer la somme cumulée de la différence des deux résultats élevée au carré. Le résultat final est dans la variable EQ
Après la boucle, on utilise encore la calculatrice bc pour effectuer la racine carrée de la moyenne.
Enfin on affiche le résultat avec la commande shell printf.
Exemple de résultat :
EQM=7.211103e-07
Ce résultat ne précise pas si l'erreur vient de la calculatrice bc ou bien du programme calculatrice. Il permet simplement de montrer que le programme calculatrice fournit des résultats acceptables.
Un pointeur est une variable qui contient l'adresse mémoire d'une variable.
La syntaxe pour la déclaration est
type *ptr_variable;
type est le type de la donnée, ptr_variable est la variable de type pointeur représenté par l'opérateur *. Le préfixe ptr_ n'est pas obligatoire, c'est plutôt une convention pour désigner une variable de type pointeur.
Exemple avec la déclaration
int entier; char *ptr_octet = &entier;
Si l'adresse mémoire de l'entier est 0x00000010 et l'adresse mémoire du pointeur est 0x00000000, la deuxième ligne permet d'affecter le pointeur ptr_octet avec l'adresse mémoire de l'entier.
Dans l'exemple présenté, l'octet de poids fort de l'adresse de la variable est stockée à l'adresse la plus basse du pointeur. On a donc un format little endian.
Avec ce format, la valeur de l'entier est donc 0x23424511
Pour comprendre les formats little et big endian, on peut se référer à la représentation mémoire des entiers et réels
On peut trouver un autre exemple d'utilisation des pointeurs pour effectuer des calculs avec cet exemple de calcul rapide de l'inverse de la racine carrée.
Un tableau est un ensemble de données de même type accessibles individuellement en utilisant un indice qui commence à 0.
La syntaxe pour la déclaration est
type tab_variable[taille];
type est le type de l'élément. tab_variable est le nom de la variable tableau, et utilisé seul, sans crochets, correspond à l'adresse mémoire du début du tableau. Comme pour le pointeur, le préfixe tab_ n'est qu'une convention d'écriture. taille entre crochets définit le nombre maximum d'éléments réservés pour ce tableau. Il ne sera pas possible de modifier la taille du tableau dans la suite du programme.
La fonction sizeof(tab_variable) retourne la taille du tableau en octets.
L'image présente l'implémentation mémoire du tableau déclaré avec la ligne suivante :
short tab_entier[3] = { 0x10, 0x20, 0x15 }; // ou short tab_entier[] = { 0x10, 0x20, 0x15 };
Il s'agit d'un tableau de 3 entiers de 16 bits où chaque élément est initialisé. Dans le cas d'un tableau initialisé, il est possible de ne pas préciser la taille, et de laisser les crochets vides. La valeur de l'indice permet d'accéder à l'élément du tableau avec la syntaxe : tab_entier[indice]. Le nom de la variable tab_entier correspond à l'adresse du début du tableau en mémoire.
Les entiers sont représentés au format little endian.
Un tableau à deux dimensions est un tableau où l'élément est un tableau, c'est un tableau de tableaux.
La syntaxe pour la déclaration est
type tab_variable[taille_tableau][taille_tableau_element];
type est le type de l'élément. tab_variable est le nom de la variable tableau, et utilisé seul, sans crochets, correspond à l'adresse mémoire du début du tableau. Comme pour le pointeur, le préfixe tab_ n'est qu'une convention d'écriture. taille_tableau correspond à la taille du tableau, taille_tableau_element correspond à la taille de l'élément tableau.
L'image présente l'implémentation mémoire du tableau déclaré avec la ligne suivante :
short tab[2][2] = { { 0x10, 0x20} , { 0x12, 0x45} };
Contrairement au tableau une dimension, les tailles doivent toujours être précisées.
L'accès à un élément se fait avec la syntaxe tab[i][j] avec i l'indice du tableau et j l'indice dans l'élément tableau, comme par exemple, l'élément tab[1][0] qui contient l'entier 16 bits 0x12 (format little endian).
Le processeur calcule l'adresse mémoire de l'entier en prenant en compte les indices ainsi que la taille en octets du type de l'élément. Dans le cas présent, on 1×2×2+0=4 avec i=1 , j=0 avec deux octets par entier et deux éléments pour chaque élément tableau.
Il est très facile d'utiliser les notations pointeurs avec des tableaux et des notations tableaux avec des pointeurs car un tableau est une adresse mémoire au même titre qu'un pointeur. La différence réside dans le fait que l'adresse du pointeur est modifiable, contrairement à l'adresse du tableau qui est fixe, mais également dans la place mémoire occupée, un tableau réserve une taille mémoire qui est le produit des deux dimensions par la taille de l'élément, un pointeur ne réserve que la taille de l'adresse mémoire. C'est pour cela qu'il faut bien utiliser ces deux types, car même si le programme compile, cela ne signifie pas qu'il va toujours fonctionner correctement.
Le tableau à deux dimensions
tab_int[0] , tab_int[1] et tab_int[2] représentent les adresses mémoires des tableaux qui représentent les lignes. Ce qui fait que tab_int et tab_int[0] représentent la même adresse mémoire.
Ce tableau est facilement utilisable avec les notations pointeurs, en prenant l'exemple :
int tab_int[3][4] = { {1,2,3,4} , {5,6,7,8} , {9,10,11,12} };
L'occupation mémoire de ce tableau, donné par la fonction sizeof(tab_int) est de 48 octets, 12 valeurs de 4 octets (32 bits).
on peut remarquer que les adresses tab_int , *tab_int et tab_int[0] représentent la même adresse. tab_int[1] représente l'adresse de la deuxième ligne, tab_int[2] représente l'adresse de la troisième ligne,
Les notations suivantes permettent d'accéder à tab_int[1][2]
Le tableau de pointeurs
Ce tableau est facilement utilisable avec les notations pointeurs, en prenant l'exemple :
int tab_int[3][4] = { {1,2,3,4} , {5,6,7,8} , {9,10,11,12} }; int *ptr_int[3]; int **ptr2_int = (int**)ptr_int; ptr_int[0] = (int *)tab_int ; ptr_int[1] = (int *)(tab_int+1) ; ptr_int[2] = (int *)(tab_int+2) ;
L'occupation mémoire de tab_int, donné par la fonction sizeof(tab_int) est de 48 octets, 12 valeurs de 4 octets.
L'occupation mémoire de ptr_int, donné par la fonction sizeof(ptr_int) est de 24 octets, 3 adresses de 8 octets.
L'occupation mémoire de ptr2_int, donné par la fonction sizeof(ptr2_int) est de 8 octets, 1 adresse de 8 octets.
*ptr_int[3] est un tableau de pointeurs, où chaque élément contient l'adresse d'une ligne du tableau. L'opérateur cast permet de passer du type pointeur depuis un tableau de 4 valeurs vers un pointeur. **ptr2_int est un pointeur vers un pointeur, qui est compatible avec un tableau de pointeurs.
Les notations suivantes permettent d'accéder à tab_int[1][2]
Le pointeur vers un tableau
int tab_int[3][4] = { {1,2,3,4} , {5,6,7,8} , {9,10,11,12} }; int (*ptr_tab)[4]; ptr_tab = tab + 1;
L'occupation mémoire de tab_int, donné par la fonction sizeof(tab_int) est de 48 octets, 12 valeurs de 4 octets.
L'occupation mémoire de ptr_tab, donné par la fonction sizeof(ptr_tab) est de 8 octets, 1 adresse de 8 octets.
(*ptr_tab)[4] est un pointeur vers un tableau de 4 entiers. L'adresse de ce pointeur est affectée avec la deuxième ligne du tableau.
Les notations suivantes permettent d'accéder à tab_int[1][2]
On peut remarquer qu'il est très facile d'écrire du code en mélangeant notations tableau et pointeur. Mais, même après avoir corrigé tous les messages d'alertes (warning obtenus avec l'option -Wall qui prend ici tout son sens) du compilateur, il est possible que le programme ne fonctionne pas correctement ou encore qu'il provoque une erreur système d'accès à la mémoire. Il est donc conseillé d'utiliser les notations appropriées les plus simples.
Une chaîne de caractères est un tableau à une dimensions d'entiers 8 bits (char) qui contiennent les codes ASCII des caractères. La dernière valeur est 0, C'est une convention utilisée en C pour indiquer la fin de la chaîne. Cette convention est utilisée par les fonctions de traitement de chaînes. On déclare une chaîne de caractères comme un tableau, en ajoutant 1 à la taille qui contiendra le 0 de fin de chaîne.
Le fichier de prototypes string.h contient les fonctions de traitement des chaînes de caractères. Lors de la déclaration des chaînes de caractères, il faut prévoir la taille maximale du résultat du traitement. Toutes ces fonctions utilisent des notations pointeurs, cela ne signifie pas qu'il faut transmettre des variables de type pointeur, mais les adresses des chaînes de caractères (adresse du tableau).
Quelques fonctions :
Ces fonctions ne traitent que les chaînes de caractères encodés en ASCII, et ne fonctionne pas avec les autres encodages comme UTF-8.
Une structure est un ensemble de données de types différents qui sont accessibles individuellement par l'intermédiaire d'une variable appelée champ.
Pour déclarer une structure, il est conseillé de déclarer un nouveau type avec typedef et le mot clé struct.
typedef struct s_structure { type1 nom1; type1 nom2; // ... } Tstructure; Tstructure nomvariablestructure;
Tstructure est le nouveau type qui permet de déclarer des variables de ce type.
L'accès à un élément de la structure s'effectue en utilisant le nom de la variable de la structure suivie d'un point et du nom du champ :
nomvariable.nom1
typedef struct s_nombre {
char octets[4];
float reel;
} T_nombre;
T_nombre nombre;
nombre.reel = −11.25;
nombre.octet[0]=0x10;
nombre.octet[1]=0x20;
nombre.octet[2]=0x30;
nombre.octet[3]=0x40;
L'occupation mémoire de T_nombre, donné par la fonction T_nombre est de 8 octets, 4 octets + un réel simple précision de 4 octets.
Pour la valeur des octets du réel, il faut se référer au chapitre sur la calculatrice.
En général les données sont alignées sur des adresses mémoires paires, ce qui peut laisser des adresses mémoires inutilisées. Ce n'est jamais un problème sauf exception qui peut être due à la ressource mémoire ou plus vraisemblablement à la compatibilité avec d'autres programmes lors d'échange de données.
Les alignements peuvent être gérés avec les options de compilation de gcc
mais également dans le code source avec l'attribut __attribute__((packed)).
typedef struct s_nombre {
char octets[3];
float reel;
} T_nombre;
T_nombre nombre;
L'occupation mémoire de T_nombre, donné par la fonction sizeof(T_nombre) est de 8 octets, 3 octets + une adresse non utilisée + un réel simple précision de 4 octets. Ceci est du à l'alignement sur une adresse paire.
Le champ reel est aligné sur une adresse paire, ce qui laisse une adresse mémoire non utilisée.
typedef struct s_nombre {
char octets[3];
float reel;
} __attribute__((packed)) Tnombre;
T_nombre nombre;
L'occupation mémoire de T_nombre, donné par la fonction sizeof(T_nombre) est de 7 octets, 3 octets + un réel simple précision de 4 octets. Ceci est du à l'alignement sur une adresse paire.
L'option annule l'alignement sur une adresse paire, il n'y a plus d'adresse mémoire inutilisée.
Une union est une structure de données qui permet d'accéder à une variable avec des types différents, dans une union tous les champs sont à la même adresse mémoire.
Pour déclarer une union, il est conseillé de déclarer un nouveau type avec typedef et le mot clé union.
typedef union u_union { type1 nom1; type1 nom2; // ... } Tunion; Tunion nomvariableunion;
Tunion est le nouveau type qui permet de déclarer des variables de ce type
typedef union s_nombre {
char octet[4];
float reel;
} T_nombre;
nombre.reel = −11.25;
L'occupation mémoire de T_nombre, donné par la fonction sizeof(T_nombre) est de 4 octets, 4 octets ou bien un réel simple précision de 4 octets.
Un programme C ne se limite jamais à la seule fonction main. Un programme se décompose en un ensemble de fonctions qui traitent chacune une partie des données.
Chaque fonction échange des données avec les autres fonctions. Le sens des échanges définit le type de la fonction.
Ces fonctions sont utilisées pour l'affichage de message. Il est possible d'utiliser des variables globales, mais cela doit être exceptionnel.
La déclaration du prototype se fait en général dans un fichier de prototype .h
void nom_fonction();
Le codage de la fonction se fait dans un fichier c (.c) qui inclut le fichier .h correspondant.
void nom_fonction() { // declaration variables locales // code de la fonction }
Lorsque la fonction est codée et utilisée dans le même fichier source, la déclaration de prototype est inutile, si le code de cette fonction se situe avant le code qu'il l'utilise. La déclaration de prototype est nécessaire sir la fonction est codée après son utilisation.
Ce type de fonction utilise les paramètres transmis, peut éventuellement les modifier, mais cette modification ne sera pas transmise à la fonction qui l'a appelée.
La déclaration du prototype se fait sous la forme
void nom_fonction(liste type parametres);
Le codage de la fonction se fait dans un fichier c (.c)
void nom_fonction(liste parametres) { // declaration variables locales // code de la fonction }
La liste des paramètres est une liste de types suivi du nom du paramètre et séparés par une virgule
L'appel de cette fonction se fait avec les noms des paramètres
Ce type de fonction peut utiliser des paramètres et les modifiés, la modification sera transmise à la fonction qui l'a appelée. C'est pour cela que ce n'est pas la valeur du paramètre qui est transmise mais son adresse mémoire, d'où le nom de paramètre par adresse.
La déclaration du prototype se fait sous la forme
void nom_fonction(liste type pointeur);
Les paramètres sont cette-fois-ci des pointeurs, qui correspondent au contenu des adresses transmises.
Le codage de la fonction se fait dans un fichier c (.c)
void nom_fonction(liste pointeurs) { // declaration variables locales // code de la fonction }
La liste des pointeurs est une liste de types pointeurs suivis du nom et séparés par une virgule.
L'appel de cette fonction se fait avec les adresses des paramètres : nom préfixé avec le symbole &.
#include <stdlib.h>
#include <stdio.h>
void echange(int *a,int *b) {
int tmp ;
tmp = *a;
*a = *b;
*b = tmp;
}
int main(int argc, char **argv) {
int x=2,y=3;
printf("%d %d\n",x,y);
echange(&x,&y);
printf("%d %d\n",x,y);
return EXIT_SUCCESS;
}
La fonction echange permet d'échanger les contenus des paramètres a et b. La méthode est simple, on sauvegarde la première valeur, transfert la deuxième dans la première, et enfin affecte le contenu de la valeur sauvegardée dans la deuxième.
La fonction principale main qui est une fonction de test, échange les contenus de x et y en utilisant la fonction echange..
La fonction utilise des pointeurs qui permettent de traiter le contenu des adresses mémoires des variables x et y.
Lors de l'appel de la fonction, on transmet les adresses des variables x et y en les préfixant du symbole &.
Le résultat de l'affichage est :
2 3 3 2
La type de la valeur de la fonction est précisé en remplaçant void par le type de cette valeur de retour.
La déclaration du prototype se fait sous la forme
type nom_fonction(liste type parametres);
Le codage de la fonction se fait dans un fichier c (.c)
type nom_fonction(liste parametres) { type valeur_retour; // declaration variables locales // code de la fonction return valeur_retour; }
La valeur de retour doit être déclarée dans le code de la fonction. La fonction se termine avec le mot clé return suivi du nom de la valeur de retour.
La valeur de retour ne doit jamais retourner une adresse de variable locale, car les variables locales sont stockées dans la pile, ne sont plus accessibles en dehors de la fonction.
Méthode itérative, code source C
#include <stdlib.h>
#include <stdio.h>
unsigned int factorielle(unsigned int n) {
unsigned int fact=1;
for(unsigned int i=1;i<=n;i+=1) {
fact *= i ;
}
return fact;
}
int main(int argc, char **argv) {
unsigned int n=0;
unsigned int fn;
if (argc != 2) {
fprintf(stderr,"Syntaxe %s n\n",argv[0]);
return EXIT_FAILURE;
}
n=strtol(argv[1],NULL,10);
fn=factorielle(n);
printf("%u!= %u \n",n,fn);
return EXIT_SUCCESS;
}
On utilise la définition de la factorielle, soit n!=n×(n-1)×(n-2)×1 au travers d'une boucle qui effectue le produit cumulé.
Les types des entiers est non signé, car la factorielle est définie pour n positif ou nul.
Il faut faire attention au dépassement dans le calcul avec, par exemple, le calcul de 13!=6 227 020 800
./factorielle 13 13!= 1932053504
Le résultat est faux : 12!=479 001 600 qui reste inférieur à 232-1=4 294 967 295 alors que 13!=6 227 020 800 est supérieur à 232-1. On a donc, lors de la dernière multiplication un dépassement qui entraîne une erreur de calcul.
La solution est d'utiliser un entier 64 bits, mais on aura le même problème avec la limite à 264-1. Quant à l'utilisation de réel, on aura un problème de précision sur les grands nombres.
Pour un entier codé sur n bits, le calcul de la factorielle donne la valeur modulo 2n, dans le cas présent, on a bien 1932053504=13! modulo 232
La valeur de n est transmis en ligne de commande sous la forme d'une chaîne de caractères, la fonction strtol(char *source,char **fin,int base) convertit la chaîne en entier long en utilisant la base 10. Le paramètre **fin correspond à l'adresse d'un pointeur qui contient la chaîne qui n'a pas pu être convertie en cas d'erreur. La valeur NULL indique de ne pas utiliser ce paramètre.
fonction récursive, code source C
unsigned int factorielle(unsigned int n) {
unsigned int fact=1;
if (n == 0) {
return 1 ;
}
fact = n*factorielle(n-1);
return fact;
}
Une fonction récursive, est une fonction qui s'appelle elle-même avec d'autres valeurs de paramètres. Une fonction récursive doit inclure une condition d'arrêt qui permet d'arrêter l'appel récursif.
Dans le cas présent on utilise la propriété de la factorielle, soit n!=n×(n-1)! avec comme condition d'arrêt 0!=1.
La fonction retourne donc 1 si n=0, sinon elle calcule n×(n-1)!
On a le même problème de dépassement dans les calculs que ceux déjà cités avec la solution itérative.
Méthode classique
#include <stdlib.h>
#include <stdio.h>
double puissance(double x,unsigned int n) {
double z=1;
for(unsigned int i=1;i<=n;i+=1) {
z *= x ;
}
return z;
}
int main(int argc, char **argv) {
double x=0;
unsigned int p=1;
double puiss;
if (argc != 3) {
fprintf(stderr,"Syntaxe %s n p\n",argv[0]);
return EXIT_FAILURE;
}
x=strtod(argv[1],NULL);
p=strtol(argv[2],NULL,10);
puiss=puissance(x,p);
printf("%lf^%u= %lf \n",x,p,puiss);
return EXIT_SUCCESS;
}
La méthode dite classique utilise la définition de la puissance entière qui est an=a×a×a×...×a en réalisant n produit de a.
On a donc une boucle qui répète n fois le produit du dernier résultat obtenu par a, après avoir initialisé le résultat à 1.
On n'a pas le problème de dépassement de la factorielle car la valeur a est réelle. on a toujours une puissance maximale liée au nombre de bits de la puissance n. Par contre on est lié à la précision relative du réel double précision dont la mantisse est exprimée sur 52 bits. On aura cette fois-ci un résultat tronqué, lorsque celui-ci ne pourra plus être représenté sur 52 bits. On aura donc une erreur de précision pour tout résultat supérieur à 252.
On va également d'intéresser au temps de calcul de la fonction puissance, le nombre d'itération de la boucle est lié à la puissance n, on a donc un temps de calcul proportionnel à n.
Algorithme itératif rapide
double puissance(double x,unsigned int n) {
double z=1;
while (n != 0) {
if (n & 1) {
z *= x ;
}
n>>= 1;
x *= x;
}
return z ;
}
Cet algorithme itératif rapide est basé sur la représentation binaire de l'exposant et inspiré de l'exponentiation rapide et de l'exponentiation modulaire. On parle également d'arithmétique distribuée.
On exprime n en fonction de la somme des valeurs des bits et des puissances de 2 :
On a transformé la somme en produit qui donne l'équation de récurrence pour k>0:
Cette équation de récurrence est l'algorithme de la fonction C du calcul de la puissance.
Cette-fois-ci la boucle dépend de la valeur du bit de poids le plus élevé à 1, ce qui fait que le temps de calcul est proportionnel à log2(n)
On a une fonction puissance entière optimisée en temps de calcul avec les mêmes contraintes de précision que la méthode classique.
Les fonctions void *calloc(size_t nmemb, size_t size); et void *malloc(size_t size); retournent un pointeur vers la zone mémoire réservée avec
Il est possible de réallouer la mémoire pour augmenter la taille de la mémoire disponible avec void *realloc(void *ptr, size_t size); qui retourne un nouveau pointeur vers la nouvelle zone mémoire réallouée
free(void *ptr);
Exemple de tableau dynamique de nombres réels.
Les fonctions de gestions du tableau sont codées dans tab1ddyn.c et définies dans tab1ddyn.h
Fichier de prototypes tab1ddyn.h
#ifndef __TAB1DDYN_H
#define __TAB1DDYN_H
double *creertab(int);
void liberertab(double *);
#endif
Fichier des codes des fonctions tab1ddyn.c
#include <stdlib.h>
#include "tab1ddyn.h"
double *creertab(int taille) {
return calloc(taille,sizeof(double));
}
void liberertab(double *ptab) {
free((void *)ptab);
}
La fonction calloc est utilisée pour créer réserver une zone mémoire de réels, dont le nombre est défini par le paramètre taille.
La fonction free est utilisée pour libérer la mémoire. Il ne faut jamais oublier de libérer la mémoire.
La variable tableau est un pointeur vers un réel. Cette variable peut être utilisée comme un tableau à une dimension.
Ces fonctions sont testées avec le programme suivant qui affecte des valeurs puis affiche le contenu du tableau avant de libérer la mémoire allouée en fin de programme.
Dans un programme le nombre de libération (free) doit être égal au nombre d'allocation (malloc,calloc).
#include <stdio.h>
#include <stdlib.h>
#include "tab1ddyn.h"
int main(int argc, char **argv) {
double *ptr_tab1d;
ptr_tab1d = creertab(4);
for(int i=0;i<4;i+=1) {
ptr_tab1d[i] = i;
}
for(int i=0;i<4;i+=1) {
printf("tab[%d] = %f\n",i,ptr_tab1d[i]);
}
liberertab(ptr_tab1d);
return EXIT_SUCCESS;
}
C'est un tableau de pointeurs qui utilise un pointeur de pointeur qui est donc accessible avec deux indices comme un tableau à deux dimensions.
Dans le cas d'un tableau à deux dimensions de réels, la variable tableau est déclarée
double **ptr_tab;
L'accès à un élément du tableau d'indices i et j se fait avec la syntaxe
ptr_tab[i][j]Fichier tab2ddyn.h
#ifndef __TAB1DDYN_H
#define __TAB1DDYN_H
void **creertab(int ,int ,int);
void liberertab(void **,int);
#endif
Fichier tab2ddyn.c
#include <stdlib.h>
#include "tab2ddyn.h"
void **creertab(int nligne,int ncolonne,int tailleelement) {
char **ptabdyn;
ptabdyn = (char **)malloc(nligne);
for(int i=0;i<nligne;i+=1) {
char *pligne = (char *)calloc(ncolonne,tailleelement);
ptabdyn[i] = pligne;
}
return (void**)ptabdyn;
}
void liberertab(void **ptabdyn,int nligne) {
for(int i=0;i<nligne;i+=1) {
free(ptabdyn[i]) ;
}
free(ptabdyn);
}
Pour la création, on commence par réserver la mémoire pour le tableau de pointeurs de taille nligne, puis pour chaque ligne on réserve la mémoire de taille ncolonne.
pour la libération, il faut d'abord libérer la mémoire allouée pour chaque ligne, puis libérer la mémoire pour le tableau de pointeur. Il faut bien évidement transmettre le nombre de lignes pour la libération.
Test des fonctions de créations et libérations
#include <stdio.h>
#include <stdlib.h>
#include "tab2ddyn.h"
int main(int argc, char **argv) {
double **ptabdyn;
int nligne = 3;
int ncolonne = 4;
ptabdyn=(double **)creertab(nligne,ncolonne,sizeof(double));
for(int i=0;i<ligne;i+=1) {
for(int j=0;j<ncolonne;j+=1) {
ptabdyn[i][j] = i+j+1;
}
}
for(int i=0;i<nligne;i+=1) {
for(int j=0;j<ncolonne;j+=1) {
printf("p[%d][%d] = %f\n",i,j,ptabdyn[i][j]);
}
}
liberertab((void **)ptabdyn,nligne);
return EXIT_SUCCESS;
}
Après avoir utiliser le tableau dynamique, il faut pas oublier de libérer la mémoire soit à la fin de l'utilisation, soit en fin de programme.
Il existe deux catégories de fonctions pour accéder aux fichiers, les fonctions dites de haut niveaux qui sont fopen, fclose, fprintf, fscanf, fwrite, fread, feof et les fonctions dites de bas niveau qui sont open, close, read, write.
En dehors des descripteurs déjà ouverts, la méthode est toujours la même :
Elles utilisent une structure de données, nommée FILE qui contient les informations pour la gestion des échanges de données avec le fichier, qui est également appelé descripteur. Il existe des descripteurs ouverts :
fd=fopen(nomfichbier,mode);
flose(fd);
fd : pointeur retourné par la fonction fopen
Les données sont lues ou écrites sans aucune conversion, identiques au contenu du buffer.
n=fread(buffer,nboctets,nbpaquets,fd);
n=fwrite(buffer,nboctets,nbpaquets,fd);
Les données lues ou écrites sous la forme de chaînes de caractères dans le fichier.
fscanf(fd,chaineformat,liste adresses variables);
fprintf(fd,chaineformat,liste variables);
fin=feof(fd);
On trouve les fonctions fgetc, fgets pour la lecture de caractère ou de chaîne et fputc, fputs pour l'écriture de caractère ou de chaîne.
caractere=fgetc(fd);
n=fputc(caractere,fd);
p=fgets(buffer,nboctets,fd);
n=fputs(chaine,fd);
#include <stdlib.h>
#include <stdio.h>
int main(int argc, char **argv) {
FILE* fd;
char ligne[81] ;
if (argc !=2) {
fprintf(stderr,"syntaxe %s fichier\n",argv[0]);
return EXIT_FAILURE;
}
fd = fopen(argv[1],"r");
while (!feof(fd)) {
fgets(ligne,80,fd);
printf("%s",ligne);
}
fclose(fd);
return EXIT_SUCCESS;
}
Le nom du fichier texte est transmis en ligne de commande.
Pour une chaîne de 80 caractères maximum, on réserve 81 caractères, il ne faut pas oublier le 0 de fin de chaîne.
On ouvre le fichier en lecture seule
Ensuite on lit le fichier tant que la fin de fichier n'est pas atteinte.
Pour lire un fichier texte, on utilise la fonction fgets qui permet de lire le contenu du fichier texte ligne par ligne avec un maximum de 80 caractères. Le caractère de fin de ligne est également lu, sauf si on a une ligne de plus de 80 caractères.
Chaque ligne est affichée sur la sortie standard. Il ne faut pas oublier la fermeture du fichier.
Remarque : on a choisi fgets à la place de fscanf, Le lecteur pourra essayer ce programme en remplaçant la ligne qui contient fgets, par
fscanf(fd,"%s",ligne);
#include <stdlib.h>
#include <stdio.h>
int main(int argc, char **argv) {
FILE* fd;
double reel;
int n=0;
if (argc !=2) {
fprintf(stderr,"syntaxe %s fichier\n",argv[0]);
return EXIT_FAILURE;
}
fd = fopen(argv[1],"r");
while (!feof(fd)) {
n += fread(&reel,sizeof(double),1,fd);
printf("%lf\n",reel);
}
printf("lu %d réels\n",n);
fclose(fd);
return EXIT_SUCCESS;
}
Ici on utilise la lecture de données de type octets, on fait une lecture réel par réel, en demandant un paquet de la taille d'un réel. La fonction fread retourne le nombre de paquets lu. Elle retourne 1 si le réel a été lu. Ce qui fait que n contient le nombre de réels lus.
Affichage du contenu du fichier des réels avec la commande hexdump -e '/8 "%f\n"'
0,100372 0,570332 0,086103 0,257617 0,950846 0,999225 0,995475 0,888895 0,036791
Le programme C affiche
0.100372 0.570332 0.086103 0.257617 0.950846 0.999225 0.995475 0.888895 0.036791 0.036791 lu 9 réels
Elles utilisent un numéro de descripteur (handle) qui correspond à l'indice du périphérique dans la table des descripteurs. Les descripteurs ouverts, dont les constantes sont définies dans le fichier de prototype unistd.h, sont :
Pour utiliser ces fonctions, il faut inclure les fichiers fcntl.h et unistd.h.
close(fd)
fd : pointeur retourné par la fonction open
n=read(fd,buffer,nb);
n=write(fd,buffer,nb);
#include <stdlib.h>
#include <stdio.h>
#include <fcntl.h>
#include <unistd.h>
int main(int argc, char **argv) {
int fd;
char buffer[80] ;
if (argc !=2) {
fprintf(stderr,"syntaxe %s fichier\n",argv[0]);
return EXIT_FAILURE;
}
fd = open(argv[1],O_RDONLY);
int n=read(fd,buffer,80);
while (n > 0) {
write(STDOUT_FILENO,buffer,n);
n=read(fd,buffer,80);
}
close(fd);
return EXIT_SUCCESS;
}
On utilise open sans le paramètre de type de fichier, car on est en mode lecture.
Dans ce cas, il n'y a pas de lecture ligne par ligne, on ne traite pas le fichier en fichier texte, mais en fichier d'octets. C'est pourquoi, on n'utilise pas, non plus, l'écriture formatée d'une chaîne de caractères sur la sortie standard, car le buffer ne contient pas de caractère 0 de fin de chaîne. On utilise la fonction d'écriture dans un fichier en utilisant le numéro de la sortie standard représenté par la constante STDOUT_FILENO.
Il n'y a pas de fonction de fin de fichier, on utilise donc la valeur de retour de la fonction read, qui est négative lorsque l'on essaie de lire des octets, alors qu'il n'y en a plus de disponible, c'est le cas en fin de fichier.
#include <stdlib.h>
#include <stdio.h>
#include <fcntl.h>
#include <unistd.h>
int main(int argc, char **argv) {
int fd;
double reel;
int nr=0;
if (argc !=2) {
fprintf(stderr,"syntaxe %s fichier\n",argv[0]);
return EXIT_FAILURE;
}
fd = open(argv[1],O_RDONLY);
int n=read(fd,&reel,sizeof(double));
while (n > 0) {
nr += 1;
if (n == sizeof(double)) {
printf("%lf\n",reel);
}
n=read(fd,&reel,sizeof(double));
}
printf("lu %d octets\n",nr);
close(fd);
return EXIT_SUCCESS;
}
Le buffer de lecture est un nombre réel, le fichier contient un nombre d'octets multiple de 8, qui est la taille en octets du réel double précision.
On transmets donc l'adresse du réel à la fonction read suivi de la taille en octets du réel en utilisant la fonction sizeof(reel). Si le fichier est un fichier de réels correct la valeur de retour n est toujours égale à la taille d'un réel en octet.
Au cas ou il y aurait une erreur de taille du fichier, on écrit le réel uniquement si le nombre d'octets lu correspond à la taille du réel.
Un programme qui est un processus peut créer un ou plusieurs processus nommés processus enfants. Lors de cette création l'environnement du processus parent est transmis au processus enfant. Le processus enfant a son propre contexte d'exécution.
La création d'un processus enfant se fait avec la fonction définie dans le fichier unistd.h
pid=fork()
Cette fonction retourne le pid du processus enfant dans le processus parent et 0 dans le processus enfant, et -1 en cas de problème.
Après la création du processus enfant, on a deux processus qui fonctionnent en parallèle.
Le processus parent doit se terminer avant tous les processus enfants. Pour cela, on peut utiliser des fonctions qui attendent la fin des processus enfants dans le processus parent. Pour cela il faut utiliser la fonction définie dans le fichier /sys/wait.h
waitpid(pidenfant,statut,options);
#include <stdlib.h>
#include <stdio.h>
#include <unistd.h>
#include <sys/wait.h>
int main(int argc, char **argv) {
int status;
pid_t pid = fork();
if (pid > 0) {
printf("ici le parent : pid enfant = %d\n", pid);
sleep(2);
printf("Le parent attend son enfant\n");
waitpid(pid, &status, 0);
printf("Fin du processus parent\n");
}
else if (pid == 0) {
printf("Je suis l'enfant\n");
sleep(5);
printf("Fin du processus enfant\n");
}
else {
fprintf(stderr,"probleme\n");
}
return EXIT_SUCCESS;
}
Ce programme crée un processus enfant, le processus parent attend la fin du processus enfant avant de se terminer.
Pour faciliter le rythme de l'affichage, on utilise la fonction
sleep(t)
t : temps en secondes
Cette fonction endort le processus en cours pendant t secondes.
La vidéo ci-dessous montre le fonctionnement du programme processus en affichant l'évolution des processus parent et enfant ainsi que leurs pid.
La commande ps, associée à la commande watch permet d'afficher les processus courants.
Un thread est un souvent appelé un processus léger car il partage le contexte d'exécution, ce qui fait que la commutation de threads est plus rapide. De plus il n'y a pas de hiérarchie parent/enfant avec les threads.
Les fonctions disponibles pour la gestion des threads sont définies dans le fichier de prototype pthread.h, et l'édition de lien se fait en ajoutant la librairie avec l'option -lpthread. Chaque thread est exécuté par une fonction associée.
La création et le démarrage du thread se fait avec la fonction
erreur=pthread_create(adresse_idthread,adresse_attributs,adresse fonction,adresse argument fonction);
void *nomfonction(void * args);
Cette fonction doit se terminer avec la fonctionpthread_exit(adressevariable);
avec adressevariable qui est l'adresse d'une mémoire statique de façon à être visible du programme qui a créé le thread.Le programme principal doit attendre que tous les threads soient terminés avant de se terminer, cela se fait avec la fonction
pthread_join(idthread,adressevaleurretour);
avec adressevaleurretour qui est l'adresse de la variable qui contient la valeur de retour de la fonction transmise par pthread_exit.
#include <stdlib.h>
#include <stdio.h>
#include <unistd.h>
#include <pthread.h>
void *fonction(void *ptr_data) {
static int r;
pthread_t threadid = pthread_self();
for(int i=0;i<5;i+=1) {
printf("fonction %lx : %d, %d\n",threadid,i,*(int*)ptr_data);
usleep(1);
}
r=*(int*)ptr_data;
pthread_exit(&r);
}
int main(int nbarg,char *args[]) {
pthread_t threadmain, thread;
int data,*retour_thread,err;
threadmain = pthread_self();
err=pthread_create(&thread,NULL,fonction,(void*)&data);
if (err) {
fprintf(stderr,"Erreur création thread\n");
return EXIT_FAILURE;
}
for(int i=0;i<5;i+=1) {
data=rand()*100.0/RAND_MAX;
printf("fonction main %lx : %d, %d\n",threadmain,i,data);
usleep(1);
}
pthread_join(thread,(void*)&retour_thread);
printf("retour fonction %d\n",*retour_thread);
return EXIT_SUCCESS;
}
La fonction affiche l'entier qui est transmis en argument qui est du type void*. Il faut donc utiliser un transtypage pour utiliser ce paramètre comme entier.
La valeur de retour de la fonction pthread_exit est déclarée static afin qu'elle reste visible de la fonction main.
La fonction pthread_self() retourne l'identifiant du thread qui est fourni lors de la création du thread avec pthread_create.
La fonction usleep permet, comme sleep, d'endormir le thread pendant un temps donné en µs. Cette fonction affiche toutes les us la valeur de la donnée partagée avec le programme principal, 5 fois de suite avant de se terminer.
Dans la fonction principale, la fonction pthread_self() retourne l'identifiant du thread qui contient la fonction main. Cela montre que la fonction main est exécutée sur un thread.
La création se fait en associant la fonction, ainsi que l'entier data. Si la création du thread se fait sans erreur, la fonction principale génère 5 valeurs aléatoires tous les us.
Enfin on appelle la fonction de synchronisation du thread afin d'attendre la fin du thread avant de terminer le programme après avoir afficher la valeur de retour de la fonction.
Exemple d'exécution du programme
fonction main 7f331745d740 : 0, 84 fonction 7f331745c700 : 0, 84 fonction main 7f331745d740 : 1, 39 fonction main 7f331745d740 : 2, 78 fonction 7f331745c700 : 1, 78 fonction main 7f331745d740 : 3, 79 fonction 7f331745c700 : 2, 79 fonction main 7f331745d740 : 4, 91 fonction 7f331745c700 : 3, 91 fonction 7f331745c700 : 4, 91 retour fonction 91
Chaque exécution donne un affichage différent.
Lors de l'exécution d'un programme avec thread, on peut observer, que l'ordre d'exécution des threads n'est pas toujours le même. On peut également observer lors de l'échange de données entre threads, il peut y avoir des pertes d'information car un thread peut être interrompu avant d'avoir terminé un traitement, ce qui produit des résultats erronés et inattendus.
Pour résoudre ce problème, on doit interdire que le traitement soit interrompu par un autre thread, on parle d'exclusion mutuelle en ajoutant un verrou qui fait qu'une portion de code ne peut pas être interrompue.
L'initialisation du verrou se fait avec la fonction
pthread_mutex_init(adresse_verrou,adresse_attribut_exclusion)
Le verrouillage du code se fait après la fonction
pthread_mutex_lock(adresse_verrou);
Le déverrouillage se fait après la fonction
pthread_mutex_unlock(adresse_verrou);
#include <stdlib.h>
#include <stdio.h>
#include <pthread.h>
pthread_mutex_t verrou;
//#define AVECVERROU
void *fonction(void *ptr_data) {
static int r;
int i;
int *ptr_compteur=(int*)ptr_data;
pthread_t threadfonction = pthread_self();
for(i=0;i<10000;i+=1) {
#ifdef AVECVERROU
pthread_mutex_lock(&verrou);
#endif
*ptr_compteur += 1;
#ifdef AVECVERROU
pthread_mutex_unlock(&verrou);
#endif
}
#ifdef AVECVERROU
pthread_mutex_lock(&verrou);
#endif
printf("thread %lx fin : %d\n",threadfonction,*ptr_compteur);
#ifdef AVECVERROU
pthread_mutex_unlock(&verrou);
#endif
r=*ptr_compteur;
pthread_exit(&r);
}
int main(int nbarg,char *args[]) {
pthread_t thread1,thread2;
int *retour_thread,err;
int compteur=0;
#ifdef AVECVERROU
pthread_mutex_init(&verrou, NULL);
#endif
err=pthread_create(&thread1,NULL,fonction,(void*)&compteur);
if (err) {
fprintf(stderr,"Erreur creation thread1\n");
return EXIT_FAILURE;
}
printf("Premier thread : %lx\n",thread1);
err=pthread_create(&thread2,NULL,fonction,(void*)&compteur);
if (err) {
fprintf(stderr,"Erreur creation thread2\n");
return EXIT_FAILURE;
}
printf("Deuxième thread : %lx\n",thread2);
pthread_join(thread1,NULL);
pthread_join(thread2,(void*)&retour_thread);
printf("retour fonction %d\n",*retour_thread);
printf("compteur = %d\n",compteur);
#ifdef AVECVERROU
pthread_mutex_destroy(&verrou);
#endif
return EXIT_SUCCESS;
}
Ce code utilise la compilation conditionnelle afin de pouvoir le tester avec et sans verrou. Si la ligne #define AVECVERROU est commentée le programme fonctionne sans exclusion mutuelle, si cette ligne n'est pas commentée, le programme fonctionne avec l'exclusion mutuelle.
La fonction incrémente un compteur qui correspond à la donnée partagée avec la fonction principale. Lorsque la valeur maximale est atteinte la fonction affiche la valeur du compteur sur la sortie standard.
La fonction principale crée deux thread qui implémente cette fonction. On obtient donc un double comptage du compteur, ce qui fait qu'à la fin du programme, le compteur doit avoir une valeur qui est le double de la valeur de fin de la boucle.
Exemple d'exécution sans exclusion mutuelle
Premier thread : 7fba5b812700 Deuxième thread : 7fba5b011700 thread 7fba5b812700 fin : 11112 thread 7fba5b011700 fin : 14770 retour fonction 14770 compteur = 14770
La valeur finale n'est pas la valeur attendue. Cela vient du fait que l'incrémentation du compteur est interrompue avant de se terminer.
Prenons un exemple de fonctionnement des deux threads, en sachant que la ligne compteur += 1 correspond à trois instructions processeurs qui sont :
registre <- compteur registre <- registre + 1 compteur <- registre
avec compteur qui représente le stockage en mémoire de la valeur du compteur
On exécute ces instructions sur les deux threads en prenant, par exemple, compteur=100
Thread1 | Thread2 | ||
---|---|---|---|
Instruction | registre | Instruction | registre |
registre <- compteur | 100 | ||
registre <- compteur | 100 | ||
registre <- registre + 1 | 101 | ||
compteur <- registre | 101 | ||
registre <- registre + 1 | 101 | ||
compteur <- registre | 101 |
A la fin de ceux instructions d'incrémentation du compteur, sa valeur est de 1 au lieu de 2.
Exemple d'exécution avec exclusion mutuelle
Premier thread : 7fa2c7bd2700 Deuxième thread : 7fa2c73d1700 thread 7fa2c73d1700 fin : 19154 thread 7fa2c7bd2700 fin : 20000 retour fonction 20000 compteur = 20000
La valeur finale est bien le double de la valeur de fin de boucle. L'exclusion mutuelle fait que l'incrémentation du compteur n'est pas interrompue, donc toutes les incrémentations sont exécutées correctement. La valeur affichée par le thread qui se termine en premier, dépend de la séquence d'exécution des thread, et est donc différente à chaque appel de ce programme.
Le calcul matriciel est réduit aux opérations d'addition et de multiplication de matrices de taille quelconque. Un matrice est enregistrée dans un tableau dynamique à deux dimensions.
Ces fonctions sont codées dans le fichier matrice.c et déclarées ainsi que la structure de données dans le fichier matrice.h
La structure de données qui représente la matrice contient : la taille de la matrice ainsi qu'un pointeur vers le tableau à deux dimensions qui contient les valeurs de la matrice.
Fichier matrice.h
#ifndef __MATRICE_H
#define __MATRICE_H
typedef struct s_matrice {
int m;
int n;
double **contenu;
} Tmatrice;
typedef Tmatrice *Tmatrice_ptr;
Tmatrice_ptr creermatrice(int ,int ,Tmatrice_ptr );
Tmatrice_ptr matalea(Tmatrice_ptr);
Tmatrice_ptr matadd(Tmatrice_ptr,Tmatrice_ptr);
Tmatrice_ptr matmul(Tmatrice_ptr,Tmatrice_ptr);
void liberemat(Tmatrice_ptr);
#endif
On utilise typedef pour créer la structure de donnée Tmatrice ainsi que le pointeur vers cette structure Tmatrice_ptr.
On crée une fonction creermatrice pour la création du tableau dynamique sans initialiser le contenu, et bien sûr une fonction de libération de la mémoire occupée par une matrice : liberemat. Pour effectuer les tests, on va créer une fonction de remplissage d'une matrice avec des valeurs aléatoires, ainsi que les fonctions d'additions et de multiplication.
La fonction de création de la matrice utilise la fonction de création d'un tableau à deux dimensions, déjà étudié auparavant.
La fonction de remplissage de valeurs aléatoires utilise la fonction rand, avec une graine initialisée à partir du temps système time() de façon a ne pas voir toujours les mêmes valeurs aléatoires au démarrage du programme. Les valeurs aléatoires sont comprises entre -5 et +5, c'est un choix totalement arbitraire.
La fonction d'addition crée la matrice résultat, après avoir vérifier que les tailles des matrices sont identiques.
La fonction de multiplication vérifie la condition nécessaire pour effectuer le produit matriciel, qui doit respecter le fait que le nombre de colonnes de la première matrice doit être égal au nombre de lignes de la deuxième.
fichier matrice.c
#include <stdio.h>
#include <stdlib.h>
#include <math.h>
#include <time.h>
#include "matrice.h"
double **creertab(int nligne,int ncolonne) {
double **ptr_tab = (double **)calloc(nligne,sizeof(double *));
for(int i=0;i&lyt;nligne;i+=1) {
double *pligne = (double *)calloc(ncolonne,sizeof(double));
ptr_tab[i] = pligne;
}
return ptr_tab;
}
Tmatrice_ptr creermatrice(int m,int n,Tmatrice_ptr matrice) {
matrice->m = m;
matrice->n = n;
matrice->contenu =creertab(matrice->m,matrice->n);
return matrice;
}
Tmatrice_ptr matalea(Tmatrice_ptr matrice) {
time_t temps = time(NULL);
srand(temps);
for(int i=0;i<matrice->m;i+=1) {
for(int j=0;j<matrice->n;j+=1) {
matrice->contenu[i][j] = ((double)rand()/RAND_MAX)*10.0-5.0;
}
}
return matrice;
}
Tmatrice_ptr matadd(Tmatrice_ptr A,Tmatrice_ptr B) {
if ((A->m != B->m) || (A->n != B->n)) {
return NULL;
}
Tmatrice_ptr R = (Tmatrice_ptr)malloc(sizeof(Tmatrice));
creermatrice(A->m,A->n,R);
for(int i=0;i<A->m;i+=1) {
for(int j=0;j<A->n;j+=1) {
R->contenu[i][j] = A->contenu[i][j] + B->contenu[i][j] ;
}
}
return R;
}
Tmatrice_ptr matmul(Tmatrice_ptr A,Tmatrice_ptr B) {
if (A->n != B->m) {
return NULL;
}
Tmatrice_ptr R = (Tmatrice_ptr)malloc(sizeof(Tmatrice));
creermatrice(A->m,B->n,R);
for(int i=0;i<A->m;i+=1) {
for(int j=0;j<B->n;j+=1) {
double scalaire=0;
for(int k=0;k<A->n;k+=1) {
scalaire += A->contenu[i][k] * B->contenu[k][j];
}
R->contenu[i][j] = scalaire;
}
}
return R;
}
void liberemat(Tmatrice_ptr matrice) {
for(int i=0;i<matrice->m;i+=1) {
free(matrice->contenu[i]) ;
}
free(matrice->contenu);
}
Fichier de test
#include <stdlib.h>
#include <stdio.h>
#include <string.h>
#include <unistd.h>
#include <fcntl.h>
#include <sys/wait.h>
#include <math.h>
#include "matrice.h"
#define NDEFAUT 1
#define TAILLE_STRMATRICE 500
char *mat2octave(Tmatrice matrice,char*nom,char *charoct) {
sprintf(charoct,"%s=[",nom);
for(int i=0;i<matrice.m;i+=1) {
for(int j=0;j<matrice.n;j+=1) {
char svaleur[TAILLE_STRMATRICE] ;
sprintf(svaleur,"%e ",matrice.contenu[i][j]);
strcat(charoct,svaleur);
}
if (i < (matrice.m -1)) {
strcat(charoct,";");
}
}
strcat(charoct,"]");
return charoct;
}
int main(int argc, char **argv) {
int status;
Tmatrice A,B;
Tmatrice_ptr C;
char sA[TAILLE_STRMATRICE], sB[TAILLE_STRMATRICE] , sC[TAILLE_STRMATRICE];
char soctavecmd[3*TAILLE_STRMATRICE+50];
int pipefd[2];
pid_t pid;
char buffer[100];
int Nmax = NDEFAUT;
double sommeerreur=0;
if (argc == 2) {
Nmax = strtol(argv[1],NULL,0);
}
creermatrice(3,4,&A);
creermatrice(4,3,&B);
for(int i=1;i<=Nmax;i+=1) {
printf("Essai n°%d\r",i);
fflush(stdout);
matalea(&A);
matalea(&B);
C=matmul(&A,&B);
mat2octave(A,"A",sA);
mat2octave(B,"B",sB);
mat2octave(*C,"C",sC);
sprintf(soctavecmd,"%s;%s;%s;R=A * B;E=max(max((C-R).^2))",sA,sB,sC);
liberemat(C);
int res=pipe(pipefd);
if (res < 0) {
fprintf(stderr,"erreur pipe\n");
return EXIT_FAILURE;
}
pid = fork();
if (pid > 0) {
waitpid(pid, &status, 0);
close(pipefd[1]);
int n=read(pipefd[0],buffer,1000);
close(pipefd[0]);
buffer[n]=0;
char *p=strchr(buffer,'=');
double erreuroctave = strtod(++p,NULL);
sommeerreur += erreuroctave;
}
if (pid == 0) {
int err=dup2(pipefd[1],STDOUT_FILENO);
if (err == -1) {
return EXIT_FAILURE;
}
execl("/usr/bin/octave-cli", "octave-cli","--eval",soctavecmd, NULL);
return EXIT_SUCCESS;
}
}
double erreurquadratiquemoyenne = sqrt(sommeerreur/Nmax);
printf("\nerreur quadratique moyenne %e\n",erreurquadratiquemoyenne);
liberemat(&A);
liberemat(&B);
return EXIT_SUCCESS;
}
Méthode
Le programme de test va effectuer une suite de multiplications de matrices, transférer les matrices ainsi que le résultat au logiciel octave. Ce dernier va effectuer le calcul et comparer le résultat avec le calcul effectué par le programme C, en calculant l'erreur quadratique maximale entre les deux matrices résultats. Le programme C va utiliser ce résultat pour calculer l'erreur quadratique moyenne de cette suite de multiplications.
Implémentation
Pour chaque jeu de matrice, après avoir effectuer le calcul en C, le programme génère une chaîne de commande pour octave, exécute le programme octave dans un processus enfant. Ce dernier transmet le résultat au processus parent en utilisant un tube, après avoir rediriger la sortie standard vers l'entrée du tube.
Le programme effectue la somme cumulée des erreurs, pour enfin calculer l'erreur quadratique moyenne et afficher le résultat.
Codage
La fonction mat2octave crée une chaîne de caractère pour octave de déclaration de la matrice à partir du tableau. Cette chaîne est de la forme :
A=[ 1 2 3 4;4 5 6 7;8 9 10 11]
Exemple de chaîne de commande octave :
A=[ 1 2 3 4;4 5 6 7;8 9 10 11]; B=[ 1 2 1;4 5 6;12 10 11,7 8 6]; C=A*B; E=max(max((C-R).^2))
La dernière ligne ne se termine pas par un point-virgule, ce qui fait que c'est le résultat de cette dernière qui sera envoyé sur la sortie standard, comme par exemple :
E = 5.564243e-6
Le programme crée un tube avec la fonction pipe qui contient un flux d'entrée (pipefd[1]) ou écriture et un flux de sortie ou lecture (pipefd[0]).
Dans le processus enfant, la sortie standard est redirigée vers l'entrée du tube avec la fonction dup2 avant d'exécuter le programme octave avec la fonction execl. Le résultat fournit par le logiciel octave sera donc écrit dans le tube et disponible dans le programme parent
Dans le processus parent, après avoir attendu la fin du processus enfant, le programme ferme le flux d'entrée, lit le contenu du flux de sortie dans un buffer, ferme le flux de sortie.
Le buffer contient la ligne retournée par octave, qui n'est pas au format chaîne de caractères. On ajoute donc un 0 en fin de buffer, pour que celui-ci soit exploitable par les fonctions de traitement de chaînes de caractères. La fonction strchr retourne un pointeur sur le caractère égal. Il reste à convertir la chaîne qui commence après le caractère égal en réel.