TP 1 : gestion de la mémoire

Préambule :

Vous allez, dans ce premier TP, (re)découvrir les notions fondamentales que sont : organisation d'un programme (SP, SD, tas, pile), gestion de la mémoire (allocation, désallocation, taille des objets), pointeurs (notamment arithmétique sur), passage de paramètres (dont passage de fonctions).

Exercice 1 : organisation d'un programme.

Cet exercice présente un cas concret de gestion de la mémoire d'un processus.

  1. Dans un premier temps, récupérez le fichier memoire01.c et compilez-le.
  2. Après exécution, représentez grossièrement l'organisation de la mémoire du processus, en distinguant le code (SP), les données (SD), le tas (heap) et la pile (stack). Rangez dans chaque section les identificateurs du programme.
  3. Dans quelle section est allouée l'identificateur "dynamic_local_string" ? Et la zone mémoire sur laquelle il pointe ? Justifiez votre réponse.
  4. Justifiez la présence des identificateurs déclarés dans le "main()" dans la portion de la mémoire du processus correspondant à la pile.
  5. Sous Unix, "tout est fichier". Vous allez pouvoir le constater de visu en cherchant le numéro de votre processus (PID) (par la commande ps). En supposant que le numéro trouvé est le PID 123, tapez la commande : cat /proc/123/maps et vous aurez l'affichage des régions correspondant aux zones de mémoires attribuées à votre processus. Identifiez la zone de code (du programme) : quelles sont ses protections et pourquoi ? Faites de même avec la zone de données et répondez aux mêmes questions. Enfin, identifiez les régions en indiquant leur adresse qui correspondent à la pile et au tas du processus.

Exercice 2 : gestion de la mémoire sous la responsabilité du programmeur.

  1. En vous inspirant du premier exercice, écrivez un rapide programme qui alloue dans le tas, donc dynamiquement, un vecteur de 10 caractères nommé ma_chaine, soit une zone de 10 octets contigus (utilisez pour cela les fonctions malloc() et sizeof()). Vérifiez que l'appel de la fonction sizeof(char) retourne bien la quantité prévue (quelle est-elle ?).
  2. Question non subsidiaire mais hors sujet : remplissez ce tableau de caractères (car s'en est un) des caractères de votre choix puis affichez cette chaîne de caractères par l'intermédiaire de l'appel de fonction printf("%s\", ma_chaine). Si ça ne fonctionne pas correctement, c'est que vous avez oublié qu'il existe une convention qui permet au C de traiter correctement les chaînes de caractères.
  3. Affichez l'adresse contenue par le pointeur ma_chaine ainsi que les deux premiers caractères de ma_chaine, puis utilisez la fonction free(ma_chaine), enfin affichez de nouveau l'adresse contenue par le pointeur ma_chaine ainsi que les deux premiers caractères de ma_chaine. Conclusion ?
  4. En reprenant ce que vous avez fait dans cet exercice, écrivez la fonction : char* allouer_chaine_10_car(void) qui retourne un pointeur sur une zone de 10 caractères contigus.

Exercice 3 : pointeurs et arithmétique sur les pointeurs.

  1. Reprenez l'exercice précédent i.e faites une copie du pointeur ma_chaine nommée la_chaine.Vous devez avoir après cela, deux pointeurs désignant la même zone mémoire : constatez-le en affichant l'adresse de ma_chaine et la_chaine (directive %p) ainsi que les chaînes de caractères correspondantes (directive %s). Changez la première lettre de la_chaine, par exemple la_chaine[0] = '*', enfin affichez ma_chaine et la_chaine. Que constatez-vous ?
  2. Affichez le premier et le deuxième caractère de ma_chaine ainsi que leur adresse respective non pas en utilisant la notation indicée (i.e les crochets) mais en utilisant l'arithmétique sur les pointeurs. Allouez dynamiquement une zone de 10 entiers longs (long int), donnez des valeurs au moins aux deux premiers entiers puis faites de même que pour ma_chaine. Conclusion ?
  3. "Un pointeur contient une adresse donc un entier non signé". Pour vérifier ceci, affectez à un entier long (long int), l'adresse du troisième caractère de ma_chaine (en n'oubliant pas d'utiliser l'opérateur de coercition, ou cast, adéquat afin d'éviter un message d'alerte) puis affichez respectivement le contenu de cet entier long avec la directive %x (affichage hexadécimal) et avec la directive %s (chaîne de caractères). Que constatez-vous (sans tenir compte des warning à la compilation) ? Conclusion.

Exercice 4 : passage de paramètres.

  1. Implantez la fonction : void min2maj(char*) void min2maj(char*) qui transforme toute minuscule de la chaîne passée en paramètre en majuscule. Remarque : n'utilisez pas le code ASCII des caractères (qui, on l'espère, sera enfin remplacé par l'UNICODE) mais plutôt les constantes caractères comme par exemple 'A' ou 'a'. Après avoir implanté la fonction ci-dessus, utilisez-la avec comme paramètre effectif ma_chaine puis affichez-la après l'appel. A présent, implantez la fonction :

    void avancer(char* ps)
    {
      ps++;
    }
    

    Puis utilisez-la avec comme paramètre effectif ma_chaine et affichez-la après l'appel. A quoi aurait-on pu s'attendre ? Comment éviter le passage par valeur du pointeur ?

  2. On souhaite mettre en place une fonction qui associe à un caractère une fonction quelconque, par exemple dans le cadre de l'association d'une réaction d'un programme à un événement clavier (i.e la pression d'une touche du clavier). Pour se faire, on doit utiliser le passage de fonction en paramètre dont on rappelle la syntaxe : void f(int (*g)()); Cette déclaration spécifie que la fonction f() attend comme paramètre une fonction quelconque de la forme int une_fonction() :

  1. dans un premier temps, implantez la fonction :

    void associer_une_touche(touche_alpha k, void (*g)())
    {
      touche[k] = g;
    }
    

    permettant d'associer n'importe quelle fonction à n'importe quelle touche. Remarque : touche_alpha est un type énuméré dont on donne la définition (on se limitera aux caractères de l'alphabet minuscule) :

    typedef enum {mya, myb, ..., myz} touche_alpha;
    

    Il est aussi nécessaire de définir un tableau de fonctions associé :

    static void (*touche[26])()={NULL, NULL, ..., NULL};
    
  2. dans un second temps, implantez la boucle d'attente d'évènements void main_loop(); qui lance une boucle, infinie, de scrutation d'évènements (qui dans notre exercice se limiteront aux touches du clavier). Après vérification qu'une fonction est bien associée à telle touche (donc à une lettre de l'alphabet), main_loop() se contente de lancer la fonction en question.

  3. enfin, testez tout ceci en implantant quelques fonctions très courtes et en définissant la fonction principale (main()) minimaliste qui convient (comme ce devrait être le cas à chaque fois).