[FAQ] fr.comp.lang.c - partie 2/4
Guillaume Rumeau <guillaume.rumeau@wanadoo.fr>
Archive-Name: fr/comp/lang/faq-c-2
Archive-Name: fr/comp/lang/faq-c-2
---------------------------------------------------------------------------
FAQ de fr.comp.lang.c
18 avril 2003
Partie 2/4 (sections 5 à 9)
---------------------------------------------------------------------------
5. Déclarations et initialisations
5.1 Quels types utiliser ?
Il existe en C plusieurs types de nombres entiers.
Si vous avez à gérer de grands nombres, il faut utiliser le type
long. Avec la norme C99 (cf. 3.7), un type long long est disponible.
Pour des nombres de petites tailles, et si la place mémoire
manque, c'est le type short qu'il faut prendre.
Le type char peut parfois être utilisé comme très petit
entier. Mais cela doit être évité au maximum. En effet, outre les
problèmes de signe, le code généré peut être plus complexe et
risque finalement de prendre plus de place et de faire perdre du
temps. Même les short font parfois perdre du temps.
Dans les autres cas, int est bien adapté.
Le mot clé unsigned est à utiliser pour les nombres
positifs, si vous avez des problèmes dus aux débordements ou pour
les traitements par bits.
Le choix entre float et double ne se pose pas.
On devrait toujours utiliser double, sauf si on a
vraiment des contraintes de mémoire (cf. 11.1
et 11.10).
5.2 Comment définir une structure qui pointe sur elle-même ?
Il y a plusieurs façons correctes de le faire.
Le problème est que dans la définition de la structure avec un
typedef, le type n'est pas encore défini. Voici la
manière la plus simple, où pour contourner le problème, on ajoute
un tag à la structure.
typedef struct node {
char * item;
struct node * next;
} node_t;
Une autre solution consiste à déclarer le type de la structure
avant sa définition, avec un pointeur.
typedef struct node * node_p;
typedef struct node {
char * item;
node_p next;
} node_t;
Cette construction récursive est utilisée pour obtenir des listes
chaînées ou des arborescences.
5.3 Comment déclarer une variable globale ?
Sauf dans des cas précis, vous devriez éviter d'utiliser des
variables globales.
Si vous y tenez vraiment, la meilleure solution est de déclarer la
variable dans un fichier xxx.c, et de mettre la
déclaration extern dans un xxx.h associé. Ceci
évitera des redéfinitions de la variable lors des inclusions
d'en-têtes.
Le mot clé static permet d'avoir des variables locales
persistantes, ce qui peut être une bonne alternative aux
globales.
5.4 Quelle est la différence entre const et #define ?
Cela n'a rien à voir.
Le #define permet de définir une macro, alors que le mot
clé const indique que l'objet qualifié est protégé en
écriture.
Lors de la compilation, la première phase est effectuée par le
pré-processeur qui remplace les macros par leurs valeurs. C'est un
simple copier/coller.
Une variable déclarée const reste quant à elle une
variable, mais on ne peut lui affecter une valeur que lors de
l'initialisation. Après, elle est n'est plus modifiable.
Le mot-clé const permet aussi au compilateur d'effectuer
des optimisations en plaçant par exemple la variable dans un
registre.
Il est à noter qu'avec la nouvelle norme C99, il est possible de
déclarer un tableau dont la taille est donnée par une variable
const. Auparavant, il fallait utiliser une macro.
Voir aussi les questions 13.7 et 5.5.
5.5 Comment utiliser const avec des pointeurs ?
Le mot-clé const permet de protéger une variable de
modifications ultérieures. Une variable constante n'est modifiable
qu'une fois, lors de l'initialisation.
Un pointeur étant une variable comme les autres, const
s'utilise de la même façon.
Voici un exemple :
const char * sz1;
char const * sz2;
char * const sz3;
char const * const sz4;
Les variables sz1 et sz2 sont des pointeurs sur
objet constant de type char. La variable sz3 est
un pointeur constant sur un objet (non constant) de type
char. Enfin, sz4 est un pointeur constant sur un
objet constant de type char.
Un petit « amusement » pour terminer, que signifie la déclaration
const char * (* f)(char * const * s); ?
Voir aussi la question 5.4.
5.6 Comment bien initialiser ses variables ?
Les variables globales, ou déclarées static, sont
initialisées automatiquement lors de leur définition.
Si aucune valeur n'est spécifiée, c'est un zéro qui est pris
(suivant le type de la variable, 0, 0.0 ou
NULL).
Ce n'est pas le cas pour les variables automatiques (les autres).
Il est donc nécessaire de le faire « à la main ».
Les variables allouées dynamiquement avec malloc() ne
le sont pas non plus.
On pourra utiliser calloc() qui initialise les
variables allouées. Il est à noter que calloc() met les
bits à 0 comme le ferait memset(). Cette
initialisation est valide pour les types entiers (char,
short, int et long), mais non portable
pour les pointeurs et les flottants.
Une bonne méthode qui initialise correctement les variables
suivant leur type est celle-ci :
{
type a[10]={0};
struct s x ={0};
}
Avec la variante dynamique :
[static] const struct s x0 ={0};
{
struct s *px =malloc(sizeof *px);
*px=x0;
}
5.7 Comment déclarer un tableau de fonctions ?
Deux cas se présentent. Si toutes les fonctions ont le même
prototype, il suffit de faire ainsi :
extern char * f(int, int); /* une fonction */
char * (*fp[N])(int, int); /* Un tableau de N fonctions */
char * sz;
fp[4] = f; /* affectation de f dans le tableau */
sz = fp[4](42, 12); /* utilisation */
Si les fonctions ont des prototypes différents, il faut déclarer
un tableau de fonctions génériques. Les fonctions génériques
n'existent pas à proprement parler. Il faut utiliser une fonction
sans argument spécifié et retournant un int.
int (*fp[N])(); /* Un tableau de N fonctions quelconques*/
Cela « capte » la plupart des cas, sauf les fonctions
au nombre d'arguments variables, comme printf().
Voir aussi la question 7.4.
5.8 Comment connaître le nombre d'éléments d'un tableau ?
On peut utiliser une macro de ce type là :
#define NELEMS(n) (sizeof(n) / sizeof *(n))
5.9 Quelle est la différence entre char a[] et char * a?
Il faut bien se rappeler qu'en C, un tableau n'est pas un
pointeur, même si à l'usage ça y ressemble beaucoup.
char a[] déclare un tableau de char de taille
inconnue (type incomplet). Dès l'initialisation, par
char a[] = "Hello";
a se transforme en char[6], soit un tableau de
six char (type complet).
Il arrive souvent de voir la confusion entre « tableau » et «
pointeur constant » (moi même je la fais parfois ;-) )
Ce « pointeur constant » est inspiré par K&R, 1ère
édition. C'était une métaphore malheureuse de K&R qui voulait
exprimer qu'un tableau se comporte en général comme
une « valeur (rvalue) du type pointeur », et bien entendu une
valeur est toujours constante. Par cette métaphore ils ont essayé
d'expliquer pourquoi on ne pouvait pas prendre l'adresse d'un
tableau.
Malheureusement ceci n'explique pas pourquoi sizeof a
donne la taille du tableau et non la taille d'un pointeur.
Sur ce sujet, la norme est plus précise en disant qu'un tableau
dans une expression --- sauf dans &a ou
sizeof a ou dans des initialisations par des chaînes
littérales "xyz" --- est converti automatiquement en
une valeur du type pointeur qui pointe sur l'élément initial du
tableau (merci à Horst Kraemer pour ces précisions).
Voir aussi la question 7.1.
5.10 Peut-on déclarer un type sans spécifier sa structure ?
Plus précisément, est-il possible de définir un type de données
dont on veut cacher à l'utilisateur de la bibliothèque
l'implémentation ?
Oui c'est possible, en encapsulant ce type dans une structure.
Il y a au moins deux méthodes. La première est de définir une
structure (publique) qui contient un pointeur void *, qui
pointe vers une variable du type privé. Ce type privé est défini
avec les fonctions, dans un .c.
Il faut alors prévoir des fonctions d'initialisation et de
destruction des objets.
Une autre solution consiste à déclarer une structure qui contient
une donnée du type à protéger, et à accéder aux types par des
pointeurs uniquement.
Voici un exemple :
/* data.h */
struct Data_s ;
typedef struct Data_s Data_t ;
extern Data_t * DataNew(int x, int y);
extern int DataFonction(Data_t * this);
/* data.c */
#include <stdio.h>
#include <stdlib.h>
#include "data.h"
struct Data_s {
int x;
int y;
};
Data_t * DataNew(int x , int y) {
Data_t * pData;
pData = malloc(sizeof *pData) ;
if (pData) {
pData->x = x;
pData->y = y;
}
return pData;
}
int DataFonction(Data_t * this) {
if (!this)
return 0;
return (this->x * this->x) + (this->y * this->y);
}
/* main.c */
#include <stdio.h>
#include "data.h"
int main(void) {
Data_t *psData;
psData = DataNew(3, 4);
printf("%d\n", DataFonction(psData));
return 0;
}
(Merci à Yves Roman pour cet exemple.)
6. Structures, unions, énumérations
6.1 Quelle est la différence entre struct et typedef struct ?
struct x1 { ... };
typedef struct { ... } x2;
La première écriture déclare un tag de structure
x1. La deuxième déclare un nouveau type nommé
x2.
La principale différence est l'utilisation.
La deuxième écriture permet un peu plus l'abstraction de type.
Cela permet de cacher le véritable type derrière x2,
l'utilisateur n'étant pas sensé savoir que c'est une structure.
struct x1 v1;
x2 v2;
6.2 Une structure peut-elle contenir un pointeur sur elle-même ?
Oui.
Voir aussi la question 5.2.
6.3 Comment implémenter des types cachés (abstraits) en C ?
L'une des bonnes façons est d'utiliser des pointeurs sur des
structures qui ne sont pas publiquement définies. On peut en plus
cacher les pointeurs par des typedef.
Voir aussi la question 5.10.
6.4 Peut-on passer des structures en paramètre de fonctions ?
Oui, c'est parfaitement autorisé. Toutefois, rappelons que les
paramètres en C sont passés par valeurs et copiés dans la pile.
Pour des grosses structures, il est préférable de passer un
pointeur dessus, et éventuellement un pointeur constant.
6.5 Comment comparer deux structures ?
Il n'existe pas en C d'opérateur ou de fonction
pour comparer deux structures. Il faut donc le faire à la main,
champs par champs.
Une comparaison bit à bit n'est pas portable, et risque de ne pas
marcher, en raison du padding (alignement sur
certains octets).
6.6 Comment lire/écrire des structures dans des fichiers ?
Il faut utiliser les fonctions fread() et
fwrite(). Attention : les fichiers obtenus ne sont pas
portables.
Une méthode plus portable consiste à enregistrer les structures
dans un fichier texte.
6.7 Peut-on initialiser une union ?
La norme prévoit d'initialiser le premier membre d'une union.
Pour le reste, ce n'est pas standard.
En C99, on peut initialiser les champs d'une union :
union { /* ... */ } u = { .any_member = 42 };
6.8 Quelle est la différence entre une énumération et des #define ?
Il y a peu de différences.
L'un des avantages de l'énumération est que les valeurs numériques
sont assignées automatiquement.
De plus, une énumération se manipule comme un type de données.
Certains programmeurs reprochent aux énumérations de réduire le
contrôle qu'ils ont sur la taille des variables de type énumération.
6.9 Comment récupérer le nombre d'éléments d'une énumération ?
Ceci n'est possible de façon automatique que si les valeurs se
suivent.
typedef enum { A, B, C, D} type_e;
Dans cette énumération, les valeurs sont données par le
compilateur dans l'ordre croissant, à partir de 0 et avec un pas de
1. Ainsi, le nombre d'éléments de type_e est D + 1.
On peut rajouter un élément à l'énumération qui donne directement
le nombre d'éléments :
typedef enum { RED, BLUE, GREEN, YELLOW, NB_COLOR} color_e;
le nombre d'éléments de color_e est donc NB_COLOR.
Si l'on est obligé, pour des raisons diverses et variées,
de fixer d'autres valeurs aux constantes, alors cette solution ne
marche pas. On peut toujours rajouter un champs dans l'énumération
et fixer manuellement sa valeur.
6.10 Comment imprimer les valeurs symboliques d'une énumération ?
On ne peut pas le faire simplement. Il faut écrire une fonction
qui le fait. Un problème qui se pose alors est la maintenance, car
une modification des valeurs de l'énumération entraîne la
nécessité d'une mise à jour de cette fonction.
Voici un code qui limite les problèmes de mise à jour :
/* Fichier foo.itm */
ITEM(FOO_A)
ITEM(FOO_B)
ITEM(FOO_C)
/**/
/* Fichier foo.h */
#ifndef FOO_H
#define FOO_H
#define ITEM(a) a,
typedef enum {
#include "foo.itm"
FOO_NB
} foo_t;
#undef ITEM
#define ITEM(a) #a,
const char * const aFoo[] = {
#include "foo.itm"
};
#undef ITEM
#endif
/**/
/* Fichier foo.c */
#include <stdio.h>
#include "foo.h"
int main(void) {
foo_t foo;
for (foo = FOO_A; foo < FOO_NB; foo++) {
printf("foo=%d ('%s')\n",foo, aFoo[foo]);
}
return 0;
}
Merci à Emmanuel Delahaye pour cet exemple.
7. Tableaux et pointeurs
7.1 Quelle est la différence entre un tableau et un pointeur ?
Un tableau n'est pas un pointeur.
Un tableau est une zone mémoire pouvant contenir N
éléments consécutifs de même type.
Un pointeur est une zone mémoire qui contient l'adresse d'une
autre zone mémoire. Toutefois, dans un grand nombre de cas, tout
se passe comme si c'était la même chose.
À ce titre, il faut bien faire la différence entre a[i]
pour un tableau et ap[i] pour un pointeur.
Voici un exemple :
char a[] = "Bonjour";
char *ap = "Au revoir";
L'expression a[3] signifie que l'on accède aux quatrième
élément du tableau. ap[3] signifie que l'on accède à la
zone mémoire pointée par (ap+3).
Autrement dit, a[3] est l'objet situé 3 places après
a[0] (a est le tableau entier),
alors que ap[3] est l'objet situé 3 places après l'objet
pointé par ap. Dans l'exemple, a[3] vaut
'j' et ap[3] vaut 'r'.
Voir aussi la question 5.9.
7.2 Comment passer un tableau à plusieurs dimensions en paramètre d'une fonction ?
Ce n'est pas si facile.
La règle de base est qu'il faut connaître la taille des
N-1 dernières dimensions. Pour un tableau à deux
dimensions, la deuxième doit être connue, et la fonction doit être
déclarée ainsi :
int f1(int a[][NCOLUMNS]);
/* a est un tableau a deux dimensions (cf. remarque) */
int f2(int (*ap)[NCOLUMNS]);
/* ap est un pointeur sur un tableau */
Si elle n'est pas connue, il faut passer la taille du tableau en
paramètre (ligne ET colonne) et un pointeur sur le tableau :
int f(int * a, int nrows, int ncolumns);
On accède aux éléments du tableau ainsi :
a[i * ncolumns + j] /* element de la ieme ligne
* et de la jeme colonne */
Une remarque :
Dans une déclaration de paramètre
int f1(int a[][NCOLUMNS])
ou
int f1(int a[42][NCOLUMNS])
a est un pointeur sur int[NCOLUMS] malgré
l'écriture. La déclaration est interprétée comme
int (*a) [NCOLUMS]
Ainsi les déclarations de f1() et f2() dans
l'exemple initial sont exactement les mêmes.
7.3 Comment allouer un tableau à plusieurs dimensions ?
La première solution est d'allouer un tableau de pointeurs, puis
d'initialiser chacun de ces pointeurs par un tableau dynamique.
#include <stdlib.h>
int ** a = malloc(nrows * sizeof *a );
for(i = 0; i < nrows; i++)
a[i] = malloc(ncolumns * sizeof *(a[i]));
Dans la vraie vie, le retour de malloc() doit être
vérifié.
Une autre solution est de simuler un tableau multi-dimensions
avec une seule allocation :
int *a = malloc(nrows * ncolumns * sizeof *a);
L'accès aux éléments se fait par :
a[i * ncolumns + j] /* element de la ieme ligne
* et de la jeme colonne */
7.4 Comment définir un type pointeur de fonction ?
On utilise typedef, comme pour n'importe quel autre
type. Voici un exemple :
int f(char * sz); /* une fonction */
int (*pf)(char *);/* un pointeur sur une fonction */
typedef int (*pf_t)(char *);
/* un type pointeur sur fonction*/
Il est toutefois préférable de ne pas cacher le pointeur dans un
typedef. La solution suivante est plus jolie :
typedef int (f_t)(char *); /* un type fonction */
f_t * pf; /* un pointeur sur ce type */
On l'utilise alors de cette façon :
pf = f;
int ret = pf("Merci pour cette reponse");
Voir aussi la question 5.7.
7.5 Que vaut (et signifie) la macro NULL ?
NULL est une macro qui représente une valeur spéciale
pour désigner un pointeur nul lorsque converti au type approprié.
Elle est définie dans <stddef.h> ou dans
<stdio.h>.
La valeur réelle de NULL est dépendante de
l'implémentation, et n'est pas nécessairement un pointeur, ni de
type pointeur. Des valeurs possibles sont ((void *)0) ou
0.
NULL permet de distinguer les pointeurs valides des
pointeurs invalides. Par exemple, malloc() renvoie une
valeur comparable à NULL quand elle échoue.
Voir aussi la question 12.3.
7.6 Que signifie l'erreur « NULL-pointer assignment » ?
Cela signifie que vous avez essayé d'accéder à l'adresse 0 de la
mémoire. Vous avez probablement déréférencé un pointeur
NULL, ou oublié de tester la valeur retour d'une
fonction, avant de l'utiliser.
7.7 Comment imprimer un pointeur ?
La seule manière prévue par la norme pour imprimer correctement un
pointeur est d'utiliser la fonction printf() avec le
code de format %p.
Le pointeur doit être d'abord casté en un pointeur
générique void *.
char * p;
printf("Pointeur p avant initialisation: %p\n",
(void *)p);
7.8 Quelle est la différence entre void * et char * ?
Le premier est un pointeur générique, qui peut recevoir l'adresse
de n'importe quel type d'objet.
Le second est un pointeur sur un caractère, généralement utilisé
pour les chaînes.
Avant la norme ANSI, le type void n'existait pas. C'était
donc char * qui était utilisé pour faire des pointeurs
génériques. Depuis la norme, ce n'est plus valide.
De nombreux programmeurs ont toutefois gardé cette habitude,
notamment dans le cast de fonctions comme
malloc() (cf. 12.1).
8. Chaînes de caractères
8.1 Comment comparer deux chaînes ?
Pour comparer deux chaînes entre elles, il faut utiliser la
fonction strcmp(), et non l'opérateur
==. Celui-ci comparera les pointeurs entre eux, ce qui
n'est probablement pas ce qui est voulu !
const char * sz = "non";
if(strcmp(sz, "oui") == 0) {
/* sz et "oui" sont egaux */
}
Il existe aussi la fonction strncmp() qui permet de
contrôler la longueur de comparaison.
8.2 Comment recopier une chaîne dans une autre ?
Il faut utiliser la fonction strcpy(),
et non l'opérateur d'affectation =.
Il faut s'assurer que l'on dispose d'espace suffisant dans la
chaîne cible avant d'utiliser strcpy(), qui ne fait
aucun contrôle de débordement.
Pour une copie plus sécurisée, on préférera la fonction
strncpy().
8.3 Comment lire une chaîne au clavier ?
Il y a de nombreuses fonctions qui le font.
Le plus sûr est d'utiliser fgets().
char tab[20];
fgets(tab, sizeof tab, stdin);
Voici un exemple complet d'utilisation propre de
fgets() pour la lecture d'une ligne, avec un contrôle
d'erreurs. Cette fonction est fournie par Emmanuel Delahaye.
#include <stdio.h>
#include <string.h>
int get_line(char *buf, size_t size) {
int ret; /* 0=Ok 1=Err 2=Incomplet
* On peut aussi definir des constantes. (Macros, enum...)
*/
if (fgets(buf, size, stdin) != NULL) {
char *p = strchr(buf, '\n'); /* search ... */
if (p != NULL) {
*p = 0; /* ... and kill */
ret = 0;
}
else {
ret = 2;
}
}
else {
ret = 1;
}
return ret;
}
Il peut être intéressant dans certains cas de faire un traitement
particulier dans le cas 2 (lecture incomplète), comme vider le
buffer du flux ou redimentionner la zone de réception (voir la question
14.5).
La fonction gets() est à proscrire, car il n'y a aucun
contrôle de débordement, ce qui peut engendrer de nombreux bugs
(stack overflow) (cf. 8.7).
8.4 Comment obtenir la valeur numérique d'un char (et vice-versa) ?
En C, un char est un petit entier. Il n'y a donc aucune
conversion à faire. Quand on a un char, on a aussi sa
valeur, et vice-versa.
8.5 Que vaut sizeof(char) ?
Un char vaut et vaudra toujours 1 indépendamment de
l'implémentation. En effet, les tailles d'allocation en C se
calculent en char (size_t). Or, un char
a une taille de 1 char. Donc
sizeof(char) == (size_t) 1
Pour autant, un char ne fait pas forcément 8 bits (un
octet) (voir aussi 16.2).
8.6 Pourquoi sizeof('a') ne vaut pas 1 ?
En C, les caractères constants sont des int, et non des
char. Ainsi,
sizeof('a') == sizeof(int)
ce qui peut valoir 2 ou 4 sur votre machine, ou autre chose.
8.7 Pourquoi ne doit-on jamais utiliser gets() ?
Serge Paccalin donne l'exemple suivant :
#include <stdio.h>
int main(void)
{
char chaine[16];
printf("Tapez votre nom :\n");
gets(chaine);
printf("Vous vous appelez %s.\n",chaine);
return 0;
}
Quand on tape une chaîne de plus de 15 caractères, rien ne va plus :
gets() accepte sans broncher la chaîne mais lorsqu'il
est question de la stocker dans
char chaine[16]
on peut obtenir un magnifique
Segmentation fault (core dumped).
Dans certains cas, avec certains compilateurs
sur certaines machines et certains OS, il est possible que ça
passe. Mais attention ! C'est un leurre, et le changement de cible
démontrera la malfaçon.
8.8 Pourquoi ne doit-on presque jamais utiliser scanf() ?
scanf() est une fonction de la bibliothèque standard, qui est
souvent la première que l'on apprend pour lire des données au clavier.
Cette fonction n'est pas plus dangereuse qu'une autre, à condition de
bien savoir l'utiliser, ce qui n'est pas donné à tout le monde.
Par exemple, regardez le programme suivant donné par Serge
Paccalin :
#include <stdio.h>
int main(void) {
int val = 0;
while (val != 1){
printf("Tapez un nombre"
"(1 pour arreter le programme) :\n");
scanf(" %d",&val);
printf("Vous avez saisi %d.\n",val);
}
return 0;
}
Et il explique : « Quand le programme demande un nombre, taper
"toto" suivi de la touche
Entrée. Le programme part en boucle parce que tous les
scanf() successifs butent sur "toto" qui reste
indéfiniment dans stdin. »
Dans la plupart des cas, l'utilisation de la fonction fgets()
sera plus simple et moins risquée.
scanf() est une fonction qui peut être utilisée dans
certaines conditions, et à condition de bien savoir ce que l'on
fait. L'utilisation de scanf() pour la lecture de
nombres (entiers ou flottants) est l'une des plus acceptable.
Par contre, la lecture d'une chaîne avec scanf() sans
contrôle de format est aussi dangereux que gets().
scanf("%s", astring) ;
est donc a proscrire.
scanf() n'est préférable à fgets() que dans
le cas ou l'on veut lire mot par mot et non ligne par ligne,
et conserver le reste dans le buffer du flux
d'entrée.
Sinon, lire un mot avec fgets() ne pose pas de problème
et est même plus simple.
scanf("%4s", astring) ;
Cette construction, par exemple, ne pose pas les problèmes cités
plus haut (si le contrôle d'erreurs est fait), et est parfois
utile.
Voir aussi les questions 8.7 et 8.3
9. Fonctions et prototypes
9.1 Pour commencer ...
Il y a trois notions :
- déclaration,
- définition,
- prototype.
La déclaration d'une fonction, c'est annoncer que tel
identificateur correspond à une fonction, qui renvoie tel type. La
définition d'une fonction est une déclaration où, en plus, on
donne le code de la fonction elle-même. Le prototype est une
déclaration de fonction où le type des arguments est également
donné.
Par exemple :
int f(); /* declaration de f(), renvoyant un int, pas de prototype */
int f(void);/* declaration de f(), renvoyant un int, prototype (0 arg) */
int f(void) /* definition de f() avec declaration avec prototype */
{
return 42;
}
int f(x) /* definition de f() avec declaration sans prototype */
int x;
{
return x;
}
int f() /* definition de f() avec declaration sans prototype */
{
return 42;
}
Ce qui n'est pas possible :
- avoir une définition sans déclaration,
- avoir un prototype sans déclaration.
Ce qui est autorisé :
- appeler une fonction déclarée, qu'elle ait un prototype ou
pas.
Ce qui était autorisé en C90 mais ne l'est plus en C99 :
- appeler une fonction non déclarée ; l'appel valait
déclaration dite « implicite », sans prototype et avec type de
retour int.
Ce qui est encore autorisé en C99 mais disparaîtra bientôt :
- déclarer/définir une fonction sans prototype.
Ce qu'il faut faire quand on veut programmer lisiblement, en
détectant les bugs et en gardant du code maintenable et compatible
avec le futur :
- utiliser des déclarations et définitions avec prototype.
9.2 Qu'est-ce qu'un prototype ?
Un prototype est une signature de fonction.
Comme tout objet en C, une fonction doit être déclarée avant son
utilisation. Cette déclaration est le prototype de la fonction.
Le prototype doit indiquer au compilateur le nom de la fonction,
le type de la valeur de retour et le type des paramètres (sauf
pour les fonctions à arguments variables, comme printf().
(cf. 9.6).
int fa(int a, char const * const b);
int fb(int, char const * const);
Les noms de paramètre sont optionnels, mais il est fortement
conseillé de les laisser. Cela donne une bonne indication sur
leurs rôles.
Les fonctions de la bibliothèque ont également leur prototype.
Avant l'utilisation de celles-ci, il faut inclure les fichiers
d'en-tête contenant les prototypes.
Par exemple, le prototype de malloc() se trouve dans
stdlib.h.
Certains préfèrent ajouter le mot clé extern au
prototype, afin de rester cohérent avec la déclaration des
variables globales.
Voir aussi les questions 9.3,
12.1, 13.5 et
14.17.
9.3 Où déclarer les prototypes ?
Un prototype de fonction doit être déclaré avant l'utilisation de
la fonction. Pour une plus grande lisibilité, mais aussi pour
simplifier la maintenance du code, il est conseillé de regrouper
tous les prototypes
d'un module (fichier xxx.c) dans un en-tête
(<xxx.h>). Ce dernier n'a plus alors qu'à être inclus
dans le code qui utilise ces fonctions. C'est le cas des fonctions de
la bibliothèque standard.
Voir aussi les questions 13.5 et
9.10.
9.4 Quels sont les prototypes valides de main() ?
La fonction main() renvoie toujours un int.
Les prototypes valides sont :
int main(void);
int main(int argc, char * argv[]);
Tout autre prototype n'est pas du tout portable et ne doit jamais
être utilisé (même s'il est accepté par votre compilateur).
En particulier, vous ne devez pas terminer la fontion
main() sans retourner une valeur positive (non nulle en
cas d'erreur).
Les valeurs de retour peuvent être 0,
EXIT_SUCCESS ou EXIT_FAILURE.
On pourra aussi rencontrer (sous Unix) le
prototype suivant :
int main (int argc, char* argv[], char** arge);
dans le but d'utiliser les variables d'environnement du
shell actif. Ce n'est ni portable ni standard,
d'autant plus que les fonctions getenv(),
setenv() et putenv() le sont et suffisent
largement.
Enfin, rappelons que le prototype suivant
int main () ;
est parfaitement valide en C++ (et est synonyme du premier
présenté ici), mais ne l'est pas en C.
9.5 Comment printf() peut recevoir différents types d'arguments ?
printf() est une fonction à nombre variable de
paramètres. Son prototype est le suivant :
int printf(const char * format, ...); /* C 90 */
int printf(const char * restrict format, ...); /* C 99 */
Le type et le nombre des paramètres n'est pas défini dans le
prototype, c'est le traitement effectué dans la fonction qui doit
les vérifier.
Pour utiliser cette fonction, il est donc impératif d'inclure
l'en-tête <stdio.h>.
Pour écrire une fonction de ce type, lire la question suivante
(9.6).
9.6 Comment écrire une fonction à un nombre variable de paramètres ?
La bibliothèque standard fournit des outils pour faciliter la
gestion de ce type de fonctions.
On les trouve dans l'en-tête <stdarg.h>.
Le prototype d'une fonction à nombre variable de paramètres doit
contenir au moins un paramètre explicite, puis se termine par ...
Exemple :
int f(int nombre, ...);
Il faut, d'une façon ou d'une autre, passer dans les paramètres le
nombre d'arguments réellement transmis.
On peut le faire en donnant ce nombre explicitement (comme
printf()), ou passer la valeur NULL en dernier.
Attention toutefois avec la valeur NULL dans ce cas.
En effet, NULL n'est pas nécessairement une valeur du
type pointeur mais une valeur qui donne un pointeur
nul si elle est affectée ou passée ou comparée à un type
pointeur. Le passage d'une valeur à un paramètre n'est pas une
affectation à un pointeur mais une affectation qui obéit aux lois
spéciales pour les paramètres à nombre variable (ou pour les
paramètres d'une fonction sans prototype). Les lois de promotion
pour les types arithmétiques sont appliquées). Si NULL est
défini par
#define NULL 0
alors (int)0 est passé à la fonction. Si un pointeur n'a
pas la même taille qu'un int ou si un pointeur nul n'est
pas représenté par « tous les bits 0 » le passage d'un 0 ne passe
donc pas de pointeur nul. La méthode portable est
donc
f(toto,titi,(void*)NULL);
ou
f(toto,titi,(void*)0);
C'est le seul cas où il faut caster NULL parce qu'il ne s'agit pas
d'un contexte syntactique « de pointeur », seulement d'un contexte
« de pointeur par contrat ».
Après cela, les fonctions va_start(),
va_arg() et va_end() permettent de parcourir
la liste des paramètres.
Voici un petit exemple :
#include <stdarg.h>
int vexemple(int nombre, ...){
va_list argp;
int i;
int total = O;
if(nombre < 1)
return 0;
va_start(argp, nombre);
for (i = 0; i < nombre; i++) {
total += va_arg(argp, int);
}
va_end(argp);
return total;
}
Merci à Horst Kraemer pour ces remarques.
9.7 Comment modifier la valeur des paramètres d'une fonction ?
En C, les paramètres sont passés par valeur. Dans la plupart des
implémentations, cela se fait par une copie dans la pile.
Lors du retour de la fonction, ces valeurs sont simplement
dépilées, et les modifications éventuelles sont perdues.
Pour pallier cela, il faut simuler un passage des paramètres par
référence, en passant un pointeur sur les variables à modifier.
Voici l'exemple classique de l'échange des valeurs entre deux
entiers :
void echange(int * a, int * b) {
int tmp = *a;
*a = *b;
*b = tmp;
}
9.8 Comment retourner plusieurs valeurs ?
Le langage C ne permet pas aux fonctions de renvoyer plusieurs
objets. Une solution consiste à passer l'adresse des objets à
modifier en paramètre.
Une autre solution consiste à renvoyer une structure, ou un
pointeur sur une structure qui contient l'ensemble des valeurs.
Généralement, quand on a ce genre de choses à faire, c'est qu'il
se cache une structure de données que l'on n'a pas identifiée.
La pire des solutions est d'utiliser des variables globales.
9.9 Peut-on, en C, imbriquer des fonctions ?
Non, on ne peut pas.
Les concepteurs ont jugé cela trop compliqué à mettre en oeuvre
(portée des variables, gestion de la pile etc.).
Certaines implémentations, comme GNU CC le supportent
toutefois. Ceci dit, on peut très bien s'en passer, en utilisant
des pointeurs sur les structures de données à partager, ou en
utilisant des pointeurs de fonctions.
9.10 Qu'est-ce qu'un en-tête ?
.
Un en-tête est un ensemble de déclarations, définitions et prototypes
nécessaires pour compiler et pour utiliser un module.
Par exemple, pour utiliser les fonctions d'entrées/sorties de la
bibliothèque standard, il est nécessaire d'inclure dans son programme
l'en-tête <stdio.h>.
Par abus, on parle souvent de fichier d'en-tête, car historiquement,
et encore aujourd'hui pour de nombreuses implémentations, ces en-têtes
sont des fichiers. C'est également le cas pour les en-têtes personnels.
Toutefois, la norme n'exige pas que les en-têtes standards soient des
fichiers à proprement parlé.
Traduit en HTML par faq2html.pl le Wed Nov 3 05:42:13 2010 pour le site Web Usenet-FR.