précédent  index  suivant

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 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).

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.)


précédent  index  suivant