INF3105 — Structures de données et algorithmes
Été 2024
UQAM
Département d'informatique

Laboratoire 2 : Pointeurs, références, classes, constructeurs et destructeurs


Lectures préalables

Objectifs

  1. Pointeurs et références.
  2. Classes, constructeurs et destructeurs.
  3. Allocation automatique.
  4. Allocation dynamique.

Tâches


Les Pointeurs

  1. Créez un fichier lab2ex1.cpp, copiez et collez le code ci-dessous.
  2. #include <iostream>
    using namespace std;
    
    int main() {
        int a = 1, b = 2, c = 3, d = 4;
        int* pa = &a;
        int* pb = &b;
        int* pc = &c, *pd = &d;
        cout << "a=" << a << "\tb=" << b << "\tc=" << c << "\td=" << d << endl;
        cout << "pa=" << pa << "\tpb=" << pb << "\tpc=" << pc << "\tpd=" << pd << endl;
        *pa = 4;
        cout << "a=" << a << "\tb=" << b << "\tc=" << c << "\td=" << d << endl;
        cout << "pa=" << pa << "\tpb=" << pb << "\tpc=" << pc << "\tpd=" << pd << endl;
        pa = pb;
        *pa = 8;
        cout << "a=" << a << "\tb=" << b << "\tc=" << c << "\td=" << d << endl;
        cout << "pa=" << pa << "\tpb=" << pb << "\tpc=" << pc << "\tpd=" << pd << endl;
        c = 10;
        d += *pd;
        cout << "a=" << a << "\tb=" << b << "\tc=" << c << "\td=" << d << endl;
        cout << "pa=" << pa << "\tpb=" << pb << "\tpc=" << pc << "\tpd=" << pd << endl;
        return 0;
    }
                    
  3. Ne le compilez pas tout de suite!
  4. Analysez le programme. Prédisez ce qu'il devrait faire.
  5. Compilez-le et exécutez-le.
  6. g++ lab2ex1.cpp -o lab2ex1
    ./lab2ex1
                    
  7. Vérifiez l'exactitude de votre prédiction.

Les Références

  1. Créez un fichier lab2ex2.cpp, copiez et collez le code ci-dessous.
  2. #include <iostream>
    using namespace std;
    
    int main() {
        int a = 1, b = 2, c = 3, d = 4;
        int& ra = a;
        int& rb = &b;
        int* pc = &c, *pd = &d;
        cout << "a=" << a << "\tb=" << b << "\tc=" << c << "\td=" << d << endl;
        cout << "ra=" << ra << "\trb=" << rb << "\tpc=" << pc << "\tpd=" << pd << endl;
        ra = 4;
        cout << "a=" << a << "\tb=" << b << "\tc=" << c << "\td=" << d << endl;
        cout << "ra=" << ra << "\trb=" << rb << "\tpc=" << pc << "\tpd=" << pd << endl;
        ra = rb;
        cout << "a=" << a << "\tb=" << b << "\tc=" << c << "\td=" << d << endl;
        cout << "ra=" << ra << "\trb=" << rb << "\tpc=" << pc << "\tpd=" << pd << endl;
        c = 10;
        d += *pd;
        cout << "a=" << a << "\tb=" << b << "\tc=" << c << "\td=" << d << endl;
        cout << "ra=" << ra << "\trb=" << rb << "\tpc=" << pc << "\tpd=" << pd << endl;
        return 0;
    }
                    
  3. Selon vous, est-ce que ce programme peut être compilé sans modification?
  4. non
  5. Essayez de compiler.
  6. g++ lab2ex2.cpp -o lab2ex2
                    
  7. Essayez de comprendre le message d'erreur.
  8. lab2ex2.cpp: In function ‘int main()’:
    lab2ex2.cpp:7:14: erreur: invalid initialization of non-const reference of type ‘int&’ from an rvalue of type ‘int*’
                    
    La partie de droite après le = doit être un entier ou une référence sur un entier et non un pointeur d'entier.
  9. Rappel : une référence ressemble à un pointeur, mais on n'a pas besoin d'expliciter la demande de l'adresse mémoire avec le symbole &.
  10. &b est de type 'int*' et non 'int'
  11. Appliquez la correction minimale pour rendre ce programme compilable.
  12. int& rb=b; // La référence rb prend automatiquement l'adresse de b. On n'a pas besoin d'expliciter cela avec & devant b comme pour un pointeur.
  13. Comme pour l'exercice 1, analysez le programme sans l'exécuter.
  14. Prédisez ce qu'il devrait produire.
  15. g++ lab2ex2.cpp -o lab2ex2
    ./lab2ex2
                    
  16. Vérifiez l'exactitude de votre prédiction.

