Section courante

A propos

Section administrative du site

Plus de programmation dans Turbo C

Dans la page Programmation avec Turbo C, nous vous avons donné un avant-goût du travail avec Turbo C ; juste assez pour vous mettre en appétit. Vous êtes maintenant prêt à approfondir certains des problèmes les plus subtils et ésotériques de la programmation en Turbo C.

Une enquête sur les structures de données

Nous avons couvert les types de données de base dans la Programmation avec Turbo C, tels que les nombres entiers, les nombres à virgule flottante, les caractères et leurs variantes. Nous parlerons de la façon d'utiliser ces éléments pour créer des structures de données - des collections d'éléments de données. Mais d'abord, nous allons explorer un concept important dans les pointeurs du Turbo C.

Pointeurs

La plupart des variables que vous avez examinées jusqu'à présent contiennent des données, c'est-à-dire les informations réelles que votre programme manipule. Mais parfois, vous souhaitez savoir où se trouvent certaines données plutôt que simplement leur valeur. Pour cela, vous avez probablement besoin de pointeurs.

Si vous avez des doutes sur les concepts d'adresses et de mémoire, voici un bref aperçu. Votre micro-ordinateur contient votre programme et les données associées dans sa mémoire (souvent appelée RAM, signifiant Random Access Memory). À son niveau le plus bas, la mémoire de votre micro-ordinateur est composée de bits, des circuits électroniques microscopiques pouvant "mémoriser" (lorsque l'ordinateur est sous tension) l'une des deux valeurs, étant généralement interprétées comme étant 0 et 1.

Huit bits sont regroupés en un seul octet. De grands groupes de bits reçoivent également souvent des noms; généralement, deux octets sont considérés comme un mot ; quatre octets sont considérés comme un mot long ; et sur les compatibles IBM PC, seize octets sont considérés comme un paragraphe.

Chaque octet de la mémoire de votre ordinateur a une adresse unique, tout comme chaque maison d'une rue donnée. Mais contrairement à la plupart des rues, les octets consécutifs ont des adresses consécutives ; si un octet donné a une adresse de N, alors l'octet précédent a une adresse de N-1, et l'octet suivant a une adresse de N+1.

Un pointeur est une variable contenant une adresse de certaines données, plutôt que les données elles-mêmes. Pourquoi est-ce utile ? Tout d'abord, vous pouvez utiliser un pointeur pour pointer vers différentes données et différentes structures de données. En modifiant l'adresse que contient le pointeur, vous pouvez manipuler (attribuer, récupérer, modifier) des informations à divers endroits. Cela vous permet, par exemple, de parcourir une liste chaînée de structures avec un seul pointeur.

Deuxièmement, l'utilisation de pointeurs vous permet de créer de nouvelles variables pendant l'exécution de votre programme. Le Turbo C permet à votre programme de demander une certaine quantité de mémoire (en octets), renvoyant une adresse que vous pouvez entreposer dans un pointeur. C'est ce qu'on appelle l'allocation dynamique ; en l'utilisant, votre programme peut s'adapter à la quantité (ou peu) de mémoire disponible sur un ordinateur donné.

Troisièmement, vous pouvez utiliser un pointeur pour accéder à différents emplacements dans une structure de données, comme un tableau, une chaîne de caractères ou une structure. Un pointeur pointe en réalité sur un seul emplacement en mémoire (un segment:déplacement ou segment:offset en anglais) ; en indexant le pointeur, vous pouvez accéder à tous les octets suivants.

Vous êtes sans aucun doute convaincu maintenant que les pointeurs sont pratiques. Alors, comment les utilisez-vous en Turbo C ? Il faut d'abord les déclarer. Considérez le programme suivant :

  1. main() {
  2.  int ivar,*iptr;
  3.  iptr = &ivar;
  4.  ivar = 421;
  5.  printf("Emplacement de ivar: %p\n",&ivar);
  6.  printf("Contenus de ivar: %d\n", ivar);
  7.  printf("Contenus de iptr: %p\n", iptr);
  8.  printf("Valeur pointant vers: %d\n",*iptr);
  9. }

Le main a déclaré deux variables : ivar et iptr. La première, ivar, est une variable entière ; c'est-à-dire qu'il contient une valeur de type de données int. Le second, iptr, est un pointeur vers une variable entière ; c'est-à-dire qu'il contient une adresse d'une valeur de type de données int. Vous pouvez dire qu'iptr est un pointeur car il est précédé d'un astérisque (*) lorsqu'il est déclaré. En Turbo C, ce * est connu comme l'opérateur d'indirection. Pour l'essentiel, ces missions sont les suivantes :

L'opérateur d'adresse de (&) obtient l'adresse de ivar. Tapez et exécutez le programme précédent ; vous obtiendrez une sortie ressemblant à ceci :

Emplacement de ivar: FFCE
Contenus de ivar: 421
Contenus de iptr: FFCE
Valeur pointant vers: 421

Les deux premières lignes montrent l'adresse et le contenu de ivar. Le troisième montre l'adresse que contient iptr. Comme vous pouvez le voir, c'est l'adresse de la variable ivar, c'est-à-dire l'emplacement en mémoire où votre programme a décidé de créer ivar. La dernière valeur affichée est la donnée entreposée à cette adresse, la même donnée déjà affectée à ivar.

Notez que le troisième appel à printf a utilisé l'expression iptr pour obtenir son contenu, l'adresse de ivar. Ensuite, le dernier appel printf a utilisé l'expression *iptr pour récupérer les données entreposées à cette adresse. Voici une légère variation par rapport au programme précédent :

  1. main() {
  2.  int ivar,*iptr;
  3.  iptr = &ivar;
  4.  *iptr = 421;
  5.  printf("Emplacement de ivar: %p\n",&ivar);
  6.  printf("Contenus de ivar: %d\n", ivar);
  7.  printf("Contenus de iptr: %p\n", iptr);
  8.  printf("Valeur pointant vers: %d\n",*iptr);
  9. }

Ce programme assigne toujours l'adresse de ivar à iptr, mais au lieu d'assigner 421 à ivar, la fonction main l'assigne à *iptr. Les résultats ? Exactement le même que le programme précédent. Pourquoi ? Parce que l'instruction *iptr = 421 est la même que l'instruction ivar = 421. Et pourquoi en est-il ainsi ? Étant donné que ivar et *iptr font référence au même emplacement mémoire, les deux instructions attribuent la valeur 421 à cet emplacement.

Allocation dynamique

Voici une autre variante du programme :

  1. #include <alloc.h>
  2.  
  3. main () {
  4.  int *iptr;
  5.  iptr = (int *) malloc(sizeof(int));
  6.  *iptr = 421;
  7.  printf("Contenuse de iptr: %p\n", iptr);
  8.  printf("Valeur pointant vers: %d\n",*iptr);
  9. }

