Classes and Objects in Delphi: OOP for Beginners

Every form you’ve built in Delphi is already a class: TForm1 = class(TForm). Every button is an object. So you’ve been using object-oriented programming (OOP) from day one without writing your own classes.

This article shows you how to write them. You’ll build a small TBankAccount class step by step and learn fields, methods, properties, constructors, freeing memory correctly, inheritance and lists of objects.

It’s part 4 of the Learn Delphi series. You should be comfortable with procedures, functions and units first.

Diagram: TBankAccount class creating three account objects, and a TSavingsAccount child class

Class vs Object

  • A class is a blueprint. It describes what data something has and what it can do.
  • An object (or instance) is one thing built from that blueprint.

TCustomer is a class. “Sara Ali, customer 1042” is an object. You can create as many objects from one class as you need.

By convention, Delphi class names start with T (for “type”): TCustomer, TInvoice, TBankAccount.

Your First Class

type
  TBankAccount = class
  private
    FOwner: string;
    FBalance: Currency;
  public
    procedure Deposit(Amount: Currency);
    function GetSummary: string;
  end;

procedure TBankAccount.Deposit(Amount: Currency);
begin
  FBalance := FBalance + Amount;
end;

function TBankAccount.GetSummary: string;
begin
  Result := Format('%s: %.2f', [FOwner, FBalance]);
end;

The parts:

PartExampleMeaning
FieldFBalance: Currency;Data stored in each object. By convention fields start with F.
Methodprocedure Deposit(...)A procedure or function that belongs to the class.
Method bodyprocedure TBankAccount.Deposit(...)Written separately, with the class name in front.
privateOnly code in this unit can use it.
publicEveryone can use it.

Inside a method, you can use the fields directly (FBalance), because the method always works on one specific object.

Creating and Freeing Objects

Objects in Delphi live on the heap. You create them with a constructor and you must free them when you’re done:

var
  Acc: TBankAccount;
begin
  Acc := TBankAccount.Create;
  try
    Acc.Deposit(100);
    Acc.Deposit(50);
    WriteLn(Acc.GetSummary);
  finally
    Acc.Free;
  end;
end;

This Create / try / finally / Free pattern is the most important habit in Delphi:

  • Create is called before try. If creating fails, there’s nothing to free.
  • The finally block always runs, even if an error happens in the middle. So the object is always freed and you don’t leak memory.

Why not just Acc.Free at the end? Because if Deposit raised an error, the code would jump out and never reach that line. try ... finally guarantees it.

On Windows, Delphi doesn’t have a garbage collector for normal objects. If you create an object and never free it, the memory stays used until the program closes. That’s a memory leak. To find leaks during development, add this line at the start of your program:

ReportMemoryLeaksOnShutdown := True;

When the app closes, Delphi shows a message listing any objects you forgot to free.

Objects Owned by the Form

Components you drop on a form are freed automatically, because the form is their owner. The same works in code:

Btn := TButton.Create(Self);   // Self = the form, which becomes the owner
Btn.Parent := Self;            // show it on the form
Btn.Caption := 'Click me';

Here you don’t call Free: the form frees the button when the form is destroyed. Your own classes like TBankAccount have no owner, so you free them yourself.

Constructors and Destructors

A constructor sets up a new object. Declare your own to pass in starting values:

type
  TBankAccount = class
  private
    FOwner: string;
    FBalance: Currency;
  public
    constructor Create(const AOwner: string; AOpening: Currency = 0);
    procedure Deposit(Amount: Currency);
    procedure Withdraw(Amount: Currency);
    function GetSummary: string;
  end;

constructor TBankAccount.Create(const AOwner: string; AOpening: Currency);
begin
  inherited Create;      // run the parent's constructor first
  FOwner := AOwner;
  FBalance := AOpening;
end;
Acc := TBankAccount.Create('Sara', 200);

Notes:

  • Fields of a new object start at zero, empty string, False or nil, so you only set what needs a different value.
  • inherited Create; calls the constructor of the parent class. Always do this first.
  • Parameter names often start with A (AOwner) so they don’t clash with fields or properties.

A destructor cleans up. You only need one when your object creates other objects:

destructor Destroy; override;     // in the class declaration

