new and delete
new allocates an object on the heap and returns its address; delete destroys it and gives the memory back. The [] forms do the same for arrays, and the two pairs must not be mixed.
The course waited until now to show them, because the standard containers and the smart pointers of the next lessons do this work correctly. This lesson uses the raw pair once, so that the leak and the mismatched delete are recognisable when they appear in older code.
The compiler also helps: with -Wall it warns at the delete site when it can see the pointer came from an array allocation, which is the message above the sanitizer report.
The two forms exist because the runtime has to know how many destructors to run; the count is stored with the block by most implementations, and trusting it is what makes the mismatch undefined.
One object
new int{41} allocates room for one int, initialises it with 41 and returns the address. The pointer is the only handle on that object, and delete ends its life and returns the memory.
The program increments the value through the pointer and prints it, then deletes. The braces with the value are initialisation, not a function call: new int alone would leave the int uninitialised.
After delete the pointer still holds the old address, which is why the code sets it to nullptr or lets it go out of scope. Using it again is the use-after-free of lesson 084.
#include <iostream>
int main()
{
int* value = new int{41};
*value += 1;
std::cout << *value << '\n';
delete value;
}
g++ -std=c++23 -Wall -Wextra single.cpp -o single
./single
42
An array
new int[3]{10, 20, 30} allocates three ints and initialises them with the brace list. The pointer to the first element is the same kind of pointer as the array name of the previous lessons.
The program prints the elements and frees the block with delete[]. The brackets are part of the deallocation: the runtime has to know how many destructors to run.
An array of a class type runs a constructor per element and a destructor per element. The count is stored with the block, which is why delete without brackets on that pointer is undefined.
#include <iostream>
int main()
{
int* values = new int[3]{10, 20, 30};
for (int i = 0; i < 3; ++i) {
std::cout << values[i] << ' ';
}
std::cout << '\n';
delete[] values;
}
g++ -std=c++23 -Wall -Wextra array.cpp -o array_new
./array_new
10 20 30
The leak
A block that is never deleted stays allocated until the program exits. For a short program that is invisible; for a server that runs for weeks it is a slow failure.
The program counts constructions and destructions of a small type and prints them. The constructor ran once and the destructor never, because the pointer was dropped without a delete.
The lesson's sanitizer run of lesson 084 reports exactly this pattern. The fix is an object that owns the block, which is the next lesson's class and then std::unique_ptr.
#include <iostream>
int created = 0;
int destroyed = 0;
struct resource
{
resource()
{
++created;
}
~resource()
{
++destroyed;
}
};
int main()
{
resource* handle = new resource;
(void)handle;
std::cout << created << ' ' << destroyed << '\n';
}
g++ -std=c++23 -Wall -Wextra leak.cpp -o leak
./leak
1 0
The mismatched delete
An array allocated with new[] must be freed with delete[]. Mixing the two is undefined behaviour, and AddressSanitizer reports it by name.
The program allocates four ints and frees them with delete. The sanitizer says the delete does not match the allocation and prints both sites.
For a type with a destructor the mismatch is worse than a wrong size: the runtime runs one destructor instead of four. The rule is simple and easy to break in a function that receives a pointer without its form.
int main()
{
int* values = new int[4]{1, 2, 3, 4};
delete values;
}
g++ -g -fdebug-prefix-map=$PWD=. -fsanitize=address -std=c++23 -Wall -Wextra mismatch.cpp -o mismatch
ASAN_OPTIONS=print_legend=0 ./mismatch 2>&1 | head -12
mismatch.cpp: In function 'int main()':
mismatch.cpp:5:12: warning: 'void operator delete(void*, long unsigned int)' called on pointer returned from a mismatched allocation function [-Wmismatched-new-delete]
5 | delete values;
| ^~~~~~
mismatch.cpp:3:40: note: returned from 'void* operator new [](long unsigned int)'
3 | int* values = new int[4]{1, 2, 3, 4};
| ^
=================================================================
==470==ERROR: AddressSanitizer: alloc-dealloc-mismatch (operator new [] vs operator delete) on 0x7058ddf00010
#0 0x7438de3c56dd in operator delete(void*, unsigned long) (/usr/lib/libasan.so.8+0xef6dd) (BuildId: 49cdec61ea4a7e68d1b3321b11e733d1f2124ac9)
#1 0x5c15c0d31324 in main mismatch.cpp:5
#2 0x7438de9db153 in libc_start_main_stage2 src/env/__libc_start_main.c:95
0x7058ddf00010 is located 0 bytes inside of 16-byte region [0x7058ddf00010,0x7058ddf00020)
allocated by thread T0 here:
#0 0x7438de3c474d in operator new[](unsigned long) (/usr/lib/libasan.so.8+0xee74d) (BuildId: 49cdec61ea4a7e68d1b3321b11e733d1f2124ac9)
#1 0x5c15c0d311e6 in main mismatch.cpp:3
#2 0x7438de9db153 in libc_start_main_stage2 src/env/__libc_start_main.c:95
Why the standard library owns memory
The same three ints are one line with a vector, and the vector frees its block in its destructor, grows when it has to, and copies itself correctly. None of that had to be written here.
The program pushes three values, prints the size and the capacity and lets the vector go out of scope. The capacity is the one the growth policy chose, which lesson 019 measured.
Raw new survives in code that talks to a C library or builds a container. Everywhere else the rule is the rule of zero: let a member own the memory, and write no delete at all.
#include <iostream>
#include <vector>
int main()
{
std::vector<int> values;
values.push_back(10);
values.push_back(20);
values.push_back(30);
std::cout << values.size() << ' ' << values.capacity() << ' ' << values[2] << '\n';
}
g++ -std=c++23 -Wall -Wextra vector.cpp -o vector_memory
./vector_memory
3 4 30
Exercises
- Allocate an array of five doubles with new[], fill it, print the sum and free it with delete[].
- Add a counter to the resource type and allocate an array of three resources, then free it correctly.
- Rewrite the first program with std::unique_ptr and check that no delete appears.
Questions
A new set of questions in a new order every time. Choose one answer for each, then check.
-
What does
new int{41}return?Answer
A pointer to a new int holding 41
new allocates and initialises the object and returns its address.
-
Which delete matches
int* p = new int[4];?Answer
delete[] p;An array allocated with new[] is freed with delete[], which runs the destructors of all elements.
-
What does this program print?
int c = 0, d = 0; struct R { R(){++c;} ~R(){++d;} }; R* p = new R; std::cout << c << ' ' << d;Answer
1 0
The constructor ran and the pointer was dropped without delete, so the destructor never ran.
-
What does AddressSanitizer report for
int* p = new int[4]; delete p;?Answer
A mismatch between the allocation and the delete
The sanitizer records how the block was allocated and reports that a scalar delete was used on an array allocation.
-
After
delete value;what is the state of the pointer variable?Answer
It still holds the old address
delete frees the object, not the variable; the address is still there and using it is undefined behaviour.
-
What does this program print?
int* v = new int[3]{10, 20, 30}; std::cout << v[1]; delete[] v;Answer
20
The brace list initialises the three elements, so index 1 holds 20.
-
What does
new int;(without braces) leave in the int?Answer
An unspecified value
Without an initialiser the int is default-initialised, which for an int means no value.
-
Why does a std::vector need no delete in the code that uses it?
Answer
Its destructor frees the block it allocated
The vector owns its buffer and frees it in the destructor, which is the rule of zero at work.
-
What happens when new cannot allocate the requested memory?
Answer
It throws std::bad_alloc
The ordinary form throws; the
new (std::nothrow)form returns nullptr instead. -
A function allocates with
new[]and returns the pointer. What must its caller know?Answer
That the block must be freed with delete[]
The form of the allocation is part of the contract; a caller who uses delete instead of delete[] has undefined behaviour.
The code on this page was built with g++ 15.2.0 and run on Alpine Linux 3.24.3 (x86_64) in a Docker container.
Article author: Arthur Isaev