Dynamic Memory

Allocate and release storage whose size and lifetime are decided while the program runs, using new and delete — and see why modern C++ code reaches for this manually as rarely as possible.

Dynamic memory lets a program request storage at runtime, use it for as long as it is needed, and release it explicitly when it is no longer needed. Unlike a local variable, a dynamically allocated object's lifetime is not tied to the scope that created it — it persists until the program deletes it (or, if it never does, until the program ends). This topic covers the mechanics of new and delete, the rules that keep them safe, and the reasons modern C++ prefers to keep this manual bookkeeping to a minimum.

What Is Dynamic Memory?

A local variable's size and lifetime are fixed by its declaration and its enclosing scope. Dynamic memory is different: storage is requested explicitly while the program is running, its size can depend on a runtime value, and it stays alive independent of any particular scope — until the program explicitly releases it.

An ordinary array needs a size known at compile time (or, for a local array, at least fixed for the scope). A dynamically allocated array can be sized by a value the program only learns while running.

C++ calls memory obtained this way an object with dynamic storage duration. It is commonly allocated from a runtime memory pool that implementations typically call the heap or the free store — those are common implementation terms, not formal requirements of the language; the standard only guarantees that the storage exists and is distinct from automatic and static storage, not how an implementation organizes it internally.

int count = readCountFromUser(); // not known until runtime
int* data = new int[count];      // storage sized at runtime

Why Dynamic Memory Exists

Dynamic memory solves problems that fixed, compile-time storage cannot:

1. The amount of data needed is only known at runtime (a file's size, a user's input, a network response).

2. An object needs to outlive the function or block that creates it — it must still exist after that scope ends.

3. The number or identity of objects needed changes while the program runs, rather than being fixed in the source code.

Automatic (local) variables cannot satisfy any of these: their size is fixed at compile time and their lifetime ends when their scope ends. Dynamic memory decouples both size and lifetime from any single scope.

Storage Durations at a Glance

It helps to see dynamic storage duration next to the two you already know from ordinary variables. A full treatment of storage duration classification belongs to its own topic; this is just enough context to place dynamic memory correctly.

Dynamic storage duration is the only one of the three whose end is decided by an explicit statement in the code, not by scope or program structure.

Storage duration | Created                | Ends                              | Typical example
Automatic         | Block entry            | Block exit                       | int x; inside a function
Static            | Program startup         | Program termination              | a namespace-scope or static variable
Dynamic           | A new-expression runs  | A matching delete-expression runs | int* p = new int;

Allocating a Single Object with new

The new operator requests storage for one object of a given type and returns a pointer to it.

Requests storage for one int on the free store and returns its address. With no initializer, a scalar type like int is default-initialized, which for a built-in type means its value is indeterminate — just like an uninitialized local int.

Providing an initializer avoids an indeterminate value. new int() and new int{} both value-initialize a scalar to zero.

int* ptr = new int;
int* ptr2 = new int(42);  // direct-initialization: value is 42
int* ptr3 = new int{42};  // list-initialization (C++11): value is 42
int* ptr4 = new int();    // value-initialization: value is 0

Dynamic Object Creation for Class Types

For a class type, new invokes a matching constructor, just as ordinary construction would.

new Point(3, 4) allocates storage and calls the Point(int, int) constructor with 3 and 4. p->x and p->y access members through the pointer, the same way -> is used for any pointer to a class object.

The object behaves like any other Point once constructed — the only difference is that its storage came from a new-expression instead of a local declaration, and it must be released with delete instead of going out of scope.

struct Point
{
    int x;
    int y;

    Point(int xIn, int yIn) : x(xIn), y(yIn) {}
};

Point* p = new Point(3, 4);
std::cout << p->x << ' ' << p->y;

Releasing a Single Object with delete

delete releases storage that new obtained for a single object. For a class type it first calls the destructor, then releases the storage.

After delete ptr;, the object no longer exists and the storage is returned to the free store. The pointer variable ptr itself still exists and still holds the old address — delete does not change ptr's value.

Every object obtained with a single-object new must eventually be released with a single-object delete exactly once. delete on a pointer that was never returned by new, or on one already deleted, is undefined behavior.

int* ptr = new int(42);
std::cout << *ptr << '\n';
delete ptr;

Deleting a Null Pointer Is Safe

The standard specifically guarantees that delete on a null pointer does nothing.

