Cours de C - IR1
2007-2008
Petits secrets du C
&
programmation avancée
Sébastien Paumier
Illustrations provenant du site
? a+=expression ? a=a+expression
? si l'évaluation de a contient un effet de bord, il n'est effecté qu'une fois:
int main(int argc,char* argv[]) { int i=0,a[2]; a[0]=a[1]=7; a[i++]+=2; printf("i=%d a[0]=%d a[1]=%d\n",i,a[0],a[1]); i=0; a[0]=7; a[i++]=a[i++]+2; printf("i=%d a[0]=%d a[1]=%d\n",i,a[0],a[1]); return 0; } |
$>gcc -Wall -ansi test.c test.c:14: warning:
operation on `i' may be
undefined $> i=1 a[0]=9 a[1]=7 i=2 a[0]=9 a[1]=7
? Mêmes règles de conversion de types que pour a=a+expression
+= -= *= /= %=
<<= >>=
&= ^= |=
? Pas de &&= ni de ||=, parce que && et || sont paresseux
? Pas de ~= ni de !=, parce que ~ et ! sont unaires
? Opérateurs non utilisables pour des initialisations (c'est logique)
int main(int argc,char* argv[]) { int y+=45; printf("y=%d\n",y); return 0; } |
$>gcc -Wall -ansi test.c test.c: In function `main': test.c:48: syntax error before '+=' token
test.c:49: `y' undeclared (first use in this function) test.c:49: (Each undeclared identifier is reported only once
test.c:49: for each function it appears in.)
? label=identificateur suivi par :
? on peut y sauter avec goto label;
int main(int argc,char* argv[]) { int d; /* */ if (d==0) goto null_error; /* */ null_error: /* */return 0; } |
? le goto et le label doivent être dans la même fonction, mais pas forcément dans le même bloc
int main(int argc,char* argv[]) { start: int d; /* */goto start; /* */return 0; } |
? le label peut être avant le goto
? le label doit précéder une instruction
$>gcc -Wall -ansi labels.c labels.c: In function `main': labels.c:9: parse error before "int"
? "Formellement, le goto n'est jamais indispensable, et en pratique, on peut facilement s'en passer en général."
Kernighan & Richie, Le langage C
? Règle d'or à suivre absolument!
? Seul 1 cas sera toléré
L'unique exception
? Gestion d'erreurs multiples:
int main(int argc,char* argv[]) { if (argc!=3) goto end; FILE* f=fopen(argv[1],"r"); if (f==NULL) goto end; FILE* f2=fopen(argv[2],"w"); if (f2==NULL) goto close_f; int* buffer=(int*)malloc(N*sizeof(int)); if (buffer==NULL) goto close_f2; if (!test(f2)) goto free_buffer; /* do the job */ free_buffer: free(buffer); close_f2: fclose(f2); close_f: fclose(f); end: /* */return 0; } |
Les fonctions imbriquées
? Interdites en C ISO
int main(int argc,char* argv[]) { void hello_world() { printf("Hello world !\n"); } hello_world(); return 0; } |
$>gcc -Wall -ansi --pedantic-errors nested.c nested.c: In function `main': nested.c:9: ISO C forbids nested functions
? N'apportent rien, donc à proscrire
Le vrai visage de main
? int main(int argc,char* argv[], char* env[]) { }
? env=tableau contenant toutes les variables d'environnement ? dernière chaîne à NULL
$>
int main(int argc,char* argv[], char* env[]) { int i; for (i=0;env[i]!=NULL;i++) { printf("%s\n",env[i]); } return 0; } |
LC_PAPER=fr_FR
LC_ADDRESS=fr_FR
LC_MONETARY=fr_FR
TERM=xterm
SHELL=/bin/bash
LC_SOURCED=1
HISTSIZE=1000
? char* getenv(const char* name);
(stdlib.h)
? renvoie une chaîne ou NULL si la variable n'est pas trouvée
|
$> Hello paumier !
? int system(const char* cmd);
(stdlib.h)
? démarre un shell qui lance la commande cmd et attend sa fin complète
? retourne le code de retour de cmd
int main(int argc,char* argv[]) { printf("exit value=%d\n", system("ls")); return 0; } |
$>
f1 dir/
exit value=0
? int atexit(void (*f)(void));
? exécute f à la fin du main ou quand exit est invoquée
? crochets empilés ? ordre inversé
? ex: dump en urgence d'un gros calcul
void end1() {printf("end 1\n");} void end2() {printf("end 2\n");} int main(int argc,char* argv[]) { atexit(end1); atexit(end2); return 0; } |
$> end 2 end 1
Mettre des crochets d'arrêt
? ne marche pas si le programme est tué
"sauvagement"
void end1() {printf("end 1\n");} void end2() {printf("end 2\n");} int main(int argc,char* argv[]) { atexit(end1); atexit(end2); fgetc(stdin); return 0; } |
$>q end 2 end 1
$>
<CTRL+c>
? quand on a besoin de mémoire de taille n juste le temps d'une fonction, on peut éviter l'allocation dynamique en C99: type nom[n];
? n=expression entière quelconque
? condition: n doit être raisonnable, car sinon la pile peut exploser
? éviter la fonction alloca qui n'est pas portable et varie selon les compilateurs
? exemple: fonction palindrome en UTF8
int palindrome2(int* s) { int i=0,n=u_strlen(s)-1;
while (i<n) {
if (s[i++]!=s[n--]) return 0;
} return 1; }
int palindrome_utf8(char* s) { int tmp[strlen(s)+1]; decode_utf8(s,tmp); return palindrome2(tmp); }
variable temporaire, allocation dynamique inutile
? sert à analyser les arguments d'un programme
? permet d'ignorer leur ordre
? standard sous UNIX/Linux (unistd.h)
? mal porté sous Windows (cygwin) :(
? si un argument commence par - (autre que - ou --), c'est une option
? option courte=1 caractère
? option longue=chaîne de caractères
? si on a -xyz, x, y et z désignent des
options courtes
? optstring définit les options courtes autorisées
? x: indique que l'option x a un paramètre
? x:: indique que l'option x a un
paramètre optionnel
int main(int argc,char* argv[]) { /* -h -> help * -i input * -o [output] */ char* optstring="hi:o::"; /* */return 0; } |
? le paramètre d'une option doit être introduit soit par un espace, soit par rien
-i xxx ou -ixxx ? arg = xxx
-i=xxx ? arg = =xxx
? dans le cas d'un paramètre optionnel, seule la forme -ixxx fonctionne
? si optstring commence par :, getopt renverra ? en cas d'option inconnue et : en cas d'argument manquant
? la variable externe int optopt contient le caractère d'option courant
? int getopt(int argc,char* const argv[],const char* optstring);
? un caractère d'option? getopt le renvoie, sinon:
– EOF si plus d'option
– en cas d'erreur, cf. transparent prédécent
? Exemple:
int main(int argc,char* argv[]) { /* -h -> help * -i input * -o [output] */ const char* optstring=":hi:o::"; int val; while (EOF!=(val=getopt(argc,argv,optstring))) { switch (val) { case 'h': printf("help\n"); break; case 'o': printf("output %s\n",optarg); break; case 'i': printf("input %s\n",optarg); break; case ':': printf("arg missing for option %c\n",optopt); break; case '?': printf("unknown option %c\n",optopt); break; } } return 0; } |
? on peut grouper plusieurs options
? ordre sans importance
$>./getopt -hi help
arg missing for option i
$>./getopt -o -h -i tutu
output (null) help input tutu
$>./getopt -i -y -z -oh
input -y unknown option z output h
? getopt réorganise les paramètres
? quand il renvoie EOF, les paramètres autres qu'options sont rangés entre optind et argc (l'ordre est conservé)
int main(int argc,char* argv[]) { const char* optstring=":hi:o::"; int val; while (EOF!=(val=getopt(argc,argv, optstring))); while (optind!=argc) { printf("%s\n",argv[optind++]); } return 0; } |
$> aa -h bb -i cc dd
aa bb dd cc n'y est pas!
? int getopt_long(int argc,char* const argv[],const char* optstring, const struct option* longopts, int *longindex);
? accepte des noms longs introduits par --
? les arguments optionnels peuvent être introduits soit avec =, soit avec un espace: --input=xxx ou --input xxx
struct option { const char* name; int has_arg; int* flag; int val;
};
? has_arg: 0=no_argument,
1=required_argument,
2=optional_argument
? flag: si NULL, getopt_long renvoie val; sinon, renvoie 0 et *flag vaut val
? val: valeur à renvoyer
? getopt_long accepte aussi les options courtes
const char* optstring=":hi:o::"; conststruct option lopts[] = { {"help", no_argument, NULL, 'h'}, {"input", required_argument, NULL, 'i'}, {"output", optional_argument, NULL, 'o'}, {NULL, no_argument, NULL, 0} }; |
? getopt_long renvoie le caractère d'option
? si longindex est non NULL et si une option longue est trouvée, alors
*longindex vaut l'indice de l'option dans le tableau
const char* optstring=":hi:o::"; conststruct option lopts[] = { {"help", no_argument, NULL, 'h'}, {"input", required_argument, NULL, 'i'}, {"output", optional_argument, NULL, 'o'}, {NULL, no_argument, NULL, 0} }; int val,index=-1; while (EOF!=(val=getopt_long(argc,argv,optstring,lopts,&index))) { char msg[32]; if (index==-1) sprintf(msg,"short option -%c",val); else sprintf(msg,"long option --%s",lopts[index].name); switch (val) { case 'h': printf("%s\n",msg); break; case 'o': printf("%s arg=%s\n",msg,optarg); break; case 'i': printf("%s arg=%s\n",msg,optarg); break; case ':': printf("argument missing for option %c\n",optopt); break; case '?': printf("unknown option %c\n",optopt); break; } index=-1; /* penser à réinitialiser index! */ } |
? s'il n'y pas d'ambiguïté, les options longues peuvent être abrégées
$> --help --he --h -h --i=toto --i tutu --output
long option --help long option --help long option --help short option -h long option --input arg=toto long option --input arg=tutu long option --output arg=(null)
? coût beaucoup plus élevé que pour des entiers
? erreurs de calcul ? ? à éviter si possible ? 2 méthodes:
– mettre des valeurs en cache
– simuler des flottants avec des entiers
? exemple trigonométrique
#define PI 3.14159265358979323846 void test() { double x,y,cosinus[360]; int i; for (i=0;i<360;i++) { double angle=i*PI/180.0; cosinus[i]=cos(angle); } int before=time(NULL); x=0; for (i=0;i<100000000;i++) { y=cos(i*PI/180.0); x=x+y*y; } int middle=time(NULL); printf("%d seconds\nx=%f\n",middle-before,x); x=0; for (i=0;i<100000000;i++) { y=cosinus[i%360]; x=x+y*y; } int end=time(NULL); printf("%d seconds\nx=%f\n",end-middle,x); } |
$>
27 seconds x=50000000.325323
3 seconds x=49999995.586343
plus rapide, mais légère perte de précision
Un calcul bizarre
|
$> sum=32768.000000
? Pourquoi pas 999999.997 ?
? Parce qu'au-delà de 32768, 0.001 est négligeable par rapport à la mantisse
? Solution: simuler des flottants avec des entiers:
? Exemple: n de 0 à 1000000 permet de représenter des flottants à 2 décimales entre 0 et 10000 ? Avantages:
– précision
– vitesse
? Inconvénient:
– limite des valeurs
/* A type for manipulating positive floats with 3 decimals */typedefunsignedint MyFloat; MyFloat new_MyFloat(float f) { if (f<0 || f>1) return -1; return (int)(f*1000); } float to_float(MyFloat f) { return f/1000.0; } MyFloat add(MyFloat a,MyFloat b) { return a+b; } MyFloat mult(MyFloat a,MyFloat b) { return a*b/1000; /* Ne pas oublier la division!! */ } |
int main(int i;int argc,char* argv[]) { faux et lent
int a=time(NULL); float sum=0.f;
for (i=0;i<999999997;i++) {
sum=sum+0.001f; $>
}printf("sum=%f\n",sum); sum=32768.000000
int b=time(NULL); 11 seconds
printf("%d seconds\n",b-a); sum=999999.997
MyFloat sum2=0; 6 seconds
MyFloat inc=new_MyFloat(0.001); for (i=0;i<999999997;i++) { sum2=add(sum2,inc);
}
printf("sum=%d.%d\n",sum2/1000,sum2%1000);
int c=time(NULL);
printf("%d seconds\n",c-b); rapide et juste return 0;
}
int main(int argc,char* argv[]) { $>
int i; sum=32768.000000
int a=time(NULL); 11 seconds
float sum=0.f; sum=999999.997
for (i=0;i<999999997;i++) { sum=999999.997000 sum=sum+0.001f; sum=1000000.000000
}printf("sum=%f\n",sum); 6 secondsint b=time(NULL);
printf(MyFloat sum2=0;"%d seconds\n",b-a); entier sum2 stocké
MyFloat inc=new_MyFloat(0.001); avec plus de for (i=0;i<999999997;i++) {
sum2=add(sum2,inc); précision dans le
}printf("sum=%d.%d\n",sum2/1000,sum2%1000); processeur printf("sum=%f\n",sum2/1000.0); printf("sum=%f\n",to_float(sum2)); int c=time(NULL);
printf("%d seconds\n",c-b); conversion return 0;
} ? perte de précision
? allocation n'est pas forcément synonyme de malloc, qui a un coût mémoire ? ne pas utiliser malloc quand:
– plein de petits objets (<32 octets)
– plein d'allocations, mais pas de désallocation
? exemple de solution: gérer sa propre mémoire avec un tableau d'octets
#include <stdio.h> #include <stdlib.h> #define SIZE_STRING 84 #define N_STRINGS 1000000 int main(int argc,char* argv[]) { int i; for (i=0;i<N_STRINGS;i++) { malloc(SIZE_STRING); } getchar(); return 0; } |
plein de malloc qui se cumulent =87572Ko
$>&
$>top
PID USER VIRT RES SHR S %CPU %MEM TIME+ COMMAND
19929 paumier 87572 84m 260 T 0.0 9.6 0:00.20 a.out
Bonne allocation personnelle
#define SIZE_STRING 84 #define N_STRINGS 1000000 #define MAX N_STRINGS*SIZE_STRING char memory[MAX]; int pos=0; char* my_alloc(int size) { if (pos+size>=MAX) returnNULL; char* res=&(memory[pos]); pos=pos+size; return res; } int main(int argc,char* argv[]) { int i; for (i=0;i<N_STRINGS;i++) { my_alloc(SIZE_STRING); } printf("%d\n",pos); getchar(); return 0; } |
utilisation d'un tableau
=83540Ko
économie =87572-83540
=4032Ko
?4Mo
$>&
$>top
PID USER VIRT RES SHR
19952 paumier 83540 300 252
Bonne allocation personnelle
? même comparaison avec des objets de 8 octets au lieu de 84 (par exemple, une structure avec 2 champs entiers)
$>top
PID USER VIRT RES SHR S %CPU %MEM TIME+ COMMAND
21944 paumier 17216 15m 260 T 0.0 1.8 0:00.08 a.out
21999 paumier 9324 300 252 T 0.0 0.0 0:00.01 a.out
? économie=17218-9324=7894Ko?7,7Mo
? si on doit utiliser realloc, il faut éviter de le faire trop souvent ?mauvais exemple:
/* Adds the given value to the given array, * enlarging it if needed */ void add_int(struct array* a,int value) { if (a->current==a->capacity) { a->capacity=a->capacity+1; a->data=(int*)realloc(a->data,a->capacity*sizeof(int)); if (a->data==NULL) { fprintf(stderr,"Not enough memory!\n"); exit(1); } } a->data[a->current]=value; (a->current)++; } |
? bonne conduite: doubler la taille, quitte à la réajuster quand on a fini de remplir le tableau
/* Adds the given value to the given array, * enlarging it if needed */ void add_int(struct array* a,int value) { if (a->current==a->capacity) { a->capacity=a->capacity*2; a->data=(int*)realloc(a->data,a->capacity*sizeof(int)); if (a->data==NULL) { fprintf(stderr,"Not enough memory!\n"); exit(1); } } a->data[a->current]=value; (a->current)++; } |
? on ne doit jamais invoquer free plus d'une fois sur un même objet mémoire
? d'où: problème de libération si on a plusieurs pointeurs sur un même objet ? solutions:
– ne pas le faire
– comptage de référence
– table de pointeurs
– garbage collector
Exemple problématique
? comment représenter un automate ?
Solution 1: éviter le problème
? chaque état est une structure
? automate=tableau de structures
? état=indice dans le tableau
? une seule allocation/libération:
le tableau de structures
? très bonne solution
Solution 2: comptage de réf.
? chaque état est une structure
? ajouter à cette structure un compteur
? à chaque fois qu'on fait pointer une adresse sur un état, on augmente son compteur
? à chaque fois qu'on déréférence un état, on décrémente son compteur et on le libère quand on atteint 0
Solution 2: comptage de réf.
? lourd à mettre en œuvre
? risque d'erreur (oubli de mise à jour du compteur)
? à éviter
Solution 3: table de pointeurs
? chaque état est identifié par son indice
? automate=tableau de pointeurs
? pour accéder à l'état n, on passe par tableau[n]
? pour libérer l'état n, on libère tableau[n] et on le met à NULL, s'il n'était pas déjà à NULL
? à n'utiliser que si la solution 1 n'est pas possible (c'est rare)
? allocation explicite, mais pas de libération
? tâche de fond ou périodique qui vérifie toute la mémoire ? comment faire:
–
– faire du Java :)