Types de passage de paramètres

Expérimentez l'exemple retrouvé à la section 2.10.1 des notes de cours.

// lab2ex3.cpp
#include <iostream>
using namespace std;
void test(int a, int* b, int* c, int& d, int*& e) {
    a = 11;  // effet local
    b++;     // change l’adresse locale de b
    *c = 13; // change la valeur pointee par c
    d = 14;  // change la valeur referee par d
    e = c;   // change la valeur du pointeur (adresse) pour celle de c.
}
int main() {
    int v1 = 1, v2 = 2, v3 = 3, v4 = 4, *p5 = &v1;
    test(v1, &v2, &v3, v4, p5);
    cout<<v1<<"\t"<<v2<<"\t"<<v3<<"\t"<<v4<<"\t"<<*p5<<"\t"<<endl;
    return 0;
}
            

Classes, constructeurs et destructeurs

  1. Créez un fichier point.h.
  2. #include <iostream>
    class Point {
      public:
        Point(double x = 0, double y = 0);
        Point(const Point&);
        ~Point();
    
        double distance(const Point&) const;
    
      private:
        double x;
        double y;
    
        friend std::ostream& operator << (std::ostream&, const Point&);
        friend std::istream& operator >> (std::istream&, Point&);
    };
                    
  3. Créez un fichier point.cpp.
  4. #include <assert.h>
    #include "point.h"
    
    Point::Point(double _x, double _y) : x(_x), y(_y) {}
    Point::Point(const Point& point) : x(point.x), y(point.y) {}
    Point::~Point() {}
    
    double Point::distance(const Point& point) const {
      // TODO
      return 0;
    }
    std::ostream& operator << (std::ostream& os, const Point& point) {
      os << "(" << point.x << "," << point.y << ")";
      return os;
    }
    std::istream& operator >> (std::istream& is, Point& point) {
      char po, vir, pf;
      is >> po;
      if(is){
        is >> point.x >> vir >> point.y >> pf;
        assert(po == '(');    assert(vir == ',');    assert(pf == ')');
      }
      return is;
    }
                    
  5. Créez un fichier lab2ex4a.cpp.
  6. #include "point.h"
    using namespace std;
    int main() {
        Point a;
        Point b(3, 4);
        cout << a.distance(b) << endl;
        return 0;
    }
                    
  7. Compilez et exécutez.
  8. g++ -o lab2ex4a lab2ex4a.cpp point.cpp
    ./lab2ex4a
                    
    0
                    
  9. Complétez la fonction Point::distance dans point.cpp. Vous aurez besoin de sqrt dans math.h ou de std::sqrt dans cmath.
  10. #include <math.h>
    ...
    double Point::distance(const Point& point) const {
      double dx = x - point.x,
             dy = y - point.y;
      return sqrt(dx*dx + dy*dy);
    }
                    
  11. Compilez et exécutez.
  12. g++ -o lab2ex4a lab2ex4a.cpp point.cpp
    ./lab2ex4a
                    
    5
                    
  13. Créez un fichier lab2ex4b.cpp.
  14. #include "point.h"
    using namespace std;
    void test(Point p1, Point& p2, const Point& p3, Point* p4) {
       cout << p1 << endl;
       p1 = p2;
       *p4 = p3;
       p4 = &p1;
    }
    int main(){
        Point a, b(3, 4), c(0, 5);
        Point*d = new Point(5, 0);
        test(a, b, c, d);
        cout << a << '\t' << b << '\t' << c << '\t' << *d << endl;
        return 0;
    }
                    
  15. Analysez le programme et essayez de comprendre ce qu'il fait.
  16. Compilez et exécutez.
  17. g++ -o lab2ex4b lab2ex4b.cpp point.cpp
    ./lab2ex4b
                    
  18. Le programme lab2ex4b.cpp libère-t-il la mémoire correctement?
  19. Non.
  20. Que faut-il ajouter pour libérer la mémoire?
  21. Il faut ajouter delete d; avant le return.
  22. Corrigez, compilez et exécutez.
  23. g++ -o lab2ex4b lab2ex4b.cpp point.cpp
    ./lab2ex4b
                    
    (0,0)
    (0,0)	(3,4)	(0,5)	(0,5)
                    
  24. Dans quel ordre sont appelés les constructeurs et destructeurs?
    1. D'abord le constructeur sans argument Point::Point(), c'est-à-dire Point::Point(double x=0, double y=0), pour a.
    2. Le constructeur Point::Point(double x, double y) pour b et c.
    3. L'opérateur new appelle le constructeur Point::Point(double x, double y) pour d.
    4. Le constructeur par copie Point::Point(const Point&&) est appelé pour p1 lors du passage de a à la fonction test.
    5. Le destructeur Point::~Point() est appelé pour p1 à la fin de la fonction test.
    6. Le destructeur Point::~Point() est appelé pour *d lors du delete.
    7. Le destructeur Point::~Point() est appelé pour les 3 objets Point alloués automatiquement sur la pile d'exécution, et ce, dans l'ordre inverse : c, b et a
  25. Pour vous aider, modifiez les constructeurs et destructeurs dans point.cpp :
    Point::Point(double _x, double _y) : x(_x), y(_y) {
      std::cerr << "Point::Point(double, double)" << std::endl;
    }
    Point::Point(const Point& point) : x(point.x), y(point.y) {
      std::cerr << "Point::Point(const Point&)" << std::endl;
    }
    Point::~Point() {
      std::cerr << "Point::~Point()" << std::endl;
    }
                        
  26. Recompilez et réexécutez.
  27. g++ -o lab2ex4b lab2ex4b.cpp point.cpp
    ./lab2ex4b
                        
    Point::Point(double, double)
    Point::Point(double, double)
    Point::Point(double, double)
    Point::Point(double, double)
    Point::Point(const Point&)
    (0,0)
    Point::~Point()
    (0,0)	(3,4)	(0,5)	(0,5)
    Point::~Point()
    Point::~Point()
    Point::~Point()
    Point::~Point()
                        