Cette version a complètement enlève la déclaration d'ivar. Au lieu de cela, il affecte à iptr la valeur renvoyée par une fonction nommée malloc, étant déclarée dans ALLOC.H (d'où la directive #include au début). Il attribue ensuite la valeur 421 à *iptr, étant l'adresse vers laquelle pointe iptr. Si vous exécutez ce programme, vous obtiendrez une valeur différente pour iptr qu'auparavant, mais *iptr sera toujours 421.

Que fait l'instruction iptr = (int *) malloc(sizeof(int)) ? Nous allons le décomposer une partie à la fois :

Compte tenu de tout cela, l'instruction entière peut être décrite comme suit : «allouez à partir de la mémoire de l'ordinateur suffisamment d'espace pour une variable de type de données int, puis attribuez l'adresse de départ de cette mémoire à iptr, étant un pointeur vers le type de données int.»

Tout cela était-il nécessaire ? Oui. Pourquoi ? Parce que sans cela, vous n'auriez aucune garantie que iptr pointe vers une zone de mémoire inutilisée. iptr aurait une certaine valeur, et c'est l'adresse qu'il utiliserait, mais vous ne sauriez pas si cette section de mémoire était utilisée pour d'autres raisons. La règle d'utilisation des pointeurs est simple : attribuez toujours une adresse à un pointeur avant de l'utiliser. Au lieu de cela, n'attribuez pas de valeur entière à *iptr sans d'abord attribuer une adresse à iptr.

Pointeurs et fonctions

Dans la page Programmation avec Turbo C, nous avons expliqué comment déclarer les paramètres des fonctions. Peut-être comprenez-vous maintenant pourquoi vous utilisez des pointeurs pour des paramètres formels dont vous souhaitez modifier les valeurs. Par exemple, considérons la fonction suivante :

  1. void swap(int *a, int *b) {
  2.  int temp;
  3.  temp = *a; *a = *b; *b = temp;
  4. }

Cette fonction, swap, a déclaré que les deux paramètres formels, a et b, sont des pointeurs vers un type de données int. Cela signifie qu'ils s'attendent à ce qu'une adresse d'une variable entière (plutôt que sa valeur) soit transmise. Toute modification apportée est apportée aux données aux adresses transmises.

Voici une fonction main appelant swap :

  1. main() {
  2.  int i,j;
  3.  i = 421;
  4.  j = 53;
  5.  printf("Avant : i = %4d j = %4d\n",i,j);
  6.  swap(&i,&j);
  7.  printf("Après : i = %4d j = %4d\n",i,j);
  8. }

Vous remarquerez que ce programme échange effectivement les valeurs de i et j. Vous pouvez considérer ce programme comme étant l'équivalent de :

  1. main() {
  2.  int i, j;
  3.  int *a,*b,temp;
  4.  i = 421;
  5.  j = 53;
  6.  printf("Avant: i = %4d j = %4d\n",i,j);
  7.  a = &i; 
  8.  b = &j;
  9.  temp = *a; *a = *b; *b = temp;
  10.  printf("Après: i = %4d j = %4d\n",i,j);
  11. }

Ce programme, bien sûr, produit les mêmes résultats : l'appel swap(&i,&j) attribue les valeurs des deux paramètres actuels (&i et &j) aux deux paramètres formels (a et b), puis exécute les instructions dans swap.

Arithmétique du pointeur

Et si vous vouliez modifier le programme pour que iptr pointe sur trois entiers au lieu d'un seul ? Voici une solution possible :

  1. #include <alloc.h>
  2. main() {
  3.  #define NUMINTS 3
  4.  int *list, i;
  5.  list = (int *) calloc(NUMINTS,sizeof(int));
  6.  *list = 421;
  7.  *(list+1) = 53;
  8.  *(list+2) = 1806;    
  9.  printf("Liste des adresses: ");
  10.  for(i = 0; i<NUMINTS; i++) printf("%4p ", (list+i));
  11.  printf("\nListe de valeurs : ");
  12.  for(i = 0; i<NUMINTS; i++) printf("%4d ",*(list+i));
  13.  printf("\n"); 
  14. }

Au lieu d'utiliser la fonction malloc, cette routine utilise calloc, prenant deux paramètres : le nombre d'éléments pour lesquels allouer de l'espace et la taille de chaque élément en octets. Alors maintenant, la liste pointe vers un bloc de mémoire de six (3 * 2) octets de long, suffisamment grand pour contenir trois variables de type de données int.

Notez très attentivement les trois déclarations suivante. La première instruction est familière : *list = 421. Elle dit simplement : «entreposer 421 dans la variable int située à l'adresse dans list». Le suivant - * (list+1) =53 - est important à comprendre. À première vue, vous pourriez interpréter cela comme «entreposer 53 dans la variable int située un octet au-delà de l'adresse dans la liste». Si tel est le cas, vous êtes probablement concerné, car ce serait en plein milieu de la variable int précédente (faisant deux octets de long). Ceci, bien sûr, gâcherait la valeur que vous avez précédemment entreposée.

Ne vous inquiétez pas; votre compilateur Turbo C est plus intelligent que cela. Il sait que list est un pointeur vers le type de données int, et donc l'expression list + 1 fait référence à l'adresse d'octet de list + (1 * sizeof(int)), de sorte que la valeur 53 n'écrase pas du tout la valeur 421.

De même, (list+2) fait référence à l'adresse d'octet de list + (2*sizeof(int)), et 1806 est entreposé sans affecter les deux valeurs précédentes.

En général, ptr + i désigne l'adresse mémoire ptr + (i * sizeof(int)). Tapez et exécutez le programme précédent ; la sortie ressemblera à ceci :

Liste des adresses: 06AA 06AC 06AE
Liste de valeurs : 421 53 1806