This guarantee is useful: code that conditionally allocates does not need a separate null check before deleting.

This guarantee applies only to a genuinely null pointer. A pointer holding an indeterminate value (an uninitialized pointer that was never set to nullptr or a valid address) is not covered by it — deleting such a pointer is undefined behavior, not a safe no-op.

int* ptr = nullptr;
delete ptr; // guaranteed no-op — safe

Dynamic Arrays with new[]

The array form of new allocates storage for a sequence of objects whose count can be a runtime value.

new T[n] returns a pointer to the first element. Appending () value-initializes every element; appending a braced initializer list (C++11 onward) initializes elements from the list, with any remaining elements value-initialized.

A dynamic array is accessed with ordinary subscripting, the same as any array-like pointer. Detailed pointer/array indexing and arithmetic are covered in their own dedicated topics.

int size = 5;
int* arr = new int[size];   // elements default-initialized (indeterminate for int)
int* zeroed = new int[size](); // value-initialization: every element is 0
int* listed = new int[3]{1, 2, 3}; // C++11: initializer list for array new
for (int i = 0; i < size; ++i)
{
    arr[i] = i * i;
}

Releasing Dynamic Arrays with delete[]

An array obtained with new[] must be released with delete[], the array form of delete. For a class type, delete[] destroys every element (typically in reverse order) before releasing the storage.

delete[] arr; destroys the array's elements (a no-op for a scalar type like int) and releases the entire block of storage that new[] requested.

int* arr = new int[5];
// ... use arr ...
delete[] arr;

Matching new/delete and new[]/delete[]

The single-object and array forms are a matched pair. Using the wrong form is undefined behavior — implementations commonly store array-length bookkeeping alongside the allocation, and delete without [] does not know to look for or use it.

There is no way to recover which form was used just by looking at the pointer's type — int* looks the same whether it came from new int or new int[5]. The programmer must track this.

Keeping the allocation and its matching release form next to each other, or hidden inside a single well-tested abstraction, is the practical way to avoid this mistake.

Allocated with | Must release with | Mixing them
new T          | delete p           | delete[] p is undefined behavior
new T[n]       | delete[] p          | delete p is undefined behavior
int* single = new int(1);
int* array = new int[10];

delete single;   // correct
delete[] array;  // correct
// delete array;  would be undefined behavior — mismatched form

Ownership

Ownership is not a C++ language keyword or guarantee — it is a design responsibility the programmer must track. The owner of a dynamically allocated object is whichever part of the program is responsible for eventually calling delete on it exactly once.

Both ptr and alias point to the same object, but only one delete should ever be issued for that object. If the program's design is unclear about which pointer owns it, double deletion or a leak becomes likely.

A pointer merely referring to a dynamically allocated object (an alias, or a pointer passed into a function) does not, by itself, imply ownership. Ownership has to be established by the program's design and, ideally, made explicit — which is exactly what smart pointers are built to express in their type.

int* ptr = new int(10);
int* alias = ptr; // alias refers to the same object but does not own it

Lifetime of a Dynamically Allocated Object

A dynamically allocated object's lifetime begins when its new-expression finishes initializing it, and ends when the matching delete-expression runs — not when any particular function returns or block ends.

Unlike a local int inside makeValue, the dynamically allocated int is not destroyed when makeValue returns. It remains valid until main explicitly deletes it.

Dynamic lifetime is controlled entirely by explicit new/delete calls, not by entering or leaving a scope.

int* makeValue()
{
    int* local = new int(99); // lifetime begins here
    return local;              // the object survives this function returning
}

int main()
{
    int* ptr = makeValue();
    std::cout << *ptr << '\n'; // still valid — the object outlived makeValue
    delete ptr;                 // lifetime ends here
    return 0;
}
new int(99)
     |
     v
+-----------+
|    99     |   <- lives here, independent of any function's scope
+-----------+
     ^
     |
  delete ptr;  <- lifetime ends only here

Allocation Failure and std::bad_alloc

A new-expression can fail if there is not enough available memory to satisfy the request. By default, new reports this by throwing an exception — std::bad_alloc (declared in <new>, derived from std::exception) — rather than returning a null pointer.

Because plain new throws on failure, code using it does not need to check for a null return the way C's malloc does — either the pointer is valid, or an exception propagates.

