Cours système d'exploitation by Guillaume Chanel, Jean-Luc Falcone and University of Geneva is licensed under CC BY-NC-SA 4.0
Développé par Dennis Ritchie, Bell Labs, entre 1969 et 1973. Intimement lié au développement d’UNIX:
Ken Thompson (gauche), un des créateur d’UNIX et Dennis Ritchie (droite)
Brian W. Kernighan and Dennis M. Ritchie , The C Programming Language, Second Edition, Prentice Hall, Inc., 1988.
Première édition: le standard “K&R-C “
ANSI C: une norme qui garantie la portabilité:
math.h
: fonctions mathématiques courantes (cos
, exp
, pow
, ...)stdio.h
: fonctions d’entrée/sortie du system (printf
, scanf
, getc
, ...)stdlib.h
: allocation et libération de mémoire (malloc
, free
, ...), control du processus en cours (exit
, ...), ...Année | Nom(s) | Addition(s) principales |
---|---|---|
1989 | C89 | |
1990 | C90 ISO-9899:1990 | Mineurs: C90 ≈ C89 |
1999 | C99 ISO-9899:1999 | Gestion de nouveau types (complexes, booleen, …), inline function, ... |
2011 | C11 ISO-9899:2011 | Exécution de threads (default GCC ≈ gnu11) |
Beaucoup de code libre et ouvert est en C (maintenant en C++):
Depuis sa création C est resté un langage populaire:
Les principaux compilateurs C:
Avantages
Langage de bas niveau proche du langage machine et du système
Langage de haut niveau
Inconvénients
Inadapté au développent occasionnel ou pour le test rapide d’algorithmes
L’efficacité peut être au dépend de la compréhension → Ne pas hésiter à mettre des commentaires claire dans le code
Langage sans garde fou: indices des tableaux non contrôlés, il est possible de tenter un accès partout en mémoire → programmation défensive:
#include <stdio.h>
#include <math.h>
#define PI 3.14159265359
double surface(float x) {
return x * x * PI;
}
/* La fonction main est toujours la première fonction du programmes à être
appellée lors que l'execution*/
int main(void) {
int input, i;
//la fonction printf affiche quelque chose
printf("Veuillez entrer une valeur: ");
scanf("%d", &input); //la fonction scanf lit l'entrée utilisateur
for(i=1; i < input; i++) {
float per = surface(i);
printf("Le resultat pour %d est: %f\n", i, per);
}
return 0;
}
Les directives de pré-compilation:
#
';
'La directive include inclue les entêtes des fonctions d’une librairie:
#include<stdio.h>
#include<math.h>
La directive define permets de créer une sorte de "constante":
#define PI 3.14159265359
Type | Base de déclaration |
---|---|
Entier | int |
Virgule flottante | float double , un float tenant su plus d'octets and mémoire → plus de valeurs possibles |
Caractère | char , en fait un petit entier |
Vide | void , absence de valeur, utile pour les fonctions sans retour et sans paramètres ainsi que pour les pointeurs sur des types inconnus |
Il est possible de combiner ces types avec des mots clef:
Mot clef | Types acceptés | Effet |
---|---|---|
short |
int |
diminue la taille de l’entier en mémoire (moins de valeurs disponibles) |
long |
int , double |
augmente la taille de la variable en mémoire (plus de valeurs disponibles) |
unsigned |
char , int |
la valeur ne peut pas être négative → la valeur maximum du nombre augmente |
float maVariable;
int ceciEstUnEntier;
unsigned int ceciEstUnEntierSuperieurAZero;
char ceciEstUnCaractère;
long int ceciEstUnEntierLong;
long long int ceciEstUnEntierTresLong; //C99
long ceciEstUnEntierLong;
double ceciEstUnDouble;
long double ceciEstUnDoubleLong = 1.3421;// Declare + init.
Attention ! Il n’y a pas de type booléen en C89/90. Depuis C99 on peut utiliser:
#include<stdbool.h>
bool unBoolean = // (en fait true==1 et false==0)
D'une manière généralle:
La taille des variables dépend des platformes et compilateurs. Pour connaitre la taille d'une variable:
sizeof(obj)
, obj
peut être un type ou un nom de variableCHAR_MIN
: valeur minimum d'un char
UINT_MAX
: valeur maximum d'un int
int8_t, int16_t, int32_t
uint8_t, uint16_t, uint32_t
Les noms de variables doivent suivre les règles suivantes:
_
;_
;if
, sizeof
, ...);N’oubliez pas d’initialiser les valeurs des variables !
Portée d’une variable:
Type | Opérateurs | Explication / Note | Exemple |
---|---|---|---|
Affectation | = | val = 5 | |
Arithmétique | + - * / % |
Reste de la division entière |
y = 3/4 * x + b 10 % 4 = 2 le reste de 10 / 4 est 2 |
Comparaison | < <= > >= == != |
Ne pas confondre = et == |
1 > 4 (retourne 0, faux) 7 != 4 (retourne !0, vrai) |
Logique | && || ! |
ET OU NON |
(y > 8) |
Binaires | & | ^ ~ << >> |
ET OU XOR NON Décalage à gauche Décalage à droite avec conservation du signe |
5 & 10 (retourne 0) 6 << 1 (retourne 12) 9 >> 2 (retourne 2) |
Etant donnée la definition de fonction suivante:
function test_if_2(int a) {
if(a = 2)
return 1;//or true
else
return 0;//or false
}
Que retourne l'appel suivant ?
test_if_2(3)
Il est possible de combiner les opérateurs arithmétiques et logiques avec l’affectation:
int y;
y = 3;
y += 4;//Equivalent à y = y + 4;
x <<= 2;//Equivalent à x = x << 2;
Il existe un opérateur d’incrément / décrément (++ et --):
int x, i = 0;
i++; //equivalent à i = i + 1
i--; //equivalent à i = i - 1
x = ++i; //x vaut 1 car l'incrément est fait avant l'affectation
x = i++; //x vaut toujours 1 car l'incrément est fait après l'affectation
L’ordre de priorité des opérateur est:
Dans un calcul le type des variables détermine le type du résultat:
int xInt = 3, resInt;
float xFloat = 3, resFloat;
char xChar = 3;
resInt = xInt / 4; //resInt vaudra 0 car le calcul est entier
resFloat = xInt / 4; //resFloat vaudra 0.00 car le calcul est entier
resFloat = xInt / 4.; //resFloat vaudra 0.75 car 4. est un float
resFloat = xFloat / 4; //resFloat vaudra 0.75 car xFloat est un float
resInt = xFloat / 4; //resInt vaudra 0 (cas resInt est un entier)
resInt = xChar + 2; //Convertion automatique et possible (resInt vaudra 5)
Le casting permet de forcer la conversion de type
esFloat = (float) xInt / 4; // resFloat vaudra 0.75 car x est convertit en float
resFloat = (float) (xInt / 4); /* resFloat vaudra 0.00 car le calcul entier est fait
avant conversion */
resInt = (float) xInt/ 4; // resInt vaudra 0 car resInt est un entier
Quel est le problème avec les calculs suivants ?
int i; long int l; float f; char c; unsigned char uc;
c = -100;
uc = c;
c = 'ï'; //code ascii = 239
uc = c;
l = 320254468;
f = 45879651324476.5;
i = l * f;
Les branchements if
int a, b, c;
...
if((a < 0) || !((b == 2) && (c != 4))) {
printf("La condition est validée\n");
}
else {
printf("La condition n'est PAS validée\n");
}
Les boucles for
//Calcul la somme des N premiers entiers
int value = 0;
for(int i=0; i < N; i++) {
printf("Nouvelle itération de la boucle\n");
value += i;
}
La boucle while
//Calcul la somme des N premiers entiers
int value = 0;
int i = 0;
while(i < N) {
value += i;
printf("Nouvelle itération de la boucle: %d\n", i++);
}
La boucle do...while
: le contenu est toujours exécuté au moins une fois
//Calcul la somme des N premiers entiers
int value = 0;
int i = 0;
do {
value += i;
printf("Nouvelle itération de la boucle: %d\n", ++i);
}
while(i < N);
Il est possible d’interrompre le déroulement d’une boucle (for, while, do/while) en utilisant les mots clefs:
continue
: passe à l’itération suivante;break
: sort de la boucle.//Calcul la somme des nombre impairs allant de 0 à N
value = 0;
i = 0;
while(1) {
if(i > N)
break;
if(i % 2 == 0) {
i++
continue;
}
value += i;
printf("Nouvelle itération de la boucle: %d\n", ++i);
}
Attention dans certains cas (cf. ci-dessus), l’utilisation de break
et continue
est à évitée car elle rend le code moins lisible.
Une fonction est toujours déclarée avant son utilisation:
typeValeurRetour nomDeLaFonction(type1 param1, ..., typeN paramN)
Le mot clef return
indique un point d’arrêt de la fonction ainsi que la valeur que la fonction doit retourner.
float pourcentage(int valeur, int centPourcent) {
return ((float) valeur/centPourcent)*100;
}
Une fonction peu ne rien retourner:
void pourcentage(int valeur, int centPourcent) {
printf("%f", ((float) valeur/centPourcent)*100);
}
Il est possible de déclarer une fonction juste avec son entête:
float pourcentage(int , int);//Déclaration de l’entête int main(void) { int a,b; ... //Utilisation de la fonction sans implémentation connue pourcentage(a,b) return 0; } float pourcentage(int valeur, int centPourcent){ return ((float) valeur/centPourcent)*100; }
float pourcentage(int , int);//Déclaration de l’entête int main(void) { int a,b; ... //Utilisation de la fonction sans implémentation connue pourcentage(a,b) return 0; } float pourcentage(int valeur, int centPourcent){ return ((float) valeur/centPourcent)*100; }
float pourcentage(int , int);//Déclaration de l’entête int main(void) { int a,b; ... //Utilisation de la fonction sans implémentation connue pourcentage(a,b) return 0; } float pourcentage(int valeur, int centPourcent){ return ((float) valeur/centPourcent)*100; }
En C les paramètres sont passés par valeur (pas de modification des variables entrées).
Il n’y a pas de passage par référence, on a recours au passage par adresse:
*
;/*********************************************************************************
Calcul le pourcentage du nombre "valeur" par rapport à "centPoucent"
IN:
valeur: contient la valeur a mettre en rapport pour le calcul
du pourcentage. Une fois la fonction exécutée valeur
contient le pourcentage.
centPourcent: valeur par rapport à laquelle le pourcentage est calculé
OUT:
retourne -1 si une valeur d'entrée n'est pas valide, 0 sinon
**********************************************************************************/
int versPourcentage(float *valeur, float centPourcent) {
if( (centPourcent > 0) && (*valeur >= 0) ) {
*valeur = (*valeur / centPourcent)*100;
return 0;
}
else
return -1;
}
/* Utilisation de la fonction */
int main(void) {
int val = 30; ret;
if ( (ret = versPourcentage(&val, 100)) < 0 )
return ret;
else
return val;
}
Les tableau sont:
Décalaration d'un tableau:
#define TAILLE_MAX 10
int tableauDEntiers[TAILLE_MAX];
double tableauDeDoubles[TAILLE_MAX];
Affecter et lire les valeurs des tableaux:
// Initialization du tableau
for(int i=0; i<TAILLE_MAX; i++)
tableauDEntiers[i] = i+1;
// Alternative mais uniquement lors de la declaration
int tableauDEntiers[TAILLE_MAX] = {1,2,3,4,5,6,7,8,9,10};
// Affichage du tableau
for(int i=0; i<0; i++)
printf("Le tableau contient à l'indice %d la valeur %d", i, tableauDEntiers[i]);
Attention aux indices des tableaux !
Hors des limites on attends un comportement indéfini:
//Lignes et colonnes sont arbitraires
#define NB_LIGNES 3
#define NB_COLONNES 5
#define NB_MORE 7
double tab2D[NB_LIGNES][NB_COLONNES];
float tab3D[NB_LIGNES][NB_COLONNES][NB_MORE];
float tab4D[NB_LIGNES][NB_COLONNES][NB_MORE][NB_MORE];
for(i=0;i<NB_LIGNES;i++)
for(j=0;j<NB_COLONNES;j++)
tab2D[i][j] = 0.0;
L'organisation en mémoire reste linéaire !
Il n’y a pas de type chaine de caractère (string) en C, pour cette raison on utilise des tableaux de caractères.
Une chaine de caractères se termine toujours par le caractère “\0”:
char maChaine[8] = "C cool!";/* \0 est ajouter après la chaine (pensez a
reserver de la place) */
char maChaine2[] = "C cool!";// Alloue automatiquement la bonne taille
printf("%s\n", maChaine2); //Affiche la chaine
maChaine[1] = '\0';// '' pour un caractère et "" pour une chaine
printf("%s\n", maChaine); //Affiche "C"
Il existe des fonctions de manipulation de chaines dans la librairie string (inclure string.h), par exemple:
Pour afficher du texte sur stdout (console) on utilise la fonction printf:
printf(texteEtFormat, variable1, variable2, ...)
texteEtFormat
: une chaine de caractère à afficher entre "" qui spécifie les positions et types des variables à afficher.variable1
, variable2
, variable à afficher dans l’ordre de leur apparence dans texteEtFormat
.Pour lire du texte sur stdin (console) on utilise la fonction scanf:
scanf(texteEtFormat, *variable1, *variable2, ...)
texteEtFormat
: une chaine de caractère comme pour printf. Attention taper du texte ici indique que ce texte doit être saisie pas l’utilisateur et non pas que le texte sera affiché sur stdout.*variable1
, *variable2
, adresse des variables ou vont être rangée les valeurs entréespar l’utilisateur. Pour avoir l’adresse du variable on utilise le symbole &.Les entête de fonction font partie du fichier stdio.h
liste des principaux spécificateur (man printf
pour une liste complète):
Spécificateurs | Affichage |
---|---|
%d, %i | integer, char |
%f | float, double |
%c | unsigned char afficher sous forme de caractère |
%u | unsigned integer, char |
%s | string (chaines de caractères) |
%lS | précède le spécifieur S par une indication de long (e.g. %ld) |
%Pd, %P.Sf | P indique la taille minimum du champ à afficher S indique le nombre de chiffres significatifs après la virgule |
On utilise les combinaisons de symboles suivantes pour les caractères spéciaux:
Symboles | Affichage |
---|---|
\n | saut de ligne |
\r | retour à la ligne |
\t | tabulation |
\\ | backslash |
\' \" | simple ou double quote |
\0 | NUL character, utilisé pour indiquer la find d'une chaine de caractère |
int getchar( void )
int putchar( int car )
char* fgets ( char* string, int size, stdin ) //!!!
int puts (const char* string)
#include<stdio.h>
/* La fonction main est toujours la première fonction du programme à être
appellée lors que l'execution*/
int main(void)
{
float inputFloat;
char inputChar;
int nbCorrespondance;
//Entrée de l'utilisateur pour un float et affichage du
//float de différente manières
printf("Veuiller entrer un float:\t");
scanf("%f", &inputFloat);
printf("Affichage de l'entree sous forme de float: %f\n", inputFloat);
printf("Idem avec 2 digits apres la virgule: %.2f\n", inputFloat);
printf("Idem avec au moins 6 caractères: %6.2f\n", inputFloat);
printf("Notation scientifique: %e\n", inputFloat);
printf("Affichage de l'entree sous forme d'entier %d\n", inputFloat);
//Boucle tant que l'utilisateur n'appuye pas sur 'enter' uniquement
do
{
//Entrée de l'utilisateur pour un caractère
//avec effacement du buffer d'entrée clavier
printf("Veuiller entrer un caractere: ");
while((inputChar = getchar()) != '\n' && inputChar != EOF);
inputChar = getchar(); //could also use scanf("%c", &inputChar);
//Si le caractère est valide l'afficher sous forme d'entier non signé
if(inputChar != '\n')
printf("Le code ASCII de %c est %u\n", inputChar, inputChar);
} while(inputChar != '\n');
return 0;
} //end main
Dans quel cas ce code provoque des erreurs?
#include<stdio.h>
#define TAILLE_MAX 3
void affiche_chaine(char*, int);
int main(void)
{
int valeur1 = 1;
char chaine[TAILLE_MAX] = {'a', 'b', '\0'};
int valeur2 = 2;
// Affiche l'état de la mémoire pour les variable ci-dessus
printf("Valeur1:\tAdresse: %x\tValeur:%d\n", &valeur1, valeur1);
affiche_chaine(chaine, TAILLE_MAX);
printf("Valeur2:\tAdresse: %x\tValeur:%d\n", &valeur2, valeur2);
// NE JAMAIS UTILISER GETS
gets(chaine);
// Affiche l'état de la mémoire pour les variable ci-dessus
printf("Valeur1:\tAdresse: %x\tValeur:%d\n", &valeur1, valeur1);
affiche_chaine(chaine, TAILLE_MAX);
printf("Valeur2:\tAdresse: %x\tValeur:%d\n", &valeur2, valeur2);
}
La fonction main est le point d'entré du programme et a l’entête suivante:
int main (int argc, char *argv[])
Arguments:
La fonction main retourne un code entier indiquant généralement:
La fonction exit(int)
permet également de terminer un programme à tout moment en renvoyant le code en paramètre (stdlib.h).
Dans le shell on peut taper “echo $?” pour avoir ce code de retour.
#include<stdio.h>
#include<stdlib.h>
int main(int argc, char *argv[])
{
int i, sum = 0;
if(argc != 0)
printf("Le nom du programme est: %s\n", argv[0]);
if(argc > 1) {
//Sum the inputs
for(i=1;i < argc; i++) {
//Affiche le paramètre traité
printf("Param %d: %s\n", i, argv[i]);
//Convert the parameter to a number and sum it
sum += atoi(argv[i]);
}
}
return sum;
}
La variable globale environ
est un tableau de chaine de caracère permettant d'accéder aux variables d'environement. Cette variable n'es pas utilisée directement, on préférera utiliser:
char* getenv (const char *name)
name
sous forme de chaine de caractère (NULL si pas définit);getenv
.int putenv (char *string)
string
est de la forme variable=value
cette définition est ajouté à l'environement. Si la forme de string
est variable
la variable est supprimée de l'environement;string
est modifiée ultérieurement, l'environement le sera également.gcc appelle automatiquement ld, pour compiler et lier un programme il suffit donc d’utiliser gcc:
$ gcc prog.c -o prog
Les options principales de gcc:
Option | Effet |
---|---|
-Wall |
Affiche tous les warning possibles |
-I dir |
Inclue le répertoire dir pour la recherche de .h |
-g |
Génère les informations symboliques pour debugage |
Lorsque gcc trouve une option qu’il ne connait pas il passe cette option et les suivantes a ld.
Il faut toujours mettre les options ld à la fin:
Option | Effet |
---|---|
-L dir |
Inclue le répertoire dir pour la recherche de librairie statiques |
-l nom |
Inclue la librairie statique libnom (ne pas mettre le lib) |
Les libraires portent le nom libnom
et ce trouvent généralement dans:
/usr/lib
/usr/lib64
Par exemple la librairie svn s’appelle “libsvn” et pour la lier on utilise:
$ gcc monClientSVN.c -o clientsvn -l svn
Si une erreur intervient lors de l’exécution d’un programme le système génère souvent un coredump.
Le coredump est une image de la mémoire (et donc de l’état) du processus lors de son arrêt.
On peut consulter ces informations en utilisant le debugger gdb (GNU debugger):
$ gdb monexecutable -c core
On peut alors voir:
Il est bien sûre préférable que l'exécutable contienne des informations symboliques de débuggage (option -g
).
Il est possible d’utiliser gdb pour debugger un programme directement:
$ gdb monProg
Une fois dans le debugger les commandes suivantes sont utiles (help):
run
: lance le programme qui poursuit sont exécution jusqu’à une erreur ou la fin;list
: liste les 10 lignes de codes autour du point actuel;break param
: positionne un breakpoint à la ligne ou la fonction param1;clear param
: supprime un breakpoint;cont
: continue l’exécution du code;step
: exécute la prochaine ligne de code;print param
: affiche la valeur courant de la variable param;info param
: information sur beaucoup de choses (info locals, info )quit
: quitte le débuggage.Un tableau (e.g. chaine de caractère) est représentable par un pointeur et une taille.
int main(int argc, char* argv[]) {
char *chainePtr;
char chaineTab[] = "Je suis bien content !";
long long int *intPtr, i;
long long int intTab[TAILLE_TAB];
//Initialisation du tableau d'entiers
for(i=0;i<TAILLE_TAB;i++)
intTab[i] = i+1;
//Mettre les pointeurs sur les tableaux
intPtr = intTab; //equivalent à intPtr = &intTab[0];
chainePtr = chaineTab;
//Affichage des equivalences
printf("Adresse chainPtr: %s, contenu chaineTab: %s\n", chainePtr, chaineTab);
printf("Adresse chainePtr: %x, adresse chaineTab: %x\n", chainePtr, chaineTab);
printf("Contenu chainPtr: %s, contenu chaineTab: %s\n", chainePtr, chaineTab);
for(i=0;i<TAILLE_TAB;i++)
printf("%ld = %ld\n", *(intPtr+i), intTab[i]);
}
Quelle est l'équivalence tab[i][j] sous forme de pointeurs ?
Quelle est la différence de taille entre
sizeof(char*)
et
sizeof(long int*)
En fait toutes les entêtes de fonctions utilisant des chaines de caractère utilisent char*.
Mais attention il reste des différences:
//Exemple de difference:
printf("Taille pointeur: %d, Taille tableau : %d\n", sizeof(intPtr), sizeof(intTab));
//Output: Taille Pointeur: 4, Taille tableau: 160 (20*8, 8 taille d’un long long int)
const
permet de s’assurer qu’une chaine de caractères (ou toute valeur pointée) ne soit pas modifiée par la fonction:
int afficheChaine(const char *tab)
{
while(*tab) //Equivalent à while(*tab != '\0')
putchar(*(tab++));
puts("\n");
//*tab = 'a'; //Erreur à la compilation !
}
L'allocation dynamique ce fait sur le tas:
Des fonctions sont disponibles dans stdlib
pour effectuer l’allocation de la
mémoire:
malloc(nbOctets)
: retourne un pointeur sur une zone allouée de nbOctets ou NULL en cas d’erreur d’allocation;calloc(nbElemens, nbOctets)
: retourne un pointeur sur une zone allouée de nbElements de nbOctets chacun initialisés à 0. Retourne NULL en cas d’erreur d’allocation;free(ptr)
: libère l’espace mémoire pointé par ptr. Ne remet pas ptr à NULL !#include<stdlib.h>
#include<stdio.h>
int main(void) {
float *dynFloat = NULL;
char *buffer = NULL;
int taille;
printf("Entrer la taille desiree: "); scanf("%d", &taille);
//malloc et calloc retournent void*, il est donc nécéssaire de
//faire un cast dans le bon type du pointeur
dynFloat = malloc(taille*sizeof(float));
buffer = calloc(taille, sizeof(char));
//Erreur lors de l'allocation
if((dynFloat == NULL) || (buffer == NULL)) {
printf("Erreur: impossible d'allouer la memoire necessaire.\n");
free(buffer); free(dynFloat);//Ne fait rien si == NULL
return 1;//ou exit(1)
}
*buffer = 'H'; *(buffer+1) = 'A', *(buffer+2) = 'L';
printf("Contenu buffer: %s\n", buffer); //Ok car mise à zero (calloc)
printf("Contenu float: %f\n", *(dynFloat+3)); //valeur indéfinie
//Libération de la mémoire
free(buffer); free(dynFloat);
return 0;
}
Il est possible de définir de nouveaux type de variable grâce au mot clé typedef
.
Cela permet de:
typedef unsigned char bool;//definition d'un type booleen
typedef int number; //peut etre facilement remplacé par un float
number traiteLesDonnees(number x);//Fonction qui ne changera pas (à priori)
// TYPES OPAQUES
typedef /*something*/ time_t;//Definition officielle de time_t (time.h)
//It is almost universally expected to be an integral value representing
// the number of seconds elapsed since 00:00 hours, Jan 1, 1970 UTC.
// Definition du type FILE utilisé (mais pas déclaré) dans stdio.h
struct _IO_FILE;
typedef struct _IO_FILE FILE;
Une structure de donnée est une collection de données hétérogènes:
Représentation mémoire
Déclaration d'une variable
struct {
int valueInt;
float valueFloat;
char valueChar;
} maVariable;
Attention: le mot clef struct ne définit pas un type !
Quel type de structures de données complexe les struct
permettent de construire ?
Une structure est définie et appelée de la manière suivante:
//Déclaration de la structure
struct personne {
char* nom;
char* prenom;
int age;
};
struct personne unePersonne; // une instance de la structure;
Pour plus de facilité on a souvent recours aux définitions de types:
typedef struct el { //el est necessaire pour faire les declaration de pointeurs
struct el *suivant;
void* contenu;
} element_t;//A partir de cette définition on peu déclarer 'element_t unElement'
typedef element_t* listeChainee;
struct personne createPersonne(const char* nom, const char* prenom, int age) {
int length;
struct personne pers;
length = strlen(nom) + 1;//+1 pour '\0'
pers.nom = calloc(length, sizeof(char));
pers.prenom = calloc(strlen(prenom) + 1, sizeof(char));
if((pers.nom == NULL) || (pers.prenom == NULL)) {
printf("Erreur allocation mémoire\n");
exit(1);
}
strcpy(pers.nom, nom);
strcpy(pers.prenom, prenom);
pers.age = age;
return pers;
}
void affichePersonne(struct personne pers) {
printf("Nom: %s, Prenom: %s, Age: %d\n", pers.nom, pers.prenom, pers.age);
}
listeChainee initListeChaineeVide() {
return NULL;
}
void addListe(listeChainee* liste, void* contenu) {
//Creation d'un element et ajout dans la liste
element_t *unEl = (element_t*) malloc(sizeof(element_t));
unEl->contenu = contenu; //(*unEl).contenu se transforme en unEl->contenu
unEl->suivant = *liste;
*liste = unEl;
}
void* removeListe(listeChainee *liste) {
listeChainee tmp;
void* ret;
ret = (*liste)->contenu;
tmp = *liste;
*liste = tmp->suivant;
free(tmp); //Libération de la mémoire
return ret;
}
int main(void)
{
struct personne *ptPersonne;
listeChainee maListe = initListeChaineeVide();
//Une première personne ajoutée
unePersonne = createPersonne("Rodepeter", "Jessica", 36);
affichePersonne(unePersonne);
addListe(&maListe, &unePersonne);
//Une seconde personne ajoutée
if((ptPersonne = malloc(sizeof(struct personne))) == NULL)
{
printf("Erreur mémoire\n");
return 1;
}
*ptPersonne = createPersonne("Page", "Marc", 8);
affichePersonne(*ptPersonne);
addListe(&maListe, ptPersonne);
//Recupère le contenu de la liste
affichePersonne(*( (struct personne*) removeListe(&maListe)));
affichePersonne(*( (struct personne*) removeListe(&maListe)));
free(ptPersonne);
return 0;
}
Les énumérations permettent de créer des types:
Exemples de déclarations:
enum typeMedaille { bronze, argent, or };
typedef enum { false, } booleen;
typedef enum { lundi = 1, mardi, mercredi, jeudi, vendredi, samedi, dimanche } jours;
typedef enum { micro = 2, mineur = 4, leger = 5, modere = 6, major = 7, important = 9,
devastateur = 10} richter;
Exemples d'utilisations:
jours aujourdhui;
aujourdhui = mardi;
if(aujourdhui != mardi)
printf("Que faites-vous la ?\n");
else
printf("Merci d'etre la.\n");
enum typeMedaille maMedaille = or;
switch(maMedaille) //Séparer les different cas que peut prendre une variable
{
case bronze:
printf("Peux mieux faire !\n");
break;//A ne pas oublier
case argent:
printf("Pas mal !\n");
break;
case or:
printf("Le controle est par la...\n");
break;
default:
printf(“Erreur: type de medaille non connu\n");
}
Attention les énumération ne sont PAS fortement typées:
//Toutes ces affectations sont autorisées !!!
//Mais à éviter !
int tremblementFukushima = 9;//Déclaré comme entier au lieux de richter
richter tremblement = 3;//La valeur 3 n’est pas dans la liste
booleen estValideMaisFaux = mardi; //mardi = 2 -> n’est pas dans la liste
Comme les structures, une union a plusieurs champs mais dans le même espace mémoire:
typedef struct {
int valueInt;
float valueFloat;
char valueChar;
} u;
typedef union {
int valueInt;
float valueFloat;
char valueChar;
} s;
Utilisé pour:
#include<stdio.h>
#include<stdint.h>
typedef union {
int16_t valueInt;
struct {
unsigned char msb; //most significant byte
unsigned char lsb; //least significant byte
} bytes;
} convert;
int main(void) {
convert value;
printf("Entrer un entier: ");
scanf("%d", &value);
printf("Valeur entree: %d\n", value.valueInt);
printf("Valeur entree (hexadecimal): %x\n", value);
printf("MSB: h%02x, LSB h%02x\n", value.bytes.lsb, value.bytes.msb);
}
Un programme complexe est divisé en modules (.c + .h)
Cela permet notament de:
On peu utiliser un module en le compilant avec son programme principal:
On peu utiliser un module en créant une librairie (statique dans cet exemple):
Pour utiliser la librairie il faut la lier:
Les types opaques sont des structures de données (i.e. typedef) qui ne sont pas définies dans l’interface (i.e. fichier header).
Cela permet:
ListeChainee.c
#include"listeChainee.h"
/*Déclaration dans le .c pour une
utilisation privée */
typedef struct el {
struct el *suivant;
void *contenu;
} element_t;
listeChainee initListeChaineeVide() {
return NULL;
}
Listechainee.h
// Anciennement typedef element_t* listeChainee;
typedef struct el* listeChainee;
listeChainee initListeChaineeVide();
void addListe(listeChainee* liste, void* contenu);
void* removeListe(listeChainee *liste);
static
Le mot clef static permet de:
main.c
#include<stdio.h> void show() { // PAS DE CONFLIT ! printf("Main show\n"); } void main() { show(); interface(); }
#include<stdio.h> void show() { // PAS DE CONFLIT ! printf("Main show\n"); } void main() { show(); interface(); }
interface.c
#include<stdio.h> static void show() { //PAS DE CONFLIT ! printf("Interface show\n"); } void interface() { show(); }
#include<stdio.h> static void show() { //PAS DE CONFLIT ! printf("Interface show\n"); } void interface() { show(); }
Attention le mot clef static
est est aussi utilisé pour des variables mais il peu avoir un sens différent dans ce cas.
Lorsque l’on a plusieurs modules qui dépendent les uns des autres il deviens difficile de savoir quel module il faut recompiler après une modification.
Un makefile est un fichier texte qui contient des objectifs de compilation:
paint: main.o images.o gui.o graphics.o
gcc main.o images.o gui.o graphics.o -o paint
main.o: main.c images.h gui.h
gcc -c main.c -o main.o
images.o: images.c images.h graphics.h
gcc -c images.c -o images.o
gui.o: gui.c gui.h graphics.h
gcc -c gui.c -o gui.o
graphics.o: graphics.c graphics.h
gcc -c graphics.c -o graphics.o
Pour compiler il suffit de taper:
$ make objectif
Pour compiler le premier objectif:
$ make
On peut également ajouter des noms de variable pour effectuer des changements facilement ou générer le makefile automatiquement:
VARIABLE = value
Pour utiliser le contenu de la variable:
$(VARIABLE) ou ${VARIABLE}
On peut aussi définir des variables lors de l'exécution de la commande make:
$ make VARIABLE=value objectif
Dans ce cas on utilise ?= si l'on souhaite remplacer la valeur de la variable dans le makefile par celle donnée en paramètre par l'utilisateur:
VARIABLE ?= value
CC = gcc
OBJS = main.o images.o gui.o graphics.o
CFLAGS = -g -Wall -c
LFLAGS = -L /home/me/blas/openblas/lib -l openblas
paint: $(OBJS)
$(CC) $(OBJS) -o paint $(LFLAGS)
main.o: main.c images.h gui.h
$(CC) $(CFLAGS) paint.c -o main.o
images.o: images.c images.h graphics.h
$(CC) $(CFLAGS) images.c -o images.o
gui.o: gui.c gui.h graphics.h
$(CC) $(CFLAGS) gui.c -o gui.o
graphics.o: graphics.c graphics.h
$(CC) $(CFLAGS) graphics.c -o graphics.o
Les branchements permettent de définir des variables ou des commandes différentes suivant une condition.
Par exemple:
libs_for_gcc = -lgnu
normal_libs =
foo: $(objects)
ifeq ($(CC),gcc)
$(CC) -o foo $(objects) $(libs_for_gcc)
else
$(CC) -o foo $(objects) $(normal_libs)
endif
source: tutorialpoint
On peut aussi utiliser ifneq
.
CC = gcc
OBJS = main.o images.o gui.o graphics.o
CFLAGS = -g -Wall -c
BLASLIB ?= openblas
ifeq ($(BLASLIB), openblas)
LFLAGS = -L /home/me/blas/openblas/lib -l openblas
else ifeq ($(BLASLIB), cblas)
LFLAGS = -L /home/me/blas/cblas/lib -l cblas
else
$(error unknown library $(BLASLIB))
endif
paint: $(OBJS)
$(CC) $(OBJS) -o paint $(LFLAGS)
...
PHONY
(bidon)Certain objectifs ne sont pas associés à des fichiers
.PHONY = clean install # pas obligatoire mais evite le test d'existence de fichiers
clean:
rm ./*.o ./paint
install:
cp ./paint /usr/local/bin
Quelles sont les actions usuelles pour compiler les sources d’un programme téléchargé sous Unix ?
Pourquoi la commande «clean» précédente peut être dangereuse si mal utilisée ?
Refaire un graph de compilation et le makefile à partir de l’exemple listechainee et personnes.
Inclure les entêtes (.h) de fonctions des librairies ou des fichier C :
#include<stdlib.h>
#include"listeChainee.h"
On peu définir des macros et constantes de la manière suivante:
#include<stdio.h>
#include"precomp.h"
#define USUAL 10; //Déjà vu
#define WHY //on peut aussi declarer des define sans valeur
//Défintion de macro:
//Notez que ? et : sont utilisable dans le code
//pour remplacer les if
#define COMPUTE (INPUT / USUAL) //INPUT has to be defined
#define MIN(a,b) (((a) < (b)) ? (a) : (b))
int main()
{
char string[] = STRING; //STRING doit etre defini lors de la compilation
printf("defined STRING: %s\n", string);
printf("defined VALUE: %d\n", INPUT); //INPUT doit être défini lors de la compilation
printf("Le plus petit nombre est: %d\n", MIN(INPUT, 5));
}
Lors de la compilation:
$ gcc precomp.c -o precomp -D STRING="\"Je suis bien content\"" -D INPUT=5
Y a-t-il des cas ou une macro ne sera pas dans un .h ?
Des constantes sont prédéfinies par la plupart des compilateurs pour chaque fichier source:
Utile pour la gestion d'erreurs:
Utile pour compiler du code différent suivant la platforme:
La compilation conditionelle:
Utile pour:
#if TAILLES==0
#define TAILLE_TAMPON 512
#elif TAILLES==1
#define TAILLE_TAMPON 1024
#else
#define TAILLE_TAMPON 2048
#endif
#ifndef _win64
//Quelque chose pour les platformes 64 bit windows
#elif defined _win32
//Quelque chose pour les platformes windows
#else
//Quelque chose pour les autres
#endif
precomp.h
#ifndef _PRECOMP_H //Evite l’inclusion infinie
#define _PRECOMP_H
//Define a constant with the system name
#ifdef __unix__
#define SYS "Unix"
#elif defined _WIN32
#define SYS "Windows"
#elif defined __APPLE__
#define SYS "Mac"
#else
#define SYS "Unknown"
#endif
/********************* User interface************************/
/*************************************************************
Détermine quel est le caractère de séparation des répertoires
qui correspond au système d'exploitation sur lequel le programme
as été compilé.
IN: vide
OUT: retourne un caractère représentant le séparateur de dossiers.
retourne -1 si il n'as pas pu être identifié (système non identifié)
**************************************************************/
char getCharSep();
#endif //#def _PRECOMP_H
precomp.c
/*************************************************************
Détermine quel est le caractère de séparation des répertoires
qui correspond au système d'exploitation sur lequel le programme
as été compilé.
IN: vide
OUT: retourne un caractère représentant le séparateur de dossiers.
retourne -1 si il n'as pas pu être identifié (système non identifié)
**************************************************************/
char getCharSep()
{
#ifdef __unix__
return '/';
#elif defined _WIN32
return '\\';
#elif defined __APPLE__
return '/';
#else
return -1;
#endif
}