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
NULL
return value. - You must
free
exactly whatmalloc
returned.
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 int
returns anint*
,new std::string
returns astd::string*
. No cast is needed. - Size Calculated Automatically: You just specify the type (
new int
), not the size in bytes. - Object Initialization:
new
calls the appropriate constructor for the object being created. - Object Cleanup:
delete
calls 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[] pointer
to free them. Mixingdelete
anddelete[]
is a serious error (undefined behavior)! - Error Handling: By default, if
new
fails 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 returnsnullptr
on 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
delete
orfree
memory 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
delete
d orfree
d. Leads to crashes or unpredictable behavior. - Double Free/Delete: Freeing or deleting the same memory twice. Leads to corruption and crashes.
delete
/delete[]
Mismatch: Usingdelete
on 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! end
Core 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
/delete
are type-safe and interact with object constructors and destructors.- Crucially, match
new
withdelete
andnew[]
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
/delete
in modern C++.
Next Lesson: We’ll officially dive into class
es in C++, building upon C struct
s 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