Copie de pointeur et gestion de mémoire

    Créez le programme lab2ex5.cpp à partir du code ci-dessous.

    #include "point.h"
    
    class Rectangle {
      public:
        Rectangle();
        Rectangle(const Point& p1, const Point& p2);
      private:
        Point point1, point2;
    };
    
    Rectangle::Rectangle() {}
    Rectangle::Rectangle(const Point& p1, const Point& p2) : point1(p1) {
        point2 = p2;
    }
    
    int main() {
        Rectangle* r1 = new Rectangle();
        Rectangle* r2 = new Rectangle(Point(0, 0), Point(10, 10));
        Rectangle* r3 = new Rectangle();
        *r3 = *r2;
        r1 = r3;
        delete r1;
        delete r2;
        delete r3;
    }
                    
  1. Ce programme est-il compilable? Sinon, comment doit-on le modifier?
  2. Oui.
  3. Que fait ce programme?
  4. Alloue 3 'Rectangle' dynamiquement.
  5. Le deuxième constructeur Rectangle::Rectangle(const Point& p1, const Point& p2) initialise p1 et p2 de façon différente. Dans vos propres mots, expliquez la différence.
  6. Compilez et exécutez.
  7. g++ -o lab2ex5 lab2ex5.cpp point.cpp
    ./lab2ex5
                    
  8. Ce programme termine-t-il anormalement? Pourquoi?
  9. Non.
  10. Le programme libère-t-il la mémoire correctement ?
  11. Non.
  12. Combien d'octets ne sont pas libérés correctement de la mémoire à la fin du main() ?
  13. Taille de 1 Rectange ou sizeof(Rectangle). Un Rectange contient deux objets Point, chaque Point contient deux objets double. Un double occupe 64 bits, soit 8 octets. sizeof(Rectangle)=2*2*8 = 32 octets.
  14. Corrigez le programme.
  15. int main() {
        Rectangle* r1 = new Rectangle();
        Rectangle* r2 = new Rectangle(Point(0, 0), Point(10, 10));
        Rectangle* r3 = new Rectangle();
        *r3 = *r2;
        delete r1; // Il faut faire delete sur r1 avant d'écraser sa valeur
        r1 = r3; // Copie du pointeur.
        delete r2;
        delete r3;
    }
    
                    
  16. Recompilez et réexécutez.
  17. g++ -o lab2ex5 lab2ex5.cpp point.cpp
    ./lab2ex5
                    

