Skip to main content

Welcome to a Grasp lesson

Create a free account to keep studying and create your own study path.

Lesson 2

Object Instantiation and Method Invocation

Recommended resources

Hello! Welcome to your second lesson in the "C++ for ESP32" course.

Introduction

In our last lesson, we learned how to define a C++ class, which acts as a blueprint for creating our own custom types. We created a DCMotor class that bundled together member variables (like speed_rpm) and member functions (like start() and stop()).

Today, we'll build directly on that foundation. The learning outcome for this lesson is to create objects by instantiating classes and call their methods. While we briefly touched on creating objects last time, this lesson will dive much deeper into the two primary ways C++ allows us to do this. You'll learn about creating objects on the stack versus the heap, a fundamental C++ concept that gives you precise control over memory and an object's lifetime. Understanding this is critical for writing efficient and stable code on resource-constrained devices like the ESP32.


1. Recap: Creating Objects on the Stack

In the previous lesson, you created objects using this simple syntax:

DCMotor motor1;

This is the most common and often preferred way to create an object. This method allocates memory for the object on a memory region known as the stack.

Key characteristics of stack-allocated objects are:

  • Automatic Lifetime: The object is automatically created when its declaration is reached and automatically destroyed when it goes out of scope (e.g., when the function it was created in ends).
  • Fast Allocation: Allocating memory on the stack is very fast.
  • Scope-Bound: The object only exists within the curly braces {} where it was defined.

You also learned that you can create multiple, independent objects from the same class blueprint. Each object has its own separate set of member variables.

To see a clear, simple example of this, please review the following text.

Classes (I)

This section from cplusplus.com, titled 'Classes (I)', provides a concise example of creating two distinct Rectangle objects from a single class. This reinforces the concept of a class as a blueprint and an object as an instance.

Read the section that starts with the heading 'The most important property of a class...'. Focus on the code example that creates rect and rectb and observe how calling the same method (area()) on each object produces a different result because their internal data (width and height) is independent.


2. Stack vs. Heap: Where Do Objects Live?

C++ gives you, the programmer, a choice of where to create your objects. This choice has significant implications for how long the object lives and how you manage its memory. The two primary locations are the stack and the heap.

The following video provides an excellent overview of this fundamental concept.

How to CREATE/INSTANTIATE OBJECTS in C++

This video, 'How to CREATE/INSTANTIATE OBJECTS in C++', from the excellent 'The Cherno' C++ series, clearly explains the difference between creating objects on the stack and on the heap.

Please watch from the beginning until 07:09. The video will cover: The concept of stack vs. heap memory (00:00 - 02:36). How to create objects on the stack and why it's the default choice (03:10 - 04:47). The automatic lifetime of stack objects and their limitations (04:47 - 07:09).

As the video explains, you should prefer creating objects on the stack whenever possible. However, there are two main reasons you might need an alternative:

  1. Lifetime: You need an object to exist beyond the scope in which it was created.
  2. Size: The object is very large, and you risk overflowing the stack, which is typically limited in size (especially on an ESP32).

For these situations, we use the heap.


3. Creating Objects on the Heap with new

To create an object on the heap, you use the new keyword. Unlike stack allocation which gives you the object directly, new allocates memory for the object on the heap and returns a pointer to that memory location.

// Stack allocation (gives you an object)
DCMotor stack_motor; 

// Heap allocation (gives you a pointer to an object)
DCMotor* heap_motor_ptr = new DCMotor(); 

Since you now have a pointer, you can't use the dot operator (.) directly to access the object's members. You must use the arrow operator (->).

Let's continue with the video to see how this works and, crucially, how to manage the memory you've allocated.

How to CREATE/INSTANTIATE OBJECTS in C++

Continuing with the same video, this next segment demonstrates heap allocation using the new keyword, the arrow operator for access, and the critical importance of using delete.

Watch from 07:09 to 10:50. Focus on: The syntax for using the new keyword and why it returns a pointer. The necessity of calling delete to free the memory and avoid memory leaks. The syntax for calling methods on a pointer using the arrow operator (->).

Manual Memory Management

As the video stressed, when you allocate memory with new, C++ makes you responsible for freeing it. If you don't, you create a memory leak. The object continues to occupy memory on the heap, but you no longer have a pointer to it, so it can never be accessed or deleted. On an ESP32 with only a few hundred kilobytes of RAM, even small memory leaks can quickly cause your program to crash.

Rule of thumb: Every new must have a corresponding delete.

DCMotor* motor_ptr = new DCMotor(); // Allocate memory

