How should developers learn memory management? Do raw pointers still have a legitimate place in modern C++? And how much low-level knowledge should a beginner acquire early?
I discussed these questions with Patrice Roy, author of C++ Memory Management, to learn his perspective.
This article is my synthesis of that Q&A, expanded with code examples and diagrams. Direct quotations from Patrice are shown as block quotes.
Start C++ Learning from High-level Concepts
C++ isn’t all about pointers, new and delete operators.
Yes, it’s an essential part of the language, but it’s better to learn first:
- Classes
- Functions
- Copy and move
- Object lifetime
- References
Why? As Patrice Roy explains:
raw arrays and pointers are advanced and tricky
aspects of C++ and one can write useful programs without pointers coming into play.
So, it makes more sense to start C++ education from the high-level concepts:
- STL-containers:
std::map,std::vector, - Range-based loops,
- Algorithms and iterators.
Although, these concepts may be hard for beginners, for example half-open ranges and functors or lambda expressions, but they create a safe ground for programming.
Build first an intuition about object lifetime, standard containers and algorithms and only then go deep into raw arrays, pointers, new and delete operators. Because the former helps to understand how things are working under the hood.
Patrice summarizes this idea as follows:
That way, newcomers can see that writing C++ can be simple, clean and efficient, and later
understand the power of being able to look underneath and see how it all works.
So, the possible learning path may be like this

Object, scopes and lifetime is one of the most important topics in C++. And the next section explains why.
Learn Lifetime Before Allocation
Developers coming from languages dominated by reference semantics may need time to adjust to the C++ object model. In C++, objects can be created directly as values, copied, moved, and destroyed deterministically when their lifetime ends.
For local objects with automatic storage duration, lifetime is normally tied to scope: the object is constructed when execution reaches its declaration and destroyed when control leaves the block.
As Patrice Roy explains:
When learning C++, what is probably the most important idea to grasp is that C++ has objects. These objects are constructed, their lifetime is associated with the scope of their declaration.
C++ defines precise rules for when object lifetimes begin and end. Dynamic allocation changes how storage is obtained and released, but it does not remove those lifetime rules.
C++ defines four storage duration categories:
- Static storage duration: the storage exists for the duration of the program. Objects with static storage duration are normally destroyed during program termination.
- Thread storage duration: the storage exists for the lifetime of the thread, and the corresponding objects are normally destroyed when the thread exits.
- Automatic storage duration: the storage normally exists until control leaves the enclosing block.
- Dynamic storage duration: the storage remains allocated until it is explicitly deallocated, often indirectly through an RAII owner such as
std::unique_ptror a standard container.
If you interested more about storage duration in C++, read more in C++ Object Creation: Stack, Heap, and Lifetime
Consider a C++ example with a local object foo constructed in the function bar():
void bar() {
auto foo = Foo{};
}
Object foo is destructed once function bar() returns and it happens automatically because foo has an automatic storage duration.
This mechanism enforces safety in C++. For example, an object with automatic storage duration may handle the lifetime of the dynamically allocated memory. For example, a std::vector does that.
void bar2(){
std::vector<std::string> vec{"one", "two"};
}
Variable vec has an automatic storage duration but it also handles the lifetime of the dynamically allocated memory

