Skip to content

Latest commit

 

History

History
799 lines (655 loc) · 18.3 KB

File metadata and controls

799 lines (655 loc) · 18.3 KB

🔝 Retour au Sommaire

3.7.4 Constructeurs et destructeurs en Object Pascal

Introduction

Les constructeurs et destructeurs sont des méthodes spéciales qui gèrent le cycle de vie d'un objet : sa création (naissance) et sa destruction (mort). Comprendre ces concepts est essentiel pour éviter les fuites mémoire et créer des applications robustes.

Les constructeurs

Qu'est-ce qu'un constructeur ?

Un constructeur est une méthode spéciale qui initialise un nouvel objet. C'est la première méthode appelée lors de la création d'une instance d'une classe. Le constructeur prépare l'objet pour être utilisé.

Analogie

Imaginez la construction d'une maison :

  • Le constructeur est l'équipe de construction qui bâtit la maison
  • Avant que vous puissiez habiter la maison, elle doit être construite
  • Le constructeur s'assure que tout est en place : fondations, murs, toit, électricité

Syntaxe d'un constructeur

En Object Pascal, un constructeur se déclare avec le mot-clé constructor :

type
  TPersonne = class
  private
    FNom: string;
    FAge: Integer;
  public
    constructor Create;  // Constructeur
    property Nom: string read FNom write FNom;
    property Age: Integer read FAge write FAge;
  end;

// Implémentation
constructor TPersonne.Create;  
begin  
  inherited Create;  // Appelle le constructeur de la classe parent
  FNom := '';
  FAge := 0;
  ShowMessage('Une personne a été créée');
end;

Utilisation du constructeur

var
  Personne: TPersonne;
begin
  // Appel du constructeur
  Personne := TPersonne.Create;
  try
    Personne.Nom := 'Marie';
    Personne.Age := 25;
    ShowMessage(Personne.Nom);
  finally
    Personne.Free;
  end;
end;

Le mot-clé inherited

Le mot-clé inherited dans un constructeur appelle le constructeur de la classe parent. C'est important car :

  • Toutes les classes héritent de TObject
  • TObject.Create alloue la mémoire nécessaire pour l'objet
  • Sans cet appel, l'objet ne serait pas correctement initialisé
constructor TMaClasse.Create;  
begin  
  inherited Create;  // TOUJOURS appeler inherited dans un constructeur
  // Votre code d'initialisation ici
end;

Constructeurs avec paramètres

Les constructeurs peuvent accepter des paramètres pour initialiser l'objet avec des valeurs spécifiques :

type
  TCompte = class
  private
    FTitulaire: string;
    FSolde: Double;
  public
    constructor Create(ATitulaire: string; ASoldeInitial: Double);
    property Titulaire: string read FTitulaire;
    property Solde: Double read FSolde write FSolde;
  end;

constructor TCompte.Create(ATitulaire: string; ASoldeInitial: Double);  
begin  
  inherited Create;
  FTitulaire := ATitulaire;
  FSolde := ASoldeInitial;
end;

Utilisation

var
  MonCompte: TCompte;
begin
  // Création avec initialisation
  MonCompte := TCompte.Create('Jean Dupont', 1000.0);
  try
    ShowMessage(Format('%s a un solde de %.2f €',
                       [MonCompte.Titulaire, MonCompte.Solde]));
  finally
    MonCompte.Free;
  end;
end;

Constructeurs multiples (surcharge)

Une classe peut avoir plusieurs constructeurs avec des paramètres différents. C'est ce qu'on appelle la surcharge (overload) :

type
  TRectangle = class
  private
    FLargeur: Double;
    FHauteur: Double;
  public
    constructor Create; overload;  // Constructeur par défaut
    constructor Create(ACote: Double); overload;  // Carré
    constructor Create(ALargeur, AHauteur: Double); overload;  // Rectangle
    property Largeur: Double read FLargeur write FLargeur;
    property Hauteur: Double read FHauteur write FHauteur;
  end;

// Constructeur par défaut : rectangle 1x1
constructor TRectangle.Create;  
begin  
  inherited Create;
  FLargeur := 1;
  FHauteur := 1;
