Tracking what code βownsβ a given piece of memory isnβt too hard in a simple program like the ones we have seen so far. But in a complex program with many functions and potential ownership transfers, it can become quite challenging. Developers must be diligent in documenting and enforcing ownership rules to avoid memory leaks and other issues. Many programming languages provide mechanisms to help with this, such as smart pointers in C++ or garbage collection in languages like Java and Python.
A C++ strategy to help manage memory ownership is to use container classes. A container class is a class that holds and manages the lifetime of dynamically allocated memory. When the object is created, it allocates the necessary memory and takes ownership of it. When the object is destroyed, it automatically deallocates the memory, ensuring that there are no memory leaks.
This is an example of the broader concept Resource Acquisition Is Initialization (RAII). That is in the idea that resource management is tied to object lifetime, making it easier to manage. This means that as long as an object is alive, it owns its resources, and when the object is destroyed, its resources are released. This is a key design principle not just in C++ but in other languages like Rust.
Say we want to keep track of a list of players in a game. The players could be complex objects (name, age, player handle), but we will just represent each player as a string (their name). To keep track of a group of players, we will make a PlayerList class. It will store an array of strings. It will also be useful to store the size of the array. (Unlike a vector, which manages its own size, in C++ you canβt ask an array what size it is. The programmer needs to keep track of that). Here are our member variables and a constructor:
module;
#include <iostream>
#include <string>
export module PlayerList;
using namespace std;
export class PlayerList {
private:
string* m_players; // pointer to the array of player names
int m_size; // number of players in the list
public:
PlayerList(int size);
void setPlayerName(int index, const string& name);
void print() const;
};
PlayerList::PlayerList(int size) {
if (size <= 0) {
throw invalid_argument("Size must be positive");
}
m_size = size;
m_players = new string[m_size]; // allocate memory for the array
}
void PlayerList::setPlayerName(int index, const string& name) {
if (index < 0 || index >= m_size) {
throw out_of_range("Index out of range");
}
m_players[index] = name; // set the player name at the given index
}
void PlayerList::print() const {
cout << "Player List: ";
for (int i = 0; i < m_size; ++i) {
cout << m_players[i] << " ";
}
cout << endl;
}
In the constructor, we allocate memory for the array of player names using new. We also check that the size is positive, throwing an exception if it is not. This ensures that we do not try to allocate an array of size 0 or negative size. We can set the names of individual players using the setPlayerName method. It will check that the player number we try to set is actually a valid index (0 through the size - 1). Finally, we have a print function just for testing.
We could make a vector<string> to store the list of players. If our only goal was to build a program that tracks players, that would likely be the best approach (vector and string are container classes that manage memory for us). But we want to understand the underlying mechanics of memory management, so we will build our own container class.
For now, like the arrays we are using, our simple container classes will be a fixed size. Later we will explore allowing the size to change while the PlayerList is in use.
If you try running our program, you will see that we have an issue. The container class is not actually deleting the array yet. At the end of main, the memory looks like:
The constructor allocates memory using new, but that memory is never deleted. We canβt delete the memory in the constructor, because we need to keep using the array until we are done with the pList variable. What we need is a special method that runs when the object is destroyed. Fortunately, C++ provides a mechanism for this: the destructor.