Skip to content

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

5.1 C++ with Classes

We worked through the concepts surrounding classes in Chapter 1, so this chapter looks at implementing those concepts in C++. As discussed, object-oriented programming languages generally support the following main features:

  • Classes and Objects: Fundamental building blocks of OOP, where classes define blueprints for creating objects, which are instances of those classes. We begin with a brief discussion on how these are used.
  • Encapsulation: Bundling data (attributes) and methods (functions) that operate on the data into a single unit, controlling access to the internal state of an object. We describe how the keywords public, private and protected allow us to control encapsulation.
  • Inheritance: A mechanism that allows a new class (derived or child class) to inherit properties and behaviours from an existing class (base or parent class), promoting code reusability. We describe how simple inheritance is effective, and later in the chapter discuss multiple inheritance and the challenges involved.
  • Polymorphism: The ability of a single interface to represent different underlying forms (data types or classes), enabling methods to operate on objects of various classes through a common interface. C++ has a very sophisticated polymorphism implementation that extends to even the operators such as +, - etc.
  • Abstraction: Simplifying complex systems by modelling classes based on essential characteristics while hiding unnecessary implementation details, focusing on what an object does rather than how it does it. The major discussion on this topic is in the following chapter on the C++ Standard Libraries but elements are described in this chapter.

A C++ Class contains:

  • An interface that allows outside user interaction with the class.
  • States that store the data within the class.
  • An implementation that provides the actual code implementation of the interface methods, and any other internal workings.

A sample C++ class can be outlined as:

// An Example Class
class AnExampleClass { // 1
private:
// state definitions // 2
public: // 3
// interface declarations // 4
private:
// implementation method declarations // 5
}; // 6
// member method implementation // 7
  1. The class keyword lets the compiler know that a class is about to be defined. The class name follows and should begin with a capital letter.
  2. The states are the class data values and are usually defined at the beginning of the class and they should usually be protected (to allow for inheritance). Note that variables are defined (not declared). Definition means “make this variable or method” here, allocating storage for the name.
  3. The public keyword indicates that all methods to follow are part of the interface, i.e., publicly visible.
  4. All methods following the public keyword are part of the interface. i.e., the part of the class that you want other programmers to see outside the class. Note that the interface methods are declared. Declaration is the introduction of a name (identifier) to the compiler. It notifies the compiler that a method or variable of that name has a certain form, and exists somewhere.
  5. All methods following the private keyword are internal methods of the class and are part of the internal implementation.
  6. The semicolon (;) defines the class definition ending. If you leave it out your code will not compile, but worse, the error messages you receive will not make it clear that you have forgotten this semicolon.
  7. Once the class is defined you can then write the implementation code for the methods. This keeps the class definition short and easy to read. If you wish, these methods can even be written in a separate file.

Here is an example C++ class of a Bank Account class as shown in Figure 1, A Sample Bank Account Class, with private notated with ”-” and public notated with ”+”. This code is in the file Account.cpp in the Git repository.

UML Diagram

The Account Class

Account
- balance : float
+ display() : void
+ makeLodgement(float amount) : void
+ makeWithdrawal(float amount) : void

Figure 1 The Account Class.

Hover or tap any member for a note.

Here is the source code:

#include<iostream>
using namespace std;
class Account{
protected:
int accountNumber;
float balance;
public:
virtual void display();
virtual void makeLodgement(float);
virtual void makeWithdrawal(float);
};
void Account::display() {
cout << "account number: " << accountNumber
<< " has balance: " << balance << " Euro" << "\n";
}
void Account::makeLodgement(float amount) {
balance = balance + amount;
}
void Account::makeWithdrawal(float amount) {
balance = balance - amount;
}
int main() {
Account a;
a.display(); //will output rubbish for the states
}

The source code for this is in Account.cpp, where the interface:

  • provides a contract between class users and the author.
  • states can only be modified using the interface. They cannot be modified directly by the user.
  • author undertakes to provide all the functionality of the interface.
  • has to be changed then requires a new contract must be negotiated with the user. This will cause the user a lot of unnecessary work.

The code above shows how we can define and implement a class. If we wish to use this class, we can create an object of the class and manipulate it directly, for example:

int main() {
Account myAccount; // 1
cout << "Account Details:";
myAccount.display(); // 2
myAccount.makeLodgement(2300.00); // 3
cout << "Account Details:";
myAccount.display();
myAccount.balance = 2300.00; // 4
// ERROR!
cout << "Account Balance:"
<< myAccount.balance << "\n"; // 5
// ERROR!
}
  1. This call creates an object of the Account class called myAccount. The balance and account number are not yet set.
  2. The display() method is called directly on the myAccount object. It will display the account details.
  3. The makeLodgement() method is called directly on the myAccount object. It will add 2,300 Euro to the balance.
  4. This call is not allowed. The balance state is a protected state of the Account class, so you cannot modify it directly from outside the class hierarchy. It is not part of the interface.
  5. This call is not allowed. The balance state is not part of the interface and does not even allow the value to be read from outside the class hierarchy.

If you wish to assign a new object to an existing variable, even after initialisation use:

int main() {
Account myAccount; // assigns an object to the variable
myAccount = Account(); // allows the assignment of a new account object
}
Concept Match

Match the OOP Concepts

Quiz
Select 0/1

What happens if you omit the semicolon (;) at the end of a class definition in C++?

Constructors allow initialisation of an object’s state when it is created. Suppose we were to initialise the states of an object using the following format:

// Is this OK?
class Account {
int myAccountNumber = 242343;
float myBalance = 0.00;
public:
//etc..
};

No, this is not sufficient:

  • Even if we were allowed to do this, we cannot leave every instance of this class with the same bank account number and balance.
  • We need a way to update the account number and initial balance when the object is being created.

A Constructor can be used for this task:

  • It is a member method that has the exact same method name as the class name.
  • A constructor must not have a declared return type (not even void).
  • A constructor cannot be virtual (discussed later).

So using constructors with the Account class (Account2.cpp):

// Basic Bank Account Example with Constructors
#include<iostream>
using namespace std;
class Account{
protected:
int accountNumber;
float balance;
public:
Account(float aBalance, int anAccountNumber);
virtual void display();
virtual void makeLodgement(float);
virtual void makeWithdrawal(float);
};
Account::Account(float aBalance, int anAccNumber) {
accountNumber = anAccNumber;
balance = aBalance;
}
void Account::display() {
cout << "account number: " << accountNumber
<< " has balance: " << balance << " Euro" << "\n";
}
void Account::makeLodgement(float amount) {
balance = balance + amount;
}
void Account::makeWithdrawal(float amount) {
balance = balance - amount;
}
int main() {
Account anAccount = Account(35.00,34234324); //OK
Account testAccount(0.0, 34234325); //OK
//Account myAccount = Account(); //Wrong!
anAccount.display();
testAccount.display();
}

This gives the output:

account number: 34234324 has balance: 35 Euro
account number: 34234325 has balance: 0 Euro

The source code for this is in Account2.cpp. Note:

  1. The constructor definition. Note it has the exact same name as the class name and has no return type.
  2. This is the constructor implementation. In this case it simply sets the states of the class to the values passed. Once a constructor is supplied it must be used. Every class has an implicit constructor (with no parameters), but once a new constructor is defined the implicit constructor is no longer available.
  3. The anAccount object is created with an account number of 34234324 and a balance of 35.00 Euro.
  4. This constructor call is exactly the same as the previous call except with a different notation, so a testAccount object is created with an account number of 34234325 and a balance of 0.00 Euro.
  5. This constructor call is not valid as the implicit constructor is no longer available (the one with no parameters) as a new non-default constructor has been defined.

Instead of the notation used above to set the states of the object, we can also use a member initialisation list, that sets the states within the constructor definition. So, if we use member initialisation lists with the previous constructor, it would look like:

// the constructor code implementation
Account::Account(float aBalance, int anAccountNumber) : //1
accountNumber(anAccountNumber), balance (aBalance) //2
{
// anything else, place here!
}
  1. The : denotes the use of the member initialisation List.
  2. The accountNumber(anAccountNumber) call is the same as using the statement accountNumber = anAccountNumber;

This format may seem complex, but if the class contains an object that must be initialised in the constructor (e.g., an inheritance relationship) then this format must be used.

Code Cloze
C++

Using Member Initialisation Lists

Quiz
Select 0/2

Which of the following statements about C++ constructors are true?

