Computer scienceProgramming languagesC++Basics of Object-Oriented Programming

Stack or heap for OOP

9 minutes read

Every C++ program has two types of memory available: Stack and Heap. They differ in lifespan, allocation and deallocation, and size restrictions. In Object-Oriented Programming (OOP), stack and heap usage is similar to that in regular (structural) programming. However, understanding a few key nuances will make it easier for you to decide when to use a stack or heap in specific situations.

Heap and stack

The stack is a memory region that expands and contracts automatically when functions push and pop data. The compiler handles memory management. Stack memory has a limit; therefore, if you surpass this limit, it can result in a stack overflow.

The heap is a memory region specifically for dynamic memory allocation. Unlike the stack, its size is only limited by the size of the addressable memory, which in practice means nearly all the free memory. You need to manage heap memory manually. If you forget to free up memory, it can lead to a memory leak.

int main() {
    int stackVar = 10; //stored on the stack
    int* heapVar = new int(20); //stored on the heap
    delete heapVar; //don't forget to delete heap allocated memory
    return 0;
}

void stackFunction() {
    int stackArray[100]; //stored on the stack
}

void heapFunction() {
    int* heapArray = new int[100]; //stored on the heap
    delete[] heapArray; //free the allocated memory
}

Here is an example of a memory leak and a stack overflow:

void pitfalls() {
    int* memLeak = new int[100]; //memory Leak, we didn't delete
    int stackOverflow[1000000]; //stack overflow, array is too large
}

Follow these recommendations to prevent memory leaks and sudden breakage in your programs:

  1. Use the stack when it's possible; it's faster and safer because the compiler handles its memory management automatically.

  2. Avoid making extensive allocations on the stack, as these may lead to stack overflow. Ideally, identify large objects at the design stage and allocate them on the heap.

  3. Always free your heap memory; use delete or delete[] to prevent memory leaks.

  4. You should consider using smart pointers; they automatically manage memory, making heap allocations safer:

#include <memory>

void smartPointer() {
    std::unique_ptr<int> smart(new int(10)); //No need to delete
}

Stack vs heap in OOP

In OOP, choosing between the stack and heap is similar to other aspects of C++ programming. Your choice hinges on the lifespan and size of the object. Stack objects get eliminated when they reach past their scope. Conversely, heap objects stick around until they're purposefully deleted.

Let's delve into the use of stack and heap to store objects in our classes. We'll craft an Employee class. This class will own two attributes, name and age:

#include <string>
#include <iostream>

class Employee {
public:
    std::string name;
    int age;

    Employee(std::string personName, int personAge) { //Constructor
        name = personName;
        age = personAge;
    }

    void display() {
        std::cout << "Name: " << name << ", Age: " << age << std::endl;
    }
};

Next, let's create Employee objects on both the stack and the heap:

int main() {
    Employee martinSuvorov("Martin Suvorov", 2); // Object created on the Stack
    martinSuvorov.display(); // Displays: "Name: Martin Suvorov, Age: 30"
    
    return 0;
}

In this instance, martinSuvorov takes form on the stack. When the main() function ends its execution, martinSuvorov gets auto-deleted as it exits its scope.

If you decide to position your object on the heap:

int main() {
    Employee* mSptr = new Employee("Martin Suvorov", 25); // Object created on the Heap
    mSptr->display(); // Displays: "Name: Martin Suvorov, Age: 25"
    delete mSptr; // Delete the object
    return 0;
}

Here, mSptr serves as a pointer to an object born on the heap. It persists even post main() function execution. Therefore, we need to delete mSptr explicitly to free the memory. Neglecting to do so will trigger a memory leak.

If we had conceived the object in a different function (not in main()), we could've abolished the object at the end of the main() function, thus permitting its existence and usage until that point.

⚠️ Make sure to note that we're invoking the display() method via the arrow handler, not the dot handler. The arrow signals that the object is dynamically allocated and situated on the heap.

When is using the heap better?

You might be wondering why we need to store a simple object on the heap and manually manage memory. It's true; storing the previous class on the stack is often more practical, especially if the number of persons is small.

Let's explore a situation where heap allocation proves beneficial. Imagine we're creating a company simulation and we don't know in advance the number of employees. Here, we'll use a std::vector to house pointers to Employee objects, which will be declared on the heap as we introduce employees to the company.

Let's initiate a Company class that contains a std::vector of Employee*(refer to the previous paragraph for the class implementation):

#include <vector>

class Company {
private:
    std::vector<Employee*> employees;
public:
    void addEmployee(std::string name, int age) {
        Employee* newEmployee = new Employee(name, age); // Create the new Employee on the heap
        employees.push_back(newEmployee); // Add it to the vector
    }

    void displayEmployees() {
        for (Employee* employee : employees) {
            employee->display();
        }
    }

    // Destructor to clean up heap-allocated Employees
    ~Company() {
        for (Employee* employee : employees) {
            delete employee;
        }
    }
};

In the main function, we can now establish a Company object and add employees:

int main() {
    Company myCompany;
    myCompany.addEmployee("John Doe", 30);
    myCompany.addEmployee("Jane Doe", 25);
    myCompany.displayEmployees();
    return 0;
}

We must resort to the heap in this situation because we cannot predict how many Employee objects you'll require. By declaring Employee objects on the heap, you can create as many as required at runtime.

In this case, the Company class object is on the stack. However, if you anticipate an object might require substantial memory space (numerous fields or values), then placing it on the heap may be more advisable.

Remember, it's crucial to delete objects from the heap once they are no longer required, to avoid memory leaks.

Notice how we employed the destructor of the Company class in this example. This is quite handy as the destructor is always invoked prior to the termination of the class's lifecycle in these circumstances:

  • When a class object is stored on the stack, and the class ends automatically (due to scope exit or other reasons).

  • When a class object is stored on the heap and manually deleted using the delete operator.

  • Or in other situations leading to the lifecycle of the object ending (such as smart pointers).

C++11 introduced smart pointers to help manage dynamically allocated memory. They automatically delete an object when no more references to it exist. This aids in preventing memory leaks. Thus, we can employ them safely for our OOP objects:

#include <memory>

void smartPointers() {
    std::unique_ptr<MyClass> smart(new MyClass()); //No need to delete
}

Best practices

Here is a summary of best practices for using the stack or heap in OOP:

  1. Use the stack for local objects: If you limit an object's lifetime to a single function, and the object isn't overly large, you should create it on the stack. Stack allocation is simple and doesn't require manual memory management. Plus, it operates faster.

  2. Use the heap for large objects or variable-sized data: The stack has size restrictions, so for large objects or groups of objects (like arrays or vectors) that may change size during runtime, allocating on the heap is a better option.

  3. Use the heap for objects with lifetimes that exceed the current scope: If an object needs to last beyond the current function or scope, allocate it on the heap.

  4. Be mindful of object ownership: When you allocate an object on the heap, make sure you know which part of your code owns that object and is in charge of deleting it when it's not needed anymore. This strategy helps avoid memory leaks and double deletions.

  5. Leverage smart pointers for safer memory management: Smart pointers automatically manage the lifetime of objects allocated on the heap. They simplify memory management greatly and aid in preventing memory leaks.

Conclusion

The decision on whether to use the stack or heap for memory allocation in OOP is similar to that of customary objects - it's about lifespan, size, and whether manual management is required. Understanding these aspects can help you write efficient and mistake-free code. Remember to manage your heap memory appropriately to avert memory leaks and undefined behavior. Take advantage of smart pointers to make memory management safer and easier. And of course, don't forget about the destructor!

7 learners liked this piece of theory. 0 didn't like it. What about you?
Report a typo