destructor TOrder.Destroy;
begin
  FLines.Free;          // free what this object created
  inherited;            // then let the parent clean up
end;

Always call Free, never Destroy directly. Free checks for nil first and then calls Destroy.

Properties

Making fields public is risky: anyone could set FBalance := -1000000. A property gives controlled access:

type
  TBankAccount = class
  private
    FOwner: string;
    FBalance: Currency;
    procedure SetOwner(const Value: string);
  public
    property Owner: string read FOwner write SetOwner;
    property Balance: Currency read FBalance;   // read-only
  end;

procedure TBankAccount.SetOwner(const Value: string);
begin
  if Trim(Value) = '' then
    raise Exception.Create('Owner name cannot be empty');
  FOwner := Trim(Value);
end;
  • read FOwner reads the field directly.
  • write SetOwner calls a method, so you can validate the value.
  • Balance has no write, so outside code can read it but can only change it through Deposit and Withdraw.

This is encapsulation: the object protects its own data. It’s exactly how components work. When you set Button1.Caption in the Object Inspector, you’re setting a property, and a setter method repaints the button.

The Full Class

type
  TBankAccount = class
  private
    FOwner: string;
    FBalance: Currency;
  public
    constructor Create(const AOwner: string; AOpening: Currency = 0);
    procedure Deposit(Amount: Currency);
    procedure Withdraw(Amount: Currency); virtual;
    property Owner: string read FOwner;
    property Balance: Currency read FBalance;
  end;

constructor TBankAccount.Create(const AOwner: string; AOpening: Currency);
begin
  inherited Create;
  FOwner := AOwner;
  FBalance := AOpening;
end;

procedure TBankAccount.Deposit(Amount: Currency);
begin
  if Amount <= 0 then
    raise Exception.Create('Deposit must be positive');
  FBalance := FBalance + Amount;
end;

procedure TBankAccount.Withdraw(Amount: Currency);
begin
  if Amount > FBalance then
    raise Exception.CreateFmt('Not enough money in %s''s account', [FOwner]);
  FBalance := FBalance - Amount;
end;

raise Exception.Create(...) stops the method and reports an error to the caller. The exception handling article in this series covers it in detail.

Inheritance: Building on a Class

A new class can inherit everything from an existing one and add or change behaviour. Here a savings account adds interest and blocks overdrawing below a minimum:

type
  TSavingsAccount = class(TBankAccount)
  private
    FRate: Double;
  public
    constructor Create(const AOwner: string; AOpening: Currency; ARate: Double);
    procedure AddInterest;
    procedure Withdraw(Amount: Currency); override;
  end;

constructor TSavingsAccount.Create(const AOwner: string; AOpening: Currency;
  ARate: Double);
begin
  inherited Create(AOwner, AOpening);
  FRate := ARate;
end;

procedure TSavingsAccount.AddInterest;
begin
  Deposit(Balance * FRate);
end;

procedure TSavingsAccount.Withdraw(Amount: Currency);
begin
  if Balance - Amount < 100 then
    raise Exception.Create('Savings must keep at least 100');
  inherited Withdraw(Amount);
end;
  • class(TBankAccount) means “TSavingsAccount is a TBankAccount, plus more”.
  • Withdraw was marked virtual in the parent, so the child can override it.
  • inherited Withdraw(Amount) runs the parent’s version after the extra check.

Why virtual Matters: Polymorphism

Because Withdraw is virtual, Delphi calls the right version based on the real object, even through a parent-type variable:

var
  Acc: TBankAccount;
begin
  Acc := TSavingsAccount.Create('Omar', 150, 0.03);
  try
    Acc.Withdraw(80);        // calls TSavingsAccount.Withdraw -> error
  finally
    Acc.Free;
  end;
end;

Without virtual/override, the parent’s Withdraw would run and the savings rule would be skipped. This is called polymorphism: one call, different behaviour depending on the object.

You can check an object’s real class with is and convert it with as:

if Acc is TSavingsAccount then
  (Acc as TSavingsAccount).AddInterest;

Lists of Objects: TObjectList<T>

Real apps hold many objects. Use TObjectList<T> from System.Generics.Collections. It owns its objects and frees them when it’s freed:

uses
  System.SysUtils, System.Generics.Collections;