A separate, non-throwing form exists for cases that specifically need to avoid exceptions: new (std::nothrow) T returns a null pointer on failure instead of throwing. It must still be checked explicitly, and it is the exception rather than the rule in ordinary C++ code.

The std::nothrow overload trades the exception for an explicit null check, which the caller must not forget to perform.

#include <new>
#include <iostream>

try
{
    // an unrealistically large request, for illustration
    int* huge = new int[1000000000000];
}
catch (const std::bad_alloc& e)
{
    std::cout << "Allocation failed: " << e.what() << '\n';
}
int* ptr = new (std::nothrow) int[1000000000000];
if (ptr == nullptr)
{
    std::cout << "Allocation failed\n";
}

Memory Leaks

A memory leak happens when a program loses every pointer to a dynamically allocated object without ever calling delete on it. The storage remains allocated, but the program can no longer reach it to free it.

When leak() returns, the local pointer variable ptr is destroyed, but the int it pointed to is not — nothing ever called delete on it. That storage is now unreachable for the remainder of the program's run.

A single small leak in a short-running program is often harmless in practice. A leak inside code that runs repeatedly — a loop, a server handling many requests, a long-running application — accumulates and can eventually exhaust available memory. Most operating systems reclaim a process's memory when the process terminates, but that is OS behavior outside anything the C++ standard specifies, and it offers no help while the program is still running.

void leak()
{
    int* ptr = new int(5); // allocated
    // no delete before ptr goes out of scope
}

Double Deletion

Double deletion (sometimes called a double free) is calling delete on the same pointer value twice. It is undefined behavior — it does not reliably produce an error message, and its effects range from silent corruption to a crash.

The first delete releases the storage; the second delete acts on storage that is no longer a valid allocation for this pointer.

Double deletion often arises indirectly rather than as two literal adjacent calls: two different pointer variables holding a copy of the same address, each independently deleted by different parts of the program, produces the same undefined behavior.

int* ptr = new int(7);
delete ptr;
delete ptr; // undefined behavior — already deleted

Dangling Pointers After delete

A dangling pointer is a pointer whose value no longer refers to a valid, live object. delete produces one directly: after delete ptr;, the object is gone, but ptr still holds the address it used to have.

Dereferencing ptr after delete accesses storage that the program no longer owns and that may already have been reused for something else.

This is the same dangling-pointer concept covered for pointers that outlive a local variable's scope, applied here to explicit deletion instead. Any copy of a dangling pointer's value is equally dangling — deleting the object does not update every pointer variable that happened to hold its address.

int* ptr = new int(3);
delete ptr;
std::cout << *ptr; // undefined behavior — ptr is now dangling

Nulling Out a Pointer After delete

Because delete does not change the pointer's own value, a good habit is to explicitly set it to nullptr immediately afterward.

A null pointer is easy to check for (if (ptr != nullptr)) and safe to delete again (delete on nullptr is a guaranteed no-op), which makes accidental reuse or a stray second delete far less dangerous.

This habit only protects the one pointer variable that gets nulled. It does nothing for any other alias that still holds the old address — nulling out a pointer after delete narrows the risk, it does not eliminate it program-wide.

int* ptr = new int(3);
delete ptr;
ptr = nullptr; // now ptr is safely null instead of dangling

Returning Dynamically Allocated Objects from Functions

A function can allocate an object with new and hand ownership to its caller by returning the pointer. The caller then becomes responsible for eventually calling delete.

createBuffer used new[], so the caller must release the result with delete[], not delete. The function's implementation is the only place that knows which form was used, so this responsibility has to be documented or made obvious by convention.

A raw pointer return value like this carries no information about ownership in its type — int* looks identical whether the caller must delete it, must delete[] it, or must not delete it at all because someone else owns it. This ambiguity is a central reason ownership-transferring functions in modern C++ prefer to return a smart pointer instead.

int* createBuffer(int size)
{
    return new int[size]; // ownership transfers to the caller
}

int main()
{
    int* buffer = createBuffer(10);
    // ... use buffer ...
    delete[] buffer; // the caller must remember to release it, with the matching form
    return 0;
}

Dynamic Memory and Exceptions

If a class type's constructor throws inside a new-expression, the language guarantees that the storage just requested is automatically deallocated — the object is never considered to have existed, so this specific case does not leak.

The guarantee covers only the storage new itself just obtained for this object — it says nothing about any other allocation elsewhere in the program.