Notez que les adresses sont séparées par deux octets, pas un seul, et que les trois valeurs ont été maintenues séparées. Pour résumer tout cela : si vous utilisez ptr, un pointeur vers le type, alors l'expression (ptr + i) indique l'adresse mémoire (ptr + (i * sizeof(type)), où sizeof(type) renvoie le nombre de octets requis par une variable de type.

Tableaux

La plupart des langages de haut niveau, y compris Turbo C, vous permettent de définir des tableaux, c'est-à-dire des listes indexées d'un type de données donné. Par exemple, vous pouvez réécrire le dernier programme pour qu'il ressemble à ceci :

  1. main() {
  2.  #define NUMINTS 3
  3.  int list[NUMINTS],i;
  4.  list[0] = 421;
  5.  list[1] = 53;
  6.  list[2] = 1806;
  7.  printf("Liste d'adresses: ");
  8.  for (i = 0; i < NUMINTS; i++) printf("%p ",&list[i]);
  9.  printf("\nListe de valeurs : ");
  10.  for(i = 0; i < NUMINTS; i++) printf("%4d ",list[i]);
  11.  printf("\n");
  12. }

L'expression int list[NUMINTS] déclare que list est un tableau d'entiers, avec un espace réservé pour exactement trois (3) variables int. La première variable est appelée list[0], la deuxième comme list[1] et la troisième comme list[2]. La déclaration générale pour tout tableau est :

type nom[taille];

type est un type de données, nom est le nom que vous donnez au tableau et taille est le nombre d'éléments de type que nom contient. Le premier élément du tableau est nom[0], tandis que le dernier est nom[size-1] ; la taille totale du tableau en octets est taille*(sizeof(type)).

Tableaux et pointeurs

Vous avez peut-être déjà compris qu'il existe une relation étroite entre les tableaux et les pointeurs. En fait, si vous exécutez le programme précédent, votre sortie vous semblera très familière :

Liste des adresses: 163A 163C 163E
Liste de valeurs : 421 53 1806

L'adresse de départ est différente, mais c'est le seul changement. La vérité est que vous pouvez utiliser le nom d'un tableau comme s'il s'agissait d'un pointeur ; de même, vous pouvez indexer un pointeur comme s'il s'agissait d'un tableau. Considérez les identités importantes suivantes :

  1. (list + i) == & (list[i])
  2. *(list + i) == list[i]

Dans les deux cas, l'expression de gauche est équivalente à l'expression de droite ; vous pouvez utiliser l'un à la place de l'autre, que vous ayez déclaré list comme pointeur ou comme tableau.

La seule différence entre la déclaration de list en tant que pointeur et en tant que tableau réside dans l'allocation. Si vous déclarez list comme un tableau, votre programme réserve automatiquement la quantité d'espace demandée. Si vous déclarez list comme un pointeur, vous devez explicitement créer un espace pour lui en utilisant calloc ou un appel de fonction similaire, ou vous devez lui affecter l'adresse d'un espace ayant déjà été alloué.

Tableaux et chaînes de caractères

Nous avons parlé des chaînes de caractères dans Programmation avec Turbo C et évoqué la déclaration d'une chaîne de caractères de deux manières légèrement différentes : en tant que pointeur vers des caractères et en tant que tableau de caractères. Maintenant, vous pouvez mieux comprendre cette différence. Si vous déclarez une chaîne de caractères comme un tableau de caractères, l'espace pour cette chaîne de caractères est alloué. Si vous déclarez une chaîne de caractères comme pointeur sur char, aucun espace n'est alloué ; vous devez soit l'allouer vous-même (en utilisant malloc ou quelque chose de similaire), soit lui attribuer l'adresse d'une chaîne de caractères existante.

Tableaux multidimensionnels

Oui, vous pouvez avoir des tableaux multidimensionnels, et ils sont déclarés comme vous pourriez le penser :

type name[size1] [size2] ... [sizeN];

Considérez le programme suivant, initialisant quelques tableaux à deux dimensions, puis effectue une multiplication matricielle sur eux :

  1. main() {
  2.  int a[3] [4] = { {5, 3, -21, 42}, { 44, 15, 0, 6}, {97, 6, 81, 2} };
  3.  int b[4] [2] = { {22, 7}, { 97, -53}, {45, 0}, {72, 1}};
  4.  int c[3] [2],i,j,k;
  5.  for (i = 0; i < 3; i++) {
  6.   for (j = 0; j < 2; j++) {
  7.    c[i][j] = 0;
  8.    for (k = 0; k < 4; k++) c[i][j] += a[i][k]*b[k][j];
  9.   }
  10.  }
  11.  for (i = 0; i < 3; i++) {
  12.   for (j=0; j<2; j++) printf("c[%d] [%d] = %d ",i,j,c[i] [j]);
  13.   printf("\n");
  14.  }
  15. }

Notez deux choses dans le programme précédent : la syntaxe d'initialisation d'un tableau à deux dimensions consiste en des listes {... } imbriquées séparées par des virgules, et des crochets ([ ]) sont utilisés autour de chaque variable d'index. Certains langages de programmation utilisent la syntaxe [i, j] ; c'est la syntaxe légale en C, mais cela revient à dire simplement [j], puisque la virgule est interprétée comme l'opérateur de virgule («évaluez i, puis évaluez j, puis laissez l'expression entière prendre la valeur de j»). Assurez-vous de mettre entre crochets chaque variable d'index.

Les tableaux multidimensionnels sont entreposés dans ce que l'on appelle l'ordre ligne-colonne. Cela signifie que le dernier indice varie le plus rapidement. En d'autres termes, étant donné le tableau arr[3][2], les éléments de arr sont entreposés dans l'ordre suivant :

arr[0][0]
arr[0][1]
arr[1][0]
arr[1][1]
arr[2][0]
arr[2][1]

Le même principe est vrai pour les tableaux de trois, quatre dimensions ou plus.

Tableaux et fonctions

Que se passe-t-il lorsque vous souhaitez passer un tableau à une fonction ? Regardez cette fonction, renvoyant l'index de la valeur la plus basse dans un tableau d'entiers :

  1. int lmin(int 1ist[],int size) {
  2.  int i, minindx, _min;
  3.  minindx = 0;
  4.  _min = list[minindx];
  5.  for (i = 1; i < size; i++) if(list[i] < _min) {
  6.   _min = list[i];
  7.   minindx = i;
  8.  }
  9.  return (minindx);    
  10. }

Ici, vous voyez l'une des grandes forces de Turbo C : vous n'avez pas besoin de connaître la taille de list[] au moment de la compilation. Pourquoi ? Parce que le compilateur se contente de considérer list[] comme l'adresse de départ du tableau, et il ne se soucie pas vraiment de savoir où il se termine. Un appel à la fonction lmin pourrait ressembler à ceci :

  1. main() {
  2.  #define VSIZE 22
  3.  int i,vector[VSIZE];
  4.  for (i = 0; i < VSIZE; i++) {
  5.   vector[i] = rand();
  6.   printf("vector[%2d] = %6d\n",i,vector[i]);
  7.  }
  8.  i = lmin(vector,VSIZE);
  9.  printf("minimum: vector[%2d] = %6d\n",i,vector[i]);
  10. }

Question : Qu'est-ce qui est exactement transmis à lmin ? Réponse : L'adresse de départ du vecteur. Cela signifie que si vous deviez apporter des modifications à la liste dans lmin, ces modifications seraient également apportées au vecteur. Par exemple, vous pourriez écrire la fonction suivante :

  1. void setrand(int list[],int size); {
  2.  int i;
  3.  for(i = 0; i < size; i++) list[i] = rand();
  4. }

Ensuite, vous pouvez appeler setrand(vector,VSIZE) dansla fonction main pour initialiser vector. Qu'en est-il des tableaux multidimensionnels passés aux fonctions ? Avez-vous la même flexibilité ? Supposons que vous vouliez modifier setrand pour travailler sur un tableau à deux dimensions.