end;

// Constructeur pour un carré
constructor TRectangle.Create(ACote: Double);  
begin  
  inherited Create;
  FLargeur := ACote;
  FHauteur := ACote;
end;

// Constructeur pour un rectangle quelconque
constructor TRectangle.Create(ALargeur, AHauteur: Double);  
begin  
  inherited Create;
  FLargeur := ALargeur;
  FHauteur := AHauteur;
end;

Utilisation des différents constructeurs

var
  Rect1, Rect2, Rect3: TRectangle;
begin
  // Utilisation du constructeur par défaut
  Rect1 := TRectangle.Create;

  // Utilisation du constructeur pour un carré
  Rect2 := TRectangle.Create(5);

  // Utilisation du constructeur pour un rectangle
  Rect3 := TRectangle.Create(4, 7);

  try
    ShowMessage(Format('Rect1 : %.1f x %.1f', [Rect1.Largeur, Rect1.Hauteur]));
    ShowMessage(Format('Rect2 : %.1f x %.1f', [Rect2.Largeur, Rect2.Hauteur]));
    ShowMessage(Format('Rect3 : %.1f x %.1f', [Rect3.Largeur, Rect3.Hauteur]));
  finally
    Rect1.Free;
    Rect2.Free;
    Rect3.Free;
  end;
end;

Les destructeurs

Qu'est-ce qu'un destructeur ?

Un destructeur est une méthode spéciale qui nettoie et libère les ressources utilisées par un objet avant sa destruction. C'est la dernière méthode appelée avant que l'objet ne soit supprimé de la mémoire.

Analogie

Continuons avec l'analogie de la maison :

  • Le destructeur est l'équipe de démolition
  • Avant de détruire la maison, il faut couper l'eau, l'électricité, le gaz
  • Il faut aussi récupérer les objets de valeur
  • Enfin, on peut démolir la structure

Syntaxe d'un destructeur

En Object Pascal, un destructeur se déclare avec le mot-clé destructor. Par convention, il s'appelle Destroy :

type
  TFichier = class
  private
    FNomFichier: string;
    FContenu: TStringList;
  public
    constructor Create(ANomFichier: string);
    destructor Destroy; override;  // Destructeur
    property NomFichier: string read FNomFichier;
  end;

constructor TFichier.Create(ANomFichier: string);  
begin  
  inherited Create;
  FNomFichier := ANomFichier;
  FContenu := TStringList.Create;  // Création d'un objet interne
  ShowMessage('Fichier ouvert : ' + FNomFichier);
end;

destructor TFichier.Destroy;  
begin  
  FContenu.Free;  // Libérer l'objet interne
  ShowMessage('Fichier fermé : ' + FNomFichier);
  inherited Destroy;  // TOUJOURS appeler inherited en dernier
end;

Points importants sur les destructeurs

  1. Toujours nommé Destroy : par convention
  2. Mot-clé override : car on redéfinit le destructeur de TObject
  3. inherited Destroy en dernier : contrairement au constructeur où inherited est appelé en premier
  4. Libérer les objets internes : tout objet créé dans le constructeur doit être libéré dans le destructeur

La méthode Free

Au lieu d'appeler directement Destroy, on utilise la méthode Free qui :

  • Vérifie si l'objet existe (n'est pas nil)
  • Appelle Destroy si l'objet existe

Note : Free ne met pas la variable à nil. Pour cela, utilisez FreeAndNil (voir plus loin).

var
  MonFichier: TFichier;
begin
  MonFichier := TFichier.Create('document.txt');
  try
    // Utilisation de l'objet
  finally
    MonFichier.Free;  // Appelle Destroy de manière sécurisée
  end;
end;

Exemple complet : Classe Connexion Base de Données

Voici un exemple illustrant l'importance des constructeurs et destructeurs :

type
  TConnexionDB = class
  private
    FServeur: string;
    FBaseDeDonnees: string;
    FConnecte: Boolean;
    FDateConnexion: TDateTime;
  public
    constructor Create(AServeur, ABaseDeDonnees: string);
    destructor Destroy; override;
    procedure Connecter;
    procedure Deconnecter;
    property Connecte: Boolean read FConnecte;
    property DateConnexion: TDateTime read FDateConnexion;
  end;