The more common danger is different: an exception thrown by other code that runs after a successful new but before the matching delete skips over that delete entirely, leaking the object.

If mightThrow() throws, the exception propagates out of risky() immediately, skipping delete ptr; entirely — the int is leaked. Raw new/delete pairs offer no protection against this; this exact problem is what RAII and smart pointers are designed to solve.

struct Fragile
{
    Fragile() { throw std::runtime_error("construction failed"); }
};

try
{
    Fragile* f = new Fragile(); // constructor throws
}
catch (const std::exception& e)
{
    std::cout << "Caught: " << e.what() << '\n';
    // no leak: the storage new requested was released automatically
}
void risky()
{
    int* ptr = new int(5);
    mightThrow();  // if this throws, execution never reaches delete below
    delete ptr;
}

Dynamic Arrays vs std::vector

A dynamic array managed by hand and a standard-library std::vector solve the same underlying problem — a runtime-sized sequence — with very different amounts of manual responsibility.

std::vector is built on top of the same underlying new/delete[] mechanics this topic covers, but wraps them in a class whose constructor and destructor handle allocation and release automatically.

Both lines allocate storage for n ints. Only the vector guarantees its storage is released no matter how the enclosing scope is exited — normal return, early return, or exception.

Aspect                    | new[] / delete[] array          | std::vector<T>
Size known at runtime      | Yes                              | Yes
Release on scope exit      | No — must call delete[] manually | Yes — automatic (RAII)
Release on an exception     | No — easily skipped, leaks       | Yes — still automatic
Bounds checking (at())      | No                                | Available via .at()
Resizing                   | Manual (allocate new, copy, delete old) | Built in (push_back, resize)
Ownership expressed by type | No — int* could mean anything    | Yes — a vector always owns its elements
int n = 5;
int* raw = new int[n];
// ... must remember delete[] raw; on every exit path ...
delete[] raw;

std::vector<int> managed(n); // released automatically, on every exit path

Why Manual Dynamic Memory Should Be Minimized

Every example above shows the same underlying pattern: new and delete are correct only if they are matched exactly once, in the right form, on every possible path through the code, including paths caused by an exception. As a function grows more branches, more early returns, and more code that might throw, keeping that matching correct by hand becomes progressively harder to guarantee.

Modern C++ treats manual new/delete as a low-level tool rather than the default way application code manages memory. The standard library and idiomatic C++ code instead lean on containers like std::vector and std::string, and on smart pointers, so that allocation and release are tied to an object's own lifetime and happen automatically — correctly, on every exit path, without the programmer re-deriving the matching logic in each function.

The Boundary: RAII and Smart Pointers

The general technique of tying a resource's release to an object's destructor — so the resource is freed automatically when that object's lifetime ends, on any exit path — is called RAII (Resource Acquisition Is Initialization). Applied specifically to dynamically allocated memory, this is what smart pointer types such as std::unique_ptr and std::shared_ptr do: they hold a pointer and call delete (or delete[]) automatically in their own destructor.

This is the exception-safety problem from the earlier section, solved: whether safer() exits normally or via an exception, ptr's destructor runs and the int is released exactly once. This is only a preview — RAII and the full set of smart pointer types are covered in their own dedicated topics.

For the rest of this topic, new and delete are worth understanding precisely because they are the mechanism smart pointers and containers are built on top of, and because some low-level or performance-sensitive code still uses them directly. Reaching for them as the everyday default in ordinary application code is generally what modern C++ tries to avoid.

#include <memory>

void safer()
{
    std::unique_ptr<int> ptr = std::make_unique<int>(5);
    mightThrow(); // if this throws, ptr's destructor still runs and releases the int
} // or, if mightThrow() didn't throw, ptr is released automatically here too

Best Practices

1. Prefer a standard container (std::vector, std::string, and similar) or a smart pointer over a raw new/delete pair whenever one fits the problem — let their destructors do the matching for you.

2. Always release with the form that matches how the memory was allocated — track single-object vs array allocations deliberately, since the pointer's type alone cannot tell you which form was used.

3. Set a pointer to nullptr immediately after deleting it, so a later use is a safe, checkable null rather than a silent dangling access.

4. Keep ownership of every dynamically allocated object clear and singular — know, for each object, exactly which part of the program is responsible for its one matching delete.

