Obligement - L'Amiga au maximum

Vendredi 26 mai 2017 - 21:02  

Translate

En De Nl Nl
Es Pt It Nl


Rubriques

 · Accueil
 · A Propos
 · Articles
 · Galeries
 · Glossaire
 · Hit Parade
 · Liens
 · Liste jeux Amiga
 · Quizz
 · Téléchargements
 · Trucs et astuces


Articles

 · Actualité (récente)
 · Actualité (archive)
 · Comparatifs
 · Dossiers
 · Entrevues
 · Matériel (tests)
 · Matériel (bidouilles)
 · Points de vue
 · En pratique
 · Programmation
 · Reportages
 · Tests de jeux
 · Tests de logiciels
 · Tests de compilations
 · Articles divers

 · Articles in english
 · Articles in other languages


Twitter

Suivez-nous sur Twitter




Liens

 · Sites de téléchargements
 · Associations
 · Pages Personnelles
 · Moteurs de recherche
 · Pages de liens
 · Constructeurs matériels
 · Matériel
 · Autres sites de matériel
 · Réparateurs
 · Revendeurs
 · Presse et médias
 · Programmation
 · Développeurs logiciels
 · Logiciels
 · Développeurs de jeux
 · Jeux
 · Autres sites de jeux
 · Scène démo
 · Divers
 · Informatique générale


Jeux Amiga

0, A, B, C, D, E, F,
G, H, I, J, K, L, M,
N, O, P, Q, R, S, T,
U, V, W, X, Y, Z


Trucs et astuces

0, A, B, C, D, E, F,
G, H, I, J, K, L, M,
N, O, P, Q, R, S, T,
U, V, W, X, Y, Z


Glossaire

0, A, B, C, D, E, F,
G, H, I, J, K, L, M,
N, O, P, Q, R, S, T,
U, V, W, X, Y, Z


Partenaires

Annuaire Amiga

Amedia Computer

Relec

Hit Parade


Soutien

N'hésitez pas à soutenir le projet Obligement



Contact

David Brunet

Courriel

 