constructor TConnexionDB.Create(AServeur, ABaseDeDonnees: string);  
begin  
  inherited Create;
  FServeur := AServeur;
  FBaseDeDonnees := ABaseDeDonnees;
  FConnecte := False;
  FDateConnexion := 0;
  ShowMessage('Objet connexion créé');
end;

destructor TConnexionDB.Destroy;  
begin  
  // Nettoyer avant de détruire
  if FConnecte then
    Deconnecter;

  ShowMessage('Objet connexion détruit');
  inherited Destroy;  // Toujours en dernier
end;

procedure TConnexionDB.Connecter;  
begin  
  if not FConnecte then
  begin
    // Simulation de connexion
    FConnecte := True;
    FDateConnexion := Now;
    ShowMessage(Format('Connecté à %s/%s', [FServeur, FBaseDeDonnees]));
  end;
end;

procedure TConnexionDB.Deconnecter;  
begin  
  if FConnecte then
  begin
    // Simulation de déconnexion
    FConnecte := False;
    ShowMessage('Déconnecté');
  end;
end;

Utilisation

var
  Connexion: TConnexionDB;
begin
  Connexion := TConnexionDB.Create('localhost', 'MaBase');
  try
    Connexion.Connecter;
    // Utilisation de la connexion
    ShowMessage('Travail avec la base de données...');
  finally
    Connexion.Free;  // Le destructeur déconnectera automatiquement
  end;
end;

Constructeurs et destructeurs avec héritage

Lorsqu'on travaille avec l'héritage, l'ordre d'appel est important :

type
  // Classe de base
  TVehicule = class
  private
    FMarque: string;
  public
    constructor Create(AMarque: string);
    destructor Destroy; override;
  end;

  // Classe dérivée
  TVoiture = class(TVehicule)
  private
    FNombrePortes: Integer;
  public
    constructor Create(AMarque: string; ANombrePortes: Integer);
    destructor Destroy; override;
  end;

// Implémentation TVehicule

constructor TVehicule.Create(AMarque: string);  
begin  
  inherited Create;
  FMarque := AMarque;
  ShowMessage('Véhicule créé : ' + FMarque);
end;

destructor TVehicule.Destroy;  
begin  
  ShowMessage('Véhicule détruit : ' + FMarque);
  inherited Destroy;
end;

// Implémentation TVoiture

constructor TVoiture.Create(AMarque: string; ANombrePortes: Integer);  
begin  
  inherited Create(AMarque);  // Appelle le constructeur du parent
  FNombrePortes := ANombrePortes;
  ShowMessage(Format('Voiture avec %d portes créée', [FNombrePortes]));
end;

destructor TVoiture.Destroy;  
begin  
  ShowMessage('Voiture détruite');
  inherited Destroy;  // Appelle le destructeur du parent
end;

Ordre d'exécution

var
  MaVoiture: TVoiture;
begin
  MaVoiture := TVoiture.Create('Renault', 5);
  // Ordre de création :
  // 1. TObject.Create (implicite)
  // 2. TVehicule.Create
  // 3. TVoiture.Create

  try
    // Utilisation
  finally
    MaVoiture.Free;
    // Ordre de destruction :
    // 1. TVoiture.Destroy
    // 2. TVehicule.Destroy
    // 3. TObject.Destroy (implicite)
  end;
end;

Gestion des erreurs dans les constructeurs

Si une erreur se produit dans un constructeur, l'objet peut être partiellement construit. Il faut gérer ce cas :

type
  TRessource = class
  private
    FDonnees: TStringList;
    FFichier: TFileStream;
  public
    constructor Create(ANomFichier: string);
    destructor Destroy; override;
  end;

constructor TRessource.Create(ANomFichier: string);  
begin  
  inherited Create;
  FDonnees := TStringList.Create;

  try
    FFichier := TFileStream.Create(ANomFichier, fmOpenRead);
  except
    // Si l'ouverture du fichier échoue, libérer ce qui a été créé
    FDonnees.Free;
    raise;  // Relancer l'exception
  end;
