Memory Management in C++

Diane Khambu
8 min readOct 2, 2023
Echium candicans from Madeira © Diane Khambu

One of the advantage of using C++ is that we are able to directly access memory address of a variable and make modifications. In this post we’ll use examples of class constructors, destructors — both implicit (stack) and explicit (heap), and copy constructor to illustrate how we can allocate and de-allocate memory.

Let’s start with constructors. Like many other languages, if you do not have explicit constructor, implicit constructor will be called.

#include <iostream>
using namespace std;

class HandCream{
public:
double price;

};

int main(){
HandCream h1;
}

In the above code, h1 instance will run implicit class constructor.

Now let’s have our custom class constructor with a wrapper class.

#include <iostream>
using namespace std;


class HandCream{
public:
double price;

HandCream(double price=1.0){
this->price = price; // (*this) == ->
cout << "HandCream default constructor" << endl;
}
};

class HandCreamBox{
public:
HandCream h;

HandCreamBox(){
cout << "HandCreamBox default constructor" << endl;
}
};

int main(){
cout << "Creating 1 HandCreamBox" << endl;
HandCreamBox box;
cout << endl;
cout << "Creating 3 HandCreamBox" << endl;
HandCreamBox boxes[3];
}

On running the above code, we get:

Creating 1 HandCreamBox
HandCream default constructor
HandCreamBox default constructor

Creating 3 HandCreamBox
HandCream default constructor
HandCreamBox default constructor
HandCream default constructor
HandCreamBox default constructor
HandCream default constructor
HandCreamBox default constructor

We created a single HandCreamBox wrapper class with HandCreamBox box; statement. If we want to create many of them, we used array syntax with size; in this example HandCreamBox boxes[3];

Next, we’ll see passing values to constructor in HandCream class:

#include <iostream>
using namespace std;


class HandCream{
public:
double price;

HandCream(double price=1.0){
this->price = price; // same as (*this).price = price
cout << "HandCream default constructor. price: " << price << endl;
}
};

int main(){
HandCream h1(2.00); // passed an argument
HandCream h2 = 3.12; // for a single argument, we can use assignment operator
HandCream h3; // default price value
}

We pass argument to constructor parameters while instantiating a class. If there is just a single argument we need to pass to a constructor, we can use assignment operator as well. If parameters have default values, we can skip passing values. If we skip assigning values and there is no default values, we’ll get error.

On running above code, we get:

HandCream default constructor. price: 2
HandCream default constructor. price: 3.12
HandCream default constructor. price: 1

You must have also seen interesting keyword this and -> short hand. this-> is syntactic sugar for (*this). . The keyword this points to current instance’s memory address.

Scope and Memory

Now we’ll look into scoping and memory in C++ .

When a variable we assigned, int x = 5; , goes out of scope, its memory is freed. Similarly, when we do the the assignment, we have allocated the variable memory.

For example, the following example should theoretically give indeterministic value.

#include <iostream>
using namespace std;


int main(){
int* p;

if (true){
int x = 3;
p = &x;
}

cout << *p << endl; // ???
}

The scope of x is within if block. So, when we try to reach the pointer p’s value outside of the if block, we don’t know what the p would be pointing to.

Similar is the situation in the following case where a variable declared in a function is out of scope once called.

#include <iostream>
using namespace std;

int* getPointerToThree(){
int x = 3;
int* p = &x;
return p;
}

int main(){
int* p;
p = getPointerToThree();
cout << *p << endl; // ???
}

`new` keyword

If we still want to persist the memory address’s value, then we have a keyword new . It allocates memory and will remain allocated until you de-allocate manually. The syntax for creating a pointer to a new memory is <dataType>* <ptrVar> = new <dataType>; .

Things to note:

  • variable created using new is stored in heap .
  • variable created without is stored in stack .

Let’s illustrate this with the above example, tweaked a bit:

#include <iostream>
using namespace std;

int* getPointerToThree(){
int* p = new int; // allocating memory
*p = 3;
return p;
}

int main(){
int* p;
p = getPointerToThree();
cout << *p << endl; // 3
delete p; // de-allocate memory
}

It’s important to de-allocate memory that was created using new , else we’ll be wasting memory leading to memory leak.

Let’s see another example where multiple pointers are called.

#include <iostream>
using namespace std;

int* getPointerToThree(){
int* p = new int;
*p = 3;
return p;
}

int main(){

int* p;
for (int i=0; i < 3; i++){
p = getPointerToThree();
cout << *p << endl;
delete p; // delete before next call to `getPointerToThree()`
}
}

You have to delete pointer p before another call to the function getPointerToThree() . If you don’t then the memory would not be de-allocated, causing memory constraint.

You cannot also have delete p; outside of the for loop, because that’ll be de-allocating just the last memory.

You can delete a pointer only once else we’ll not be knowing what that pointer would be pointing to next time. Also don’t use the memory after deletion. Code would be something like this:

delete p; // delete a pointer
delete p; // not twice delete
cout << *p << endl; // don't use the pointer after deletion

If there are variable number of items in a container of variable type, use delete[] variableName; syntax. For example, an array can have a variable number of items.

#include <iostream>
using namespace std;