Vous devriez faire quelque chose comme ceci :

  1. void setrand(int matrix[][CSIZE],int rsize) {
  2.  int i, j;
  3.  for (i = 0; i < rsize; i++) {
  4.   for (j = 0; j < CSIZE; j++) matrix[i] [j) = rand();
  5.  }
  6. }

Le CSIZE est une constante globale fixant la taille de la deuxième dimension du tableau. En d'autres termes, tout tableau que vous avez passé à selrand devrait avoir une deuxième dimension de CSIZE.

Il existe cependant une autre solution. Supposons que vous ayez une matrice de tableau[15][7] que vous souhaitez transmettre à setrand. Si vous utilisez la déclaration originale de setrand(int list[], int size), vous pouvez l'appeler comme suit :

  1. setrand(matrix,15*7);

La matrice de tableau ressemblera alors à setrand comme un tableau unidimensionnel de taille 105 (qui est 15*7), et tout fonctionnera très bien.

Structures

Les tableaux et les pointeurs vous permettent de créer des listes d'éléments du même type de données. Et si vous voulez construire quelque chose à partir de différents types de données ? Déclarer une structure.

Une structure est une structure de données conglomérale, un regroupement de différents types de données. Par exemple, supposons que vous vouliez garder une trace des informations sur une étoile : nom, classe spectrale, coordonnées,... Vous pouvez déclarer ce qui suit :

  1. typedef struct {
  2.  char nom[25];
  3.  char classe;
  4.  short sousclass;
  5.  float decl,RA,distance;
  6. } etoile;

Cela définit le type de structure etoile. Après l'avoir déclaré, c'est-à-dire avoir placé la définition précédente au début de votre fichier de programme, vous pouvez l'utiliser comme suit :

  1. main() {
  2.  etoile monetoile;
  3.  strcpy (monetoile.nom, "Epsilon Eridani");
  4.  monetoile.classe = 'K';
  5.  monetoile.sousclass = 2;
  6.  monetoile.decl = 3.5167;
  7.  monetoile.RA = -9.633;
  8.  monetoile.distance = 0.303;
  9.   /* Reste de la fonction main() */
  10. }

Vous faites référence à chaque membre d'une variable de structure en le faisant précéder du nom de la variable suivi d'un point (.). La construction nomvariable.nommembre est considérée comme équivalente au nom d'une variable du même type que nommembre, et vous pouvez effectuer toutes les mêmes opérations.

Structures et pointeurs

Vous pouvez déclarer des pointeurs vers des structures, tout comme vous pouvez déclarer des pointeurs vers d'autres types de données. Cette capacité est essentielle pour créer des listes chaînées et d'autres structures de données dynamiques. En fait, les pointeurs vers des structures sont si souvent utilisés en Turbo C qu'il existe un symbole spécial pour désigner le membre d'une structure pointé par un pointeur. Considérez la réécriture suivante du programme précédent :

  1. #include <alloc.h>
  2.  
  3. main () {
  4.  etoile *monetoile;
  5.  monetoile = (etoile *) malloc(sizeof(etoile));
  6.  strcpy(monetoile->nom,"Epsilon Eridani");
  7.  monetoile->classe = 'K';
  8.  monetoile->sousclass = 2;
  9.  monetoile->decl = 3.5167;
  10.  monetoile->RA = -9.633;
  11.  monetoile->distance = 0.303;
  12.  /* Le reste de la fonction main() */
  13. }

Cette réécriture déclare monetoile comme étant un pointeur vers le type etoile, plutôt que comme une variable de type etoile. Il alloue de l'espace pour monetoile via l'appel à malloc. Désormais, lorsque vous vous référez aux membres de monetoile, vous utilisez nomptr->nommembre. Le symbole «->» signifie "membre de la structure pointé par" ; c'est une notation abrégée pour (*nomptr).nommembre.

L'instruction switch

Vous pouvez vous retrouver à construire longtemps if .. else if .. else if .. à construire. Regardez la fonction suivante :

  1. #include <ctype.h>
  2.  
  3. do_main_menu(short *done) {
  4.  char cmd;
  5.  *done = 0;
  6.  do {
  7.   cmd = toupper(getch());
  8.   if (cmd == 'F') do file_menu(done);
  9.   else if (cmd == 'R') run_program();
  10.   else if (cmd == 'C') do_compile();
  11.   else if (cmd == 'M') do_make();
  12.   else if (cmd == 'P') do_roject_menu();
  13.   else if (cmd == 'O') do_option_menu();
  14.   else if (cmd == 'E') do_error_menu();
  15.   else handle others(cmd,done);
  16.  } while (! *done);
  17. }

Ceci est si courant en programmation que le Turbo C a une structure de contrôle spéciale pour cela : l'instruction switch. Voici la même fonction, mais réécrite à l'aide de l'instruction switch :

  1. #include <ctype.h>
  2.  
  3. do_main_menu(short *done) {
  4.  char cmd;
  5.  *done = 0;
  6.  do {
  7.   cmd = toupper(getch());
  8.   switch (cmd) {
  9.    case 'F': do_file_menu(done); break;
  10.    case 'R': run_program(); break;
  11.    case 'C': do_compile(); break;
  12.    case 'M': do_make(); break;
  13.    case 'P': do_project_menu(); break;
  14.    case '0': do_option_menu(); break;
  15.    case 'E': do_error_menu(); break;
  16.    default: handle_others(cmd,done);
  17.   } 
  18.  } while (!*done);    
  19. }

Cette fonction entre dans une boucle lisant un caractère, le convertit en majuscule et l'entrepose dans cmd. Il exécute ensuite l'instruction switch en fonction de la valeur de cmd. La boucle continue jusqu'à ce que la variable done soit assignée à zéro (vraisemblablement dans les fonctions do_file_menu ou handle_others).

L'instruction switch prend la valeur de cmd et la compare à chacune des étiquettes case. S'il y a une correspondance, l'exécution commence à cette étiquette et continue jusqu'à ce que vous rencontriez une instruction break ou que vous atteigniez la fin de l'instruction switch. S'il n'y a pas de correspondance et que vous avez inclus l'étiquette par défaut (default) dans votre instruction switch, l'exécution commence là ; s'il n'y a pas de valeur par défaut, l'intégralité de l'instruction switch est ignorée.