There is very little difference between pointers to variables as discussed in the section called “Pointers in C/C++” and pointers to objects. First off, we can define a pointer p to objects of a certain class type using:

Account anAccount(5.0, 12345), *p;

So, pointer p is a pointer to objects of the Account class. Since the pointer is still just the storage of a memory address there is no need to attempt any type of construction call in the creation of the pointer. When the pointer is first created, it is not pointing to an object of the Account class, nor is an object of the Account class created. The pointer simply points at nothing (well nothing of consequence).

If we wish to make the pointer p point to an object of the Account class, we could use a statement like:

Account anAccount(5.0, 12345), *p;
p = &anAccount;
// alternative notation would be (just like pointers to variables):
Account anAccount(5.0, 12345);
Account *p = &anAccount;

Previously, several methods were defined for the Account class that allowed operations to be applied to the account object, such as display(), makeLodgement and makeWithdrawal(). How are these methods used when dealing with pointers?

Well, just like we applied the . operator to objects, we can apply the -> operator to pointers to objects. The notation is simply a (minus) - followed by a (greater-than symbol) >.

So for example, to demonstrate the object notation and the pointer notation:

//object notation
Account anAccount = Account(35.00, 12344);
anAccount.makeLodgement(20.0);
anAccount.display();
//pointer notation
Account testAccount = Account(5.0, 12345);
Account *testPtr = &testAccount;
testPtr->makeLodgement(20.0);
testPtr->display();

A source code example for this segment is listed in AccountWithPointers.cpp.

The output from this example would be:

account number: 12344 has balance: 55 Euro
account number: 12345 has balance: 25 Euro

Note that testPtr->balance is exactly the same as (*testPtr).balance, but the -> notation is a lot easier to comprehend when combined with other operators and function calls.

Every non-static member method in C++ has access to a hidden pointer called this. It points to the object on which the method was called. The compiler provides it automatically. Its primary uses are:

  • Resolving name ambiguity when a parameter shares a name with a member variable.
  • Returning a reference to the current object, enabling chained method calls.
  • Passing a pointer to the current object to another function.
#include <iostream>
using namespace std;
class Account {
protected:
int accountNumber;
float balance;
public:
Account(float balance, int accountNumber)
: accountNumber(accountNumber), balance(balance) {}
Account& makeLodgement(float amount) {
balance += amount;
return *this; // return reference to current object for chaining
}
void display() const {
cout << "Account " << this->accountNumber
<< ": " << this->balance << " Euro" << "\n";
}
};
int main() {
Account a(100.0f, 12345);
a.makeLodgement(50.0f).makeLodgement(25.0f); // chained calls
a.display();
}

This gives the output:

Account 12345: 175 Euro

Inside a method, this->member and member are equivalent; the this-> prefix is usually written only when disambiguation is needed or when returning *this.

Code Cloze
C++

Returning *this for Method Chaining

Quiz
Select 0/1

When using a pointer to an object, which operator is used to call the object's methods?

Since the responsibility for initialising a class lies with the class’s author, it’s reasonable to expect they also have a role in the object’s destruction. In C++, we can write a destructor for a class to perform clean-up operations, like releasing resources or notifying other objects.

The destructor doesn’t destroy the object itself. Its purpose is to perform clean up just before the object’s memory is deallocated. The compiler automatically handles the actual memory deallocation after the destructor has run.

To define a destructor, use the ~ symbol in front of the class name. For an Account class, the declaration would look like this:

class Account{
// state definition
public:
Account(int theNumber, float theBalance);
virtual ~Account(); //destructor
// other member methods here.
};

And the definition would be:

Account::~Account() {
cout << "Account object being destroyed!" << "\n";
}

A destructor must take no parameters and cannot return a value. While a constructor cannot be virtual, a destructor can (and should) be virtual for a base class to ensure correct destruction in polymorphic scenarios.

A destructor is not invoked explicitly. It is called automatically when an object goes out of scope. For example, in the main method below, a CurrentAccount object is created.

// main() method where CurrentAccount is a child of the Account class.
int main() {
CurrentAccount b(50.0, 12345, 200.0);
b.display();
// 'b' goes out of scope here.
}

When the main function ends, the CurrentAccount object b goes out of scope. Its destructor is then called.