end;

destructor TRessource.Destroy;  
begin  
  FFichier.Free;
  FDonnees.Free;
  inherited Destroy;
end;

Exemple avancé : Pool d'objets

Voici un exemple montrant l'utilisation de constructeurs/destructeurs pour gérer un compteur d'instances :

type
  TConnexion = class
  private
    class var FNombreInstances: Integer;  // Variable de classe
    FID: Integer;
  public
    constructor Create;
    destructor Destroy; override;
    class function ObtenirNombreInstances: Integer;
    property ID: Integer read FID;
  end;

constructor TConnexion.Create;  
begin  
  inherited Create;
  Inc(FNombreInstances);
  FID := FNombreInstances;
  ShowMessage(Format('Connexion #%d créée (Total: %d)',
                     [FID, FNombreInstances]));
end;

destructor TConnexion.Destroy;  
begin  
  ShowMessage(Format('Connexion #%d détruite (Restant: %d)',
                     [FID, FNombreInstances - 1]));
  Dec(FNombreInstances);
  inherited Destroy;
end;

class function TConnexion.ObtenirNombreInstances: Integer;  
begin  
  Result := FNombreInstances;
end;

Utilisation

var
  Conn1, Conn2, Conn3: TConnexion;
begin
  Conn1 := TConnexion.Create;  // Connexion #1 créée (Total: 1)
  Conn2 := TConnexion.Create;  // Connexion #2 créée (Total: 2)
  Conn3 := TConnexion.Create;  // Connexion #3 créée (Total: 3)

  ShowMessage('Instances actives : ' +
              IntToStr(TConnexion.ObtenirNombreInstances));

  Conn2.Free;  // Connexion #2 détruite (Restant: 2)

  ShowMessage('Instances actives : ' +
              IntToStr(TConnexion.ObtenirNombreInstances));

  Conn1.Free;
  Conn3.Free;
end;

La méthode FreeAndNil

Pour éviter les erreurs avec des pointeurs dangling (pointant vers de la mémoire libérée), utilisez FreeAndNil :

var
  MonObjet: TMonObjet;
begin
  MonObjet := TMonObjet.Create;
  try
    // Utilisation
  finally
    FreeAndNil(MonObjet);  // Libère ET met à nil
  end;

  // MonObjet vaut maintenant nil, pas de risque d'accès invalide
end;

FreeAndNil fait deux choses :

  1. Appelle Free sur l'objet
  2. Met la variable à nil

Constructeurs virtuels

Les constructeurs peuvent être virtuels, ce qui permet le polymorphisme :

type
  TDocument = class
  public
    constructor Create; virtual;
    procedure Sauvegarder; virtual; abstract;
  end;

  TDocumentTexte = class(TDocument)
  public
    constructor Create; override;
    procedure Sauvegarder; override;
  end;

  TDocumentImage = class(TDocument)
  public
    constructor Create; override;
    procedure Sauvegarder; override;
  end;

constructor TDocument.Create;  
begin  
  inherited Create;
  ShowMessage('Document générique créé');
end;

constructor TDocumentTexte.Create;  
begin  
  inherited Create;
  ShowMessage('Document texte créé');
end;

constructor TDocumentImage.Create;  
begin  
  inherited Create;
  ShowMessage('Document image créé');
end;

Utilisation avec les métaclasses

type
  TDocumentClass = class of TDocument;

procedure CreerEtUtiliser(ClasseDocument: TDocumentClass);  
var  
  Doc: TDocument;
begin
  Doc := ClasseDocument.Create;  // Polymorphisme au niveau de la classe
  try
    Doc.Sauvegarder;
  finally
    Doc.Free;
  end;
end;

begin
  CreerEtUtiliser(TDocumentTexte);
  CreerEtUtiliser(TDocumentImage);
end;

Bonnes pratiques

1. Toujours utiliser try...finally

// ✅ Bon
var
  Obj: TMonObjet;
begin
  Obj := TMonObjet.Create;
  try
    // Utilisation
  finally
    Obj.Free;
  end;