Dans l'instruction switch, la valeur doit être compatible avec les nombres entiers. En d'autres termes, il doit être facilement converti en entier ; il peut s'agir d'un char, de n'importe quel type d'énumération et (bien sûr) d'un int avec toutes ses variantes. Vous ne pouvez pas utiliser de réels (tels que float et double), des pointeurs, des chaînes de caractères ou d'autres structures de données (bien que vous puissiez utiliser des éléments compatibles avec les entiers d'une structure de données).

Bien que (valeur) puisse être n'importe quelle expression (constante, variable, appel de fonction ou toute combinaison de celles-ci), les étiquettes de cas elles-mêmes doivent être des constantes. De plus, vous ne pouvez répertorier qu'une seule valeur par mot clef case. Si do_main_menu n'avait pas utilisé la fonction toupper pour convertir cmd en majuscule, alors l'instruction switch aurait pu ressembler à ceci :

  1. switch (cmd)
  2.  case 'f':
  3.  case 'F':
  4.   do_file_menu(done);
  5.   break;
  6.   case 'r' :
  7.   case 'R' : 
  8.    run_program();
  9.    break;
  10.    ...
  11. }

Cette instruction exécute la fonction do_file_menu si cmd est un F minuscule ou majuscule, et ainsi de suite pour le reste des options. N'oubliez pas que vous devez utiliser l'instruction break lorsque vous avez terminé avec un cas donné. Sinon, les instructions restantes seront exécutées (jusqu'à ce que, bien sûr, vous rencontriez une instruction break). Si vous aviez omis l'instruction break après l'appel à do_file_menu, taper la lettre F entraînerait un appel à do_file_menu, suivi d'un appel à run_program.

Il y a des moments où vous voulez faire cela, cependant; considérez ce code :

  1. typedef enum { dimanche, lundi, mardi, mercredi, jeudi, vendredi, samedi } jours;
  2.  
  3. main() {
  4.  jours aujourdhui;
  5.  switch (aujourdhui) {}
  6.   case lundi:
  7.   case mardi:
  8.   case mercredi:
  9.   case jeudi:
  10.   case vendredi: puts ("Aller travailler!"); break;
  11.   case samedi: printf("nettoyer la cour et ");
  12.   case dimanche: puts("Détendez-vous !");
  13.  }
  14.  /* ... */
  15. }

Avec cette instruction switch, les valeurs lundi à vendredi finissent toutes par exécuter la même instruction puts, après quoi l'instruction break vous fait quitter le switch. Cependant, si aujourdhui est égal à samedi, alors la fonction printf est exécuté, après quoi l'instruction puts("Détendez-vous !") est exécutée ; si aujourdhui est égal à dimanche, alors seul ce dernier puts est exécuté.

Instructions de flux de contrôle

Il existe des commandes supplémentaires à utiliser dans les structures de contrôle ou pour simuler d'autres structures de contrôle. L'instruction return vous permet de quitter les fonctions plus tôt. Les instructions break et continue sont conçues pour être utilisées dans des boucles et vous aident à ignorer les instructions. L'instruction goto vous permet de sauter dans votre code. Et l'expression conditionnelle (?:) vous permet de compresser certaines instructions if...else sur une seule ligne.

Un conseil : réfléchissez-y à deux fois avant de les utiliser (sauf, bien sûr, pour return). Il y a des situations où ils représentent la meilleure solution, mais le plus souvent vous pouvez résoudre votre problème clairement sans avoir recours à eux. Surtout évitez l'utilisation de l'instruction goto ; étant donné les instructions return, break et continue, cela ne devrait pas être si nécessaire.

L'instruction return

Il existe deux utilisations principales de l'instruction return. Tout d'abord, si une fonction renvoie une valeur, vous devez l'utiliser pour retransmettre cette valeur à la routine appelante. Par exemple :

  1. int imax(int a, int b) {
  2.  if (a > b) return (a);
  3.        else return (b);    
  4. }

Ici, la routine utilise l'instruction return pour renvoyer le maximum des deux valeurs acceptées.

La deuxième utilisation majeure de l'instruction return est de quitter une fonction à un moment autre que sa fin. Par exemple, une fonction peut détecter très tôt une condition qui nécessite qu'elle se termine. Plutôt que de mettre le reste de la fonction à l'intérieur d'une instruction if, vous pouvez simplement appeler return pour quitter. Si la fonction est de type void, vous pouvez simplement utiliser return sans aucune valeur renvoyée.

Considérez cette modification de la fonction lmin donnée précédemment :

  1. int lmin(int list[], int size) {
  2.  int i, minindx, _min;
  3.  if (size <= 0) return (-1);
  4.  /* ... */
  5. }

Dans ce cas, si la taille du paramètre est inférieure ou égale à zéro, alors il n'y a rien dans la liste ; par conséquent, return est appelé dès le départ, pour sortir de la fonction. Notez qu'une valeur d'erreur de -1 est renvoyée. Puisque -1 n'est jamais un index valide dans un tableau, la routine appelante sait qu'elle a fait quelque chose de mal.

L'instruction break

Parfois, vous souhaitez quitter rapidement et facilement une boucle avant d'atteindre sa fin. Considérez le programme suivant :

  1. #define LIMIT 100
  2. #define _MAX 10
  3.  
  4. main () {
  5.  int i,j,k,score;
  6.  int scores [LIMIT] [_MAX];
  7.  for (i = 0; i < LIMIT; i++) {
  8.   j = 0;
  9.   while (j < _MAX-1) {
  10.    printf("SVP entrez le pointage #%d: ",j);
  11.    scanf("%d",score);
  12.    if (score < 0) break;
  13.    scores[i][++j] = score;
  14.   }
  15.   scores [i][0] = j;
  16.  }    
  17. }

Notez l'instruction «if (score < 0) break;». Cela signifie que si l'utilisateur entre une valeur négative pour le score, la boucle while est terminée. La variable j est utilisée à la fois pour indexer les scores et pour suivre le nombre total de scores dans chaque ligne ; ce nombre est ensuite entreposé dans le premier élément de la ligne.

Vous pouvez vous souvenir de l'instruction break depuis son utilisation dans l'instruction switch de la page Programmation avec Turbo C. Dans ce cas, le programme a quitté l'instruction switch ; ici, cela amène le programme à sortir de la boucle et à poursuivre le programme. L'instruction break peut être utilisée avec les trois boucles (for, while et do...while), ainsi que dans l'instruction switch ; cependant, il ne peut pas être utilisé dans une instruction if... else ou simplement dans le corps main d'une fonction.

L'instruction continue

Parfois, vous ne voulez pas sortir complètement de la boucle ; vous voulez juste ignorer le reste de la boucle et recommencer en haut. Dans ces situations, vous pouvez utiliser l'instruction continue, faisant exactement cela. Regarde ce programme :

  1. #define LIMIT 100
  2. #define _MAX 10
  3.  
  4. main () {
  5.  int i,j,k,score;
  6.  int scores [LIMIT] [_MAX];
  7.  for (i = 0; i < LIMIT; i++) {
  8.   j = 0;
  9.   while (j < _MAX-1) {
  10.    printf("SVP entrez le pointage #%d: ",j);
  11.    scanf("%d",score);
  12.    if (score < 0) continue;
  13.    scores[i][++j] = score;
  14.   }
  15.   scores[i][0] = j;
  16.  }
  17. }