int main(){
int size;
cout << "Enter size of an array: " << endl;
cin >> size;

int* arr = new int[size];

for (int i=0; i < size; i++){
cout << "Enter item: ";
cin >> arr[i];
}
cout << endl;

int* p;
for (int i=0; i<size; i++){
cout << "Items are: ";
cout << arr[i] << endl;
}

delete[] arr; // delete arr

}

Output would be something like this:

Enter size of an array: 
2
Enter item: 23
Enter item: 1

Items are: 23
Items are: 1

On this note, while declaring an array, if you do not know the size of an array, then you have to use new to create a pointer to it.

int arr[5]; // we know the arr size ahead.
int* arr2 = new int[size]; // when we don't know the array size.
delete[] arr2; // you need to de-allocate the memory for arr2 as it was
// created with `new` keyword.

Let’s create a HandCream instance using new keyword.

#include <iostream>
using namespace std;

class HandCream{
public:
double price;
bool isVegan;

HandCream(double price=1.00, bool isVegan=true){
this->price = price;
this->isVegan = isVegan;
cout << "constructor with 2 args is called." << endl;
}
};

int main(){
HandCream* p = new HandCream();
cout << "Price: " << (*p).price << endl;
if (p->isVegan){
cout << "isVegan: true" << endl;
}else{
cout << "isVegan: false" << endl;
}

delete p; // de-allocate memory of HandCream instance
}

The output is:

constructor with 2 args is called.
Price: 1
isVegan: true

This brings us to the topic of destructor.

Destructor

When class instance is created using new , appropriate constructor is invoked. Destructor is called when the class instance is de-allocated. If instance is created using new , heap allocated, the destructor is invoked when the instance gets de-allocated. If created without, stack-allocated, the destructor is invoked when the instance gets out of scope.

#include <iostream>
using namespace std;

class HandCream{
public:
double price;
bool isVegan;
string* creamSet;

HandCream(double price=1.00, bool isVegan=true){
this->price = price;
this->isVegan = isVegan;

creamSet = new string[3]; // pointer to string array
creamSet[0] = "Peach";
creamSet[1] = "Cherry Blossom";
creamSet[2] = "Aqua";

cout << "constructor invoked" << endl;

}
~HandCream(){
delete[] this->creamSet; // on heap
cout << "destructor invoked" << endl;
}
};

int main(){
HandCream* h = new HandCream(2.00); // on heap
delete h;

if (true){
HandCream h1; // on stack
}
cout << "h1 out of scope" << endl;
}

On running the file, we get:

constructor invoked
destructor invoked

constructor invoked
destructor invoked
h1 out of scope

There are also instances where we copy class instances. This brings to our next topic: copy constructor.

Copy Constructor

#include <iostream>
using namespace std;

class HandCream{
public:
double price;
bool isVegan;
string* creamSet;

HandCream(double price=1.00, bool isVegan=true){
this->price = price;
this->isVegan = isVegan;

creamSet = new string[3]; // pointer to string array
creamSet[0] = "Peach";
creamSet[1] = "Cherry Blossom";
creamSet[2] = "Aqua";

cout << "constructor invoked" << endl;

}
~HandCream(){
delete[] this->creamSet; // on heap
cout << "destructor invoked" << endl;
}
};

int main(){
HandCream h1; // on stack

if (true){
HandCream h2 = h1; // on stack
}
cout << "h1 out of scope" << endl;
cout << h1.creamSet[0] << endl;
}

On running this, we get:

constructor invoked
destructor invoked
h2 out of scope
Peach
...malloc: *** error for object 0x6000..: pointer being freed was not allocated
...malloc: *** set a breakpoint in malloc_error_break to debug

When h2 goes out of scope, it calls destructor. Now when h1 is going out of scope before terminating the program, we get error pointer being freed was not allocated . Here the pointer of h1 is called a dangling pointer. It’s because the h2 deleted the values for the h1 when it went out of scope.

This is where copy constructor comes to rescue.

#include <iostream>
using namespace std;

class HandCream{
public:
double price;
bool isVegan;
string* creamSet;

HandCream(double price=1.00, bool isVegan=true){
this->price = price;
this->isVegan = isVegan;

creamSet = new string[3]; // pointer to string array
creamSet[0] = "Peach";
creamSet[1] = "Cherry Blossom";
creamSet[2] = "Aqua";

cout << "constructor invoked" << endl;

}
HandCream(HandCream &o){
price = o.price;
isVegan = o.isVegan;
creamSet = new string[3];

for (int i=0; i<3; i++){
creamSet[i] = o.creamSet[i];
}
cout << "copy constructor invoked" << endl;
}
~HandCream(){
delete[] this->creamSet; // on heap
cout << "destructor invoked" << endl;
}
};

int main(){
HandCream h1; // on stack

if (true){
HandCream h2 = h1; // on stack
}
cout << "h2 out of scope" << endl;
cout << h1.creamSet[0] << endl;
}

On running the above file, we get:

constructor invoked
copy constructor invoked
destructor invoked
h2 out of scope
Peach
destructor invoked

Now we have error-free termination of the program.

In conclusion, C++ allows us to

  • create custom constructors but has default constructor
  • persist variable even when out of scope by creating pointer using new keyword
  • pointers created using new must be de-allocated to prevent memory leak
  • do deep copy of instances using copy constructor

Congratulations for coming this far! 🎈 I hope you were able to learn something new.

See you in my next post. Until then, ✨.

Inspiration:

You can support me in Patreon!

--

--