In an inheritance hierarchy, destructors are called in the opposite order of constructors. When a derived class object is destroyed:

  1. The derived class destructor is called first.
  2. Then, the base class destructor is called.
  3. Finally, the object’s memory is deallocated.
Account type: Current Account
account number: 12345 has balance: 50 Euro
And overdraft limit: 200
Current Account object being destroyed!
Account object being destroyed!

In the code output above, it can be seen that the CurrentAccount destructor and then the Account destructor implementation were called, as main() method ended, and just before the application ran to completion.

If you don’t declare a destructor, C++ generates an implicit destructor (also known as a default destructor). This default destructor is sufficient for classes that only manage resources with automatic storage duration, but it does not handle dynamic memory or other external resources.

You should define a custom destructor for specific actions, such as:

  • Notifying other objects of impending destruction.
  • Closing open files, database connections, or network sockets.
  • Freeing dynamically allocated memory to prevent memory leaks. If an object does not free memory that was allocated dynamically, that memory will remain in use until the application terminates.

Using a destructor to free dynamically allocated memory is a critical practice. A common pattern is to pair dynamic allocation in the constructor with deallocation in the destructor, as shown below:

class SomeClass {
// states
public:
SomeClass();
virtual ~SomeClass(); // The destructor
// other methods
};
SomeClass::SomeClass() { // constructor implementation
// Allocate extra space here in the constructor!
}
SomeClass::~SomeClass() { // destructor implementation
// De-allocate that extra space!
}

This pattern ensures that any resources acquired during an object’s lifetime are properly released when the object is destroyed, preventing resource leaks.

Concept Match

Match Lifecycle and Resource Concepts

Quiz
Select 0/1

In a typical scenario, when should a C++ programmer explicitly call a destructor (e.g., obj.~Account())?

For efficiency reasons, you should not remove methods and make class data members public. Instead, use accessor (getter) and mutator (setter) methods to provide controlled access to private or protected states. Just like other methods, their name should contain an active verb, like get or set. For example:

class Account {
protected:
int accountNumber;
public:
// Accessor (getter)
int getAccountNumber() { return accountNumber; }
// Mutator (setter)
void setAccountNumber(int number) { accountNumber = number; }
};

This approach is far better than making accountNumber a public variable, as it preserves encapsulation. By using accessors and mutators, you control how the state is accessed and modified, rather than exposing the data directly.

For example, in the Account class:

int getAccountNumber(); // accessor
void setAccountNumber(int newNumber); // mutator

This allows full access to the accountNumber state through methods, while still retaining control over the implementation.

As described in Chapter 3, the C programming language introduced the structure (struct), which groups related data together into a single complex data type (e.g., a complex number, or a Person record). In C, a struct might look like this:

struct A {
char* someString;
int someInt;
};
int main() {
// usage
A testA;
A* ptrToA = new A;
testA.someString = "something";
ptrToA->someInt = 4;
}

C++ extends this concept by allowing methods to be associated with a struct:

struct Account {
Account(/* ... */); // constructor
virtual void makeLodgement(/* ... */); // method
private:
int accountNumber;
float balance;
};

Structs and classes in C++ are almost identical:

  • In a struct, members are public by default.
  • In a class, members are private by default.

In C, only public member data is allowed within structs (there is no concept of private or protected). In C++, however, structs can use all access specifiers (public, private, protected).

C++ also provides unions, which allow different data types to share the same memory location. A union allocates only enough memory to store the largest member, so the data members overlap in memory. This makes unions very memory-efficient, but also requires careful handling. For example:

#include <iostream>
using namespace std;
union StudentID {
char* byName; // memory for a pointer OR
int byNumber; // memory for an int
};
int main() {
StudentID s;
s.byNumber = 12345;
cout << s.byNumber << "\n"; // outputs: 12345
s.byName = "Hello";
cout << s.byName << "\n"; // outputs: Hello
cout << s.byNumber << "\n"; // outputs: (garbled integer)
}

In this example:

  • When s.byNumber is set, the memory holds the integer 12345.
  • When s.byName is then set, the same memory is reused for the pointer, overwriting the integer value.
  • Printing s.byNumber afterwards produces meaningless output, since its memory has been repurposed.
Quiz
Select 0/1

What is the primary difference between a 'struct' and a 'class' in C++?

Quiz
Select 0/1

Why is std::variant preferred over a raw union in modern C++?