Programmation : C - les variables (les types énumérés, le mot-clé typedef, classes de stockage)
(Article écrit par Batchman et extrait d'A-News (Amiga News) - novembre 1989)


Telle la misérable petite fourmi traversant la savane immense et impitoyable de la connaissance, nous avançons dans notre découverte du langage C. Après avoir vu la structure des programmes, assimilé les types de base, ingurgité les types composés, souffert les pointeurs, nous en terminons avec tout ce qui concerne les variables. Nous parlerons des types énumérés, des classes de stockage et de quelques autres subtilités.

Les types énumérés

N'ayez aucune crainte, ils ne sont pas dangereux, surtout si vous ne les attaquez pas. Qu'est-ce qu'un type énuméré ? C'est un type de variable qui est défini par un ensemble de valeurs que vous donnez. Vous les énumérez. Exemple tout de suite :

enum Type_Feu {
rouge,
orange,
vert,
clignotant,
eteint
};

Ceci définit un type dont les variables ne peuvent prendre que ces valeurs. C'est plus parlant pour celui qui écrit un programme (et pour ceux qui le lisent) d'écrire :

Feu = vert;

...que d'écrire :

Feu = 3;

...en ayant quelque part LE renseignement utile disant que "3" correspond à "vert". En général, on a toujours besoin de ce genre de renseignement et il faut le rechercher en permanence (heu... éteint, c'est combien déjà ?). Ici, plus de problème, tout est clair.

Pour déclarer une variable "Feu" du type "Type_Feu", il faut écrire :

enum "Type_Feu" Feu;

Il est possible, comme pour les structures, de déclarer des variables en même temps que le type. Par exemple :

enum booleen {
vrai,
faux
} flag;

Mais, pour la même raison (ça ne met pas en relief la déclaration de la variable (flag) par rapport à celle du type), il vaut mieux ici aussi l'éviter.

Vous pouvez enfin donner une valeur aux constantes qui composent votre type. Par exemple, si cela vous arrange, vous pouvez écrire :

enum Type_Feu {
rouge 1,
orange = 3,
vert = 8
};

Mais attention, l'intervalle de valeurs couvrira toutes les valeurs entières jusqu'à 8. Voilà en effet la faille dans ce tableau idyllique : derrière ces valeurs que vous énumérez, le compilateur cache de banals entiers. Et, à part un léger contrôle de type, facilement évitable au demeurant, vous pouvez faire à peu près n'importe quoi. En particulier, une variable du type "Type_Feu" peut prendre n'importe quelle valeur entre 0 et 8, ce qui est contraire à la définition que vous donnez.

Autre problème, le type énuméré est juste une notion abstraite. En d'autres termes, il est impossible d'afficher les valeurs littérales d'une variable de type "Type_Feu". C'est dommage car cela restreint beaucoup cette utilisation. Mais c'est compréhensible si l'on pense que ces valeurs cachent en réalité des entiers.

Par exemple, si vous écrivez à l'écran la valeur de la variable "Feu", vous obtiendrez une valeur numérique allant de 0 (rouge) à 4 (éteint) mais en aucun cas les chaînes de caractères rouge à éteint.

En résumé, on utilise guère les types énumérés, en tout cas, seulement quand on n'a pas besoin d'afficher leurs valeurs. Même dans ce cas, cela serait possible mais il faudrait utiliser une grille de codage et ça devient trop lourd.

Finalement, même si on ne s'en sert pas très souvent, il faut les connaître car ça peut être très utile.

Le mot-clef "typedef"

Voici quelque chose de bien pratique. Ce mot-clef permet de donner un autre nom à un type de données. Tout de suite, un exemple très apprécié des fainéants :

typedef struct TypeConstruit Construit;

Si vous avez déjà déclaré "TypeConstruit" en tant que structure, cette ligne vous permet de définir le nouveau type "Construit". Par conséquent, au lieu de déclarer vos structures par :

struct TypeConstruit Machin;

...vous n'avez qu'à écrire :

Construit Machin;

Évidemment, c'est plus court. Mais c'est peut-être moins parlant car ça ne dit pas clairement à celui qui lit le listing que "Machin" est une structure.

Autre utilisation plus astucieuse (il faut dire "plus génie logiciel"), supposons que vous utilisez dans un programme des variables dont les valeurs vont de -4000000 à +4000000. Vous écrivez en Lattice, voyez que le type "int" est codé sur 32 bits, donc qu'il convient parfaitement et vous déclarez des centaines de variables, dans tous les fichiers de votre programme avec le type "int". Et puis, un de vos copains (plus pour longtemps...) vient vous voir et compile votre programme avec son Manx. C'est là que les choses se gâtent : "Hey mec, ce n'est pas cool, ton programme, il fait rien que de se planter !". "Normal", pensez-vous, "le Manx code les entiers sur 16 bits par défaut. Il suffit de remplacer les "int" dans mes fichiers par des "long". Misère ! J'en ai partout ! Des heures de travail pour porter mon programme sur Manx...". Sans compter que vous risquez d'oublier des modifications et donc, de vous retrouver avec les bons bogues des familles...

Revenons quelques lignes en arrière et examinons le comportement du génilogiciéliste avisé. Il ne va pas déclarer ses variables du type "int" mais du type "Donnee", par exemple. Et écrire bien en vue dans son programme :

typedef int Donnee;

Si l'on veut ensuite changer le type "Donnee" en "long", pas de problème, il n'y a qu'un seul mot à changer.

Autres exemples :

typedef char *chaine;

...déclare le type pointeur de caractères (donc aussi de chaînes). Vous pouvez donc écrire après :

chaine toto "Bonjour";

...pour déclarer la chaîne toto.

typedef int tableau[10];

...définit le type "tableau de dix caractères".

Les classes de stockage

Voici des notions extrêmement importantes pour sortir de l'ère de la bidouille et entrer dans celle de la programmation intelligente. Les classes de stockage sont la plus grande invention depuis celle de la touche F10. En effet, en jouant sur ces classes de stockage, vous pouvez augmenter la performance de vos programmes, diminuer leur taille, et surtout faciliter leur compréhension.

Les classes de stockages définissent le statut de vos variables, la façon dont elles sont mémorisées. Elles sont au nombre de quatre :
  • auto.
  • static.
  • extern.
  • register.
Chacune de ces classes correspond à un traitement différent, voyons-les en détail.

Les variables automatiques (auto)

C'est le nom utilisé en C pour parler des variables dynamiques. Si vous n'avez jamais vu ce mot dans un programme C, c'est parce qu'il est facultatif. Lorsque vous déclarez une variable dynamique (à l'intérieur d'une fonction), cette classe de stockage est donnée implicitement. Mais vous pourriez très bien écrire :

void fonction(){

  auto int entier;
...
}

Cette classe de stockage correspond à une allocation dynamique de la variable entier. Nous en avons déjà parlé plusieurs fois donc je serai bref : une variable dynamique est réservée sur la pile à l'entrée de la fonction ; elle n'est connue du programme qu'à l'intérieur de cette fonction. Enfin, elle est détruite à la fin de l'exécution de cette fonction.

Autrement dit, au prochain appel de la fonction, cette variable sera recréée mais il ne faut pas espérer y retrouver la valeur qu'elle avait lors du dernier appel (ça peut se produire éventuellement si vous appelez la même fonction deux fois de suite, mais programmer comme ça, c'est plus dangereux que de rouler sur l'autoroute à contresens, sans sa ceinture, avec deux grammes de sang par litre d'alcool).

Naturellement, elle n'est pas connue des autres fonctions, même celles qui sont appelées par celle où elle se trouve.

La classe static

Elle désigne les variables statiques et correspond à deux utilisations différentes.

1. A l'intérieur d'une fonction, elle déclare une variable rémanente. C'est-à-dire que sa valeur est conservée d'un appel de la fonction à l'autre, mais cette variable est tout de même locale, connue uniquement de cette fonction. C'est l'inverse d'une variable automatique, que nous venons de voir ; la variable, bien que locale à la fonction, est ici statique. Deux exemples pour illustrer la différence :

void fonction(){

  int toto;

  toto = toto + 1;
  printf("%d\n",toto);
}

(on aurait pu écrire : "auto int loto;", c'était strictement équivalent)

void fonction(){

  static int toto;

  toto = toto + 1;
  printf("%d\n",toto);
}

Si on appelle plusieurs fois de suite ces deux fonctions, que se passe-t-il ? Dans le premier cas, la variable "toto" est allouée, non initialisée. Sa valeur est incrémentée, puis affichée. Plusieurs appels successifs de cette fonction ne rendront pas une suite de valeurs croissantes : ça peut se produire mais rien n'est moins sûr puisque la variable est allouée et libérée à chaque appel de la fonction.

Dans le deuxième cas, vous aurez à coup sûr droit à une suite croissante d'entiers car la variable "toto" est maintenant statique (ou rémanente). Sa valeur est conservée entre deux appels parce qu'elle n'est pas détruite à la fin de la fonction. On dit que c'est une variable privée.

Pourquoi utiliser une variable locale à une fonction, on aurait pu la déclarer globale au programme (et donc statique aussi) ? me direz-vous. Effectivement, déclarer une variable locale statique n'est pas indispensable mais c'est un bon style de programmation. Car, si vous estimez que cette variable ne doit être connue que de cette fonction et d'aucune autre, c'est la solution à utiliser. Ça évitera ensuite toute interaction, erreur ou autre tracasserie lors de modifications ultérieures du programme.

2. Hors d'une fonction, cette classe de stockage déclare une variable locale à un fichier. Je m'explique : nous avons déjà vu que toutes les variables déclarées hors d'une fonction sont statiques. Sans le mot-clé static, elles sont globales au programme, alors qu'avec static, elles sont locales à un fichier.

Nous y reviendrons en détail lorsque nous parlerons de compilation séparée, mais vous savez tous que le compilateur génère pour chaque fichier source un fichier objet avec le suffixe ".o". A partir d'un ou de plusieurs fichiers objets, l'éditeur de liens (linker) produit un exécutable.

Une variable déclarée sans static, hors d'une fonction, est connue dans tout le programme, vous pouvez la nommer dans les autres fichiers le constituant (à une condition que nous verrons juste après).

Une variable déclarée avec stade, hors d'une fonction, est ignorée par l'éditeur de lien et connue exclusivement dans le fichier où elle est déclarée. Vous ne pouvez pas l'utiliser dans les autres fichiers qui constituent le programme. Ça vous rappelle le principe de la variable privée d'une fonction ? Bravo, c'est un peu la même chose : la variable est ici privée, mais pour un fichier. Ici aussi, vous pouvez mettre en oeuvre un meilleur style de programmation en créant des modules, réalisant chacun une fonctionnalité de votre programme, et qui s'interdisent de savoir ce que font les autres.

La classe extern

Elle déclare des variables externes (si, si, sans blague). Nous avons vu qu'une variable globale, déclarée sans static, était globale au programme. Encore faut-il dire aux autres fichiers qui constituent le programme que cette variable existe. Cela se fait par :

int toto;

...dans un des fichiers qui utilisent "toto" et :

extern int toto;

...dans tous les autres fichiers qui ont besoin d'accéder à cette variable.

Ainsi, dans tous ces derniers fichiers, le compilateur ne réserve pas la variable "toto" mais prend en note qu'il s'agit d'une variable externe. Il remplace donc son adresse par une étiquette. C'est l'éditeur de liens qui remplacera cette étiquette par l'adresse réelle de "toto", qui sera connue dans le fichier objet contenant la première des deux déclarations.

En résumé, dans tous les fichiers qui doivent accéder à la variable "toto" sauf un, vous devez la déclarer avec extern.

Il est possible cependant de ne pas déclarer comme externes les variables entières, car elles le sont par défaut. Mais je regrette de vous l'avoir dit car c'est le principe même de l'attrape-nigaud (et je suis resté poli). Si vous voulez écrire un générateur de bogues aléatoires, vous avez la formule.

Conséquence : chaque fois que vous vous référez à une variable globale d'un autre fichier, déclarez-la avec :

extern son_type son_nom;

S'il s'agit d'une fonction, il est sympathique, mais pas obligatoire, de déclarer le type de ses arguments. C'est utile parce que l'éditeur de lien ne fait aucune vérification là-dessus. Il est donc recommandé d'écrire :

extern son_type son_nom (liste donnant le type de chaque paramètre);

La classe register

C'est la classe registre. Elle est réservée exclusivement à certaines variables dynamiques. En sont donc exclues toutes les variables globales, externes et locales statiques. Plus d'autres que nous allons voir. Ce mot-clé indique au compilateur que le code qu'il doit générer doit stocker, dans la mesure du possible, cette variable dans un registre plutôt qu'en mémoire. Vous comprenez pourquoi les variables statiques n'y ont pas droit : elles existent tout au long du programme et leur nombre dépasse certainement celui des registres du microprocesseur.

C'est pourquoi seules celles allouées à l'intérieur des fonctions (donc les locales dynamiques) ont le droit d'utiliser cette classe. Au lieu d'utiliser une variable allouée sur la pile, le programme utilise un registre.

Ça peut permettre de gagner beaucoup de temps sur la manipulation d'indices de boucle, ou sur des calculs.

Il faut noter que les paramètres de fonctions, en tant que variables locales dynamiques, rentrent eux aussi dans la catégorie des variables qui peuvent utiliser cette classe.

Par contre, il faut en exclure les variables dont vous devez connaître l'adresse (par &toto, par exemple en paramètre d'une fonction). En effet, qui peut me dire quelle est l'adresse d'un registre ?

Voici un exemple tout simple :

int Moyenne(a,b)
register int a,b; {

  register int m;

  m = (a + b) / 2;
  return(m);
}

On peut rêver mieux en matière d'efficacité mais ça illustre bien les possibilités d'utilisation de ce mot-clé.

Les instructions affectant les registres étant bien plus rapides que celles utilisant la mémoire, vous pouvez tirer de la classe register des gains de temps substantiels. Mais il convient de modérer l'enthousiasme qui est en train de vous gagner à la lecture de ces lignes... Pour deux raisons :
  • D'abord, le compilateur stocke les variables en registre, dans la mesure du possible. Par conséquent, rien n'assure que telle variable y sera effectivement rangée. Il faut donc réserver register à un petit nombre de variables, celles dont on veut absolument qu'elles soient en registre, pour une fonction donnée. A déclarer trop de variables en register, on risque de ne pas y avoir les plus intéressantes.

  • D'autre part, les compilateurs, surtout les versions récentes, font leurs propres optimisations et décident d'eux-mêmes de mettre certaines variables dans les registres. Et dans ce cas-là, le mot-clé register ne sert à rien. Il n'est pourtant pas tout à fait inutile si l'on désire porter le programme sur une autre machine, qui n'a pas un compilateur aussi malin.
A titre d'exemple, vous pouvez essayer d'écrire un petit programme avec des variables dynamiques en register, je ne pense pas que vous obteniez de meilleurs résultats que sans ce mot-clé. Dès la version 3.10 du compilateur Lattice, on ne notait pas de différence. Pire, un programme que je pensais avoir optimisé à fond tournait plus lentement que sa version bestiale. Tout simplement parce que, en lui ordonnant de mettre certaines variables en registres, je démolissais les optimisations qu'il aurait faites normalement...

En résumé, sachez apprécier et consommer avec modération.

Et voilà, c'est fini pour les variables. Le mois prochain, nous attaquerons les choses sérieuses et parlerons structures de contrôle.


[Retour en haut] / [Retour aux articles]