5. When a function allocates memory that the caller must release, document that responsibility explicitly (or, better, return a smart pointer whose type makes it self-evident) rather than leaving it implicit.

6. Treat any code path between a successful new and its matching delete as exception-sensitive — if anything in between can throw, wrap the allocation in RAII (a smart pointer or container) rather than relying on manual cleanup after the fact.

7. Never assume an object obtained through new is still valid after any delete on that same pointer value or on a pointer holding a copy of its address.

delete ptr;      // for a single new
delete[] arr;    // for a new[]
delete ptr;
ptr = nullptr;

Complete Example

createSquares allocates a runtime-sized array with new[], fills it, and returns ownership to main. main uses the array, releases it with the matching delete[], and nulls the pointer out afterward.

createSquares(5) allocates 5 ints and fills them with 1, 4, 9, 16, 25 main prints each value through the returned pointer delete[] squares releases the array, matching the new[] used to create it squares is set to nullptr, and the following check confirms it The program's output.

#include <iostream>

int* createSquares(int count)
{
    int* values = new int[count];
    for (int i = 0; i < count; ++i)
    {
        values[i] = (i + 1) * (i + 1);
    }
    return values; // ownership transfers to the caller
}

int main()
{
    int count = 5;
    int* squares = createSquares(count);

    std::cout << "Squares: ";
    for (int i = 0; i < count; ++i)
    {
        std::cout << squares[i] << ' ';
    }
    std::cout << '\n';

    delete[] squares; // matches new[] in createSquares
    squares = nullptr;

    if (squares == nullptr)
    {
        std::cout << "Buffer released.\n";
    }

    return 0;
}
Squares: 1 4 9 16 25 
Buffer released.

Dynamic Memory Mental Model

Picture two separate regions: one where automatic (local) variables live, managed entirely by scope, and one where dynamically allocated objects live, managed entirely by explicit new and delete calls.

A pointer variable itself lives wherever it was declared (often automatic storage) and is cleaned up by scope like any other local. What it points to, if it was obtained from new, lives in dynamic storage and follows entirely different rules — it is freed only by an explicit matching delete.

Whenever you see a pointer that came from new, ask two questions: which delete form matches this allocation, and which part of the program is responsible for calling it exactly once. Those two questions are the whole discipline this topic teaches.

Automatic storage (scope-managed)      Dynamic storage (explicitly managed)
+----------------------+                +---------------------------+
| main()'s locals       |                |  new int(5)   -> [ 5 ]    |
|   int count = 5;      |    ptr  ------>|  new int[5]() -> [0,0,0,0,0]|
|   int* ptr;           |----------------|                           |
+----------------------+                +---------------------------+
     freed automatically                    freed only by delete / delete[]
     when main() returns                    (or leaked if never called)

Quick Reference

The core new/delete vocabulary this topic covers.

Operation                    | Example                          | Notes
Allocate single object       | int* p = new int(5);             | Release with delete
Allocate array                | int* a = new int[n];             | Release with delete[]
Value-initialize array        | int* a = new int[n]();           | All elements zeroed
List-initialize array (C++11) | int* a = new int[3]{1,2,3};      | Remaining elements value-initialized
Release single object         | delete p;                        | Calls destructor for class types
Release array                 | delete[] a;                      | Calls destructor on every element
Delete a null pointer         | delete p; // p == nullptr        | Guaranteed no-op
Non-throwing allocation        | new (std::nothrow) int[n]        | Returns nullptr on failure instead of throwing
Allocation failure exception  | std::bad_alloc                   | Thrown by default new/new[] on failure
After deleting                | p = nullptr;                     | Avoids leaving p dangling

Key Takeaways

new requests dynamic storage and returns a pointer to it; delete releases storage that new returned, running the destructor first for a class type.

A dynamically allocated object's lifetime is controlled entirely by explicit new and delete, not by any enclosing scope.

new[] must be released with delete[], and single-object new must be released with plain delete — mixing the two forms is undefined behavior.

delete on a genuinely null pointer is a guaranteed no-op; delete on an already-deleted pointer (double deletion) is undefined behavior.

A pointer left dangling after delete still holds an address, but the object it referred to no longer exists; dereferencing it is undefined behavior.

By default, failed allocation throws std::bad_alloc rather than returning a null pointer; the std::nothrow form is the explicit, checked exception to that rule.