Once function bar2 returns, the variable vec gets destructed and its destructor also frees the dynamically allocated memory. That’s how RAII (Resource Acquisition is Initialization) works in C++: RAII binds the lifetime of a resource to the lifetime of an owning object. The resource is acquired during initialization and released by the owner’s destructor. Local objects with automatic storage duration provide the clearest example, because cleanup happens automatically when control leaves the scope. Read more about RAII in C++.
Patrice summarizes this idea as follows:
It becomes useful when handling resources, and can be mentioned when a program uses containers that grow, like vector or map, and these objects reach the end of their lifetime. It can also appear when doing file I/O: your streams are responsible for their resources and close them upon destruction.
RAII pattern is applied in may types in Standard Library:
- smart pointers,
- containers
- I/O types
And it ensures that resources are released deterministically when the owning object is destroyed.
The Role of Raw Pointers in C++
C++ offers references and Standard Library smart pointers with clear ownership semantics. However, as Patrice Roy explains:
raw pointers are unavoidable because one wants to write abstractions over low-level facilities or call operating system APIs and these are usually exposed through C conventions.
Besides that it’s acceptable to apply raw pointers as non-owning observers
What we have been trying to teach users for well over a decade now is that unless
otherwise specified, for example when writing a class that manages memory like
containers do, one should see raw pointers as observers, thus as non-owning entities
If a function doesn’t do specifically memory memory management it should treat a parameter passed as a raw pointer as a non-owning observer and it shouldn’t delete it.
For example, a function printFoo takes foo via a raw pointer and applies only read operations:
struct Foo {
std::string name;
};
void printFoo(const Foo *foo){
if(foo == nullptr){
std::cout << "Foo{null}\n";
} else {
std::cout << "Foo{name: " << foo->name << "}\n";
}
}
The parameter type const Foo* means that printFoo cannot modify the pointed-to Foo through this pointer. The pointer itself is still passed by value and is not const.
The same goes for a function that returns a raw pointer, for example
unique_ptr::get(). That’s not a hard rule of course (considerunique_ptr::release()which, as one might guess, releases ownership of the previously owned pointer!).
The general idea with raw pointer – they shouldn’t be used to express an ownership on the boundary of a function. Patrice explains this as follows:
I pass you a raw pointer, you use it but it’s still mine. If my function returns a raw pointer (think about
std::string::c_str()), you can use it but it remains mine.
A codebase may introduce a vocabulary type such as a custom or third-party observer_ptr<T> to make non-owning intent more visible. Such a wrapper communicates intent, but it does not manage the pointee’s lifetime or prevent dangling pointers.
Nullability is a Function Contract
What is the peculiarity of using pointers? They may be null. Dereferencing a null pointer results in undefined behavior. A crash is a common outcome, but it is not guaranteed.
The function printFoo(const Foo *foo)does the first of all the check if the pointer is null or not and only if that check is positive – access the member foo->name.
Function printFoo expects and handles properly null object. But in some cases null-pointer is unacceptable value and breaks the function contract. For example, if a function retrieveUser takes a user id and pointer to DataBaseConnection, it expects that DataBaseConnection is not-null pointer:
struct User{
std::size_t id;
std::string name;
};
User retrieveUser(DataBaseConnection *connection, std::size_t id){
auto result = connection->select(
"SELECT id, name from users WHERE id = ?", id);
// handle the result and retrieve it to `User` struct
}
Function expects that connection is not-null pointer because it doesn’t make a sense to call that function with arguments like that:
auto user = retrieveUser(nullptr, 10);
The Patrice Roys opinion about that precondition check is the following:
In most of the code I write, I tend to use «that pointer is not null» as a precondition that users can check (I make sure it’s checkable, of course) if need be, to make sure there’s no added cost to using my types, but remember that my main customers and users are game programmers and other low-latency programmers.
A null check has a cost, but in most application code that cost is negligible. It should only influence API design in a measured hot path where branch latency actually matters. In the low-latency code Patrice works with, expressing non-nullness as a precondition can avoid repeated runtime checks.
However, the pointers aren’t the main tool in C++. I personally needed a quite time to figure out good code example. Because in some cases it makes sense to apply a reference instead of a pointer.
The general rule is the following:
C++ favors objects, not pointers to objects, so the best idiomatic-to-C++ solution is probably to use T instead of T* as much as you can. The «null-safe-operation» aspect of pointer-like entities seems like an interesting design challenge if you’re willing to undertake it.
C++ Gives You Safe Defaults and Sharp Tools
You can totally use C++ in memory-safe ways.
Modern C++ provides several facilities that make safer designs easier to express:
- standard containers manage their own element storage;
- RAII types release resources deterministically;
- smart pointers communicate ownership;
- algorithms reduce duplicated traversal logic and manual pointer arithmetic;
- hardened library modes can diagnose some violations of library preconditions.
These facilities reduce important classes of errors, but they do not make every C++ program memory-safe. Iterators may still be invalidated, pointers may still dangle, and library preconditions may still be violated.
As Patrice Roy explains:
We cannot stop programmers from using casts and working around the type system to get themselves in trouble (nor would we want to do so, as sometimes they need to do that!), but we do make significant efforts to ensure the clean and safe code is also the efficient code.
However, in the book “C++ Memory Management” Patrice Roy took a lot of time about the responsibility: if you go there, be responsible about it» aspect of C++ programming. It’s a book that does go on pathways
where one could get in trouble when acting irresponsibly so it’s important to
make that aspect of programming at a lower-level very clear.
Final Thoughts: Teach From Objects Downward
Modern C++ memory management is not primarily about calling new and delete. It is about understanding objects, lifetime, ownership, and the relationship between resources and the objects that manage them.
A practical learning path starts with values and scopes, then moves to containers, RAII, references, and smart pointers. Raw pointers and manual allocation still matter, especially when working with low-level abstractions and C APIs, but they are easier to understand once the higher-level model is already clear.
C++ gives developers both safe abstractions and low-level control. The deeper you go below those abstractions, the more responsibility you accept.