tete du loic

 Loïc YON [KIUX]

  • Enseignant-chercheur
  • Référent Formation Continue
  • Responsable des contrats pros ingénieur
  • Référent entrepreneuriat
  • Responsable de la filière F2 ingénieur
  • Secouriste Sauveteur du Travail
mail
loic.yon@isima.fr
phone
(+33 / 0) 4 73 40 50 42
location_on
ISIMA
  • twitter
  • linkedin
  • viadeo

[C] TP 2 : Pointeurs et outils

Date de première publication : 2014/09/26

Dans ce TP de C, nous allons nous intéresser à différents aspects des pointeurs.

Ne faites que les parties dont les thèmes ont été abordés en cours ! Vous pouvez débuter la séance en terminant le premier TP. Les parties non faites durant cette séance seront à faire dès que le cours a été vu. S'il reste du temps, faites le TP Sudoku.

Variable locale statique

L'idée de l'exercice est de vous montrer comment marche une variable locale statique. Pour cela, vous devez reprendre la version récursive de la suite de Fibonnacci que vous avez codée pendant les semaines bloquées.

un+2 = un+1 + un avec u0=0 et u1=1

Compter maintenant (et afficher) le nombre de fois où la fonction est appelée par exemple pour Fibo(7).

Sous le capot

Sizeof de types "simples"

Afficher la taille en octets que prend chacun des types suivants :


char
short int 
long
long int

float
double

Vous pouvez également regarder les types normalisés :


int8_t
int16_t
int32_t
int64_t

int_least32_t
int_fast16_t

bool

Pour ces derniers, vous devrez ajouter les bons entêtes

stdint et stdbool

Sizeof d'un tableau

L'opérateur sizeof ne marche pas dans toutes les situations.


#include <stdio.h>
#include <stdlib.h>

void afficher(int tab[]) {
   printf("taille memoire afficher() : %lu\n", sizeof(tab));
   // il y a desormais un avertissement pour cet usage
}

int main()
{

  double tab[20];

  printf("taille memoire main(): %lu\n", sizeof(tab));
  printf("nb elements : %lu\n", sizeof(tab)/sizeof(tab[0]));

  afficher(tab);

   return 0;
}

Tableau à deux dimensions

  1. Définir un tableau statique d'entiers tab de 3 lignes et de 4 colonnes
  2. Remplir ce tableau des nombres 1 à 12 ligne par ligne
  3. Afficher t[0][0], t[1][1], t[3][4] et t[0][5]. Que constatez-vous ? Dans quelles situations les outils sont-ils utiles ?

Opérations sur les pointeurs

int main()
{
   int i  , *ptri  = &i;
   char c1 = '1', *ptrc1 = &c1;

   printf("ptri = %u ptrc1 = %u \n",ptri, ptrc1);
   printf("ptri = %x ptrc1 = %x \n",ptri, ptrc1);
   // seule la version suivante ne genere pas d'avertissement
   printf("ptri = %p ptrc1 = %p \n",ptri, ptrc1);
   printf("*ptri = %d et *ptrc1=%c", *ptri, *ptrc1);
   
   ptri++;
   ptrc1++;
   
   printf("ptri = %u ptrc1 = %d \n",ptri, ptrc1);
   // cela permet de voir la taille d'un int et d'un char en memoire
   // sizeof(int)  sizeof(char)

   return 0 ;
}

Après avoir testé ce code et ajouté le bon include, vous allez l’adapter en faisant les modifications suivantes :

Attention, 2, "2" et '2' sont des choses complètement différentes !

Vous devez compiler avec les options de compilation –Wall –Wextra, vous avez donc des avertissements concernant l’affichage des adresses mémoires. On vous a proposé d’utiliser les formats %u et %x mais ces formats ne sont pas tout à faits correct : %u permet d’afficher un entier non signé (unsigned int) et %x affiche le nombre non signé en hexadécimal.

Pour vous aider à déboguer, le C considère désormais que les adresses s’affichent avec %p (entier au format hexadécimal) et encore, il faut caster les pointeurs en void *.

Passage par valeur ou par adresse de paramètres d'une fonction

  1. Écrire une fonction echangeParValeur() qui "échange" deux variables a et b entières et que l'on passe par valeur. Dans le corps de la fonction, les valeurs sont affichées avant et après l'échange.

Rappel : le passage par valeur est le passage par défaut en langage C, on travaille sur des copies de variables.

  1. Écrire un programme principal qui teste cette fonction en affichant les valeurs de 2 variables entières i et j (déclarées localement dans le main()) avant et après l'appel à la fonction echangeParValeur().
  2. Ajouter une fonction echangeParTableau() où les variables i et j sont placées aux deux premières positions du tableau. Si la fonction permute les éléments, vérifier que les éléments sont bien permutés au retour de fonction.
  3. Ajouter une fonction echangeParAdresse() qui échange vraiment les deux variables a et b entières. Pour ce faire, on fait un passage par adresse. Dans le corps de la fonction, les valeurs sont également affichées avant et après l'échange. Dans le programme principal, on ajoute un affichage avant et après l’échange des valeurs des 2 variables entières i et j par la fonction echangeParAdresse().