Losing every pointer to a dynamically allocated object without deleting it is a memory leak, and an exception thrown between a successful new and its matching delete is a common, easy-to-miss way one happens.

Ownership is a design responsibility, not a language guarantee — know, for every dynamically allocated object, exactly which part of the program is responsible for its one matching delete.

RAII and smart pointers exist to make this matching automatic and exception-safe; modern C++ reaches for manual new/delete only when nothing higher-level fits.

What is the practical difference between the lifetime of a local int and the lifetime of an int created with new?

A local int's lifetime ends automatically when its enclosing scope ends. An int created with new has dynamic storage duration: it keeps existing after the scope that created it ends, and its lifetime ends only when a matching delete runs (or, if none ever runs, it leaks for the remainder of the program's run).

Why does delete[] need to exist as a separate operator instead of just reusing delete for arrays?

new[] and delete[] are a matched pair with bookkeeping (such as the number of elements, needed so every element's destructor can run) that plain new/delete do not carry. Using delete on a pointer from new[], or delete[] on a pointer from plain new, is undefined behavior because the release mechanism does not match how the allocation was made.

A function successfully allocates memory with new, and later in the same function, before the matching delete, another call throws an exception. What happens to the allocated memory, and why?

The exception propagates out of the function immediately, skipping any code after the point it was thrown — including the delete that would have released the memory. The object is leaked, because raw new/delete offers no automatic cleanup on an exceptional exit path. This is exactly the problem RAII and smart pointers solve.

Why is delete on a null pointer safe, but delete on an uninitialized pointer is not?

The standard specifically guarantees that delete applied to a pointer whose value is genuinely nullptr does nothing. An uninitialized pointer variable holds an indeterminate value, not a guaranteed null — it may look like an address, and deleting it is undefined behavior, not a safe no-op.

Two pointer variables, ptrA and ptrB, are both set to the same address returned by a single new int(5). What must the program ensure about calling delete on ptrA and ptrB, and what goes wrong if it doesn't?

delete must be called exactly once for that object, through only one of the two pointers — not through both. If both ptrA and ptrB are deleted, the second delete is a double deletion, which is undefined behavior. If neither is ever deleted, the object leaks. Clear ownership (deciding which pointer, if either, is responsible for the delete) is what prevents both outcomes.

What does this program print, and why doesn't setting ptr to nullptr also affect alias?

It prints 1 0. ptr = nullptr; only changes the ptr variable itself, not the object it used to point to and not any other pointer variable that happens to hold a copy of the same old address. So ptr becomes null (prints 1 for true), but alias still holds the old, now-dangling address and is not null (prints 0 for false) — even though the object alias refers to has already been deleted, making *alias undefined behavior if it were dereferenced.

Write a function named makeRange that takes an int n and returns a pointer to a dynamically allocated array of n ints containing the values 0 through n-1. Write a main that calls it, prints the values, and releases the memory correctly.

makeRange allocates an array with new[] and fills it with 0..n-1, then returns ownership to the caller. Because the allocation used new[], the caller must release it with delete[] (not plain delete) — using the wrong form would be undefined behavior. Nulling range out afterward is good practice, though not strictly required here since the program ends immediately after. Output: 0 1 2 3 4 5

How many times does "destroyed" print, and why?

It prints "destroyed " three times, once for each of the three Loud elements. delete[] on an array of class-type objects calls the destructor for every element in the array before releasing the underlying storage — that per-element destructor call is exactly why delete[] must be used instead of plain delete for anything allocated with new[].

Write a function named safeDivide that takes two ints, allocates a single int on the heap holding their quotient (assume the divisor is never zero), and returns the pointer. Then write a main that calls it, prints the result through the pointer, and releases the memory with the correct delete form.

safeDivide allocates one int with plain new (not new[]), so the caller must release it with plain delete, not delete[] — using delete[] here would be undefined behavior since there is no array bookkeeping to match against. main prints the pointed-to value (20 / 4 = 5) before releasing it. Output: 5

int* ptr = new int(10);
int* alias = ptr;
delete ptr;
ptr = nullptr;
std::cout << (ptr == nullptr) << ' ' << (alias == nullptr);
struct Loud
{
    ~Loud() { std::cout << "destroyed "; }
};

int main()
{
    Loud* arr = new Loud[3];
    delete[] arr;
    return 0;
}