INFO517 : Programmation C
Cours du semestre 5 de la licence STIC INFO.
Responsable pour 2008--2009: Lionel Vaux.
Pensez à consulter les indications pour compiler un petit programme sur une machine des salles de TP.
N'hésitez pas à contribuer au wiki, et en particulier à cette page: clarifications, compléments, exemples… Si vous n'avez pas compris un point particulier, vous pouvez signaler votre problème sur la page de discussion (onglet en haut de cette page) ou par les moyens habituels. Il sera ensuite très positif de revenir sur cette page et de consigner ce qui vous posait problème et ce qui vous a permis de mieux comprendre.
Fonctionnement
Cet enseignement comprendra 10 séances de cours/TD (1h30) et 3 séances de TP (4h).
La distinction entre cours et TD restera floue. Je vous demanderai généralement d'écrire quelques petits programmes d'une semaine sur l'autre. Autant que possible, envoyez-moi vos fichiers sources à l'adresse lionel.vaux@univ-savoie.fr, afin que je puisse évaluer le niveau de chacun et ajuster le contenu des séances suivantes.
Et dites-moi si ça ne va pas, ou je risque d'avancer trop vite.
Objectifs du cours
- Principes généraux et particularités du langage (programmation impérative, typage fort, adressage mémoire)
- Syntaxe
- Bibliothèque standard (pour les entrées-sorties et l'interaction avec le système d'exploitation)
- Gestion de la mémoire
- Bonnes pratiques
- Outils et concepts:
- automatisation de la compilation (make),
- analyse de l'exécution et déboguage (gdb, valgrind),
- documentation (doxygen),
- boîte à outils graphique (gtk+)
Séances
Cours/TD 1 : lundi 22 septembre 2008
Présentation tout-en-un.
Le but de ce cours est de fournir le minimum vital aux étudiants pour:
- écrire un programme simple et court utilisant les types de base
- le compiler et l'exécuter
- trouver de la documentation
Après cette première séance, les étudiants devraient être capable de s'amuser un peu avec le langage.
Les exemples vus en cours
bateau.c <source lang="c">
- include <stdio.h>
int main () { /* Écrit une chaîne */ puts("Bateau !");
/* Renvoie la valeur de sortie en cas de succès */ return 0; } </source>
euros-francs-v1.c <source lang="c">
- include<stdio.h>
/* Écrit une table de conversion euros/francs
* pour euros = 0, 5, 10, ..., 100 : * version initiale */
main() {
int euros, euros_max, pas ; float francs, un_euro ;
un_euro = 6.55957 ; /* taux de conversion */
pas = 5 ; /* pas d'itération */ euros = 0 ; /* valeur initiale */ euros_max = 100 ; /* valeur maximale */ while (euros <= euros_max) { francs = un_euro * euros ; printf("%d\t%f\n", euros, francs) ; euros = euros + pas ; }
} </source>
euros-francs-v2.c <source lang="c">
- include<stdio.h>
/* Écrit une table de conversion euros/francs
* pour euros = 0, 5, 10, ..., 100 : * correction de l'alignement */
main() {
int euros, euros_max, pas ; float francs, un_euro ;
un_euro = 6.55957 ; /* taux de conversion */
pas = 5 ; /* pas d'itération */ euros = 0 ; /* valeur initiale */ euros_max = 100 ; /* valeur maximale */ while (euros <= euros_max) { francs = un_euro * euros ; printf("%3d\t%6.2f\n", euros, francs) ; euros = euros + pas ; }
} </source>
euros-francs-v3.c <source lang="c">
- include<stdio.h>
/* Écrit une table de conversion euros/francs
* pour euros = 0, 5, 10, ..., 100 : * avec un `for' */
main() {
int euros ; for (euros = 0 ; euros <= 100 ; euros = euros + 5) printf("%3d\t%6.2f\n", euros, 6.55957*euros) ;
} </source>
euros-francs-v4.c <source lang="c">
- include<stdio.h>
- define UN_EURO 6.55957 /* un euro en francs */
/* Écrit une table de conversion euros/francs
* pour euros = 0, 5, 10, ..., 100 : * définition pour le préprocesseur */
main() {
int euros ; for (euros = 0 ; euros <= 100 ; euros = euros + 5) printf("%3d\t%6.2f\n", euros, UN_EURO*euros) ;
} </source>
arrondi.c <source lang="c">
- include<stdio.h>
- define INCR 0.00001 /* incrément pour le test de précision */
- define NUM 100000 /* nombre de pas */
/* Calcule INCR*NUM en ajoutant NUM fois INCR à 0 */ main() {
float accu ; int i ; accu = 0 ; for (i=0 ; i < NUM ; i=i+1) accu = accu + INCR ; printf("%f=%f?\n",NUM*INCR,accu) ;
} </source>
arrondi-double.c (cette variante a été signalée pendant les rappels de la deuxième séance) <source lang="c">
- include<stdio.h>
- define INCR 0.00001 /* incrément pour le test de précision */
- define NUM 100000 /* nombre de pas */
/* Calcule INCR*NUM en ajoutant NUM fois INCR à 0.
* Cette version utilise un accumulateur de type `double' * pour limiter les erreurs d'arrondi */
main() {
double accu ; // On calcule en double précision int i ; accu = 0 ; for (i=0 ; i < NUM ; i=i+1) accu = accu + INCR ; printf("%f=%f?\n",NUM*INCR,accu) ;
} </source>
copie-v1.c <source lang="c">
- include<stdio.h>
/* Copie l'entrée standard sur la sortie standard */ main() {
int c;
c = getchar(); while (c != EOF) { putchar(c); c = getchar(); }
} </source>
copie-v2.c <source lang="c">
- include<stdio.h>
/* Copie l'entrée standard sur la sortie standard :
* assignation comme valeur */
main() {
int c;
while ((c = getchar()) != EOF) { putchar(c); }
} </source>
Exercices pour le 29 septembre
- Au choix:
- Sur la machine et le système de votre choix, écrire et compiler un programme C (par exemple bateau.c), puis envoyer le fichier source et le binaire obtenu à l'adresse lionel.vaux@univ-savoie.fr.
- Ne pas y parvenir et alors me contacter au plus tôt pour y remédier. Ensuite revenir au choix 1, évidemment.
- Modifier l'un des fichiers euros-francs-v?.c pour afficher une ligne d'en-tête alignée sur les résultats (et quelques fioritures). C'est-à-dire que la sortie doit ressembler à:
Euros: Francs: 0 -> 0.00 5 -> 32.80 10 -> 65.60 15 -> 98.39 20 -> 131.19 ...
- Écrire un programme francs-euros.c qui affiche une table de conversion dans le sens contraire (les comptes ronds sont en francs).
- Écrire un programme qui affiche la valeur entière, de type int, de EOF (vérifier qu'elle n'est pas dans l'intervalle entier [0..255]).
- Écrire un programme qui affiche la valeur entière du « caractère » € (il est possible que vous ne compreniez pas très bien ce qui vous arrive: on en parlera).
- Écrire un programme qui compte le nombre de caractères (au sens de getchar()) dans un fichier.
Solutions possibles pour les exercices
Pour les variations sur euros-francs-v?.c, voilà un programme qui rassemble un peu tout:
conversion.c <source lang="c">
- include<stdio.h>
- define UN_EURO 6.55957 /* un euro en francs */
/* Écrit une table de conversion euros/francs
* pour euros = 0, 5, 10, ..., 100 : * définition pour le préprocesseur */
main() {
int euros ; int francs ; printf("Euros:\t\tFrancs:\n") ; for (euros = 0 ; euros <= 100 ; euros = euros + 5) printf(" %3d\t\t %6.2f\n", euros, UN_EURO*euros) ;
printf("\nFrancs:\t\tEuros:\n") ; for (francs = 0 ; francs <= 100 ; francs = francs + 5) printf(" %3d\t\t %6.2f\n", francs, francs/UN_EURO) ;
} </source>
Un programme qui affiche la valeur entière (type int) de EOF:
<source lang="c">
- include<stdio.h>
/* Écrit la valeur entière de EOF */ main() { printf("%d\n",EOF) ; } </source> On sauve ça dans EOF.c, puis on compile avec
$ gcc -Wall -o EOF EOF.c
Les erreurs produites sont standard (mauvais prototype pour main). L'exécution sur ma machine donne:
$ ./EOF -1
La valeur entière de €, sur le même modèle:
<source lang="c">
- include <stdio.h>
main () { int euro = '€' ;
printf("%d\n",euro) ; } </source> On sauve ça dans euro.c, puis on compile avec:
$ gcc -Wall -o euro euro.c
ce qui produit les avertissements:
euro.c:3: attention : return type defaults to «int» euro.c:4:13: attention : constante caractère multi-caractères euro.c: Dans la fonction «main» : euro.c:7: attention : control reaches end of non-void function
La deuxième ligne est le premier signe que quelque chose de bizarre est à l'œuvre. À l'exécution, on obtient:
$ ./euro 14844588
Savez-vous expliquer ce qui se passe ?
Pour compter le nombre de caractères: ce qu'on a vu en cours fait mieux.
Cours/TD 2 : lundi 29 septembre 2008
Rappels et précisions; fonctions; tableaux.
Les exemples vus en cours
puissance-v1.c <source lang="c">
- include<stdio.h>
- define MAXEXP 16
/* Fonction puissance(a: int, b: int) : int
* renvoie a à la puissance b, si b positif, 1 sinon */
int puissance (int a, int b) { int i, ret ; ret = 1 ; for (i=0; i<b; ++i) /* ++i incrémente i */ ret = a * ret ; return ret ; }
/* Procédure affiche(a: int, b: int)
* formatte le résultat de puissance(a,b) */
void affiche(int a, int b) { printf("%2d^%2d = %10d\n",a,b,puissance(a,b)) ; }
/* Fonction principale: affiche les puissances de 2, de 1 à 2^MAXEXP */ void main () { int i ; for (i=0; i<=MAXEXP; ++i) affiche(2,i) ; } </source>
puissance-v2.c
<source lang="c">
- include<stdio.h>
- define MAXEXP 16
/* Prototype de la fonction puissance(a: int, b: int) : int
*/
int puissance (int a, int b) ;
/* Procédure affiche(a: int, b: int)
* formatte le résultat de puissance(a,b) */
void affiche(int a, int b) { printf("%2d^%2d = %10d\n",a,b,puissance(a,b)) ; }
/* Fonction principale: affiche les puissances de 2, de 1 à 2^MAXEXP */ void main () { int i ; for (i=0; i<=MAXEXP; ++i) affiche(2,i) ; }
/* Code de la fonction puissance :
* puissance(a,b) renvoie a à la puissance b, si b positif, 1 sinon */
int puissance (int a, int b) { int ret ; ret = 1 ; /* On utilise l'argument b comme une variable locale */ for (/* pas d'initialisation nécessaire */; b>0; --b) ret = a * ret ; return ret ; } </source>
fibonacci.c
<source lang="c">
- include<stdio.h>
- define MAX 30
/* calcule u[n], où u[0]=u[1]=1 et u[n+2]=u[n+1]+u[n] */ int fibo (int n, int rec) { if (n<=1) return 1 ; else return (fibo(n-1,rec+1)+fibo(n-2,rec+1)) ; }
/* affiche fibo(n) pour n de 0 à MAX */ int main () { int i ; for (i=0;i<=MAX;++i) printf("fibo(%2d)=%8d\n",i,fibo(i,0)) ; return 0 ; } </source>
tracerec.c
<source lang="c">
- include<stdio.h>
- define N 15
/* Procédure blanks(n: int)
* insère n espaces. */
void blanks (int n) {
for(;n>0;--n) putchar(' ') ;
}
/* Calcule fibo(n) (voir fibo.c) et affiche le résultat.
* Cet affichage dans la fonction permet de tracer le * flot de récursion (l'ordre dans lequel les appels se font). * * On représente la profondeur de récursion par l'indentation, * au moyen du second argument : tous les affichages sont décalés * de rec espaces. L'entier rec est incrémenté dans l'appel récursif, * et on démarre avec rec=0 dans main() * */
int fibo_rec (int n, int rec) {
blanks(rec) ; printf("fibo(%2d)=?\n",n) ; if (n<=1) { blanks(rec) ; printf("fibo(%2d)=1\n",n) ; return 1 ; } else {
/* On peut déclarer des variables locales à un bloc */
int ret=fibo_rec(n-1,rec+1)+fibo_rec(n-2,rec+1) ; blanks(rec) ; printf("fibo(%2d)=%d\n",n,ret) ;
return ret ;
}
}
/* Calcule fibo(N) avec affichage des étapes de récursion */
int main () {
fibo_rec(N,0) ; return 0 ;
} </source>
segfault.c
<source lang="c">
/* CE CODE EST VOLONTAIREMENT FAUTIF ! */
/* Notre première erreur de segmentation:
* les appels récursifs s'empilent sans limite et on sort de la mémoire * disponible. */
- include<stdio.h>
- define MAX 30
/* calcule u[n], où u[0]=u[1]=1 et u[n+2]=u[n+1]+u[n] */ int fibo (int n, int rec) { return (fibo(n-1,rec+1)+fibo(n-2,rec+1)) ; }
/* affiche fibo(n) pour n de 0 à MAX */ int main () { int i ; for (i=0;i<=MAX;++i) printf("fibo(%2d)=%8d\n",i,fibo(i,0)) ; return 0 ; } </source>
textstat.c
<source lang="c">
- include<stdio.h>
/* En théorie, les valeurs entières des caractères
* sont dans un intervalle non précisé par la norme. * * Dans cet exemple, on considère que les caractères sont des entiers sur 8 * bits non signés (cf. CHAR_MIN, CHAR_MAX). * * Ceci était passé sous silence lors du cours. * */
- define CHAR_MIN 0
- define CHAR_MAX 255
/* CHAR_NUM = CHAR_MAX - CHAR_MIN + 1 */
- define CHAR_NUM 256
/* Une macro évidente */ void aff (char c, int n) { printf("%c:%2d ",c,n) ; }
/* Compte le nombre d'occurrences de chaque caractère
* et affiche certains résultats */
int main() { int c, i ; int nb [CHAR_NUM] ;
/* Mise à zéro */ for (i=0;i<CHAR_NUM;++i) nb[i]=0 ;
/* Calcul */ while((c=getchar()) != EOF) ++nb[CHAR_MIN+c] ;
/* Affichage */
/* Les lettres minuscules sont consécutives de 'a' à 'z'. */
printf("Minuscules:\n") ; for (c='a'; c<='z' ; ++c) aff(c,nb[CHAR_MIN+c]) ;
/* De même pour les majuscules de 'A' à 'Z'. */
printf("\nMajuscules:\n") ; for (c='A'; c<='Z' ; ++c) aff(c,nb[CHAR_MIN+c]) ;
/* Et les chiffres de '0' à '9'. */
printf("\nChiffres:\n") ; for (c='0'; c<='9' ; ++c) aff(c,nb[CHAR_MIN+c]) ;
printf("\nSauts de lignes: %d\n",nb[CHAR_MIN+'\n']) ;
return 0 ; }
</source>
Exercices pour le 6 octobre
Sur les fonctions
- Réécrire le programme conversion.c en utilisant une fonction pour l'affichage. Rendre ce programme plus modulaire en écrivant une procédure avec le prototype void francs_euros (int min, int max, int pas) qui afffiche la conversion de n francs en euros pour n variant de min à max par pas de pas et une procédure void euros_francs (int min, int max, int pas) qui affiche la même chose mais dans l'autre sens de conversion.
- Écrire une fonction int fact (int n) qui calcule la factorielle de n de deux manières différentes: une par récursion, une en utilisant une boucle. Essayer ces fonctions sur des valeurs pas trop petites et détecter quand ça dégénère.
Sur les tableaux et les chaînes
- Écrire une procédure void aff (int t[], int n) qui affiche les n premiers éléments du tableau t.
- On donne le programme:
<source lang="c">
- include <stdio.h>
void incr (int n) { ++n ; }
void incr_t0 (int t[]) { ++t[0] ; }
int main () { int n = 0 ; int t[1] = { 0 } ; incr(n) ; incr_t0(t) ; printf("n: %d\n",n) ; printf("t[0]: %d\n",t[0]) ; return 0 ; } </source> Compiler et exécuter ce programme. Que remarque-t-on ? Qu'en déduire sur les tableaux comme arguments de fonctions.
- Que fait le programme fin_chaine.c suivant ? Expliquer.
<source lang="c">
- include <stdio.h>
int main () { printf("Bateau.\n\0Et puis aussi...") ; return 0 ; } </source>
- Écrire un programme qui affiche les lignes données en entrée seulement si elles comportent plus de 20 caractères. Penser à structurer intelligemment.
- Écrire une fonction void pal(char s[]) qui renvoie 1 si la chaîne s est un palindrome et 0 sinon.
- Écrire une procédure void renverse(char s[]) qui renverse la chaîne s.
Solutions possibles
Un exemple complet de programme utilisant les fonctions (on n'hésite pas à en rajouter):
#include <stdio.h> #define UN_EURO 6.55957 /* un euro en francs */ void aff_col (int a, float b) { printf(" %3d\t\t %6.2f\n", a, b ) ; } void aff_en_tete (const char a[], const char b[]) { printf("%s\t\t%s\n", a, b ) ; } void table_conv (float taux, int min, int max, int pas) { int i ; if (pas > 0) /* si le pas est positif on va du min au max */ for (i=min; i<=max; i=i+pas) aff_col(i,taux*i) ; else if (pas < 0) /* si le pas est négatif on va du max au min */ for (i=max; i>=min; i=i+pas) aff_col(i,taux*i) ; } void francs_euros (int min, int max, int pas) { aff_en_tete("Francs","Euros") ; table_conv (1/UN_EURO,min,max,pas) ; } void euros_francs (int min, int max, int pas) { aff_en_tete("Euros","Francs") ; table_conv (UN_EURO,min,max,pas) ; } int main() { francs_euros(10,100,10) ; euros_francs(1,10,-1) ; return 0 ; }
Sur les factorielles:
#include <stdio.h> #define MAX 30 /* calcule n! par récurrence sur n */ unsigned long int fact_rec (unsigned int n) { if (n<=1) return 1 ; else return n*fact_rec(n-1) ; } /* calcule n! par comme un produit itéré */ unsigned long int fact_it (unsigned int n) { unsigned int i ; unsigned long int acc = 1; for (i=2;i<=n;++i) acc=i*acc ; return acc ; } /* fonctions d'affichage */ void aff (unsigned int n, unsigned long int res, const char nom[]) { printf("On calcule %u!=%lu par %s\n", n, res, nom) ; } void aff_fact_rec (unsigned int n) { aff(n,fact_rec(n),"fact_rec") ; } void aff_fact_it (unsigned int n) { aff(n,fact_it(n),"fact_it") ; } /* affiche fact_rec(n) et fact_it(n) pour n de 0 à MAX */ /* noter que malgré toutes nos précautions, ça dégénère vite, * car n! croit vite */ int main () { unsigned int i ; for (i=1; i<MAX; ++i) { aff_fact_rec(i) ; aff_fact_it(i) ; } return 0 ; }
Afficher un tableau d'entiers:
/* Affiche les n premiers éléments du tableau t sous la forme * {t[0],t[1],...,t[n-1]} * */ void aff (int t[], int n) { int i ; putchar('{') ; for (i=0;i<n;++i) { printf("%d",t[i]) ; if (i!=n-1) putchar(',') ; } putchar('}') ; putchar('\n') ; }
Afficher uniquement les lignes avec au moins 20 caractères (ici, une version plus générique):
void ligne_au_moins (int n) { char s[n] ; int c = getchar() ; while (c != EOF) { // Dans cette boucle, on traite une ligne, qui commence par c. int i = 0 ; // On commence par essayer de stocker les n premiers caractères : while (c != '\n' && c != EOF && i<n) { s[i] = c ; ++ i ; c = getchar() ; } // Si on a lu n caractères sans rencontrer de retour à la ligne // ni de fin de fichier, on recopie et on vide la ligne. if (i==n) { // On recopie s for (i=0; i<n; ++i) putchar(s[i]) ; // On recopie la fin de la ligne while (c != '\n' && c != EOF) { putchar(c) ; c = getchar() ; } // Retour à la ligne, si besoin if (c == '\n') { putchar(c) ; c = getchar () ; } } else if (c == '\n') { // Si la ligne se termine avant le n-ième caractère, // on passe à la suivante. c = getchar() ; } } }
Pour le test de palindrome et le renversement, on a d'abord besoin de connaître la longueur de la chaîne:
unsigned int longueur (char s[]) { unsigned int l ; for (l=0; s[l]!='\0'; ++l) ; return l ; } int pal (char s[]) { unsigned int l = longueur(s) ; int i ; int ret = 1 ; // On parcourt la moitié du tableau (au plus) et on s'arrête si jamais // on trouve des valeurs différentes dans des cases symétriques. for (i=0; i<l/2 && ret ; ++i) ret = (s[i]==s[l-i-1]) ; return ret ; } void renverse (char s[]) { unsigned int l = longueur(s) ; int i ; char c ; // On parcourt la moitié du tableau (au plus). for (i=0; i<l/2 ; ++i) { c = s[i] ; s[i] = s[l-i-1] ; s[l-i-1] = c ; } }
Cours/TD 3 : lundi 6 octobre 2008
Exercices: feuille 1.
Devoir à la maison pour le 13 octobre
Le sujet: INFO517-DM1.pdf et les fichiers sources dm1.c et mat.c.
Cours/TD 4 : lundi 13 octobre 2008
Représentation de la mémoire: pointeurs.
Les exemples vus en cours
Appel par adresse: <source lang="c">
- include <stdio.h>
void inc (int *i) { ++(*i) ; }
int main () { int n = 0 ; printf("%d\n",n) ; inc(&n) ; printf("%d\n",n) ; return n ; } </source>
Rappel (tableaux): <source lang="c">
- include <stdio.h>
void inc (int t[]) { t[0]++ ; }
int main () { int t[] = { 0 } ; printf("%d\n",t[0]) ; inc(t) ; printf("%d\n",t[0]) ; return t[0] ; } </source>
Les tableaux sont des pointeurs particuliers: <source lang="c">
- include <stdio.h>
void aff (char *s) { while (*s != '\0') { putchar(*s) ; s++ ; } }
int main () { aff("bateau\n") ; return 0 ; } </source>
Affichage des éléments d'un tableau d'entiers (c'est pareil): <source lang="c">
- include <stdio.h>
/* affiche les n premiers éléments de t */ void aff (int *t, int n) { putchar('{') ; for (;n>0;n--) { printf("%d",*t) ; if (n>1) putchar(',') ; t++ ; } putchar('}') ; putchar('\n') ; }
int main () { int t[] = {1,7,3,4,0,-1} ; aff(t,3) ; return 0 ; } </source>
Attention: rien ne contrôle le fait qu'on est bien dans le tableau. C'est-à-dire que ce qui suit est un programme C valide, qui compile et s'exécute: <source lang="c">
- include <stdio.h>
/* affiche les n premiers éléments de t */ void aff (int *t, int n) { putchar('{') ; for (;n>0;n--) { printf("%d",*t) ; if (n>1) putchar(',') ; t++ ; } putchar('}') ; putchar('\n') ; }
int main () { int t[] = {1,7,3,4,0,-1} ; aff(t,7) ; // PROBLÈME ! return 0 ; } </source> On obtient par exemple:
{1,7,3,4,0,-1,-1077769696}
Toutefois, il ne faut pas pousser le bouchon trop loin: on finit par écrire dans des zones qui ne sont pas allouées en mémoire. Ce programme, bien que valide en C, plante: <source lang="c">
- include <stdio.h>
/* affiche les n premiers éléments de t */ void aff (int *t, int n) { putchar('{') ; for (;n>0;n--) { printf("%d",*t) ; if (n>1) putchar(',') ; t++ ; } putchar('}') ; putchar('\n') ; }
int main () { int t[] = {1,7,3,4,0,-1} ; aff(t,32000) ; // PROBLÈME ! return 0 ; } </source> C'est notre deuxième erreur de segmentation: on y reviendra, en précisant où sont les choses en mémoire.
On peut afficher les adresses d'un peu tout. Ici, par exemple, celles des arguments des fonctions qui se retrouvent sur la pile. <source lang="c">
- include <stdio.h>
void f (char a, char b) { printf("char:\n") ; printf("&a: %u\n&b: %u\n", &a,&b) ; printf("a: %u\nb: %u\n", a,b) ; }
void g (int a, int b) { printf("int:\n") ; printf("&a: %u\n&b: %u\n", &a,&b) ; printf("a: %u\nb: %u\n", a,b) ; }
void h (long double a, long double b) { printf("long double:\n") ; printf("&a: %u\n&b: %u\n", &a,&b) ; printf("a: %Lf\nb: %Lf\n", a,b) ; }
int main () { f(0,1) ; g(0,1) ; h(0,1) ; return 0 ; } </source> À l'exécution, on remarque que l'écart entre ces adresses change (et aussi, qu'elles ne sont pas forcément dans l'ordre attendu):
char: &a: 3216755060 &b: 3216755056 a: a b: b int: &a: 3216755072 &b: 3216755076 a: 97 b: 98 long double: &a: 3216755072 &b: 3216755084 a: 97.000000 b: 98.000000
En effet, les types considérés ont des tailles différentes. La primitive sizeof donne (en octets) l'espace mémoire nécessaire pour stocker une donnée du type considéré: <source lang="c">
- include <stdio.h>
void aff_taille (const char nom[], size_t taille) { printf("%16s : %2u\n",nom,taille) ; }
/* Affiche les tailles de types à taille fixe les plus courants */
int main () {
printf("Liste de tailles :\n") ; aff_taille("void",sizeof(void)) ; aff_taille("char",sizeof(char)) ; aff_taille("short int",sizeof(short int)) ; aff_taille("int",sizeof(int)) ; aff_taille("long int",sizeof(long int)) ; aff_taille("float",sizeof(float)) ; aff_taille("double",sizeof(double)) ; aff_taille("long double",sizeof(long double)) ; aff_taille("void*",sizeof(void*)) ; aff_taille("int*",sizeof(int*)) ; aff_taille("long double*",sizeof(long double*)) ;
return 0 ; } </source> Sur ma machine, on obtient la sortie:
Liste de tailles : void : 1 char : 1 short int : 2 int : 4 long int : 4 float : 4 double : 8 long double : 12 void* : 4 int* : 4 long double* : 4
Noter que la taille des pointeurs est toujours la même (et c'est celle du type int).
Le type du pointeur renseigne le compilateur sur le décalage d'adresse à produire lors d'opérations arithmétiques: <source lang="c">
- include <stdio.h>
int main () { char *s ; int *p ; long double *q ;
int i ;
for (i=0;i<4;++i) { ++s ; printf("s+%d : %u\n",i,s) ; }
for (i=0;i<4;++i) { ++p ; printf("p+%d : %u\n",i,p) ; }
for (i=0;i<4;++i) { ++q ; printf("q+%d : %u\n",i,q) ; }
return 0 ;
} </source> On observe que les écarts reflètent bien la taille du type annoncé (1, 4 et 12 ici):
s+0 : 3086073265 s+1 : 3086073266 s+2 : 3086073267 s+3 : 3086073268 p+0 : 134513757 p+1 : 134513761 p+2 : 134513765 p+3 : 134513769 q+0 : 3216681396 q+1 : 3216681408 q+2 : 3216681420 q+3 : 3216681432
Revenons sur l'allocation des tableaux avec le code suivant: <source lang="c">
- include <stdio.h>
- define TAILLE 1024
int t1 [TAILLE] ; int t2 [1] ; int t3 [TAILLE] ; int t4 [1] ;
void aff_point (int *p) { printf("%u = %x -> %i\n",p,p,*p) ; }
int * tab_nouv (int n) { int t[TAILLE] ; int i ; for (i=0; i<TAILLE; ++i) t[i]=n ; return t ; }
/* Affiche les adresses des tableaux statiques et de deux "tableaux" créés par
* tab_nouv() */
int main () { int *t5 = tab_nouv(4) ; int *t6 = tab_nouv(2) ; printf("Tableaux statiques:\n") ; aff_point(t1) ; aff_point(t2) ; aff_point(t3) ; aff_point(t4) ; printf("Tableaux dynamiques (?):\n") ; aff_point(t5) ; aff_point(t6) ; return 0 ; } </source> L'intention de l'auteur est visiblement de retourner un tableau « frais » à chaque appel de tab_nouv. C'est un peu raté, car la sortie de ce programme ressemble à:
Tableaux statiques: 134522656 = 804a720 -> 0 134518496 = 80496e0 -> 0 134518528 = 8049700 -> 0 134522624 = 804a700 -> 0 Tableaux dynamiques (?): 3219749988 = bfe97c64 -> 2 3219749988 = bfe97c64 -> 2
Les adresses renvoyées par tab_nouv sont les mêmes et le contenu est écrasé. C'est bien normal: ces tableaux sont alloués sur la pile, puis immédiatement oubliés à la sortie de la fonction.
On souhaite donc faire de l'allocation dynamique: c'est le rôle de la fonction void *malloc(size_t taille) qui renvoie l'adresse de début d'une zone de mémoire allouée, d'une taille au moins égale à taille. On pourra donc réécrire: <source lang="c"> int * tab_nouv (int n) { int *t = malloc(TAILLE*sizeof(int)) ; // la taille est donnée en octets !
int i ; for (i=0; i<TAILLE; ++i) t[i]=n ; return t ;
} </source> On peut vérifier que la nouvelle version fonctionne bien sur la sortie:
Tableaux statiques: 134522720 = 804a760 -> 0 134518560 = 8049720 -> 0 134518592 = 8049740 -> 0 134522688 = 804a740 -> 0 Tableaux dynamiques: 134529032 = 804c008 -> 4 134533136 = 804d010 -> 2
Notez que les adresses rendues par malloc sont proches de celles des variables statiques.
Application à la lecture de lignes de longueur non bornée:
<source lang="c">
#include <stdlib.h>
- include <stdio.h>
// On alloue par blocs de talle MORCEAU.
- define MORCEAU 1024
/* Lit une ligne en entrée _quelle que soit sa taille_
* et la stocke dans *p (p est un pointeur sur une chaîne). * Renvoie le nombre de caractère lus (-1 en cas d'erreur). */
int lit_ligne (char **p) { char c ; char *s ; int pos, blocs, i ;
// La position courante dans la chaîne est i+MORCEAU*blocs. pos = blocs = i = 0 ;
// On alloue le premier morceau. s = malloc ((++blocs)*MORCEAU*sizeof(char)) ; if (s == NULL) return -1 ;
while ((c=getchar())!=EOF && c!='\n') { s[pos] = c ; if (i==MORCEAU-1) { // on déborderait en faisant ++i if ((s=realloc(s,(++blocs)*MORCEAU*sizeof(char))) == NULL) { return -1 ; // plus de mémoire : on quitte } i=0 ; } else { ++i; } ++pos ; }
// si c == '\n' il faut l'ajouter
if (c=='\n') { s[pos] = c ; // on pourrait déborder en ajoutant le '\0': on gère ça if (i==MORCEAU-1) { if ((s=realloc(s,(pos+1)*sizeof(char))) == NULL) return -1 ; // plus de mémoire : on quitte } ++pos ; }
s[pos]='\0' ; *p=s ; return pos ;
}
/* Lit des lignes sur l'entrée et les affiche à l'envers. */ int main () { char *s ; int ret ; while ((ret=lit_ligne(&s))>0) { if (s[ret-1]=='\n') ret--; for (;ret>0;--ret) putchar(s[ret-1]) ; putchar('\n') ; free(s) ; } return ret ; } </source> La fonction void *realloc(void *p, size_t taille) demande de changer la taille de la zone allouée par malloc pointée par p, en en conservant le contenu.
La fonction void free(void *p) libère la zone de mémoire allouée par malloc ou realloc et la rend au système: on a fini de s'en servir. C'est essentiel, car on alloue autant de zones que de lignes lues: sans l'appel à free en fin de boucle, on conserverait en mémoire toutes les lignes alors que seule la dernière nous intéresse.
TP 1 : mercredi 15 octobre 2008
Les sujets de TP se trouvent sur cette page. Pour le premier TP, vous devez traiter le sujet TP0 — Préliminaires.
Note à l'usage de l'enseignant
Il faudra demander à la DSI l'installation des paquets: vim-full, gcc-doc, ddd.
Cours/TD 5 : lundi 20 octobre 2008
Modèle mémoire: pile, tas, segment de code, allocation dynamique.
Les exemples vus en cours
Le code suivant affiche la chaîne abcdef en minuscules puis en majuscules: <source lang="c">
- include <stdio.h>
void maj (char s[]) { int i ; for (i=0 ; s[i]!='\0' ; i++) { char c = s[i] ; if (c>='a' && c<='z') s[i] = c+'A'-'a' ; } }
int main () { char s[] = "abcdef" ; /* tableau = pointeur ? */
puts(s) ; maj(s) ; puts(s) ; return 0 ; } </source>
Si on change la déclaration de s en <source lang="c"> char s* = "abcdef" ; </source> on obtient une erreur de segmentation. Pourquoi ?
Les tableaux ne sont pas des variables comme les autres: un tableau t doit être considéré comme un alias pour &t[0], qui n'est pas une valeur gauche (quelque chose qui va à gauche du signe =): c'est de la même nature que n+1 par exemple. Lors de la déclaration d'un tableau, son contenu (dont la taille est connue à la compilation) est alloué sur la pile. Par contraste, un pointeur est une variable entière qui contient une adresse. Lors de la déclaration <source lang="c"> char s* = "abcdef" ; </source> le tableau constant "abcdef" est alloué dans le segment de code en lecture seule et s pointe sur le premier caractère: c'est très différent.
On va mettre en évidence ce phénomène dans la suite en précisant l'emplacement mémoire des divers éléments d'un programme.
Voilà d'abord une petite bibliothèque pour afficher des adresses.
mem.h <source lang="c">
- ifndef MEM_H
- define MEM_H
- include <stdio.h>
void mem_indent (int n) ;
/********** Affichage de pointeurs **********/
void mem_addr (void *p) ; void mem_addr_ptr (void **p) ; void mem_addr_ch (char **p) ; void mem_ch (char *p) ; void mem_addr_int (int *p) ;
/********** Affichage de la pile **********/
int * mem_debut_pile ; void mem_aff_pile (int *haut) ; void mem_aff_pile_chars (int *haut) ;
- endif
</source> mem.c <source lang="c">
- include <stdio.h>
- include "mem.h"
/********** Fonctions utiles **********/
/* Affiche un caractère s'il est directement imprimable,
* un code standard s'il est connu et le code octal sinon. */
void mem_print_char (char c) { if ((c>='a' && c<='z') || (c>='A' && c<='Z') || (c>='0' && c<='9')) printf("%4c",c) ; else switch (c) { case '.': case ',': case ';': case '!': case '?': case ':': printf("%4c",c) ; break ; case ' ': printf(" ' '") ; break ; case '\n': printf(" \\n") ; break ; case '\t': printf(" \\t") ; break ; default: printf("\\%03o",(unsigned char)c) ; } }
/* Indente avec `n' tabulations */ void mem_indent (int n) { int i ; for (i=1;i<=n;i++) putchar('\t') ; }
/********** Affichage de pointeurs **********/
/* Affiche la valeur d'un pointeur (c'est-à-dire l'adresse de la case vers
* laquelle il pointe. */
void mem_addr (void *p) { printf("%10u == %8x\n", (unsigned int) p, (unsigned int) p) ; }
/* Affiche l'adresse et la valeur d'un pointeur. */ void mem_addr_ptr (void **p) { printf("%10u == %8x -> %10u == %8x\n", (unsigned int) p, (unsigned int) p, (unsigned int) *p, (unsigned int) *p); }
/* Affiche l'adresse et la valeur d'un pointeur sur `char',
* puis l'affiche comme une chaîne. */
void mem_addr_ch (char **p) { printf("%10u == %8x -> %10u == %8x -> %s\n", (unsigned int) p, (unsigned int) p, (unsigned int) *p, (unsigned int) *p, *p) ; }
/* Affiche la valeur d'un pointeur sur `char',
* puis l'affiche comme une chaîne. */
void mem_ch (char *p) { printf("%10u == %8x -> %s\n", (unsigned int) p, (unsigned int) p, p) ; }
/* Affiche l'adresse et la valeur d'un `int' */ void mem_addr_int (int *p) { printf("%10u == %8x -> %11d\n", (unsigned int) p, (unsigned int) p, *p) ; }
/* Affiche l'adresse et la valeur d'un `int',
* avec les conversions non-signée et hexa. */
void mem_addr_ints (int *p) { printf("%10u == %8x -> %11d == %10u == %8x\n", (unsigned int) p, (unsigned int) p, *p, (unsigned int) *p, (unsigned int) *p) ; }
/* Variante de la précédente avec la conversion en caractères. */ void mem_addr_chars (int *p) { char *s ; int i ; printf("%10u == %8x -> %11d == %8x == {", (unsigned int) p, (unsigned int) p, *p, (unsigned int) *p) ; s=(char *)p ; mem_print_char(s[0]) ; for (i=1;i<sizeof(int);++i) { printf(","); mem_print_char(s[i]) ; } printf("}\n") ; }
/********** Affichage de la pile **********/
/* Le pointeur mem_debut_pile doit être initialisé à l'adresse d'une variable
* de pile */
void mem_aff_pile (int *haut) { int *p ; for (p=haut ; p<= mem_debut_pile ; p++) mem_addr_ints(p) ; }
void mem_aff_pile_chars (int *haut) { int *p ; for (p=haut ; p<= mem_debut_pile ; p++) mem_addr_chars(p) ; } </source>
Le programme suivant met en évidence l'allocation des chaînes constantes dans le segment de code, des tableaux locaux dans la pile, et l'allocation dynamique dans le tas. Le code a les adresses les plus basses. Le tas a des adresses petites et croissantes. La pile a des adresses grandes et décroissantes. <source lang="c">
- include <stdio.h>
- include <stdlib.h>
- include "mem.h"
- define TAILLE 40
- define REC 5
char tab_ext [] = "tableau externe"; char *ptr_ext = "pointeur externe";
void f (int n) { char tab_f [] = "f: tableau"; char *ptr_f = "f: pointeur"; char *ptr_all_f = malloc(TAILLE) ; snprintf(ptr_all_f,TAILLE,"f: pointeur alloué %d", n) ;
if (n<REC) { mem_indent(n), mem_addr_int(&n) ; mem_indent(n), mem_ch(tab_f) ; mem_indent(n), mem_addr_ch(&ptr_f) ; mem_indent(n), mem_addr_ch(&ptr_all_f) ; f(n+1) ; } }
int main () { char tab_main [] = "main: tableau"; char *ptr_main = "main: pointeur"; char *ptr_all_main = malloc(TAILLE) ; snprintf(ptr_all_main,TAILLE,"main: pointeur alloué") ;
mem_ch(tab_ext) ; mem_addr_ch(&ptr_ext) ; mem_ch(tab_main) ; mem_addr_ch(&ptr_main) ; mem_addr_ch(&ptr_all_main) ;
f(0) ; return 0 ; } </source>
À l'exécution du programme suivant, on remarque que les adresses de la pile décroissent plus vite que ce qu'on aurait en n'empilant que les arguments. <source lang="c">
- include <stdio.h>
- include <stdlib.h>
- include "mem.h"
- define REC 8
void g (int n) ;
void f (int n) { if (n<REC) { mem_addr_int(&n) ; g(n) ; } }
void g (int n) { if (n<REC) { mem_addr_int(&n) ; f(n+1) ; } }
int main () { int n = 0 ; mem_addr_int(&n) ; f(1) ;
return 0 ;
} </source> C'est donc qu'il y a autre chose sur la pile.
Le programme suivant affiche l'intégralité de la pile: <source lang="c">
- include <stdio.h>
- include <stdlib.h>
- include "mem.h"
- define REC 8
void g (int n) ;
void f (int n) { if (n<REC) { mem_addr_int(&n) ; g(n) ; } else { printf("Affichage de la pile :\n") ; mem_aff_pile(&n) ; } }
void g (int n) { if (n<REC) { mem_addr_int(&n) ; f(n+1) ; } }
int main () { int n = 0 ; mem_debut_pile = &n ;
mem_addr_int(&n) ; f(1) ; return 0 ;
} </source> On remarque la pile contient des pointeurs vers des emplacements dans le segment de code. Il s'agit en fait d'adresses de retour vers le code de f et g, comme mis en évidence par l'exécution du programme suivant: <source lang="c">
- include <stdio.h>
- include <stdlib.h>
- include "mem.h"
- define REC 8
void g (int n) ;
void f (int n) { if (n<REC) { g(n) ; } else { printf("Affichage de la pile :\n") ; mem_aff_pile(&n) ; } }
void g (int n) { if (n<REC) { f(n+1) ; } }
int main () { int n = 0 ; mem_debut_pile = &n ;
printf("Adresse de f : "), mem_addr((void *)f) ; printf("Adresse de g : "), mem_addr((void *)g) ; f(1) ; return 0 ; } </source> À noter que les noms de fonctions peuvent être considérés comme des pointeurs dans le segment de code. Les adresses de retour mises en évidence dans la pile correspondent à des adresses dans le code de f et g.
On revient sur le deuxième exemple en affichant la pile, et en convertissant les entiers en blocs de caractères: on retrouve les tableaux de caractères alloués sur la pile. <source lang="c">
- include <stdio.h>
- include <stdlib.h>
- include "mem.h"
- define TAILLE 40
- define REC 5
char tab_ext [] = "tableau externe"; char *ptr_ext = "pointeur externe";
void f (int n) { char tab_f [] = "f: tableau"; char *ptr_f = "f: pointeur"; char *ptr_all_f = malloc(TAILLE) ; snprintf(ptr_all_f,TAILLE,"f: pointeur alloué %d", n) ;
if (n<REC) { mem_indent(n), mem_addr_int(&n) ; mem_indent(n), mem_ch(tab_f) ; mem_indent(n), mem_addr_ch(&ptr_f) ; mem_indent(n), mem_addr_ch(&ptr_all_f) ; f(n+1) ; } else mem_aff_pile_chars(&n) ; }
int main () { int n = 0 ; char tab_main [] = "main: tableau"; char *ptr_main = "main: pointeur"; char *ptr_all_main = malloc(TAILLE) ; snprintf(ptr_all_main,TAILLE,"main: pointeur alloué") ;
mem_ch(tab_ext) ; mem_addr_ch(&ptr_ext) ; mem_ch(tab_main) ; mem_addr_ch(&ptr_main) ; mem_addr_ch(&ptr_all_main) ;
mem_debut_pile = &n ; f(0) ;
return 0 ;
} </source>
Références
- The C programming language, de Kernighan et Ritchie;
- Le langage C, version française du précédent;
- Le polycopié de Bernard Cassagne, disponible ici, au format html (consultable en ligne) ou pdf;
- Le wikilivre Programmation C: un livre de cours sur le mode wikipedia.