end;

// ❌ Mauvais - risque de fuite mémoire en cas d'exception
var
  Obj: TMonObjet;
begin
  Obj := TMonObjet.Create;
  // Utilisation
  Obj.Free;
end;

2. Initialiser tous les champs dans le constructeur

constructor TPersonne.Create;  
begin  
  inherited Create;
  FNom := '';           // Toujours initialiser
  FAge := 0;            // Ne pas laisser de valeurs indéfinies
  FActif := False;
  FDateCreation := Now;
end;

3. Libérer tous les objets créés dans le destructeur

constructor TMaClasse.Create;  
begin  
  inherited Create;
  FListe := TStringList.Create;
  FDictionnaire := TDictionary<string, Integer>.Create;
end;

destructor TMaClasse.Destroy;  
begin  
  FDictionnaire.Free;  // Libérer tout ce qui a été créé
  FListe.Free;
  inherited Destroy;   // Toujours en dernier
end;

4. inherited Create en premier, inherited Destroy en dernier

// ✅ Bon
constructor TMaClasse.Create;  
begin  
  inherited Create;  // EN PREMIER
  // Initialisation
end;

destructor TMaClasse.Destroy;  
begin  
  // Nettoyage
  inherited Destroy;  // EN DERNIER
end;

5. Utiliser FreeAndNil pour éviter les accès invalides

procedure TForm.FermerConnexion;  
begin  
  FreeAndNil(FConnexion);  // Libère et met à nil
  // FConnexion vaut maintenant nil, sûr pour les tests ultérieurs
end;

6. Valider les paramètres du constructeur

constructor TRectangle.Create(ALargeur, AHauteur: Double);  
begin  
  inherited Create;

  if (ALargeur <= 0) or (AHauteur <= 0) then
    raise Exception.Create('Les dimensions doivent être positives');

  FLargeur := ALargeur;
  FHauteur := AHauteur;
end;

Erreurs courantes à éviter

1. Oublier inherited

// ❌ Mauvais
constructor TMaClasse.Create;  
begin  
  // Manque inherited Create !
  FValeur := 10;
end;

// ✅ Bon
constructor TMaClasse.Create;  
begin  
  inherited Create;
  FValeur := 10;
end;

2. Ne pas libérer les objets internes

// ❌ Mauvais - fuite mémoire !
constructor TMaClasse.Create;  
begin  
  inherited Create;
  FListe := TStringList.Create;
end;

destructor TMaClasse.Destroy;  
begin  
  // Manque FListe.Free !
  inherited Destroy;
end;

// ✅ Bon
destructor TMaClasse.Destroy;  
begin  
  FListe.Free;
  inherited Destroy;
end;

3. Appeler Free au lieu de Destroy

// ❌ Mauvais
MyObject.Destroy;  // Ne faites jamais ça !

// ✅ Bon
MyObject.Free;  // Utilisez toujours Free

Résumé

  • Constructeur (constructor Create)

    • Initialise un nouvel objet
    • Appelé avec le mot-clé Create
    • inherited Create doit être appelé en premier
    • Peut avoir plusieurs versions (surcharge avec overload)
    • Peut accepter des paramètres pour l'initialisation
  • Destructeur (destructor Destroy)

    • Nettoie et libère les ressources
    • Toujours nommé Destroy
    • inherited Destroy doit être appelé en dernier
    • Libère tous les objets internes créés
    • Ne jamais appeler directement, utiliser Free
  • Bonnes pratiques

    • Toujours utiliser try...finally avec Free
    • Initialiser tous les champs dans le constructeur
    • Libérer tous les objets internes dans le destructeur
    • Utiliser FreeAndNil pour éviter les pointeurs dangling
    • Valider les paramètres du constructeur
  • Gestion mémoire

    • Les objets Delphi ne sont pas libérés automatiquement
    • Toujours appeler Free pour éviter les fuites mémoire
    • Le bloc try...finally garantit la libération même en cas d'exception

Maîtriser les constructeurs et destructeurs est essentiel pour créer des applications Delphi robustes et éviter les fuites mémoire.

⏭️ Interfaces