Lorsque l'instruction continue est exécutée, le programme ignore le reste de la boucle et effectue à nouveau le test de boucle. En conséquence, ce programme fonctionne différemment du précédent. Au lieu de quitter la boucle interne lorsque l'utilisateur entre un score de -1, il suppose qu'une erreur a été commise et retourne au début de la boucle while. Comme j n'a pas été incrémenté, il redemande le même score.

L'instruction goto

Oui, il y a une instruction goto en Turbo C. Le format est simple : goto label, où label est un identificateur, associé à une instruction donnée. Cependant, la plupart des utilisations intelligentes de l'instruction goto sont prises en charge par les trois instructions précédentes, alors réfléchissez bien si vous avez vraiment besoin de l'utiliser.

L'expression conditionnelle (?:)

Vous voudrez peut-être à l'occasion choisir entre deux expressions (et les valeurs résultantes), en fonction de certaines conditions.

Ceci est généralement accompli avec une instruction if..else, telle que :

  1. int imin(int a, int b) {
  2.  if (a < b) return(a);
  3.        else return(b);
  4. }

Cela arrive assez souvent pour qu'il y ait une construction spéciale pour effectuer ce type de sélection. Son format est le suivant :

expr1 ? expr2 : expr3

Ceci est interprété comme suit : «Si expr1 est vrai, alors évaluez expr2 et laissez l'expression entière prendre sa valeur ; sinon, évaluez expr3 et prenez sa valeur.» En utilisant cette construction, vous pouvez réécrire la fonction imin comme suit :

  1. int imin(int a, int b) {
  2.  return ((a < b) ? a : b);
  3. }

Mieux encore, vous pouvez réécrire imin en tant que macro en ligne :

  1. #define imin(a,b) ((a < b) ? a: b)

Maintenant, chaque fois que votre programme voit l'expression imin(e1, e2), il la remplace par ((e1<e2) ? e1 : e2) et continue la compilation. Il s'agit en fait d'une solution plus générale, puisque a et b ne sont plus limités à être entiers ; ils peuvent être de n'importe quel type autorisant la relation <.

Style en programmation C : moderne ou classique

En programmation Turbo C, il existe un certain nombre de tendances actuelles pour adopter certaines techniques rendant le Turbo C plus facile à utiliser. Beaucoup de ces tendances vont à l'encontre des traditions ou des méthodes classiques de programmation en C. La plupart ont été rendues possibles par les extensions de langage de programmation définies par le comité des normes ANSI C. Cette section devrait vous donner une idée de la façon dont les choses ont été faites dans le passé et comment les nouvelles normes peuvent vous aider à écrire de meilleurs programmes en Turbo C. Le Turbo C, bien sûr, prend en charge à la fois le style de programmation classique et le style moderne.

Utilisation de prototypes de fonctions et de définitions de fonctions complètes

Dans le style classique de la programmation Turbo C, vous déclarez des fonctions simplement en spécifiant le nom et le type renvoyés. Par exemple, vous définiriez la fonction swap comme :

  1. int swap();

Aucune information sur les paramètres n'est donnée, ni quant au nombre ni au type. La définition de style classique de la fonction ressemble à ceci :

  1. int swap(a,b)
  2. int *a,*b;
  3. {
  4.   /* Corps de la fonction */
  5. }

Ce style entraîne très peu de vérification des erreurs, ce qui peut entraîner des bogues très subtils et difficiles à tracer. Il préférable de l'éviter. Le style moderne implique l'utilisation de prototypes de fonctions (pour les déclarations de fonctions) et de listes de paramètres (pour les définitions de fonctions). Redéclarez swap en utilisant un prototype de fonction :

  1. int swap(int *a, int *b);

Désormais, lorsque votre programme se compile, il dispose de toutes les informations dont il a besoin pour effectuer une vérification complète des erreurs lors de tout appel à swap. Et vous pouvez utiliser un format similaire lorsque vous définissez la fonction :

  1. int swap(int *a, int *b) {
  2.  /* Corps de la fonction */
  3. }

Le style moderne augmente la vérification des erreurs effectuée même si vous n'utilisez pas de prototypes de fonction ; si vous utilisez des prototypes, le compilateur s'assurera que les déclarations et les définitions concordent.

Utilisation des définitions enum

En C classique, les listes de valeurs sont définies à l'aide de la directive #define, comme ceci :

  1. #define dimanche 0
  2. #define lundi 1
  3. #define mardi 2
  4. #define mercredi 3
  5. #define jeudi 4
  6. #define vendredi 5
  7. #define samedi 6

De nos jours, cependant, vous pouvez déclarer des types de données énumérés à l'aide du mot clef enum, comme illustré ici :

  1. typedef enum {dimanche, lundi, mardi, mercredi, jeudi, vendredi, samedi} jours;    

Cela a le même effet que la méthode classique, jusqu'au coucher dimanche = 0 et samedi = 6 ; cependant, la méthode moderne fait plus de masquage et d'abstraction d'informations que la longue liste de directives #define. Et vous pouvez déclarer des variables de type jours.

Utilisation de typedef

En C de style classique, les types de données définis par l'utilisateur étaient rarement nommés, à l'exception des structures et des unions - et même avec eux, vous deviez faire précéder toute déclaration du mot clef struct ou union.

Dans le C de style moderne, un autre niveau de masquage d'informations est utilisé lors de l'utilisation de la directive typedef. Cela vous permet d'associer un type de données donné (y compris les structures et les énumérations) à un nom, puis de déclarer des variables de ce type.

Voici quelques exemples de définitions de type avec des déclarations de variables :

  1. typedef int *entierptr;
  2. typedef char nomchainedecaracteres[30];
  3. typedef enum {mal, femelle, inconnu } sexe;
  4. typedef struct {
  5.  nomchainedecaracteres dernier, premier;
  6.  char ssn[9];
  7.  sexe genre;
  8.  short age;
  9.  float gpa;
  10. } etudiant;
  11.  
  12. typedef etudiant class[100];
  13. class hist104,ps102;
  14. etudiant valedictorian;
  15. entierptr iptr;

L'utilisation de typedef rend le programme plus lisible ; il vous permet également de modifier un emplacement unique - où un type est réellement défini - et de propager ce changement dans tout le programme.

Déclarer des fonctions void

Dans la définition originale de C, chaque fonction renvoyait une valeur d'un certain type ; si aucun type n'était déclaré, la fonction était supposée être de type int. De la même manière, les fonctions renvoyant des pointeurs "génériques" (non typés) étaient généralement déclarées renvoyer un pointeur vers char, simplement parce qu'elles devaient renvoyer quelque chose.