// ... use the motor object via the pointer ...
motor_ptr->start();

delete motor_ptr; // Deallocate the memory when done
motor_ptr = nullptr; // Good practice to prevent using a dangling pointer

4. Pointers to Objects: A Closer Look

Working with pointers to objects is fundamental in C++. The arrow operator (->) is simply shorthand. Accessing a member with motor_ptr->start() is equivalent to first dereferencing the pointer to get the object, and then using the dot operator: (*motor_ptr).start(). The arrow operator is just much cleaner and more common.

The following resource provides a fantastic textual summary and a clear table of the different operators you'll use with objects and pointers.

Classes (I)

This section from 'Classes (I)' on cplusplus.com details how to work with pointers to class objects. It provides a complete code example and a very useful table summarizing the different access operators.

Read the section titled 'Pointers to classes'. Pay close attention to the code example showing the different ways objects and pointers are used, and study the table that explains the meaning of *, &, ., ->, and [] in this context.


5. Putting It All Together

Let's see a complete example using our DCMotor class that demonstrates both stack and heap allocation.

#include <iostream>
#include <string>

// (DCMotor class definition from the previous lesson goes here)
class DCMotor {
public:
    std::string name;
    int speed_rpm = 0;
    bool is_rotating = false;

    void start() {
        is_rotating = true;
        std::cout << name << " motor started." << std::endl;
    }
    void stop() {
        is_rotating = false;
        speed_rpm = 0;
        std::cout << name << " motor stopped." << std::endl;
    }
    void set_speed(int new_speed) {
        if (is_rotating) {
            speed_rpm = new_speed;
            std::cout << name << " speed set to " << speed_rpm << " RPM." << std::endl;
        } else {
            std::cout << "Cannot set speed. " << name << " is not rotating." << std::endl;
        }
    }
};

void control_a_temporary_motor() {
    std::cout << "--- Entering temporary motor control scope ---" << std::endl;
    
    // 1. STACK Allocation: Object is created on the stack.
    DCMotor pump_motor;
    pump_motor.name = "Water Pump";

    // Call methods using the dot (.) operator
    pump_motor.start();
    pump_motor.set_speed(500);
    pump_motor.stop();

    std::cout << "--- Exiting temporary motor control scope ---" << std::endl;
    // `pump_motor` is automatically destroyed here as it goes out of scope.
}

int main() {
    control_a_temporary_motor();

    std::cout << "\n--- Back in main scope ---\n" << std::endl;

    // 2. HEAP Allocation: Object is created on the heap.
    // We get a pointer to the object.
    DCMotor* fan_motor_ptr = new DCMotor();
    
    // We must check if allocation was successful (important on embedded systems!)
    if (fan_motor_ptr == nullptr) {
        std::cout << "Failed to allocate memory for fan motor!" << std::endl;
        return 1;
    }

    // Access members and call methods using the arrow (->) operator
    fan_motor_ptr->name = "Cooling Fan";
    fan_motor_ptr->start();
    fan_motor_ptr->set_speed(3000);
    fan_motor_ptr->stop();

    // The object at `fan_motor_ptr` still exists. We are responsible for it.
    std::cout << "--- Manually deleting heap object ---" << std::endl;
    delete fan_motor_ptr; // CRITICAL: Free the memory.
    fan_motor_ptr = nullptr; // Good practice.

    return 0;
}

Compile and run this code. Observe how pump_motor is confined to its function, while fan_motor_ptr is managed manually within main.


Conclusion

In this lesson, you've moved beyond simply creating an object to understanding the critical choices involved in its creation and lifetime management.

Key Takeaways:

  • Stack Allocation (ClassName obj;):
    • Creates the object directly. Access members with the dot (.) operator.
    • Memory is managed automatically (created on entry, destroyed on exit of scope).
    • Fast, simple, and safe. This should be your default choice.
  • Heap Allocation (ClassName* ptr = new ClassName();):
    • Creates the object and returns a pointer to it. Access members with the arrow (->) operator.
    • Memory must be managed manually. You must call delete for every new.
    • Use this when an object must outlive its creation scope or is too large for the stack.
  • Forgetting to delete heap-allocated memory causes a memory leak, a serious issue in any program, but especially on a memory-constrained ESP32.

This knowledge is not just academic; it directly impacts the stability and performance of your embedded applications.

In the next lesson, we will explore constructors and destructors. Constructors are special methods that run automatically when an object is created (both on the stack and heap), allowing you to initialize it correctly. Destructors run when an object is destroyed, giving you a perfect place to perform cleanup tasks. This will make managing your objects even safer and more powerful.