Skip to content

Search is only available in production builds. Try building and previewing the site to test it out locally.

5.6 Dynamic Compilation & Multiple Inheritance

C++ allows objects to be created dynamically at runtime using pointers in combination with the new and delete keywords. This gives the programmer direct control over memory allocation and deallocation. Dynamic memory is extremely powerful: it allows programs to create objects whenever they are needed (for example, allocating space for an image buffer, or creating temporary records during program execution).

However, with this flexibility comes risk. If memory is not managed correctly, it can lead to serious problems such as memory leaks, dangling pointers, or double-deletes.

Example: Memory Leak (Full source code: accountDynamicCreation.cpp)

int main() {
Account* a = new Account("John", 10.50, 123456);
Account* b = new Account("Derek", 12.07, 123457);
a->display();
b->display();
b = a; // problem here
a->display();
b->display();
}

Explanation: When this program runs:

  1. Two Account objects are created dynamically using new, and their addresses are stored in the pointers a and b.
  2. Calling display() on each pointer correctly prints John’s and Derek’s account details.
  3. The assignment b = a; causes b to point to the same object as a.

At this point, the pointer to Derek’s account is lost. Since there are no references left to that object, the memory it occupies cannot be released — this is a memory leak.

Now both a and b point to the same object (John’s account), so calling display() on either pointer shows the same details. Derek’s account still exists in memory, but the program can no longer access or free it. This is the output:

account number: 123456 has balance: 10.5 Euro
This account is owned by: John
account number: 123457 has balance: 12.07 Euro
This account is owned by: Derek
account number: 123456 has balance: 10.5 Euro
This account is owned by: John
account number: 123456 has balance: 10.5 Euro
This account is owned by: John

This can be further illustrated as in this reference figure:

Reference figure

Memory Leak from Reassignment

One assignment, one stranded object — and no way to ever delete it.

C++main.cpp
1int main() {
2 Account* a = new Account("John", 10.50, 123456);
3 Account* b = new Account("Derek", 12.07, 123457);
4 a->display();
5 b->display();
6 b = a; // ← problem here
7 a->display();
8 b->display();
9}
Before line 6
a → John · b → Derek
Heap
Anonymous
Account
name : "John"
balance : 10.50 Euro
accountNumber : 123456
Anonymous
Account
name : "Derek"
balance : 12.07 Euro
accountNumber : 123457
aAccount *
bAccount *
After line 6
b = a;
a → John · b → John · Derek leaked
Heap
Anonymous
Account
name : "John"
balance : 10.50 Euro
accountNumber : 123456
Anonymous
Account⚠ leaked
name : "Derek"
balance : 12.07 Euro
accountNumber : 123457
aAccount *
bAccount *
  • Before line 6, b was the only reference to the Derek Account. The assignment overwrites it, so afterwards nothing points to that object.
  • The Derek object is still alive on the heap — new succeeded and the destructor will never run. Its memory cannot be reclaimed for the rest of the program. That's a leak.
  • The fix is to delete b; before reassigning it, or — better — to use a smart pointer (std::unique_ptr) that releases the previous object automatically on assignment.

Heap objects only stay reachable as long as something points to them. Reassign that last pointer and the object is lost.

After the assignment b = a;, the pointer b now points to the first Account object (John’s account). The second object (Derek’s account) no longer has any references and is effectively lost in memory. Its destructor will never be called, and the memory it occupies will only be released when the operating system cleans up after the program terminates. This is a classic memory leak.

A further complication of dynamic memory management in C++ is that the programmer is responsible for calling delete to deallocate memory. When delete is called on a pointer, the destructor of the object it points to is executed, allowing for any clean-up code to run (e.g., writing a record to disk, closing a file, releasing network connections).

For example, if we add a destructor to the Account class that simply announces when an account is being destroyed, we can observe this behaviour (Full source code: accountDynamicCreationTest.cpp):

int main() {
Account* a = new Account("John", 10.50, 123456);
Account* b = new Account("Derek", 12.07, 123457);
a->display();
b->display();
b = a; // Derek's account is now lost (memory leak)
delete a; // destroys John's account (and calls its destructor)
}

Explanation of Behaviour:

  • Both accounts are created on the heap using new.
  • The assignment b = a; causes the pointer to Derek’s account to be lost — the object remains in memory, but its destructor is never called.
  • When delete a; executes, John’s account is destroyed and its destructor runs.
  • Derek’s account is leaked and will only be reclaimed when the operating system ends the process.

This highlights a key danger of manual memory management in C++:

  • If delete is forgotten, memory is leaked.
  • If delete is called twice on the same pointer, the program will likely crash (undefined behaviour).
  • If delete is called on an invalid pointer, the program may behave unpredictably.