var
  Accounts: TObjectList<TBankAccount>;
  Acc: TBankAccount;
  Total: Currency;
begin
  Accounts := TObjectList<TBankAccount>.Create;   // OwnsObjects = True
  try
    Accounts.Add(TBankAccount.Create('Sara', 500));
    Accounts.Add(TBankAccount.Create('Ali', 120));
    Accounts.Add(TSavingsAccount.Create('Lina', 1000, 0.02));

    Total := 0;
    for Acc in Accounts do
    begin
      WriteLn(Format('%-6s %8.2f', [Acc.Owner, Acc.Balance]));
      Total := Total + Acc.Balance;
    end;
    WriteLn(Format('Total  %8.2f', [Total]));
  finally
    Accounts.Free;   // frees the list AND the three accounts
  end;
end;

Output:

Sara     500.00
Ali      120.00
Lina    1000.00
Total   1620.00
CollectionUse it for
TList<T>Simple values (Integer, records) or objects you free yourself
TObjectList<T>Objects the list should own and free
TDictionary<K,V>Fast lookup by key, such as customers by ID
TStringListLists of strings, with sorting, CommaText and Values['Key']

How This Connects to Database Work

In a database app, a common design is one class per business entity:

type
  TCustomer = class
  public
    ID: Integer;
    Name: string;
    City: string;
  end;

A function in your data module reads rows with a parameterised query and returns a TObjectList<TCustomer>. The forms work with clean objects instead of raw query fields. You’ll see this style in many larger Delphi codebases. (Public fields like ID are fine for simple data holders; use properties when you need validation.)

Common Beginner Mistakes

  1. Forgetting to free objects. Use Create / try / finally / Free every time, and turn on ReportMemoryLeaksOnShutdown.
  2. Putting Create inside the try. If Create fails, finally calls Free on an unassigned variable and crashes.
  3. Using an object after freeing it. If a variable lives on, use FreeAndNil(Obj) so it becomes nil and the bug shows up clearly.
  4. Forgetting override. Declaring the same method again without override hides the parent’s method instead of replacing it, and polymorphism stops working. The compiler warns: “Method hides virtual method of base type”.
  5. Forgetting inherited in constructors and destructors.
  6. Freeing components that have an owner. Their owner frees them. Freeing them yourself is allowed, but don’t do both by accident in a destructor.

Practice Exercises

Exercise 1. Add a read-only property TransactionCount to TBankAccount that counts deposits and withdrawals.

Exercise 2. Create a TCheckingAccount class that allows the balance to go down to -500 (an overdraft).

Exercise 3. Find the bug:

Acc := TBankAccount.Create('Sara');
Acc.Deposit(100);
Acc.Withdraw(500);
Acc.Free;

Answers

Exercise 1: add a field FCount: Integer;, the property property TransactionCount: Integer read FCount;, and Inc(FCount); at the end of Deposit and Withdraw.

Exercise 2:

type
  TCheckingAccount = class(TBankAccount)
  public
    procedure Withdraw(Amount: Currency); override;
  end;

procedure TCheckingAccount.Withdraw(Amount: Currency);
begin
  if Balance - Amount < -500 then
    raise Exception.Create('Overdraft limit is 500');
  // FBalance is private to TBankAccount's unit, so this only works
  // if both classes are in the same unit. Otherwise make FBalance
  // protected in TBankAccount.
  FBalance := FBalance - Amount;
end;

This is a good example of protected: a section visible to child classes in other units, but not to outside code.

Exercise 3: Withdraw(500) raises an error, so Acc.Free never runs and the object leaks. Wrap it:

Acc := TBankAccount.Create('Sara');
try
  Acc.Deposit(100);
  Acc.Withdraw(500);
finally
  Acc.Free;
end;

Summary

  • A class is a blueprint; an object is one instance created with Create.
  • Fields hold data, methods hold behaviour, and properties give controlled access to fields.
  • Always use Create / try / finally / Free. Objects with an owner are freed by the owner.
  • Use virtual and override to change behaviour in child classes, and inherited to call the parent.
  • TObjectList<T> holds and frees a list of objects.

Next in the series: exception handling with try..except, try..finally and raise. All parts are in the Delphi category.

Related articles

Scroll to Top