Maintenant, il existe un type standard void, pouvant être considéré comme une sorte de type "null". Toute fonction ne renvoyant pas explicitement une valeur doit être déclarée comme étant de type void. Notez que de nombreuses routines d'allocation de mémoire à l'exécution (telles que malloc) sont déclarées comme étant de type void *. Cela signifie qu'ils renvoient un pointeur non typé, que vous pouvez ensuite (en Turbo C) attribuer à un pointeur de n'importe quel type sans transtypage (bien que vous deviez transtyper quand même, pour préserver la portabilité).

Utiliser les extensions

Il existe un certain nombre d'extensions mineures au langage de programmation Turbo C facilitant la lisibilité du programme, remplacent certains anachronismes et vous permettent d'avancer. Voici une brève liste.

Littéraux de chaîne de caractères

En C classique, vous deviez utiliser des caractères de continuation ou une sorte de concaténation afin d'avoir de grands littéraux de chaîne dans votre programme. En C de style moderne, vous pouvez facilement étaler un grand littéral sur plusieurs lignes, comme ceci :

  1. main() {
  2.  char *msg;
  3.  msg="Il y a cent vingt sept ans, nos pères"
  4.       " ont fait surgir sur\nce continent"
  5.       "une nouvelle nation, vouée à l'idéal "
  6.       "que tous les\nommes sont créés égaux";
  7.  printf("%s",msg);    
  8. }

Constantes de caractères hexadécimaux

En C classique, les séquences d'échappement spécifiant des codes ASCII particuliers étaient toutes faites en octal (base 8). En effet, le C avait été développé à l'origine sur des machines où les nombres binaires étaient généralement représentés sous forme octale. De nos jours, la plupart des micro-ordinateurs utilisent l'hexadécimal (base 16) pour représenter les nombres binaires. Pour cette raison, le C moderne vous permet de déclarer des constantes de caractères en notation hexadécimale. Le format général est '\xDD', où DD représente un ou deux chiffres hexadécimaux (0..9, A..F). Ces séquences d'échappement peuvent être directement affectées à des variables char, ou elles peuvent être incorporées dans des chaînes, par exemple, chr='\x20'.

Types signed

Le C classique supposait que tous les types basés sur des entiers étaient signés et incluait donc le modificateur de type non signé afin que vous puissiez spécifier le contraire. Par défaut, les variables de type char étaient considérées comme signées, ce qui signifiait que l'intervalle de valeurs sous-jacente était de -128 à 127. Sur les micro-ordinateurs d'aujourd'hui, cependant, le type char est souvent considéré comme non signé, et le Turbo C dispose d'une option de compilation pour vous permettent d'en faire la valeur par défaut. Dans un tel cas, cependant, vous voudrez peut-être toujours pouvoir déclarer un char signé. En C moderne, vous pouvez le faire, puisque signé est reconnu comme un modificateur valide.

Les pièges de la programmation en Turbo C

Les programmeurs commettent un certain nombre d'erreurs courantes lorsqu'ils commencent à coder en Turbo C. Voici une liste de certaines d'entre elles, ainsi que des suggestions pour les éviter.

Noms de chemin avec des chaînes de caractères Turbo C

Tout le monde sait que la barre oblique inverse (\) dans DOS indique un nom de répertoire. Cependant, en Turbo C, la barre oblique inverse est le caractère d'échappement d'une chaîne de caractères. Ce conflit pose un petit problème si vous donnez un nom de chemin avec une chaîne de caractères Turbo C. Par exemple, si vous aviez la déclaration suivante :

  1. file = fopen("c:\new\tools.dat", "r");

vous vous attendriez à ouvrir le fichier TOOLS.DAT dans le répertoire NEW sur l'unité de disque C. Vous ne le ferez pas. Au lieu de cela, le \n vous donne la séquence d'échappement pour le caractère de nouvelle ligne (LF) et le \t vous donne le caractère de tabulation. Le résultat est que votre nom de fichier aura intégré les caractères de nouvelle ligne et de tabulation. Le DOS rejetterait la chaîne de caractères comme un nom de fichier incorrect, car les noms de fichiers peuvent ne pas contenir de nouvelle ligne ou de tabulation. La bonne chaîne de caractères est :

  1. "c:\\new\\tools.dat"    

Utiliser et abuser des pointeurs

Les pointeurs peuvent bien être le problème le plus déroutant pour les programmeurs Turbo C novices. Quand utilisez-vous des pointeurs et quand ne l'utilisez-vous pas ? Quand utilisez-vous l'opérateur d'indirection (*) ? Quand utilisez-vous l'adresse de l'opérateur (&) ? Et comment pouvez-vous éviter de vraiment gâcher le système d'exploitation pendant l'exécution ?

Utilisation d'un pointeur non initialisé

Un grave danger est d'attribuer une valeur à l'adresse contenue par un pointeur sans avoir attribué d'adresse à ce pointeur. Par exemple :

  1. main() {
  2.  int *iptr;
  3.  *iptr = 421;
  4.  printf("*iptr = %d\n",*iptr);
  5. }

Ce qui rend cet écueil si dangereux, c'est que vous pouvez souvent vous en sortir. Dans l'exemple précédent, le pointeur iptr contient une adresse aléatoire ; c'est là que la valeur 421 est entreposée. Ce programme est suffisamment petit pour qu'il y ait très peu de chances que quoi que ce soit soit saboté. Dans un programme plus vaste, cependant, il y a de plus en plus de chances que cela se produise, car vous pourriez bien avoir d'autres informations entreposées à l'adresse que iptr contient. Et si vous utilisez le petit modèle de mémoire, où les segments de code et de données occupent le même espace, vous courez le risque de corrompre le code machine lui-même.

Chaîne de caractères

Vous vous souvenez peut-être que vous pouvez déclarer des chaînes de caractères en tant que pointeurs vers char ou en tant que tableaux de char. Vous pouvez également vous rappeler que ce sont les mêmes avec une différence très importante : si vous utilisez un pointeur vers char, aucun espace pour la chaîne de caractères n'est alloué ; si vous utilisez un tableau, l'espace est alloué et la variable tableau contient l'adresse de cet espace. Ne pas comprendre cette différence peut conduire à deux types d'erreurs. Considérez le programme suivant :

  1. main() {
  2.  char *name;
  3.  char msg[10];
  4.  printf("Quel est votre nom ? ");
  5.  scanf("%s",name);
  6.  msg = "Bonjour, ";
  7.  printf("%s%s",msg,name);
  8. }

À première vue, cela peut sembler parfaitement correct ; un peu maladroit, mais toujours acceptable. Mais cela a introduit deux erreurs distinctes. La première erreur concerne l'énoncé :

  1. scanf("%s",name)