account number: 123456 has balance: 10.5 Euro
This account is owned by: John
account number: 123457 has balance: 12.07 Euro
This account is owned by: Derek

So, in the previous example, the destructor is called only on the Account object containing John’s details, while the destructor for Derek’s account is never called because its pointer was lost.

Now, suppose we alter the program to explicitly delete both pointers a and b:

int main() {
Account* a = new Account("John", 10.50, 123456);
Account* b = new Account("Derek", 12.07, 123457);
a->display();
b->display();
b = a;
delete a; // deletes John's account
delete b; // attempts to delete John's account again
return 0;
}

(Full source code: accountDynamicCreationTest2.cppCaution: running this code will likely cause your program to crash with a segmentation fault or a memory allocation error.)

What Happens?

  • Both accounts are created dynamically with new.
  • b = a; makes both pointers refer to the same object (John’s account).
  • delete a; correctly destroys John’s account.
  • delete b; then attempts to destroy the same object again.

This results in undefined behaviour. On many systems the program will crash immediately, but the actual behaviour is unpredictable — it might appear to run, might corrupt memory, or might terminate with a runtime error.

This example illustrates two key dangers of manual memory management in C++:

  1. Memory leaks — when an object is lost without being deleted (as in the previous example).
  2. Double deletion — when the same object is deleted twice, leading to undefined behaviour.

Both errors are notoriously difficult to track down in large programs. Smart pointers are discussed in the next chapter that solve many of these issues.

Concept Match

Match Dynamic Memory Errors

Quiz
Select 0/1

If you assign pointer 'b' to pointer 'a' (b = a) where both point to dynamically allocated objects, what immediately happens to the object previously pointed to by 'b'?

Code Cloze
C++

Proper Heap Allocation and Deallocation

An array is an indexed collection of elements, where all elements share the same type. In C++ you can create arrays of objects just as you would arrays of fundamental types:

ElementType theArray[numberOfElements];
Account allTheAccounts[2000];

Each element in the array is initialised using the class’s default constructor. If the class does not have a default constructor, you must explicitly initialise each element.

Example: Array of Objects (Full source code: accountArrayExample.cpp)

int main() {
Account someAccounts[3] = {
Account("Tom", 20.00, 123456),
Account("Richard", 55.00, 123457),
Account("Harry", 99.00, 123458) // 1
};
Account* p = &someAccounts[0]; // 2
someAccounts[2].display(); // 3: displays Harry
p->display(); // 4: displays Tom
(++p)->display(); // 5: displays Richard
(p+1)->display(); // 6: displays Harry
// Warning! p now points at Richard, not Tom! (line 7)
}

This code has the output:

Account number: 123458 has balance: 99 Euro
This account is owned by: Harry
Account number: 123456 has balance: 20 Euro
This account is owned by: Tom
Account number: 123457 has balance: 55 Euro
This account is owned by: Richard
Account number: 123458 has balance: 99 Euro
This account is owned by: Harry

Step-by-Step Behaviour:

  1. The array someAccounts has three elements ([0] to [2]), each initialised by calling the constructor.
  2. p is set to point at the first element (someAccounts[0]).
  3. someAccounts[2] accesses the third element (Harry’s account).
  4. p->display() shows Tom’s account because p points to someAccounts[0].
    (++p)->display() pre-increments p, so it now points to someAccounts[1] (Richard).
    • If p++ had been used, the increment would occur after the call, and Tom’s account would have been displayed.
  5. (p+1)->display() offsets p by one, so it displays Harry’s account (someAccounts[2]). Importantly, p itself remains pointing to Richard.
    Correctly distinguishing between pointer incrementing (++p) and offsetting (p+1) is crucial when working with arrays and pointers.

Dynamically Allocated Arrays
Pointers can also be used to allocate arrays of objects at runtime using new:

Account* ptr = new Account[10]; // allocate 10 Account objects

A pointer to an array of objects looks identical to a pointer to a single object. To deallocate, you must use delete[]:

delete[] ptr; // correct
delete ptr; // undefined behaviour

If you use delete ptr; instead of delete[] ptr;, the destructors of the array elements will not be called correctly, leading to resource leaks or undefined behaviour. See the example using_delete.cpp.

Important Points

  • Arrays of objects are initialised by calling their constructors.
  • Array elements can be accessed directly (someAccounts[i]) or via pointers.
  • Be careful with pointer arithmetic (++p vs p+1).
  • For dynamically allocated arrays, always use delete[] to deallocate.
Quiz
Select 0/1

If you allocate an array of objects dynamically using 'new Account[10]', what is the correct way to deallocate it?

Code Cloze
C++

Pointer Arithmetic vs Incrementing

C++ allows a class to inherit from more than one base class. This is known as multiple inheritance.

