🚀 C# ile Programlamaya Giriş: Kodlamaya İlk Adımını At, Kendi Projelerini Geliştirmeye Başla!
00HOME 01POSTS 02COURSES 03ELECTRİCAL_ENERGY 04AUTOMOTİVE_ENGİNEERİNG 05TOOLS 06LAB 07GLOSSARY 08TAGS ♡MY LIST ⌕SEARCH
TRENG

Memory management: new, delete and smart pointers

The difference between the stack and the heap, the pitfalls of new/delete, RAII, and safe memory management with unique_ptr and shared_ptr.

What makes C++ powerful is that you can manage memory by hand; what makes it dangerous is exactly the same thing. In this lesson we will look at the two main regions of memory, the pitfalls of manual management and how modern C++ makes this job safe.

The stack and the heap

  • Stack: The region where local variables live. It is fast, and variables are deleted automatically when the function ends. Its size is limited.
  • Heap: The region where you allocate as much memory as you want with new. It is large, but you must give back what you allocate.

new and delete

int* p = new int(42);        // create an int on the heap
std::cout << *p << '\n';
delete p;                    // give it back
p = nullptr;                 // clear the address that is now invalid

double* arr = new double[100];   // array of 100 elements
delete[] arr;                    // delete[] is a must for arrays

Here is the problem: if you forget to write delete, the memory is never given back (a memory leak). If you delete twice, the program crashes. Using the pointer after delete is undefined behavior. If you leave branching code early (for example with return or an exception), the delete line is never reached. That is why manual management is so error-prone.

RAII: tie the resource to an object

C++'s solution is the idea of RAII (Resource Acquisition Is Initialization): acquire the resource in an object's constructor and release it in its destructor. When the object goes out of scope, the destructor runs automatically. This makes forgetting impossible. The idea applies not only to memory but also to files, locks and serial port connections.

The standard library provides smart pointers for this. Header: #include <memory>.

#include <iostream>
#include <memory>

class Sensor {
public:
    explicit Sensor(int id) : id_(id) {
        std::cout << "Sensor " << id_ << " opened\n";
    }
    ~Sensor() {
        std::cout << "Sensor " << id_ << " closed\n";
    }
    void read() const {
        std::cout << "Sensor " << id_ << " reading\n";
    }
private:
    int id_;
};

int main() {
    Sensor* raw = new Sensor(1);
    raw->read();
    delete raw;                          // manual: forget it and you leak

    {
        auto single = std::make_unique<Sensor>(2);
        single->read();
    }                                    // block ended: deleted automatically

    auto a = std::make_shared<Sensor>(3);
    {
        auto b = a;                      // ownership is shared
        std::cout << "count: " << a.use_count() << '\n';
    }
    std::cout << "count: " << a.use_count() << '\n';
    std::cout << "main is ending\n";
    return 0;
}
Sensor 1 opened
Sensor 1 reading
Sensor 1 closed
Sensor 2 opened
Sensor 2 reading
Sensor 2 closed
Sensor 3 opened
count: 2
count: 1
main is ending
Sensor 3 closed

unique_ptr and shared_ptr

std::unique_ptr

The object has a single owner. It cannot be copied; it can only be handed over with std::move. When the owner goes out of scope, the object is deleted. This is the default choice in almost every case, and its extra cost compared to a raw pointer is next to nothing. To create one, use std::make_unique<Type>(arguments).

std::shared_ptr

It can have several owners. A counter keeps track of how many owners there are; when the counter drops to zero, the object is deleted. In the example, the line b = a raised the counter to 2, it dropped to 1 when the inner block ended, and Sensor 3 was closed only when main ended. Since managing the counter has a small cost, unique_ptr is preferred when sharing is not needed.

The rule

When writing modern C++, aim not to use naked new and delete. If you need an array, use std::vector; if you need a single object, use make_unique. Raw new should appear only in low-level library code.

Embedded systems note: In many microcontroller projects dynamic memory is banned completely (new, malloc, even std::vector growth). The reason: on devices that run for a long time the heap gets fragmented and one day an allocation fails, which means the device freezes in the field. In such systems memory is allocated at compile time: static/global objects or std::array. Learning the rules matters, but follow the rules of your target platform.

There are tools for finding memory bugs: if you add the -fsanitize=address flag with g++, leaks and out-of-bounds accesses are reported at run time. Valgrind (Linux) does a similar job. This is the first thing to try on a suspicious program.

Try it yourself

  1. Using the Sensor class from the example, create a std::vector<std::unique_ptr<Sensor>>; add three sensors with push_back(std::make_unique<Sensor>(i)) and watch the destructors run at the end.
  2. Comment out the delete raw; line and catch the leak with -fsanitize=address.
  3. Try to copy a unique_ptr into another variable; read the compiler error and fix it with std::move.

// Mini quiz

Click an option; you will see right away, with the reason, whether it is correct or not.

1. Which command should be used to delete an array created with new?
2. What is the essence of the RAII idea?
3. Which smart pointer can have more than one owner?

Mark it when you finish: your progress is saved and you earn +20 XP.

Tip: ← → keys let you move between lessons.