In the previous unit, we covered structs and discussed how they are great for bundling multiple member variables into a single object that can be initialized and passed around as a unit. In other words, structs provide a convenient package for storing package and moving related data values.
Consider the following struct:
#include <iostream>
struct Date
{
int day{};
int month{};
int year{};
};
void printDate(const Date& date)
{
std::cout << date.day << '/' << date.month << '/' << date.year; // assume DMY format
}
int main()
{
Date date{ 19, 11, 23 }; // initialize using aggregate initialization
printDate(date); // can pass entire struct to function
return 0;
}
In the above example, we create a Date
object and then pass it to a function that prints the date. This program prints:
19/11/23
The class invariant problem
Perhaps the biggest difficulty with structs is that they do not provide an effective way to document and enforce class invariants. we defined an invariant as , “a condition that must be true while some component is executing”.
Introduction to Classes
Just like structs, a class is a program-defined compound type that can have many member variables with different types.
Defining a class
Because a class is a program-defined data type, it must be defined before it can be used. Classes are defined similarly to structs, excepts we use the class
keyword instead of struct
. For example, here is a definition for a simple employee class:
class Employee
{
int m_id {};
int m_age {};
double m_wage {};
};
Inner Classes
When building complex software systems, grouping related functionality logically is crucial for maintainability, readability, and scalability. One such organizational feature provided by C++ is the inner class, also known as a nested class.
What is an Inner Class in C++?
An inner class in C++ is a class defined inside the scope of another class. It's a way to logically associate one class with another, helping encapsulate behavior and reduce naming clutter.
class Outer {
public:
class Inner {
public:
void display() {
std::cout << "Inside Inner class" << std::endl;
}
};
void show() {
std::cout << "Inside Outer class" << std::endl;
}
};
How to Use an Inner Class
To use an inner class, you must refer it with the scope resolution operator ::
.
int main() {
Outer::Inner obj; // Scope resolution
obj.display();
Outer o;
o.show();
return 0;
}
Characteristics of Inner Classes
Feature | Behavior |
---|---|
🔒 Access to Outer Class Members | ❌ Inner class cannot directly access outer class's non-static members. |
🧭 Scope | Inner class is scoped within the outer class. |
🔐 Access Modifiers | Inner class can be public , private , or protected . |
📛 Name Resolution | Must use Outer::Inner to refer to the class. |
👁 Friendship | Inner class can be declared as a friend to access private members of the outer class. |
Access Limitation: No Implicit Link to Outer Class
Inner classes don't automatically access the outer class's members:
class Outer {
int secret = 42;
public:
class Inner {
public:
void accessOuter() {
// std::cout << secret; ❌ Compiler error
}
};
};
To access secret
, the inner class must use an object or pointer of the outer class.
class Outer {
int secret = 42;
public:
class Inner {
public:
void accessOuter(const Outer& outer) {
std::cout << "Secret: " << outer.secret << std::endl;
}
};
};
You can define and instantiate inner classes like this:
Outer::Inner innerObj;
innerObj.accessOuter(outerInstance);
Use Cases
1️⃣ Tree Data Structures
#include <iostream>
using namespace std;
class BinaryTree {
private:
// Inner class representing a node of the tree
class Node {
public:
int data;
Node* left;
Node* right;
Node(int value) : data(value), left(nullptr), right(nullptr) {}
};
Node* root;
// Helper function for recursive traversal
void inorderTraversal(Node* node) {
if (!node) return;
inorderTraversal(node->left);
cout << node->data << " ";
inorderTraversal(node->right);
}
public:
BinaryTree() : root(nullptr) {}
void insert(int value) {
root = insertRec(root, value);
}
void printInorder() {
inorderTraversal(root);
cout << endl;
}
private:
// Recursive helper for insertion
Node* insertRec(Node* node, int value) {
if (!node) return new Node(value);
if (value < node->data)
node->left = insertRec(node->left, value);
else
node->right = insertRec(node->right, value);
return node;
}
};
// Usage
int main() {
BinaryTree tree;
tree.insert(10);
tree.insert(5);
tree.insert(15);
cout << "Inorder Traversal: ";
tree.printInorder(); // Output: 5 10 15
return 0;
}
2️⃣ Linked List with Node
as an Inner Class
#include <iostream>
using namespace std;
class LinkedList {
private:
// Inner class representing a node of the linked list
class Node {
public:
int data;
Node* next;
Node(int value) : data(value), next(nullptr) {}
};
Node* head;
public:
LinkedList() : head(nullptr) {}
void insertAtEnd(int value) {
Node* newNode = new Node(value);
if (!head) {
head = newNode;
return;
}
Node* temp = head;
while (temp->next) {
temp = temp->next;
}
temp->next = newNode;
}
void printList() const {
Node* temp = head;
while (temp) {
cout << temp->data << " -> ";
temp = temp->next;
}
cout << "NULL" << endl;
}
};
// Usage
int main() {
LinkedList list;
list.insertAtEnd(10);
list.insertAtEnd(20);
list.insertAtEnd(30);
cout << "Linked List: ";
list.printList(); // Output: 10 -> 20 -> 30 -> NULL
return 0;
}
3️⃣ Helper/Utility Classes
Sometimes you want a small class used only internally within another class. Inner classes make that possible without polluting the global namespace.