Analyse d'un programme

Notez que cet exercice est inspiré d'une question d'examen de mi-session.

Soit le programme lab2ex6.cpp suivant.

#include <iostream>

class Allo {
  public:
    Allo(int x_ = 0) : x(x_) {
      std::cout << "A" << x << " ";
    }
    Allo(const Allo& autre) : x(autre.x) {
      std::cout << "B" << x << " ";
    }
    ~Allo() {
      std::cout << "C" << x << " ";
    }

    int x;
};


void f1(Allo a1, Allo& a2, Allo* a3) {
    a1.x++;
    a2.x--;
    a3 += 1;
    (a3->x) += 2000;
}

int main() {
    Allo tab[3];
    Allo a(20);
    Allo* b = new Allo(5);
    Allo* c = tab + 1;
    f1(a, b, c);
    std::cout << std::endl << "-------" << std::endl;
    Allo* t = new Allo[4];
    t[2] = Allo(9);
    std::cout << std::endl;
    return 0;
}
            

Avant de compiler et d'exécuter le programme ci-haut, essayez de répondre aux questions suivantes.

  1. Le programme ci-haut ne peut être compilé correctement en raison d’une erreur. Quelle est la correction minimale nécessaire pour compiler le programme?
  2. int main() {
        Allo tab[3];
        Allo a(20);
        Allo* b = new Allo(5);
        Allo* c = tab + 1;
        f1(a, *b, c);
        std::cout << std::endl << "-------" << std::endl;
        Allo* t = new Allo[4];
        t[2] = Allo(9);
        std::cout << std::endl;
        return 0;
    }
                    
  3. Une fois corrigé, est-ce que le programme s’exécute correctement (sans terminaison anormale) ? Sinon, indiquez l’erreur et la correction minimale pour éviter une terminaison anormale.
  4. Dessinez l’état de la pile d’exécution et du tas (heap) immédiatement avant le retour de la fonction f1.
  5. Qu’affiche le programme ?
  6. A0 A0 A0 A20 A5 B20 C21
    -------
    A0 A0 A0 A0 A9 C9
    C20 C2000 C0 C0
                    
  7. Le programme libère-t-il la mémoire correctement ?
  8. Non.
  9. Combien d'octets ne sont pas libérés correctement de la mémoire à la fin du main() ?
  10. 5 * sizeof(Allo) --> b (1 Allo) + t (4 Allo). sizeof(Allo) = sizeof(int) = 4. Donc, 5*4 = 20 octets.
  11. Comment corrigeriez-vous le programme?
  12. int main() {
        Allo tab[3];
        Allo a(20);
        Allo* b = new Allo(5);
        Allo* c = tab + 1;
        f1(a, *b, c);
        std::cout << std::endl << "-------" << std::endl;
                         Allo* t = new Allo[4];
                         t[2] = Allo(9);
                         std::cout << std::endl;
        delete b;
        delete[] t;
        return 0;
    }
                    

    Après correction, le programme affiche:

    A0 A0 A0 A20 A5 B20 C21
    -------
    A0 A0 A0 A0 A9 C9
    C4 C0 C9 C0 C0 C20 C2000 C0 C0
                    

Fin !