La déclaration elle-même est légale et correcte. Étant donné que name est un pointeur vers char, vous n'avez pas besoin d'utiliser l'opérateur d'adresse de (&) devant lui. Cependant, le programme n'a alloué aucune mémoire pour le nom ; le nom que vous tapez sera entreposé à n'importe quelle adresse aléatoire que ce nom aura. Vous recevrez un avertissement à ce sujet (utilisation possible de 'name' avant la définition), mais pas d'erreur. Le deuxième problème provoquera une erreur. Le problème réside dans l'instruction msg = "Bonjour, ". Le compilateur pense que vous essayez de remplacer msg par l'adresse de la chaîne de caractères constante "Bonjour, ". Vous ne pouvez pas faire cela, car les noms de tableau sont des constantes ne pouvant pas être modifiées (tout comme 7 est une constante, et vous ne pouvez pas dire "7 = i"). Le compilateur vous donnera un message d'erreur Lvalue required. Quelles sont les solutions à ces erreurs ? L'approche la plus simple consiste à inverser la manière dont name et msg ont été déclarés :

  1. main() {
  2.  char name[10];
  3.  char *msg;
  4.  printf("Quel est votre nom ? ");
  5.  scanf("%s",name);
  6.  msg = "Bonjour, ";
  7.  printf("%s%s",msg,name);
  8. }

Cela fonctionne parfaitement bien. Le nom de la variable a un espace réservé pour contenir votre nom au fur et à mesure que vous le tapez, tandis que msg vous permet de lui attribuer l'adresse de la chaîne constante "Bonjour, ". Si, toutefois, vous êtes lié et déterminé à conserver les déclarations telles qu'elles étaient, vous devrez apporter les modifications suivantes au code :

  1. #include <alloc.h>
  2. main () {
  3.  char *name;
  4.  char msg[10];
  5.  name = (char *) malloc(10);
  6.  printf("Quel est votre nom ? ");
  7.  scanf("%s",name);
  8.  strcpy(msg,"Bonjour, ");
  9.  printf("%s%s",msg,name);
  10. }

L'appel à la fonction malloc met de côté dix octets de mémoire et attribue l'adresse de cette mémoire à name, en prenant soin de notre premier problème. La fonction strcpy effectue une copie caractère par caractère de la chaîne de caractères constante "Bonjour, " vers le tableau msg.

Confondre affectation (=) avec égalité (==)

Dans les langages de programmation Pascal et BASIC, une comparaison d'égalité est faite avec l'expression si (a = b). En Turbo C, c'est une construction valide, mais elle a une signification assez différente. Regardez ce fragment de code :

  1. if(a = b) puts("Égale");
  2.      else puts("Pas égale");

Si vous êtes un programmeur Pascal ou BASIC, vous pouvez vous attendre à ce qu'il affiche Égale si a et b ont la même valeur, et Pas égale sinon. Ce n'est pas ce qui se passe en Turbo C. En Turbo C, l'expression a = b signifie "attribuer la valeur de b à a", et l'expression entière prend la valeur de b. Ainsi, le fragment précédent attribuera la valeur de b à a, puis affichera Égale si b et a ont une valeur différente de zéro, sinon il affichera Pas égale. Ce que vous voulez vraiment, c'est ceci :

  1. if(a == b) puts("Égale");
  2.       else puts("Pas égale");

Oublier le break dans les instructions switch

Vous vous souvenez peut-être que l'instruction break est utilisée dans une instruction switch pour mettre fin à un cas particulier. Veuillez continuer à vous en souvenir. Si vous oubliez de mettre une instruction break pour un cas donné, le ou les cas suivants sont également exécutés.

Indexation des tableaux

N'oubliez pas que les tableaux commencent à [0], pas à [1]. Une erreur courante consiste à écrire un code comme celui-ci :

  1. main () {
  2.  int list [100],i;
  3.  for (i = 1; i <= 100; i++) list[i] = i*i;    
  4. }

Ce programme laisse le premier emplacement dans list - à savoir list[0] - non initialisé, et il entrepose une valeur dans un emplacement inexistant de list - list[100] - en écrasant éventuellement d'autres données dans le processus. Le code correct doit être écrit comme ceci :

  1. main() {
  2.  int list[100],i;
  3.  for(i = 0; i < 100; i++) list[i] = i*i;
  4. }

Défaut de passage de l'adresse

Regardez le programme suivant et déterminez ce qui ne va pas :

  1. main() {
  2.  int a,b,sum;
  3.  printf("Entrer deux valeurs: ");
  4.  scanf("%d %d",a,b);
  5.  sum = a + b;
  6.  printf("La somme est %d\n",sum);
  7. }

Vous abandonner ? L'erreur se trouve dans l'instruction scanf("%d %d", a, b). N'oubliez pas que scanf vous oblige à transmettre des adresses au lieu de valeurs ? Il en va de même pour toute fonction dont les paramètres formels sont des pointeurs. Le programme précédent se compilera et s'exécutera, car scanf prendra toutes les valeurs aléatoires de a et b et les utilisera comme adresses dans lesquelles entreposer les valeurs que vous entrez.

L'instruction correcte doit être scanf("%d %d",&a,&b); de cette façon, les adresses de a et b sont transmises à scanf et les valeurs que vous entrez sont correctement entreposées dans ces variables :

  1. main() {
  2.  int a,b,sum;
  3.  printf("Entrer deux valeurs: ");
  4.  scanf("%d %d",&a,&b);
  5.  sum = a + b;
  6.  printf("La somme est %d\n",sum);
  7. }

Ce même écueil peut se produire avec vos propres fonctions. Vous souvenez-vous de la fonction swap définie dans la section sur les pointeurs ? Que se passerait-il si vous l'appeliez ainsi :

  1. main() {
  2.  int i,j;
  3.  i = 421;
  4.  j = 53;
  5.  printf("Avant: i = %4d j = %4d\n",i,j);
  6.  swap(i,j);
  7.  printf("Après: i = %4d j = %4d\n",i,j);
  8. }

Les variables i et j auraient les mêmes valeurs avant et après l'appel à swap ; cependant, les valeurs aux adresses de données 421 et 53 auraient leurs valeurs permutées, ce qui pourrait causer des problèmes subtils et difficiles à tracer. Comment éviter ça ? Utilisez des prototypes de fonctions et des définitions de fonctions complètes. En fait, vous auriez eu une erreur de compilation dans la version précédente de main si swap avait été défini comme il l'était plus tôt dans cette page. Si, toutefois, vous le définissiez de la manière suivante, le programme se compilerait parfaitement :

  1. void swap(a,b)
  2. int *a,*b;
  3. {
  4.  /* ... */
  5. }

Déplacer les définitions de a et b hors des parenthèses désactive la vérification des erreurs qui se poursuivrait autrement, ce qui est la meilleure raison de ne pas utiliser le style classique de définition de fonction.



Dernière mise à jour : Mardi, le 18 janvier 2022