int main(void), return, compilateur,
cc, gcc, ./a.out,
cc -o(), argument, paramètre, prototype,
cc -Wall#define, #include,
cc -E, exit/* */, //, #if 0,
#endifprintf, stdio.h,
sortie standard stdout, chaîne de caractères littérale,
séquence d'échappement, \nprintf%d, %c, %s,
%f, caractère littéral, 'A'+, -, *, /,
%, ~, <<,
>>, &, |, ^,
<, <=, >,
>=, ==, !=,
!, &&, ||, ?:,
,int, char,
unsigned, float, double,
initialisation, =, visibilité, static++, --, +=, -=
*=, /=, %=, &=,
|=, ^=, <<=,
>>=while, do, for,
break, continue, getchar,
entrée standardswitch, case, default,
if, else[], initialisation, chaîne de caractères,
'\0'&, *, NULL,
conversion tableau-pointeur, constint main(int argc, char *argv[])malloc, realloc, free,
sizeof, forçage de type (type),
size_t, typedef, void*struct, ., ->, fonction UNIX#include "file", ifndefCe premier programme correspond au programme UNIX true
qui ne fait rien et termine avec succès (selon
man true) :
int main(void)
{
return 0;
}
trueint main(void) |
La fonction |
{ |
La définition de toute fonction est entourée d'une
paire d'accolades |
return 0; |
Le mot-clé |
} |
|
Un programme C qui n'est pas destiné à un environnement particulier doit
au minimum définir la fonction main. En effet, toutes les
instructions sont dans des fonctions et la première fonction exécutée par
un programme est main.
L'environnement (un shell interactif, un script, make...)
qui lance notre programme récupère son statut de sortie.
La valeur retournée par main est le statut de sortie,
0 signifiant « terminé avec succès.»
C est un langage compilé. Il faut donc utiliser un compilateur
pour construire un fichier exécutable en langage machine à partir
des fichiers sources en C. Sur UNIX le compilateur C est la commande
cc (C Compiler) ou gcc pour utiliser les outils GNU
(cc est gcc sur Linux).
| fichier source | commande | exécutable produit |
|---|---|---|
true.c |
cc true.c |
a.out |
Fichier texte décrivant notre programme en langage C. |
Commande traduisant notre programme du C en langage machine. |
Fichier exécutable produit par le compilateur, nommé
|
L'option -o de cc spécifie le nom du fichier à
produire, par exemple cc true.c -o true pour produire
true au lieu de a.out.
Vérifier le fonctionnement de la commande standard true
(lancer true && echo $? dans
bash).
Créer le fichier source C true.c (l'extension
.c est obligatoire). En tapant le C, prendre notamment garde
aux lettres minuscules, parenthèses, accolades et point-virgule.
Construire le programme avec la commande
cc true.c en corrigeant les éventuelles erreurs
détectées par le compilateur. Vérifier que a.out a été
produit.
Lancer a.out et vérifier son statut de sortie.
Si on nomme notre programme exécutable true au lieu de
a.out, à quoi faut-il faire attention en le lançant ?
$ true && echo $? 0 $
Après édition :
$ cat true.c
int main(void)
{
return 0;
}
$
$ cc true.c $ ls -l * -rwxrwxr-x 1 marc marc 4684 oct 7 22:40 a.out -rw-rw-r-- 1 marc marc 31 oct 7 21:49 true.c $
La taille de a.out peut varier. Le site
A
Whirlwind Tutorial on Creating Really Teensy ELF Executables for
Linux décrit le contenu des milliers d'octets de
a.out.
Dans bash on peut vérifier le succès et afficher le
statut de sortie ainsi :
$ ./a.out && echo $? 0 $
Il faut faire attention à ne lancer ni la commande true
du shell (voir help true dans bash)
ni le programme true du système (voir
man true). Pour être sûr de lancer notre
true il faut donner son chemin, comme
./true depuis le répertoire courant.
On se fait aussi facilement piéger en appelant son programme
test...
void mainOn tombe souvent sur des programmes définissant main
ainsi :
void main(void) {}
Cela signifie que main ne retourne
rien. Un certain nombre de compilateurs (notamment
sur Windows) le supportent. Mais dans tous les standards C et C++,
main retourne un int.
Le livre The C Programming Language, Second edition de Brian W. Kernighan et Dennis M. Ritchie décrit extrêmement bien C en moins de 300 pages, y compris des exemples avec UNIX.
Pour le reste, la fréquence élevée de l'erreur void main en
fait un excellent critère d'achat de livres sur C ou C++. Si celui-ci utilise
void main il faut s'attendre à des exemples non standards,
non portables, voire complètement erronés.
main(){}Il s'agit d'une syntaxe obsolète du
K&R C.
L'important est qu'elle équivaut à int main(){}.
On peut définir et utiliser autant de fonctions qu'on veut :
int somme(int x, int y) { return x + y; }
int produit(int a, int b) { return a * b; }
int happy_end(void) { return produit(2, somme(-3, 3)); }
int main(void) { return happy_end(); }
happy_end() est
une expression, qui signifie « appeler la
fonction happy_end », qui a le type de
happy_end (soit int) et qui a
la valeur retournée par happy_end (soit 0).
L'opérateur d'appel de fonction est () ;
il n'est pas facultatif, même vide.
L'expression produit(2, somme(-3, 3)) appelle les
fonctions somme puis produit. Elle se lit comme
une expression mathématique. Elle vaut 0, soit 2 * 0 soit 2 * (-3 + 3).
-3 et 3 sont les arguments passés à
somme pour initialiser les paramètres
x et y. 2 et 0 (retourné par
somme(-3,3)) sont ensuite passés à produit pour
initialiser a et b.
Dans cet exemple les fonctions sont définies avant d'être appelées. Ainsi le compilateur sait exactement à quoi il a affaire en compilant les appels. C'est simple et propre.
int somme(int, int); int produit(int, int); int happy_end(void); int main(void) { return happy_end(); } int happy_end(void) { return produit(2, somme(-3,3)); } int somme(int x, int y) { return x + y; } int produit(int a, int b) { return a * b; }
La façon propre d'appeler une fonction avant sa définition (voire même sans connaître sa définition) est de précéder les appels des prototypes des fonctions appelées. Un prototype donne à l'appelant tout ce dont il a besoin de savoir : le type retourné, le nom de la fonction et les types des paramètres.
Dans les projets non triviaux, l'usage des prototypes est systématique pour utiliser des fonctions réparties dans plusieurs fichiers sources et bibliothèques (voir chapitre Compilation séparée).
On n'écrit en principe pas le prototype de main car on
n'appelle généralement pas main.
Une fonction qui ne retourne aucune valeur (appelée procédure dans les
autres langages) retourne void.
void f1(void) {}
void f2(void) { f1(); }
void f3(void) { f2(); f1(); }
int main(void){ f3(); f2(); return 0; }
Les fonctions appelées sont dans l'ordre : main,
f3... à compléter.
Que fait ce programme ?
void f(void) { f(); }
int main(void) { f(); return 0; }
Si on remplace main par Main le compilateur
se plaint de l'absence de main mais ne dit rien à propos de
Main. Pourquoi ?
main, f3, f2, f1,
f1, f2, f1.
main appelle f puis f
s'appelle elle-même indéfiniment. Comme un appel consomme de la mémoire,
le programme se plante après avoir tout consommé.
Définir une fonction Main est tout à fait permis, elle n'a
simplement rien à voir avec main. L'absence de
main empêche toutefois de créer un programme exécutable.
En C (mais pas en C++) on peut aussi appeler une fonction sans prototype préalable :
int main(void) { return happy_end(); }
int happy_end(void) { return 0; }
GCC compile ceci sans avertissement, à moins d'utiliser l'option
-Wall. Mais c'est une survivance du
K&R C à éviter.
Elle crée en effet une déclaration implicite de la fonction appelée avec des
implications inattendues quant aux
types des paramètres.
Soit le fichier ex4.c :
int main(void) { f(); g(); h(); return 0; }
void f(void) {}
g() {}
int h(void) {return 0;}
Pourquoi la fonction f cause-t-elle un avertissement de
gcc ex4.c et pas g ni
h ?
Les appels f();g();h(); créent des déclarations implicites
des fonctions. Or une fonction est déclarée implicitement comme retournant un
int et non void comme le fait f,
d'où l'avertissement.
À noter que la définition g() {} utilise la syntaxe obsolète du
int implicite de K&R C.
Étant équivalente à int g() {} elle correspond à la
déclaration implicite.
On ne devrait pas être confronté à ce genre de problème dans un programme bien écrit. Mais un oubli de prototype ou une erreur de frappe cause souvent ce genre d'avertissement.
Voici une version particulièrement portable de false :
#include <stdlib.h> int main(void) { return EXIT_FAILURE; }
C permet de retourner un entier quelconque de main.
Sur UNIX il suffit de retourner 1 pour signaler l'échec du programme.
Mais sur les autres systèmes comme Windows ou VMS ?
Les standards C ou
C++ garantissent la signification de
seulement trois valeurs : 0 (succès),
EXIT_SUCCESS (succès) et EXIT_FAILURE (échec).
#define)EXIT_SUCCESS et EXIT_FAILURE sont
des macros qui doivent (d'après les standards) être définies par
le fichier include stdlib.h. Par
exemple :
#define EXIT_SUCCESS 0 #define EXIT_FAILURE 1
La définition de macro #define EXIT_FAILURE 1 définit que
chaque occurrence de l'identificateur EXIT_FAILURE
doit être remplacée par le texte 1.
La première étape de compilation d'un fichier source C consiste à le
passer à un pré-processeur. Il ne fait essentiellement que des substitutions
de texte. Il interprète chaque ligne commençant par #
comme étant une commande. La commande finit à la fin de la ligne.
Contrairement aux instructions C, il n'y a donc pas besoin d'un ;
pour marquer la fin d'une commande du pré-processeur.
#includeLa commande
#include<stdlib.h>
est substituée par le texte du fichier stdlib.h.
Autrement dit, le fichier est inclus.
Les caractères < et >
indiquent de chercher stdlib.h dans des répertoires
prédéfinis (comme /usr/include). Si on utilise le délimiteur
" à la place, alors le fichier est recherché dans le
répertoire du fichier pré-processé.
stdlib.h est un fichier include standard. Les
fichiers includes doivent aussi être en C pour pouvoir être inclus
dans nos sources. Dans les fichiers includes standards chaque
fabricant de compilateur définit des macros comme EXIT_FAILURE
avec les valeurs idoines pour le système (UNIX, Windows, VMS...)
EXIT_FAILURE sans inclure stdlib.h ?
cc -E false.c pour observer la sortie du
pré-processeur.
L'abus de macros cause souvent des bugs difficiles à trouver.
Par exemple la commande cc ex3.c échoue à cause
d'un petit bug, lequel ?
ex3.c | ex3_1 | ex3_2 |
|---|---|---|
#include "ex3_1" #include "ex3_2" |
#define MAIN int main ( void ) #define FAUX return EXIT_FAILURE #include <stdlib.h> |
MAIN{ FAUX }
|
EXIT_FAILURE ne représente rien de connu pour le
compilateur, donc la compilation échoue.
On voit que notre source est précédé de centaines de lignes qui
correpondent au contenu de stdlib.h et des fichiers inclus
par ce dernier. À noter que les sources incluses sont aussi modifiées par
le pré-processeur. On voit enfin que EXIT_FAILURE a été
remplacé par un nombre.
Ce source transformé est ce que reçoit le compilateur proprement dit. Après le pré-processeur, les messages d'erreur du compilateur se rapportent à ce source transformé.
Si on utilise l'option -E on voit qu'il manque un
; à la fin de l'instruction return.
exitSouvent un problème fatal apparaît dans une autre fonction que
main. Un moyen pratique de terminer immédiatement le programme
est alors d'appeler la fonction standard exit :
#include <stdlib.h> void autre(void) { exit(EXIT_FAILURE); } int main(void) { autre(); return 0; }
Souvent la valeur -1 est utilisée comme statut de sortie, mais la
documentation de la libc n'autorise que des valeurs entre 0 et
255. On peut vérifier qu'une valeur hors de ces limites est tronquée sur
8 bits. La valeur conventionnelle sur les systèmes POSIX comme Linux est 1.
Un bon programme a des commentaires :
/* * true */ int main(void) { return 0; /* succès */ }
Un commentaire C commence par
/* et fini par */.
Ce qui est pratique, c'est qu'un commentaire peut couvrir plusieurs lignes.
Ce qui est moins pratique, c'est qu'un commentaire ne peut pas contenir
un autre commentaire car la première occurrence de */
stoppe tout.
Pour commenter des lignes contenant des commentaires, on peut heureusement utiliser une commande du pré-processeur :
int f(){}
#if 0
/* Tout ceci est supprimé par le pré-processeur
* jusqu'au #endif correspondant au #if.
*/
void f(){} /* autre définition dont on ne veut plus */
#endif
On peut encore noter que de nombreux compilateurs supportent aussi les commentaires C++ (et maintenant C99) :
// Ceci est un commentaire C++ (et C99)
// Il commence par // et se termine tout
// seul à la fin de la ligne.
/*// Il peut être inclu dans un commentaire C (pratique).*/
// Mais ce n'est pas du C (ni ANSI C ni K&R C) !
Pour séparer les mots d'un programme C, on peut indistinctement utiliser des espaces, des tabulations, des retours à la ligne ou des commentaires. Ils équivalent tous à ce qu'on appelle un espace blanc. On peut même tout coller tant que cela n'introduit pas d'ambiguïté.
Soit le programme :
#include <stdlib.h>
int main(void)
{
return EXIT_FAILURE;
}
Le pré-processeur C est sensible aux lignes alors il faut garder le retour à la ligne après la commande d'inclusion.
#include<stdlib.h>
int main(void){return EXIT_FAILURE;}
# include <stdlib.h>
int
main
(
void
)
{
return
EXIT_FAILURE
;
}
Voici le fameux programme hello, world (qui termine avec succès) :
#include <stdio.h> int main(void) { printf("hello, world\n"); return 0; }
hello, world
stdio.h (pour STanDard Input/Output)
contient le prototype de printf. C'est une fonction
de la bibliothèque standard de C. Elle est donc documentée dans le manuel,
qu'on peut consulter avec man 3 printf (ou
info libc qui donne toute la documentation de la
GNU C Library).
La fonction printf est compilée dans la libc.
Il s'agit d'une bibliothèque partagée (fichier .so). Par défaut
GCC sur Linux utilise les fonctions standards des bibliothèques partagées,
ce qui rend nos exécutables bien plus petits tout en optimisant l'utilisation
des ressources. La commande ldd exe permet
de lister les bibliothèques partagées utilisées par
exe.
printf sort sur la sortie standard
(appelée stdout) la chaîne de caractères
Hello, world\n.
Le concept de sortie standard est très abstrait. Si nous lançons simplement
./hello c'est la console. Si nous lançons
./hello>dump.txt c'est le fichier
dump.txt. Si nous lançons ./hello|wc
c'est le programme wc. Dans un script CGI le serveur Web redirige
la sortie standard sur lui-même pour en faire la page Web.
Une chaîne de caractères littérale est délimitée par des guillemets
". Une chaîne doit tenir sur une ligne. Depuis
ANSI C on peut toutefois écrire une chaîne
en plusieurs morceaux séparés par des espaces blancs. On pourrait donc écrire
"hello" "," " world\n".
Dans la chaîne
"hello, world\n",
\n est une séquence d'échappement qui représente
le caractère de retour à la ligne. On ne pourrait pas mettre le retour à
la ligne tel quel dans une chaîne qui doit tenir sur une seule ligne.
Les séquences utiles sont :
|
|
|
Supprimer le retour à la ligne de hello, world, lancer le programme et observer.
Modifier hello, world pour qu'il affiche :
hello, world Il a écrit : "'lut \-)"
À noter une espace insécable (caractère ISO-8859-1 de code décimal 160) avant les deux-points et une tabulation horizontale avant le premier double guillemet. Il peut être utile de rediriger la sortie standard sur un fichier pour l'enregistrer et la vérifier.
Si au lieu de printf("hello, world\n") on compile
printf(123), que se passe-t-il ? En quoi l'inclusion de
stdio.h change-t-elle quelque-chose ?
$ ./a.out hello, world$
Il manque un retour à la ligne avant la nouvelle invite.
Solution compacte :
printf("hello, world\nIl a écrit\240:\n\t\"'lut \\-)\"\n");
Solution plus jolie :
printf("hello, world\n"
"Il a écrit\240:\n"
"\t\"'lut \\-)\"\n");
Il n'est pas garanti que la console affiche correctement les caractères
non ASCII comme '\240' (160 en base 10).
Grâce au prototype contenu dans stdio.h, le compilateur
voit qu'il y a un problème avec printf(123) et nous en
averti :
$ cc hello.c -o hello hello.c: Dans la fonction « main »: hello.c:5: AVERTISSEMENT: passage de arg 1 de « printf » transforme en pointeur un entier sans transtypage $
Il ne s'agit cependant que d'un avertissement, donc un programme
hello est tout de même produit, mais il plante :
$ ./hello Erreur de segmentation $
Sans l'inclusion de stdio.h le compilateur ne voit plus
le prototype de printf, donc nous ne sommes pas averti.
Mais le programme produit plante tout autant.
printfOn peut aussi afficher des nombres et des caractères avec
printf :
#include <stdio.h>
int main(void)
{
printf("%d plus %d font %d.\n", 2, 2, 2+2);
printf("Le cube de %g est %g.\n", 0.125, 0.125 * 0.125 * 0.125);
printf("Le code du caractère '%c' est %d (%o octal, %x hexadécimal).\n",
'A', 'A', 'A', 'A');
printf("Le code du caractère '%c' est %d (%o octal, %x hexadécimal).\n",
123, 123, 123, 123);
printf("%s, %s\n", "hello", "world!");
return 0;
}
2 plus 2 font 4.
Le cube de 0.125 est 0.00195312.
Le code du caractère 'A' est 65 (101 octal, 41 hexadécimal).
Le code du caractère '{' est 123 (173 octal, 7b hexadécimal).
hello, world!
La fonction printf a la particularité d'accepter de 1 à
N paramètres. Le premier est une chaîne de formatage.
Elle peut contenir N-1 spécifications de conversion
(%truc) qui doit correspondre à un des N-1
paramètres suivants. Les conversions les plus utiles sont :
|
|
Il existe encore quelques autres lettres de conversion. Mais surtout il
existe des possibilités de variations d'alignement, de précision ou de
remplissage. La documentation de printf donne tous les
détails.
Il faut remarquer que pour le compilateur, le littéral 'A'
est un nombre entier, qui vaut le code du caractère A. Il n'existe donc pas
de conversion d'entier en caractère en C. Il n'y a que des entiers,
dont on peut afficher le caractère de code correspondant.
Le programme suivant utilise des possibilités de formatage documentées, que sort-il ?
#include <stdio.h>
int main(void)
{
#define PI 3.1415926535
printf("%f %.3f %010f %+10.10f\n", PI, PI, PI, PI);
return 0;
}
Que vaut 'A'+'A' ?
3.141593 3.142 003.141593 +3.1415926535
65 + 65 soit 130.
Voici un exemple d'utilisation de chaque opérateur de calcul.
| arithmétique | sur bits | comparaison | logique | autre | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
|
|
|
Le complément sur les opérateurs détaille l'usage et la précédence de chaque opérateur.
Les opérateurs ont la même précédence qu'en mathématiques et on peut
utiliser des parenthèses pour en forcer une autre. Ainsi
3*4-10/(5-3)==7 vaut 1.
Dans une opération artihmétique, si les deux opérandes n'ont pas la même
précision, alors l'opérande de basse précision est automatiquement
promue en haute précision, puis l'opération a lieu en haute
précision. Donc 2/4.0 est d'abord promu en 2.0/4.0
puis le calcul est fait en nombres à virgule, d'où le résultat de 0.5 au lieu
de 0 en division entière (quotient).
Pour les opérations sur les bits, la notation hexadécimale est généralement
plus lisible. Ainsi ~0 inverse tous les bits, ce qui sur 32 bits
donne 11111111111111111111111111111111 en binaire, 4294967295 en décimal ou
FFFFFFFF en hexadécimal. Le préfixe 0x permet d'utiliser la
notation hexadécimale en C.
-1 - -1 ?4 == 3 || 4 != 3 ?(100 / 11) * 11 ?(100 / 11) * 11 + 100 % 11 ?3 == 2 ? 1 : 0 ?4 / 0 ?0 && 4 / 0 ?(0x16 | 0x7) == (0x16 | 0x7) ?0x16 | 0x7 == 0x16 | 0x7 ?1.74e20 - 7.2e19 ?Les opérandes peuvent être évaluées dans n'importe quel ordre : dans
2*3+4*5 on ne sait pas quel produit est calculé en premier.
Seuls les opérateurs &&, ||,
?: et , introduisent un
point de séquencement. Autrement dit, l'opérande de droite est
évaluée après celle de gauche et tous ses effets de bord. En plus
l'opérande de droite n'est évaluée qu'en cas de nécessité. Donc on a :
f() + g(); /* On ne sait pas quelle fonction est appelée en premier. */ f() || g(); /* f est appelée et seulement si elle retourne 0, g est appelée. */ f() , g(); /* f est appelée, puis g. */
Une variable sert à stocker une valeur pendant l'exécution :
int main(void)
{
int x =3;
short y;
y = x;
return x - y; /* retourne 0 */
}
Toute variable doit être déclarée avant d'être utilisée. Pour ceci on donne son type et son nom. On peut aussi l'initialiser.
En principe on utilise le type int pour stocker un nombre
entier et double pour un nombre à virgule.
On utilise souvent les types suivants :
| type | usage |
|---|---|
char | pour stocker un caractère |
unsigned char | octet non signé, pour accéder à des données binaires |
short | entier de 16 bits |
int | entier efficace (32 bits sur i386) |
long | entier long, pour portabilité sur des systèmes ou int est plus court |
float | nombre à virgule court (imprécis) mais efficace |
double | nombre à virgule |
Assigner une valeur à une variable signifie qu'on
stocke une valeur dans une variable. L'opérateur d'assignation est
=. Les types de différentes précisions
forcent parfois une conversion de la valeur lors d'initialisation ou
d'assignation :
double f = 1.1; /* f == 1.1 */ int i = 1.1; /* i == 1 */ unsigned char u = 1.1; /* u == 1 */ u = 0xffff; /* u == 0xff */ i = u = 0x1234; /* u == 0x12 et i == 0x12 */
À noter qu'une assignation est une expression, qui vaut ce qui est
enregistré dans la variable, après l'assignation, d'où la possibilité d'écrire
i=u=0x1234.
Toutes les variables ne sont pas créées avec la même durée de vie et visibilité :
int n = 0; void plus_un(int n) { printf("plus_un = %d\n", n = n+1); } void compteur_global(void) { printf("compteur global = %d\n", n = n+1); } void compteur(void) { static int n = 0; printf("compteur = %d\n", n = n+1); } void un(void) { int n = 0; printf("un = %d\n", n = n+1); }
Si plusieurs variables ont le même nom alors celle qui est visible masque les autres.
Créée et initialisée avant même que main commence, une
variable globale existe en exemplaire unique aussi
longtemps que le programme. Elle est visible après sa déclaration.
Un paramètre formel est créé lorsque l'exécution entre dans la fonction et est détruit à la sortie de la fonction. Il est initialisé par l'argument correspondant passé par la fonction appelante. Il n'est visible que par sa fonction.
Une variable statique est créée et initialisée (0 par défaut) lorsque l'exécution passe la première fois par là. Les passages suivants ne créent et n'initialisent plus la variable qui existe déjà. Une fois créée, elle existe jusqu'à la fin du programme. Elle est visible de sa déclaration à la fin de son bloc. Les variables statiques sont peu utilisées en pratique.
Une variable locale est créée (et éventuellement initialisée, sinon sa valeur est indéfinie) lorsque l'exécution passe par là. Elle est détruite lorsque l'exécution sort de son bloc. Elle est visible de sa déclaration à la fin de son bloc. Les variables locales sont les plus utilisées en pratique. On les appelle parfois «automatique», en opposition à «statique».
En C les variables déclarées dans un bloc (statiques ou automatiques) doivent précéder les instructions :
{
déclarations;
instructions;
{
déclarations;
instructions;
}
instructions;
}
int i; double d; char c; i = d = c = 'A';
Est-ce que i égale d et d égale
c après ces 3 assignations ?
Avec les fonctions ci-dessus, qu'est-ce que le programme suivant affiche ?
int main(void)
{
plus_un(0);
compteur_global();
compteur();
un();
plus_un(-8);
compteur_global();
compteur();
un();
n = 123;
plus_un(0);
compteur_global();
compteur();
un();
return 0;
}
Oui, i==d && d==c vaut 1.
plus_un = 1 compteur global = 1 compteur = 1 un = 1 plus_un = -7 compteur global = 2 compteur = 2 un = 1 plus_un = 1 compteur global = 124 compteur = 3 un = 1
Par « int est un type efficace » j'entends que
les opérations sont plus rapides dans ce type. Pour int, il faut
particulièrement noter que c'est la précision minimale avec laquelle une
opération est faite. Si par exemple on additionne deux opérandes
short, les deux sont promues en int avant
l'opération, dont le résultat est aussi de type int,
d'où inefficacité.
Tous les types entiers ont une version unsigned, notamment
utile pour manipuler les bits.
Le complément sur les types de base
donne des définitions plus formelles de tous les types de base.
Quel avertissement peut causer la compilation de cette fonction, et pourquoi ?
short somme(short x, short y) { return x + y; }
Certains compilateur émettent un avertissement car l'expression
x + y produit un résultat de type int, qui est
converti en short, d'où risque de perte de précision, pour
être retourné.
Il existe des opérateurs d'assignation dont la seule utilité est de
rendre les expressions plus lisibles. Par exemple si
int x=0; alors :
| expression | valeur de l'expression | valeur assignée à x |
|---|---|---|
++x | 1 | 1 |
x++ | 0 | 1 |
--x | -1 | -1 |
x-- | 0 | -1 |
Si int x=2; alors :
|
|
On a l'équivalence suivante :
expr1 op= expr2
équivaut à
expr1 = (expr1) op (expr2)
sauf que expr1 n'est évaluée qu'une fois.
Soit int i=0; :
i++ + 1 ?i++, i+1 ?i += 6 + i ?i=i+i, i*=2,
i+=i, i^=i, i<<=1.
i^=i est la seule expression qui ne double pas i.
Il ne faut pas assigner deux fois la même variable dans une expression.
Il ne faut même pas l'utiliser une variable assignée pour calculer autre chose
que la valeur à assigner. En effet, à moins d'un point de séquencement
intermédiaire (introduit par les opérateurs &&,
||, ?: ou ,), le résultat est
indéterminé :
i = i++; /* BUG */ f(i, ++i); /* BUG */ ++i * i; /* BUG */ --i && 3/i; /* OK, grâce à && */
Ce programme copie telle quelle l'entrée standard sur la sortie standard.
#include <stdio.h>
int main(void)
{
int c;
c = getchar();
while (c != EOF) {
putchar(c);
c = getchar();
}
return 0;
}
$ echo "hello, world" | ./a.out hello, world $
getchargetchar() est déclaré dans stdio.h
et retourne le caractère (en fait un octet) lu sur l'entrée standard.
L'entrée standard est le pendant de la sortie standard, par défaut c'est le
clavier, un fichier avec ./a.out <fichier ou la
sortie d'un autre programme avec ls | ./a.out.
S'il n'y a plus rien à lire, alors getchar()
retourne l'entier EOF. putchar(c)
écrit le caractère c sur la sortie standard.
En C, 0 signifie «faux», tout autre nombre signifie «vrai». Les opérateurs logiques et de comparatifs retournent 0 et 1 pour signifier «faux» et «vrai», donc il n'y a aucun soucis à avoir.
Il n'y a pas de type booléen en C. Le type bool ayant les
valeurs true et false a été ajouté en C++.
Il existe 3 syntaxes de boucle. La boucle est exécutée tant que l'expression de condition est vraie :
/* boucle while */
while (condition) {
instructions;
}
/* boucle do while */
do {
instructions;
} while (condition);
/* boucle for */
for (initialisation; condition; post-itération) {
instructions;
}
Les accolades sont optionnelles s'il n'y a qu'une instruction dans la boucle. Formellement, les accolades forment un bloc qui est une instruction composée. Et une boucle a toujours exactement une instruction (qui peut être composée).
Les boucles avec while bouclent tant que la condition est
vraie.
La boucle for est la plus complexe. L'initialisation
est une expression évaluée exactement une fois, avant de commencer la boucle.
L'expression de condition est évaluée avant chaque entrée dans la boucle,
comme pour une boucle while. Enfin l'expression post-itération
est évaluée à la fin de chaque itération.
Les trois expressions de la boucle for sont optionnelles.
On utilise souvent la syntaxe for(;;) lorsqu'on veut une boucle
infinie.
Il existe 2 instructions pour contrôler une boucle de l'intérieur :
break; /* sort de la boucle */ continue; /* va directement après la dernière instruction de la boucle */
À l'intérieur de plusieurs boucles imbriquées, ces instructions ne s'appliquent qu'à la boucle la plus imbriquée.
for(i=0;i<3;i++); est-elle
exécutée ?
for(i=3;i;i--); est-elle
exécutée ?
for(;;)break; est-elle
exécutée ?
for(i=2;i;--i){continue;i=0;}
est-elle exécutée ?
for
au lieu d'une boucle while.
while, mais
en appelant getchar() en un seul endroit. Il est conseillé
d'utiliser le fait que c = getchar() retourne la valeur lue.
i valant 0, 1 puis 2). À noter que cette boucle
contient seulement une instruction vide : ;.
i valant 3, 2 puis 1). On constate que
l'expression i suffit.
break;.
i valant 2 puis 1) car l'instruction i=0;
n'est jamais exécutée à cause du continue; précédent.
int c; for (c = getchar(); c != EOF; c = getchar()) putchar(c); return 0;
Comme il n'y a plus que l'instruction putchar(c); dans
la boucle, on peut se passer des accolades.
int c; while ((c = getchar()) != EOF) putchar(c); return 0;
La précédence de l'opérateur = étant inférieure à celle
de !=, les parenthèses sont nécessaires. Autrement
c aurait été assigné avec le résultat de l'inégalité (soit
0 ou 1).
Ce programme copie l'entrée sur la sortie en remplaçant le caractère «a» par «@», le «e» par «3» et le «l» par «1» :
#include <stdio.h>
int main(void)
{
int c;
while ((c = getchar()) != EOF) {
switch (c) {
case 'a':
putchar('@');
break;
case 'e':
putchar('3');
break;
case 'l':
putchar('1');
break;
default:
putchar(c);
}
}
return 0;
}
$ echo "hello, world" | ./a.out h311o, wor1d $
switch case defaultLe switch (expression) évalue
l'expression qu'on donne puis saute directement au label (étiquette)
case de même valeur. En pratique les
accolades sont toujours employées même si
formellement il s'agit d'exactement une instruction pouvant être composée.
Si aucun label case ne correspond, on peut
récupérer le cas avec un default. S'il n'y a pas de
default, tout est sauté.
Pour sortir d'un switch avant la fin il faut utiliser
l'instruction break;, comme pour les boucles.
Sinon on exécute tous les case suivant celui où on
est arrivé !
if else
if (condition) {
instructions;
}
Comme pour les boucles, la condition est une expression et les accolades
sont optionnelles s'il n'y a qu'une instruction. On peut donner une
alternative avec un else :
if (condition) {
instructions1;
} else {
instructions2;
}
Modifier le programme d'exemple pour utiliser des if
else au lieu de switch case
default.
Récrire le programme pour que les «A», «E» et «L» soient également transformés en «@», «3» et «1».
Ce programme calcule et sort le même résultat qu'un programme UNIX standard, lequel ?
#include <stdio.h>
int main(void)
{
int c;
int nl = 0, w = 0, t = 0, in_word = 0;
while ((c = getchar()) != EOF) {
switch (c) {
case '\n':
++nl;
/* pas de break */
case ' ': case '\r': case '\t': case '\v':
in_word = 0;
break;
default:
if (!in_word) {
in_word = 1;
++w;
}
}
++t;
}
printf("% 7d % 7d % 7d\n", nl, w, t);
return 0;
}
if (c == 'a')
putchar('@');
else if (c == 'e')
putchar('3');
else if (c == 'l')
putchar('1');
else
putchar(c);
On peut par exemple regrouper les labels en utilisant la propriété
qu'il faut un break; pour les séparer :
switch (c) {
case 'a':
case 'A':
putchar('@');
break;
case 'e':
case 'E':
putchar('3');
break;
case 'l':
case 'L':
putchar('1');
break;
default:
putchar(c);
}
Une autre solution plus légère utilise la fonction standard
tolower qui retourne la minuscule d'une lettre :
#include <stdio.h>
#include <ctype.h> /* contient le prototype de tolower */
/*...*/
switch (tolower(c)) {
case 'a':
putchar('@');
break;
case 'e':
putchar('3');
break;
case 'l':
putchar('1');
break;
default:
putchar(c);
}
./a.out <fichier affiche
la même chose que wc <fichier.
Ce programme affiche à l'envers les lignes reçues sur l'entrée standard.
#include <stdio.h> #include <stdlib.h> #define LIGNE_MAX 80 int main(void) { int c; char ligne[LIGNE_MAX]; int m = 0; while ((c = getchar()) != EOF) { if (c == '\n') { /* affiche la ligne à partir des derniers caractères */ while (m > 0) putchar(ligne[--m]); putchar(c); } else if (m < LIGNE_MAX) /* enregistre le caractère */ ligne[m++] = c; else { /* erreur, ligne trop longue */ fprintf(stderr, "ligne trop longue\n"); return EXIT_FAILURE; } } return 0; }
$ echo "hello, world" | ./a.out dlrow ,olleh $ echo "hello, world" | ./a.out | ./a.out hello, world $
Le principe est de mémoriser dans le tableau ligne
tous les caractères de la ligne avant de les afficher en commençant par le
dernier. char ligne[LIGNE_MAX] déclare un tableau
de LIGNE_MAX éléments de type char.
En pratique cela revient à avoir 80 variables de type char
côte à côte en mémoire.
La syntaxe la plus naturelle pour nommer un élément de tableau est
nom_du_tableau[indice]. Les indices
d'un tableau commencent toujours à partir de zéro.
Donc en l'occurrence on a :
ligne[0] | ligne[1] | ligne[2] | ... | ligne[LIGNE_MAX-2] | ligne[LIGNE_MAX-1] |
Une limitation de notre programme est que le tableau à une taille limitée.
Si on essaie d'écrire dans ligne[TAILLE_MAX] où encore plus loin,
alors le programme va planter, car en fait on écrase ce qui suit le tableau
dans la RAM ! Malheureusement la dimension des tableaux est fixe. On
quitte après avoir affiché une erreur sur
stderr si on tombe sur une ligne trop longue.
On pourrait utiliser un tableau plus grand, mais essentiellement cela ne fait que repousser le problème en consommant beaucoup de mémoire généralement inutilisée. Ce problème ne peut pas être résolu proprement avec un tableau de taille fixe.
Une chaîne de caractères comme "hello, world\n" est en fait
un tableau de caractères. Voici donc une nouvelle version de
hello, world :
#include <stdio.h>
int main(void)
{
char h[] = "hello, world\n";
printf(h);
return 0;
}
On constate que la taille du tableau h est automatiquement
déduite de la taille de la chaîne initialisatrice. Il faut bien noter qu'il
s'agit d'une initialisation et non d'une assignation. Il n'est plus possible
d'écrire h="hello, world\n" par la suite.
Il n'existe aucun opérateur traitant un tableau dans son ensemble.
Un tel opérateur serait inefficace car il devrait potentiellement traiter
des milliers de données. Or l'inefficacité va à l'encontre de C.
Voici donc le moyen classique de copier le tableau t1 dans le
tableau t2 :
int i, t1[TAILLE], t2[TAILLE]; /* ... */ for (i = 0; i < TAILLE; i++) t2[i] = t1[i];
Pour être vraiment précis, une chaîne de caractères C est en fait une
suite de caractères en mémoire terminée par le caractère nul (zéro). On peut
donc initialiser le tableau h ainsi, ça revient au même :
#include <stdio.h>
int main(void)
{
char h[] = {'h','e','l','l','o',',',' ','w','o','r','l','d','\n','\0'};
printf(h);
return 0;
}
La syntaxe
type t[]={expr0,expr1,exprN};
est la syntaxe normale d'initialisation de tableau.
Le caractère '\0' n'est pas affiché, il est utilisé pour
détecter la fin de la chaîne. En effet, printf, comme toutes
les fonctions qui reçoivent des chaînes, ne connaît pas la taille de la
chaîne qui lui est donnée, mais elle sait qu'il y a un caractère nul à la fin.
Et si suite à une erreur il n'y en a pas, le programme plante.
Qu'affiche printf("hello, w\0orld\n") ?
Après l'initialisation int t[] = {1, 6, 9}; que vaut
t[1] ?
Après l'instruction t[t[0]] = t[2];, quelles sont les valeurs
dans le tableau ?
Que fait ce morceau de programme ?
for (i = 0; i < 3; i++) t[i] = i;
Qu'affiche ce morceau de programme ?
char text[] = "ABC";
int i;
for (i = 0; text[i]; ++i);
printf("%d", i);
t les valeurs
0, 1 et 2.
"ABC", sans compter
le caractère nul final.
#include <stdio.h>
int main(void)
{
char h[] = "hello\n";
char * p;
for (p = &h[0]; *p != '\n'; ++p)
printf(p);
return 0;
}
$ ./a.out hello ello llo lo o $
Pour comprendre ce qu'est un pointeur, il faut savoir comment fonctionne
la mémoire vive d'un ordinateur. La mémoire vive est constituée de bytes
numérotés. On appelle adresse le numéro d'un byte. Dans tous les
ordinateurs modernes, un byte contient 8 bits, soit un octet. En C,
le type char a la taille d'un byte, donc en pratique d'un
octet.
Dans l'exemple qui suit, le tableau h se trouve dans les
octets d'adresses 2 à 8.
| adresse | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | ... |
|---|---|---|---|---|---|---|---|---|---|---|---|---|
| contenu | 22 | 0 | 'h' | 'e' | 'l' | 'l' | 'o' | '\n' | '\0' | 123 | 'a' |
Les autres adresses contiennent d'autres variables, des instructions, n'importe quoi... Typiquement, dans un ordinateur 32 bits, les adresses font 32 bits, ce qui permet d'adressé un maximum théorique de 4294967296 octets (4 Go).
char * p; déclare (sans
l'initialiser) que la variable p est un
pointeur sur un char. Cela signifie que
p contient l'adresse d'un char.
On dit que p pointe sur un char.
L'opérateur unaire & donne l'adresse de l'objet
auquel il est appliqué. En l'occurrence l'expression
&h[0] donne l'adresse de
h[0], soit 2 dans l'exemple précédent.
Donc p vaudrait 2 après
p = &h[0]. Il faut cependant noter
qu'un pointeur n'est pas un entier comme int, même si en C (pas
en C++) la conversion se fait automatiquement.
L'opérateur unaire * retourne l'objet pointé.
Ainsi *p
est le caractère pointé par p. Donc
si p vaut 2 dans l'exemple précédent, alors
*p est
h[0], soit l'octet à l'adresse 2.
Dans le programme précédent, p vaut d'abord
&h[0]. L'incrémentation
++p fait que p
vaut ensuite &h[1], puis &h[2] jusqu'à
&h[7]. Alors la boucle se termine car h[7], donc
*p, contient
'\n'.
Ajouter ou soustraire 1 à un pointeur le fait par définition pointer sur l'élément suivant, respectivement précédent du tableau. Si on ne pointe pas dans un tableau alors l'opération est indéfinie.
Par définition la différence de deux pointeurs retourne le nombre d'éléments de tableau qui les séparent. La différence ne retourne un résultat défini que si les deux pointeurs pointent dans le même tableau.
L'addition de pointeurs n'existe pas car elle n'a pas de sens.
On peut aussi comparer des pointeurs avec les mêmes opérateurs que pour
les entiers (==, <, ...)
Il faut encore noter que l'opérateur [] que nous utilisons
pour indexer le tableau s'applique en fait sur des pointeurs (voir le
sous-chapitre de conversion de tableau en pointeur).
0 est par définition le pointeur nul. C'est une
adresse impossible où il est garanti que rien ne se trouve jamais.
Déréférencer (*p) un pointeur nul plante le programme.
La macro NULL est aussi souvent utilisée pour être plus
explicite, mais ça revient au même, comme on le voit dans l'exemple
suivant :
int *p1 = 0, *p2 = NULL;
if (p1 || p2) printf("impossible\n");
if (p1 != 0 || p2 != 0) printf("impossible\n");
if (pi != NULL || p2 != NULL) printf("impossible\n");
if (p1 != p2) printf("impossible\n");
Typiquement les fonctions qui retournent une adresse retournent le pointeur nul en cas d'échec.
printf(p) signifie qu'on donne à
printf l'adresse du premier caractère à afficher.
printf attend comme premier paramètre un
const char*. Le const signifie simplement que
printf ne va pas profiter de savoir où est notre chaîne pour
la modifier (pointeur sur un caractère constant).
Le char* est bien le type de p, donc
tout est normal.
Mais alors comment se fait-il que l'on ait pu écrire
printf(h) avec h de type char[] ?
Et même comment se fait-il qu'on puisse écrire
printf("hello\n") sachant que "hello\n" est de type
char[7] ?
La subtilité est qu'un tableau se converti tout seul en un pointeur sur
son premier élément lorsqu'on l'utilise dans un contexte où un pointeur est
attendu. Donc printf(h) signifie implicitement
printf(&h[0]) et printf("hello\n") signifie
printf(&"hello\n"[0]).
Les expressions mélangeant tableaux et pointeurs sont très courantes dans les programmes C. La première raison est qu'il n'est pas possible de donner un tableau à une fonction (il y a toujours conversion en pointeur). La seconde raison est que les chaînes de caractères sont très utilisées et sont des tableaux.
for(p=&h[0]; *p!='\n' ;++p) par
for(p=&h[0]; *p ;++p), que se passe-t-il ?
for(p=&h[0]; *p!='\n' ;++p) par
for(p=h; *p!='\n' ;++p), que se passe-t-il ?
Qu'affiche le morceau de programme suivant ?
char t[] = "hello\n";
char * p = t;
while (*p++);
printf("%u\n", p-t);
Qu'affiche le morceau de programme suivant ? Combien de bytes occupe le tableau ?
int t[] = {'h','e','l','l','o','\n',0};
int * p = t;
while (*p++);
printf("%u\n", p-t);
*p pointe sur '\0', soit 0,
soit «faux», donc le test échoue et la boucle est terminée.
"hello\n" avec le
caratère nul final. L'expression *p++ est particulièrement
illisible mais (malheureusement) commune. Elle équivaut à *(p++).
C'est-à-dire qu'elle retourne l'élément pointé par p puis
incrémente p. Donc la boucle while(*p++); incrémente
le pointeur p tant qu'il pointe sur une valeur non nulle.
La boucle s'arrête donc lorsque p pointe sur le caractère nul
suivant le '\n'.
int
de 32 bits et des char de 8 bits.
Assigner à un pointeur un entier différent de 0 est fortement découragé,
cause un avertissement et même une erreur en C++.
C'est ce que l'on a fait dans les exercices de hello, world en
écrivant printf(123), et le programme a planté car il n'avait pas
le droit d'accéder à l'adresse 123. En fait donner une adresse numériquement
n'a de sens que pour programmer directement le matériel. Or système comme
UNIX ne laisse les utilisateurs programmer directement le matériel...
Formellement la différence de pointeurs est de type ptrdiff_t.
Sur i386 c'est un typedef de unsigned, d'où la
spécification de conversion %u dans les exercices.
Mais ce n'est pas portable puisque ça dépend du typedef
de ptrdiff_t. Plus portable mais inélégant serait de forcer
une conversion dans le plus grand type entier, unsigned long en C.
C++ offre une solution vraiment élégante avec les fonctions surchargées.
Voici un programme qui affiche une liste numérotée des mots de la ligne de commande utilisée pour lancer le programme.
#include <stdio.h>
int main(int argc, char * argv[])
{
int i;
for (i = 0; i < argc; ++i) {
printf("%d: %s\n", i, argv[i]);
}
return 0;
}
Il existe deux définitions de main répondant aux
standards C(++) : int main(void) et
int main(int argc, char *argv[]). Les noms argc
et argv sont conventionnels. argc indique combien
de paramètres la ligne de commande comporte. argv pointe sur ces
paramètres. Sur UNIX le premier paramètre est en principe le nom du programme.
La notation char *argv[] est trompeuse car elle est en fait
équivalente à char * * argv. En effet, en tant que paramètre de
fonction, var[] est synonyme de
*var. D'ailleurs en dehors de main on
n'utilise pratiquement jamais la notation [] pour les paramètres
d'une fonction.
argv pointe donc sur un tableau de pointeurs qui pointent
chacun sur un paramètre de la ligne de commande.
./a.out 1000 x affiche
1000 fois la lettre x. La fonction standard atoi est utile pour
convertir une chaîne en int.Pour pouvoir inverser des lignes de toute taille (tenant en RAM) il faut pouvoir s'allouer la mémoire à mesure des besoins.
#include <stdio.h>
#include <stdlib.h>
int main(void)
{
int c;
char * ligne;
size_t longueur = 1, m = 0;
ligne = (char*)malloc(longueur * sizeof(char));
if (!ligne) {
fprintf(stderr, "Pas assez de RAM\n");
return EXIT_FAILURE;
}
while ((c = getchar()) != EOF) {
if (c == '\n') {
while (m > 0)
putchar(ligne[--m]);
putchar(c);
}
else if (m < longueur)
ligne[m++] = c;
else {
longueur = longueur * 1.1 + 1;
if (!(ligne = (char*)realloc(ligne, longueur * sizeof(char)))) {
fprintf(stderr, "Pas assez de RAM\n");
free(ligne);
return EXIT_FAILURE;
}
ligne[m++] = c;
}
}
free(ligne);
return 0;
}
malloc, realloc, free,
sizeof
Ce programme utilise les fonctions standards malloc,
realloc et free à la place
d'un tableau pour allouer, redimensionner et libérer la mémoire.
Le pointeur ligne pointe sur le premier
char de mémoire allouée.
malloc alloue le nombre de bytes demandé.
On multiplie donc le nombre d'éléments voulu
(longueur) par la taille en bytes de l'élément
donnée par l'opérateur sizeof.
En l'occurrence on pourrait s'en passer car par définition
sizeof(char) vaut 1 (mais ça fait une illustration).
realloc change la taille de la mémoire allouée
à l'adresse donnée par ligne. Ici on s'en sert pour
augmenter de 10% la mémoire allouée.
free libère toute la mémoire allouée à
l'adresse donnée par ligne.
size_t et typedefLa longueur et l'indice m
ont été déclaré de type size_t car on pourrait
théoriquement avoir besoin de tellement de mémoire qu'un entier de type
int déborderait. size_t est en fait le
synonyme d'un type de base définit dans stdlib.h. Il est d'usage
en C de définir des synonymes pour des types ayant de telles contraintes de
taille. On utilise le mot-clé typedef pour cela. En l'occurrence
mon stdlib.h définit :
typedef unsigned int size_t;
(type), void*Les fonctions de très bas niveau comme malloc servent à
allouer de la mémoire en vrac, dans laquelle on peut mettre des
char, des int, des double**, peu
importe. Dans notre cas on veut y mettre des char donc nous
assignons le résultat de malloc à un char*.
Si on avait voulu y mettre des double** on aurait assigné le
résultat de malloc à un double***.
Le problème est que malloc ne retourne pas un
char* (ni un double***) ; il retourne un
pointeur universel de type void*. Nous utilisons l'opération
(char*) pour forcer le pointeur de type
void* en char*.
En fait en C un void* se convertit tout seul en tout autre
pointeur, le forçage (char*) n'est donc qu'illustratif.
On trouve cependant de nombreux programmes utilisant ce forçage inutile en C.
On peut arguer que c'est devenu obligatoire en Standard C++, cependant
malloc est fort peu utile en C++.
Une structure est une collection de variables nommées. Quelques exemples :
/* Une coordonnée (x,y) */
struct coord { double x; double y; };
/* Un tirage de lotto */
struct tirage { int numeros[6]; int complementaire; }
/* Informations sur un fichier retournées par la fonction UNIX
int stat(const char *file_name, struct stat *buf); */
struct stat {
dev_t st_dev; /* device */
ino_t st_ino; /* inode */
mode_t st_mode; /* protection */
nlink_t st_nlink; /* number of hard links */
uid_t st_uid; /* user ID of owner */
gid_t st_gid; /* group ID of owner */
dev_t st_rdev; /* device type (if inode device) */
off_t st_size; /* total size, in bytes */
unsigned long st_blksize; /* blocksize for filesystem I/O */
unsigned long st_blocks; /* number of blocks allocated */
time_t st_atime; /* time of last access */
time_t st_mtime; /* time of last modification */
time_t st_ctime; /* time of last change */
};
Exemple d'usage avec la fonction UNIX stat.
#include <sys/types.h>
#include <sys/stat.h>
#include <unistd.h>
int main(int argc, char * argv[])
{
int c;
for (c = 1; c < argc; ++c) {
struct stat file_infos;
if (stat(argv[c], &file_infos) == 0) {
printf("Informations sur %s :\n", argv[c]);
printf(" taille de %lu octets ;\n", (unsigned long)file_infos.st_size);
printf(" soit %lu octets sur disque ;\n", (unsigned long)file_infos.st_blocks * 512);
printf(" bits de protection %o.\n", file_infos.st_mode & 0777);
}
}
return 0;
}
.On utilise l'opérateur . pour accéder à un
membre de structure. L'obligation de répéter le mot-clé
struct à chaque déclaration étant assez lourde,
on utilise souvent un typedef pour raccourcir, comme dans :
typedef struct { double x; double y; } coord_t;
coord_t un_point = { 1.0, 3.4 };
On voit qu'on peut initialiser une structure comme un tableau. Il faut noter que l'assignation est définie pour les structures.
->Généralement les fonctions ont comme paramètre un pointeur sur une structure plutôt que la structure elle-mêmes. Il y a deux raisons à cela. D'abord en K&R C c'était la seule solution. Ensuite il est plus efficace de passer un simple pointeur que toute une structure.
/* Retourne un pointeur sur la structure du plus grand fichier */
struct stat * plus_grand(const struct stat * rhs,
const struct stat * lhs)
{
return rhs->st_size > lhs->st_size ? rhs : lhs;
}
On remarque l'usage de l'opérateur -> qui est
en fait un raccourci d'écriture. On a l'équivalence suivante :
(*a).b est
équivalent à
a->b.
On aurait pu écrire la fonction compare_stat ainsi :
struct stat * plus_grand(struct stat * rhs,
struct stat * lhs)
{
return (*rhs).st_size > (*lhs).st_size ? rhs : lhs;
}
Il est possible d'écrire une version du programme d'inversion de chaînes
qui utilise une liste de blocs alloués au besoin. Une telle version
n'a pas besoin de realloc. Cette exercice est long.
Il y a deux sources implémentant cette solution. Le second optimise un peu le premier en allouant des blocs de plus en plus grands.
Jusqu'à présent nous avons utilisé le compilateur pour directement créer un fichier exécutable à partir d'un fichier source. Mais la construction d'un fichier exécutable passe par plusieurs étapes :
cc -M hello.c.cc -E hello.c.cc -S hello.c.cc -c hello.c.hello.c dépend, sous une forme
utilisable par l'outil make. C'est en fait la liste récursive
des inclusions.
hello.s contenant la traduction en assembleur
du source C. Surtout utile aux passionnés d'optimisation.
hello.o en langage machine, mais pas encore
exécutable. C'est la forme intermédiaire la plus aboutie de la compilation,
très utile pour recompiler partiellement des gros projets.
global.h
#ifndef GLOBAL_H
#define GLOBAL_H
typedef int INT32;
#endif
|
||
part1.h
#ifndef PART1_H #define PART1_H #include "global.h" void print1(INT32); #endif |
part2.h
#ifndef PART2_H #define PART2_H #include "global.h" void print2(INT32); #endif |
|
part.c
#include "part1.h"
#include "part2.h"
int main(void)
{
print1(22);
print2(33);
return 0;
}
|
part1.c
#include "part1.h" #include <stdio.h> void print1(INT32 a) { printf("part1 %d\n", a); } |
part2.c
#include "part2.h" #include <stdio.h> void print2(INT32 a) { printf("part2 %d\n", a); } |
Un projet classique a plusieurs centaines de fonctions, normalement
réparties dans plusieurs fichiers sources. Ici il y a 3 fichiers sources
.c et 3 fichiers include .h.
En général on a un fichier.h par
fichier.c. Le fichier.h
contient tous les prototypes, typedef, #define et
autres déclarations qui peuvent être utiles aux
autre_fichier.c. Ici part.c n'a pas son
.h car il n'est utile à aucun autre fichier. global.h
en revanche est nécessaire à tous les autres fichiers puisqu'il définit
INT32.
part.c inclu part1.h et part2.h
afin d'avoir les prototypes des fonctions print1 et
print2 utilisée par main.
Tout comme pour stdio.h et printf, cette inclusion
n'est pas obligatoire, mais permet au compilateur de mieux vérifier les
appels print1(22) et print2(33) faits dans
main.
En outre chaque fichier.c inclus son
fichier.h car cela permet un contrôle de cohérence
entre les prototypes du .h et la définition dans le
.c.
#include "fichier.h"On a utilisé des guillemets " au lieu de < et
> autour des noms de nos fichiers include. Cela indique au
compilateur de les chercher en partant du répertoire du fichier
.c.
Si à la place nous avions utilisé < et >
aussi pour nos fichiers, nous aurions dû ajouter le répertoire courant à la
liste des répertoires contenant des include standards avec l'option de
compilation -I..
Il est courant de devoir utiliser l'option -I lorsqu'on
utilise des include d'une bibliothèque non standard.
#ifndef MACHIN_HLes commandes du pré-processeur #ifndef MACHIN_H,
#define MACHIN_H et #endif sont un bricolage
inévitable pour régler les problèmes d'inclusion multiple. Ici
global.h est inclus deux fois depuis part.c,
une fois à travers part1.h et une fois à travers
part2.h. Si le compilateur tombait deux fois sur
typedef int INT32; ce serait une erreur.
Pour éviter ce problème, lors de la première inclusion le pré-processeur
définit le symbole GLOBAL_H. Ainsi lors des inclusions
suivantes, tout ce qui est compris entre le #ifndef GLOBAL_H et le
#endif est substitué par un espace blanc.
cc -cPour compiler séparément un fichier.c on utilise
la commande cc -c fichier.c. Ceci produit
un fichier objet fichier.o.
Une fois tous les fichiers .o obtenus, il ne reste plus qu'à
les lier pour obtenir l'exécutable final (cc *.o).
Lorsqu'on utilise une fonction de bibliothèque autre que la
libc, il faut donner le nom de cette bibliothèque à l'éditeur de
liens pour qu'il puisse y chercher la fonction. Par exemple les fonctions
mathématiques standards (cos, sin, ...) se trouvent
dans la libm. Il faut donner l'option -lm lors
de l'édition de liens.
Produire un exécutable avec ce fichier pi.c :
#include <math.h>
#include <stdio.h>
int main(void)
{
printf("Pi vaut %.10f.\n", acos(-1));
return 0;
}
cc pi.c -lm
© 2002, Marc Mongenet