C++ for C Developers | Lesson 4: Managing Memory: new, delete, and the Basics of RAII
Goal: Understand C++’s new and delete operators for managing memory on the heap, see how they differ from C’s malloc and free, and get introduced to the vital C++ concept of RAII for safer resource management.
1. Dynamic Memory in C: malloc and free Recap
In C, you use malloc (or calloc, realloc) from <stdlib.h> to allocate memory on the heap and free to release it.
#include <stdio.h>
#include <stdlib.h>
int main() {
int *p_int = NULL;
size_t num_elements = 5;
// Allocate memory for one integer
p_int = (int*)malloc(sizeof(int)); // Returns void*, requires cast, size calculation needed
if (p_int == NULL) {
perror("Failed to allocate memory");
return 1;
}
*p_int = 42;
printf("Value: %d\n", *p_int);
free(p_int); // Deallocate memory
p_int = NULL; // Good practice: nullify pointer after free
// Allocate memory for an array of integers
int *p_array = (int*)malloc(num_elements * sizeof(int));
if (p_array == NULL) {
perror("Failed to allocate array memory");
return 1;
}
for (size_t i = 0; i < num_elements; ++i) {
p_array[i] = i * 10;
}
printf("Array element 3: %d\n", p_array[3]);
free(p_array); // Deallocate array memory
p_array = NULL;
return 0;
}
Key points about malloc/free:
- Returns
void*, requiring an explicit cast. - Requires manual calculation of bytes needed (
sizeof). - Doesn’t initialize the allocated memory (unless using
calloc). - Doesn’t understand C++ objects (doesn’t call constructors/destructors).
- Error indication is via
NULLreturn value. - You must
freeexactly whatmallocreturned.
2. C++ Way: new and delete Operators
C++ introduces the new and delete operators, which are type-aware and integrate with object lifecycles.
#include <iostream>
#include <string> // For a simple object example
int main() {
// Allocate memory for a single integer
int *p_int = new int; // Type is known, no cast needed, no sizeof needed
*p_int = 123;
std::cout << "Value: " << *p_int << std::endl;
delete p_int; // Deallocate the integer's memory
p_int = nullptr; // C++ equivalent of NULL (preferred)
// Allocate memory for a single C++ object (std::string)
std::string *p_str = new std::string("Hello from heap!"); // Allocates AND calls the string's constructor
std::cout << "String: " << *p_str << std::endl;
std::cout << "Length: " << p_str->length() << std::endl; // Use -> for member access via pointer
delete p_str; // Calls the string's destructor AND deallocates memory
p_str = nullptr;
// Allocate memory for an array of doubles
size_t num_elements = 5;
double *p_array = new double[num_elements]; // Allocate an array
for (size_t i = 0; i < num_elements; ++i) {
p_array[i] = i * 1.1;
}
std::cout << "Array element 2: " << p_array[2] << std::endl;
delete[] p_array; // CRITICAL: Use delete[] for arrays allocated with new[]
p_array = nullptr;
return 0;
}
Key points about new/delete:
flowchart LR
subgraph C [C: malloc/free]
A1("malloc() returns void*")
A2("Requires cast")
A3("Requires sizeof")
A4("Does NOT call constructor")
A5("free() deallocates manually")
end
subgraph CPP [C++: new/delete]
B1("new returns typed pointer")
B2("No cast or sizeof needed")
B3("Calls constructor")
B4("delete calls destructor and frees memory")
B5("new[] / delete[] for arrays")
end- Type-Safe:
new intreturns anint*,new std::stringreturns astd::string*. No cast is needed. - Size Calculated Automatically: You just specify the type (
new int), not the size in bytes. - Object Initialization:
newcalls the appropriate constructor for the object being created. - Object Cleanup:
deletecalls the appropriate destructor for the object before freeing memory. (We’ll cover constructors/destructors in detail soon). - Array Syntax: Use
new Type[size]for arrays anddelete[] pointerto free them. Mixingdeleteanddelete[]is a serious error (undefined behavior)! - Error Handling: By default, if
newfails to allocate memory, it throws an exception (std::bad_alloc) rather than returningnullptr. (You can request the non-throwing version:new (std::nothrow) int;which returnsnullptron failure, similar tomalloc).
Comparison Table:
| Feature | malloc/free (C) | new/delete / new[]/delete[] (C++) |
|---|---|---|
| Return Type | void* (needs cast) | T* (typed pointer, no cast needed) |
| Size | Manual calculation (sizeof) | Automatic based on type |
| Objects | Unaware (no constructor/destructor calls) | Aware (calls constructor on new, destructor on delete) |
| Arrays | Manual calculation (N * sizeof(T)) | new T[N] syntax |
| Array Free | free(ptr) | delete[] ptr (Must match new[]) |
| Error | Returns NULL | Throws std::bad_alloc (default) or returns nullptr |
3. The Dangers of Manual Memory Management
Whether using malloc/free or new/delete, manual management is error-prone:
- Memory Leaks: Forgetting to
deleteorfreememory that’s no longer needed. The program consumes more and more memory over time. - Dangling Pointers: Using a pointer after the memory it pointed to has been
deleted orfreed. Leads to crashes or unpredictable behavior. - Double Free/Delete: Freeing or deleting the same memory twice. Leads to corruption and crashes.
delete/delete[]Mismatch: Usingdeleteon an array allocated withnew[]or vice-versa. Undefined behavior.
These errors become much harder to manage in complex code with multiple exit points (returns, loops, and especially exceptions).
4. RAII: Resource Acquisition Is Initialization (The C++ Way)
This is one of the most fundamental C++ idioms for managing resources (like dynamic memory, file handles, network sockets, mutexes, etc.).
sequenceDiagram
participant StackObj as "RAII Object"
participant HeapRes as "Heap Resource"
StackObj->>HeapRes: Constructor acquires resource (new)
Note right of StackObj: Resource tied to object's lifetime
alt Normal scope exit
StackObj-->>HeapRes: Destructor releases resource (delete)
else Exception thrown
StackObj-->>HeapRes: Destructor still releases resource!
endCore Idea: Tie the lifetime of a resource to the lifetime of an object on the stack.
- Acquire the resource in the object’s constructor.
- Release the resource in the object’s destructor.
Since C++ guarantees that destructors of stack-based objects are called when the object goes out of scope (either normally or due to an exception), the resource is automatically cleaned up.
Simple Conceptual Example (Manual RAII Wrapper):
#include <iostream>
// Simple wrapper class for a dynamically allocated int
class ManagedInt {
private:
int* m_ptr; // Pointer to the heap-allocated integer
public:
// Constructor: Acquires the resource (allocates memory)
ManagedInt(int value) {
m_ptr = new int(value); // Allocate and initialize
std::cout << "ManagedInt created (allocated memory for " << *m_ptr << ")" << std::endl;
}
// Destructor: Releases the resource (deallocates memory)
~ManagedInt() {
std::cout << "ManagedInt destroying (deallocating memory for " << *m_ptr << ")" << std::endl;
delete m_ptr; // Free the memory
m_ptr = nullptr;
}
// Method to access the value
int getValue() const {
return *m_ptr;
}
// Prevent copying (for simplicity in this example - copy requires careful handling)
ManagedInt(const ManagedInt&) = delete;
ManagedInt& operator=(const ManagedInt&) = delete;
};
void useManagedInt() {
ManagedInt mi(100); // Object created on stack, constructor allocates memory
std::cout << "Inside function, value = " << mi.getValue() << std::endl;
// ... maybe some code that could return early or throw an exception ...
if (mi.getValue() > 50) {
std::cout << "Value is large enough." << std::endl;
// No need to manually delete here!
}
} // mi goes out of scope here. Its destructor (~ManagedInt) is automatically called!
int main() {
std::cout << "Entering main..." << std::endl;
useManagedInt();
std::cout << "Exited useManagedInt. Memory should be freed." << std::endl;
std::cout << "Exiting main." << std::endl;
return 0;
}
The Power of RAII: Notice how we didn’t need a delete call inside useManagedInt. When mi goes out of scope (at the end of the function), its destructor ~ManagedInt() is automatically invoked by C++, ensuring the delete m_ptr; happens reliably. This works even if the function exits early or an exception occurs (exception handling details later).
flowchart TD
A1["Manual Management"] -->|allocate| M1["new / malloc"]
M1 -->|use| M2["do stuff"]
M2 -->|free| M3["delete / free"]
M2 -->|forget to free| M4["Memory Leak"]
M2 -->|exception| M5["No cleanup"]
A2["RAII"] -->|create object| R1["Constructor allocates"]
R1 --> R2["use object"]
R2 -->|scope ends| R3["Destructor auto-cleans"]
5. Smart Pointers: RAII in the Standard Library
Manually writing RAII wrappers for everything is tedious. The C++ Standard Library provides smart pointers that implement RAII for dynamically allocated memory. The most common one for single ownership is std::unique_ptr (from <memory>).
#include <iostream>
#include <memory> // Include header for smart pointers
#include <string>
void useSmartPointer() {
// Create a unique_ptr managing a dynamically allocated int initialized to 200
std::unique_ptr<int> smart_p_int(new int(200));
// Create a unique_ptr managing a string
std::unique_ptr<std::string> smart_p_str(new std::string("Smart!"));
// Access the managed object using * and -> just like raw pointers
std::cout << "Smart int: " << *smart_p_int << std::endl;
std::cout << "Smart string: " << *smart_p_str << ", Length: " << smart_p_str->length() << std::endl;
// *smart_p_int = 300; // Can modify the managed object
} // smart_p_int and smart_p_str go out of scope here.
// Their destructors are automatically called, which in turn call 'delete'
// on the raw pointers they manage. No manual 'delete' needed!
int main() {
std::cout << "Using smart pointers..." << std::endl;
useSmartPointer();
std::cout << "Smart pointers went out of scope. Memory freed automatically." << std::endl;
return 0;
}
Takeaway: Prefer smart pointers (std::unique_ptr, std::shared_ptr) over raw new and delete. They implement RAII for you, making code significantly safer and easier to manage. We’ll revisit these later, but the principle is crucial.
Summary of Lesson 4:
- C++ uses
new(for single objects/primitives) andnew[](for arrays) to allocate memory dynamically. - C++ uses
delete(for single objects/primitives) anddelete[](for arrays) to deallocate memory. new/deleteare type-safe and interact with object constructors and destructors.- Crucially, match
newwithdeleteandnew[]withdelete[]. - Manual memory management (
new/delete) is error-prone (leaks, dangling pointers, double deletes). - RAII (Resource Acquisition Is Initialization) is a core C++ idiom: tie resource lifetime to object lifetime using constructors and destructors for automatic cleanup.
- Smart pointers (like
std::unique_ptr) are standard library classes that implement RAII for dynamic memory, greatly reducing errors. Prefer smart pointers over rawnew/deletein modern C++.
Next Lesson: We’ll officially dive into classes in C++, building upon C structs to add behavior (member functions) and control access (public/private) – the foundation of Object-Oriented Programming in C++.
More Sources:
cppreference – new expression: https://en.cppreference.com/w/cpp/language/new
cppreference – RAII Explanation: https://en.cppreference.com/w/cpp/language/raii