Tableaux, pointeurs et chaînes de caractères

Manipulation de pointeurs

S'amuser avec le code suivant :

int main()
{
   int tab[] = {0,1,2,3,4,5};

   printf("%lu %lu %lu\n", sizeof(char), sizeof(int), sizeof(double));

   int  * p1;
   char * p2;

   p1 = tab; 
   ++p1;
   printf("%d ", *p1);

   p2 = (char *) p1;
   p2 += sizeof(int);

   printf("%d", *((int*)p2));
   printf("%d", *(p1+6));

   p1 = NULL;
   printf("%d", *p1);

   return 0;
}

Le code produit un segmentation fault comme annoncé en cours ! Il suffit de mettre cette partie coupable en commentaire pour que le reste marche.

Chaines de caractères

Comparer les fonctions suivantes :

int compter1(char * chaine) 
{   
  int i = 0;

  while (*(chaine+i) != '\0')
     ++i;

  return i; 
}

int compter2(char * chaine) 
{   
  char * s = chaine;

  while (*chaine != '\0')
     ++chaine;

  return chaine - s; 
}

int compter3(char * chaine) 
{   
  char * s = chaine;

  while (*chaine++);

  return chaine - s; 
}

int main() {
   char * s1 = "loic";
   //  *s1 = 'L';          // 1
   printf("%s", s1);
   printf("%c", *s1);
   printf("%s", s1+2);    // 2
   // printf("%s", ++s1); // 3 

  char s2[] = "isima";
  s2[0] = 'I';
  printf("%s", s2); 
  // printf("%s", ++s2);  // 4

 return 0;
} 

Révision de la manipulation des outils

Nous allons tester et corriger le programme suivant :

#include<stdio.h>

void saisir(char * s) 
{
  printf("Saisir une chaine\n");
  scanf("%s", s);
}

int main() 
{
  char *s;

  printf("Entrer votre prenom. ");
  saisir(s);
  printf("Bonjour %s!\n", s);

  if (s=="ddd") printf("bizarre \n");

  return 0;
}

Compilez le programme et exécutez-le. Deux problèmes sont à résoudre.

Un fichier core ou coredump peut être généré. Il contient alors une image partielle de la mémoire qui peut être utile au débogage.

Je suis sûr que vous pouvez trouver directement ce qui ne va pas avec ce programme mais nous allons utiliser la surcouche ddd du débogueur gdb. Pour ajouter des informations dans le code à analyser, il faut compiler avec l'option -g.

$ ddd nom_executable_a_tester &

L'interface graphique de débogueur se lance alors :

fenetre de ddd

Dans le support de cours, vous trouverez les commandes pour utiliser le débogueur en version texte.

L'erreur est encore plus rapide à trouver avec valgrind.

Explication pour le premier bug :

La variable s n'est pas explicitement initialisée par le programme. Suivant les compilateurs, cela peut générer un segmentation fault (dans le cas où s est non nulle) ou plus vicieux, le scanf() ne lit pas de chaine si s est initialisée à NULL. Dans tous les cas, valgrind nous aide en déterminant que s n'est pas initialisée et que ce n'est pas normal.

Explication pour le deuxième bug :

En ce qui concerne le test, cela ne peut pas marcher car le programme compare des adresses de chaines de caractères. Il faut utiliser strcmp() !

Allocation dynamique

  1. Écrire un programme qui déclare un pointeur tab sur des réels flottants double précision,
  2. puis qui alloue dynamiquement 1000000 éléments. L'adresse du tableau sera stockée dans tab.
  3. Initialiser ce tableau dynamique : chaque élément prendra la valeur du carré de son indice dans le tableau.
  4. Afficher le tableau

Cela marche ? Super ! mais en fait, on a oublié quelque chose !!! Compilez avec l'option -g et lancez valgrind :

$ valgrind nom_executable_a_tester

Dans les deux premiers TPs, vous avez utilisé valgrind pour trouver des erreurs de contexte comme des variables non initialisées. Nous allons découvrir maintenant un deuxième usage de valgrind. Valgrind compte le nombre d'allocations mémoire (malloc() et consœurs) et le nombre de rendus mémoire .... mais quel rendu mémoire ? Celui fait par free() bien sûr !

Comparer les deux sorties écran de valgrind, avec et sans rendu mémoire. Vous ne devriez pas avoir d'erreur de contexte.