const
et #
define
?const
avec des pointeurs ?char a[]
et char * a
?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
span 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).
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.
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.
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.
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.
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; }
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.
On peut utiliser une macro de ce type là :
#define NELEMS(n) (sizeof(n) / sizeof *(n))
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.
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.)