Think back to the earlier Transaction, Lodgement, and Withdrawal classes. Suppose we want to create a FundTransfer class that combines the behaviour of both a lodgement and a withdrawal. This structure is shown in the first option “1. Without Virtual” of the Diamond Problem Example below.

This is valid because:

  • A Lodgement is a Transaction.
  • A Withdrawal is a Transaction.
  • At different points during execution, a FundTransfer is both a Lodgement and a Withdrawal.

Here’s how it might look in code:

class FundTransfer : public Lodgement, public Withdrawal {
private:
// additional states...
public:
virtual int getTimeTaken() {
return (Lodgement::timeOfTransaction -
Withdrawal::timeOfTransaction);
}
};
  • Multiple parents are listed, separated by commas.
  • Both Lodgement and Withdrawal contain a timeOfTransaction state, so ambiguity must be resolved by explicitly qualifying with the parent class name (Lodgement::timeOfTransaction, Withdrawal::timeOfTransaction).

If the base class Transaction also had a display() method, calling it directly would be ambiguous:

FundTransfer* fundPtr;
fundPtr->display(); // ambiguous (not allowed)
fundPtr->Lodgement::display(); // OK
fundPtr->Withdrawal::display(); // OK

Although this resolves the ambiguity, it is awkward. A better design would be to declare a new display() method in FundTransfer that calls or adapts the appropriate base class methods.

Reference figure

The Diamond Problem

What happens when a class inherits the same base twice — and how virtual inheritance fixes it.

Transaction⚠ duplicated
- timeOfTransaction : Time
 
Transaction⚠ duplicated
- timeOfTransaction : Time
 
Lodgement
 
 
Withdrawal
 
 
FundTransfer
 
 
Two copies of timeOfTransaction
  • Each Transaction box is a separate sub-object in the FundTransfer layout — so FundTransfer contains two distinct timeOfTransaction members, one inherited via Lodgement and one via Withdrawal.
  • Writing ft.timeOfTransaction is ambiguous and rejected by the compiler. You'd have to disambiguate with ft.Lodgement::timeOfTransaction or ft.Withdrawal::timeOfTransaction — neither of which is what you actually want.
  • You're also paying for two copies of every Transaction member, even though logically a single fund transfer has one time.
C++
class Lodgement : public Transaction { /*...*/ };
class Withdrawal : public Transaction { /*...*/ };
class FundTransfer : public Lodgement, public Withdrawal { /*...*/ };
// FundTransfer ft;
// ft.timeOfTransaction; // ← ambiguous — which one?

UML draws the diamond the same way in both cases; only the C++ declaration of the middle classes changes —virtualcollapses the two paths to the base into one shared sub-object.

When Two Base States Should Not Be Duplicated

Section titled “When Two Base States Should Not Be Duplicated”

In the FundTransfer example, it was correct to have two separate Transaction objects (one for lodgement and one for withdrawal). However, there are cases where duplication is undesirable.

For example, suppose we want to design a new type of account — let’s call it CashSave — that combines the benefits of a Deposit account and a Current account. See the second example “2. With Virtual” in the Diamond Problem Example above.

Here:

  • Both Deposit and Current inherit from Account.
  • If we inherit from both directly, we would end up with two copies of Account inside CashSave — and thus two separate balance states.

Solution: Virtual Base Classes
To avoid duplication, we declare the common base (Account) as a virtual base class:

class Current : public virtual Account {
// ...
};
class Deposit : public virtual Account {
// ...
};
class CashSave : public Current, public Deposit {
// no need to repeat "virtual" here
};

This ensures that CashSave contains only one shared instance of Account.

The downside: this decision must be made when designing the intermediate classes (Current and Deposit). If they are not declared with virtual inheritance at design time, later derived classes like CashSave may suffer from duplicated base states.

Important Points on Multiple Inheritance:

  • Multiple inheritance allows a class to inherit from more than one base.
  • Ambiguities (e.g., duplicate states or methods) must be resolved explicitly.
  • Virtual inheritance avoids duplication of a common base class.
  • Constructors for virtual bases must be handled carefully by the ultimate child class.

Unlike C++, some modern languages such as Java and Rust do not support multiple inheritance of classes. This decision was made to avoid the ambiguity and complexity associated with the “diamond problem.”

  • Java allows a class to inherit from only one base class, but it can implement multiple interfaces to achieve similar flexibility.
  • Rust also avoids multiple inheritance but provides traits, which can be combined to give classes (structs) multiple sets of behaviours without inheriting from several parent classes.
Quiz
Select 0/1

When a class multiply inherits from two base classes that both have a method with the exact same name, how does C++ handle a direct call to that method?

Concept Match

Match Multiple Inheritance Concepts

Quiz
Select 0/1

In a C++ diamond inheritance structure using virtual base classes, which class is responsible for calling the constructor of the shared virtual base class?