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,privateandprotectedallow 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.
Classes in C++
Section titled “Classes in C++”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 { // 1private: // state definitions // 2public: // 3 // interface declarations // 4private: // implementation method declarations // 5}; // 6
// member method implementation // 7- The
classkeyword lets the compiler know that a class is about to be defined. The class name follows and should begin with a capital letter. - The
statesare 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. - The
publickeyword indicates that all methods to follow are part of the interface, i.e., publicly visible. - 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.
- All methods following the
privatekeyword are internal methods of the class and are part of the internal implementation. - 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. - 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.
The Account Class
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!}- This call creates an object of the
Accountclass calledmyAccount. The balance and account number are not yet set. - The
display()method is called directly on themyAccountobject. It will display the account details. - The
makeLodgement()method is called directly on themyAccountobject. It will add 2,300 Euro to the balance. - This call is not allowed. The
balancestate is a protected state of theAccountclass, so you cannot modify it directly from outside the class hierarchy. It is not part of the interface. - This call is not allowed. The
balancestate 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}🧩Knowledge Check
Section titled “🧩Knowledge Check”Match the OOP Concepts
What happens if you omit the semicolon (;) at the end of a class definition in C++?
Constructors
Section titled “Constructors”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 Euroaccount number: 34234325 has balance: 0 EuroThe source code for this is in Account2.cpp. Note:
- The constructor definition. Note it has the exact same name as the class name and has no return type.
- 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.
- The
anAccountobject is created with an account number of34234324and a balance of35.00Euro. - This constructor call is exactly the same as the previous call except with a different notation, so a
testAccountobject is created with an account number of34234325and a balance of0.00Euro. - 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.
Member Initialisation Lists
Section titled “Member Initialisation Lists”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 implementationAccount::Account(float aBalance, int anAccountNumber) : //1 accountNumber(anAccountNumber), balance (aBalance) //2{ // anything else, place here!}- The
:denotes the use of the member initialisation List. - The
accountNumber(anAccountNumber)call is the same as using the statementaccountNumber = 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.
🧩Knowledge Check
Section titled “🧩Knowledge Check”Using Member Initialisation Lists
Which of the following statements about C++ constructors are true?
Classes and Pointers to Objects
Section titled “Classes and Pointers to Objects”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 notationAccount anAccount = Account(35.00, 12344);anAccount.makeLodgement(20.0);anAccount.display();
//pointer notationAccount 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 Euroaccount number: 12345 has balance: 25 EuroNote 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.
The this Pointer
Section titled “The this Pointer”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 EuroInside a method, this->member and member are equivalent; the this-> prefix is usually written only when disambiguation is needed or when returning *this.
🧩Knowledge Check
Section titled “🧩Knowledge Check”Returning *this for Method Chaining
When using a pointer to an object, which operator is used to call the object's methods?
Destructors
Section titled “Destructors”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 definitionpublic: 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:
- The derived class destructor is called first.
- Then, the base class destructor is called.
- Finally, the object’s memory is deallocated.
Account type: Current Accountaccount number: 12345 has balance: 50 Euro And overdraft limit: 200Current 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 { // statespublic: 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.
🧩Knowledge Check
Section titled “🧩Knowledge Check”Match Lifecycle and Resource Concepts
In a typical scenario, when should a C++ programmer explicitly call a destructor (e.g., obj.~Account())?
Accessors and Mutators
Section titled “Accessors and Mutators”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(); // accessorvoid setAccountNumber(int newNumber); // mutatorThis allows full access to the accountNumber state through methods, while still retaining control over the implementation.
Structs in C++
Section titled “Structs in C++”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(/* ... */); // methodprivate: 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).
Unions in C++
Section titled “Unions in C++”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.byNumberis set, the memory holds the integer12345. - When
s.byNameis then set, the same memory is reused for the pointer, overwriting the integer value. - Printing
s.byNumberafterwards produces meaningless output, since its memory has been repurposed.
🧩Knowledge Check
Section titled “🧩Knowledge Check”What is the primary difference between a 'struct' and a 'class' in C++?
Why is std::variant preferred over a raw union in modern C++?
© 2026 Derek Molloy, Dublin City University. All rights reserved.