Question

How to Declare a 2D Array with new in C++

cpparraysmultidimensional-arraydynamic-allocation

Question

How can I declare a 2D array dynamically in C++ using new?

For a one-dimensional array, I would write:

int* ary = new int[size];

But this does not work the way I expect:

int** ary = new int[sizeY][sizeX];

It either fails to compile or does not behave like a normal 2D array such as:

int ary[sizeY][sizeX];

What is the correct way to allocate a 2D array with new, and how is it different from a regular stack-allocated 2D array?

Short Answer

By the end of this page, you will understand how dynamic 2D arrays work in C++, why int** is not the same as int[sizeY][sizeX], how to allocate and free memory correctly with new, and when to prefer safer alternatives like std::vector.

Concept

In C++, a true 2D array and a pointer-to-pointer are not the same thing.

A fixed 2D array like this:

int ary[rows][cols];

creates one contiguous block of memory containing rows * cols integers. The compiler knows the column size and can calculate where each element lives.

When you use new, you are doing dynamic allocation, which means memory is created at runtime instead of being placed on the stack.

There are two common ways people try to build a 2D array dynamically:

  1. Array of pointers

    int** ary = new int*[rows];
    for (int i = 0; i < rows; i++) {
        ary[i] = new int[cols];
    }
    

    This gives you rows that can be accessed with ary[i][j], but the rows are allocated separately.

  2. Single contiguous block

    int* data = new int[rows * cols];
    

    Then you access elements manually using an index formula:

    data[i * cols + j]
    

Why does this matter?

  • A real 2D array stores data contiguously.
  • int** usually means "pointer to pointer," not "2D array."
  • Separate row allocations can be less cache-friendly and require more cleanup.
  • Dynamic allocation is useful when sizes are only known at runtime.

In modern C++, developers usually prefer std::vector over raw new because it manages memory safely and automatically.

Mental Model

Think of a fixed 2D array as a spreadsheet printed on a single sheet of paper.

  • Every cell is laid out in one continuous grid.
  • The compiler knows exactly how wide each row is.

Now think of int** as a folder of separate sheets.

  • The folder holds pointers to each row.
  • Each row can be allocated separately.
  • The rows might not be next to each other in memory.

Both can look similar when you write ary[i][j], but internally they are organized differently.

A contiguous 2D array is one big rectangle. A pointer-to-pointer is a list of smaller rectangles.

Take Quiz

Syntax and Examples

1. Dynamic 2D array using an array of pointers

int rows = 3;
int cols = 4;

int** ary = new int*[rows];
for (int i = 0; i < rows; i++) {
    ary[i] = new int[cols];
}

ary[1][2] = 42;

To free the memory:

for (int i = 0; i < rows; i++) {
    delete[] ary[i];
}
delete[] ary;

This works, but each row is allocated separately.

2. Dynamic 2D array using one contiguous block

int rows = 3;
int cols = 4;

int* ary = new int[rows * cols];

ary[1 * cols + 2] = 42;

To free it:

delete[] ary;

This layout is closer to a normal 2D array because all elements are stored together.

3. Modern C++ approach with std::vector

#include <vector>

int rows = 3;
int cols = 4;
std::vector<std::vector<int>> ary(rows, std::vector<int>(cols));

ary[1][2] = 42;

Or as one contiguous block:

#include <vector>

int rows = 3;
int cols = 4;
std::vector<int> ary(rows * cols);

ary[1 * cols + 2] = 42;

std::vector is usually the safest and simplest option.

4. Dynamic array with true 2D indexing using new

If the column count is known at compile time, you can do this:

int rows = 3;
const int cols = 4;

int (*ary)[cols] = new int[rows][cols];
ary[1][2] = 42;

delete[] ary;

Notice the type:

int (*ary)[cols]

This means "pointer to an array of cols integers." It is not int**.

Step by Step Execution

Consider this example using an array of pointers:

int rows = 2;
int cols = 3;

int** ary = new int*[rows];
for (int i = 0; i < rows; i++) {
    ary[i] = new int[cols];
}

ary[0][0] = 10;
ary[0][1] = 20;
ary[1][2] = 30;

Step by step:

  1. int** ary = new int*[rows];

    • Allocate space for rows pointers.
    • ary now points to an array that can hold row addresses.
  2. ary[i] = new int[cols];

    • For each row, allocate an array of cols integers.
    • Now each ary[i] points to one row.
  3. ary[0][0] = 10;

    • Go to row 0.
    • Then go to column 0 inside that row.
    • Store 10.
  4. ary[0][1] = 20;

    • Same row, next column.
  5. ary[1][2] = 30;

    • Go to row 1.
    • Then column 2.

Memory picture:

ary ----> [ ptr ][ ptr ]
            |      |
            v      v
          [int int int]   [int int int]

Cleanup must happen in reverse:

for (int i = 0; i < rows; i++) {
    delete[] ary[i];
}
delete[] ary;

Why reverse?

  • First delete each row array.
  • Then delete the array of row pointers.

If you skip the first step, you leak memory.

Real World Use Cases

Dynamic 2D structures are useful when dimensions are not known until runtime.

Common examples

  • Game boards
    • Tic-tac-toe, chess, tile maps, pathfinding grids
  • Image processing
    • Pixels arranged by row and column
  • Spreadsheets or tables
    • Rows and columns loaded from a file or database
  • Scientific computing
    • Matrices, heatmaps, simulation grids
  • Dynamic input data
    • A grid size entered by the user

Example: reading a map size from input

int rows, cols;
std::cin >> rows >> cols;

int** grid = new int*[rows];
for (int i = 0; i < rows; i++) {
    grid[i] = new int[cols];
}

This is useful when rows and cols are not compile-time constants.

Take Quiz

Real Codebase Usage

In real C++ projects, raw new for 2D arrays is much less common than it used to be.

What developers usually do

Use std::vector

std::vector<std::vector<int>> grid(rows, std::vector<int>(cols, 0));

This is common because:

  • memory is cleaned up automatically
  • resizing is easier
  • code is safer and clearer

Use one flat container for performance

std::vector<int> grid(rows * cols, 0);

auto at = [&](int r, int c) -> int& {
    return grid[r * cols + c];
};

This pattern is common in:

  • game engines
  • numeric code
  • image buffers
  • performance-sensitive code

Use guard clauses for bounds

if (r < 0 || r >= rows || c < 0 || c >= cols) {
    return;
}

Validate dimensions before allocation

if (rows <= 0 || cols <= 0) {
    throw std::invalid_argument("Invalid grid size");
}

Avoid manual memory management when possible

Raw pointers increase the chance of:

  • memory leaks
  • double deletes
  • partial allocation bugs
  • exception-safety issues

That is why modern codebases prefer RAII containers such as std::vector, std::array, or smart pointers.

Common Mistakes

1. Assuming int** is the same as a 2D array

Broken code:

int** ary = new int[sizeY][sizeX];

Why it is wrong:

  • new int[sizeY][sizeX] does not produce an int**
  • a pointer-to-pointer and a 2D array type are different

2. Forgetting to allocate each row

Broken code:

int** ary = new int*[rows];
ary[0][0] = 5;

Problem:

  • ary[0] does not point to a valid row yet

Fix:

for (int i = 0; i < rows; i++) {
    ary[i] = new int[cols];
}

3. Forgetting to delete every row

Broken cleanup:

delete[] ary;

Problem:

  • only deletes the pointer array
  • row arrays are leaked

Correct cleanup:

for (int i = 0; i < rows; i++) {
    delete[] ary[i];
}
delete[] ary;

4. Mixing delete and delete[]

Broken code:

int* ary = new int[10];
delete ary;

Fix:

delete[] ary;

Rule:

  • use delete for a single object allocated with new
  • use delete[] for arrays allocated with new[]

5. Using uninitialized values

int* ary = new int[rows * cols];

These integers are not automatically zeroed.

If you need zeros:

int* ary = new int[rows * cols]();

Or with vector:

std::vector<int> ary(rows * cols, 0);

6. Ignoring safer alternatives

If you do not specifically need raw pointers, prefer:

std::vector<std::vector<int>>

or

std::vector<int>

Comparisons

ApproachSyntaxMemory layoutProsCons
Fixed 2D arrayint a[r][c]ContiguousSimple, fast indexingSize usually must be known in advance
Pointer-to-pointerint** a with row allocationsSeparate rowsFlexible row allocationMore complex cleanup, not truly contiguous
Flat dynamic arrayint* a = new int[r*c]ContiguousEfficient, simple allocationManual index math
std::vector<std::vector<int>>vector<vector<int>>Usually separate rowsSafe, readableMay be less cache-friendly
Flat std::vector<int>vector<int>ContiguousSafe and efficientManual index math

int** vs true 2D array

Featureint**int[row][col] or new int[rows][cols]
Same type?NoNo
Supports a[i][j]Yes, if rows are allocatedYes
Contiguous memoryUsually noYes
Requires per-row allocationYesNo
Easy cleanupNoMore straightforward
Take Quiz

Cheat Sheet

Quick reference

1D dynamic array

int* a = new int[size];
delete[] a;

2D dynamic array with separate rows

int** a = new int*[rows];
for (int i = 0; i < rows; i++) {
    a[i] = new int[cols];
}

// use a[i][j]

for (int i = 0; i < rows; i++) {
    delete[] a[i];
}
delete[] a;

2D dynamic array as one flat block

int* a = new int[rows * cols];

// element at row r, col c:
a[r * cols + c]

delete[] a;

True dynamic 2D array when column size is known

const int cols = 4;
int (*a)[cols] = new int[rows][cols];
a[1][2] = 7;
delete[] a;

Key rule

int** != int[][cols]

Prefer modern C++

std::vector<std::vector<int>> grid(rows, std::vector<int>(cols));

or

std::vector<int> grid(rows * cols);

FAQ

Why does int** ary = new int[rows][cols]; not compile?

Because new int[rows][cols] does not return an int**. It returns a pointer to an array type, not a pointer to pointer.

Is int** a real 2D array in C++?

Not exactly. It is usually an array of pointers, where each pointer refers to a row allocated separately.

Which dynamic 2D approach is best?

For most code, use std::vector. For performance and contiguous memory, use a flat std::vector<int> or flat dynamic array.

Can I use ary[i][j] with dynamic allocation?

Yes, if you allocate rows separately with int**, or if you use a pointer-to-array type such as int (*ary)[cols].

Do I need to free memory manually when using new?

Yes. Anything allocated with new or new[] must be released with delete or delete[].

Why do many developers avoid raw new?

Because manual memory management is error-prone. std::vector and smart pointers are safer and easier to maintain.

Is a flat array faster than int**?

Often yes, because contiguous memory is usually more cache-friendly.

Can the column count be variable in int (*ary)[cols] = new int[rows][cols];?

It depends on the compiler and language rules you are using. In modern C++, std::vector is usually the clearer and more portable choice when sizes are only known at runtime.

Take Quiz

Mini Project

Description

Build a small grid program that creates a dynamic 2D structure, fills it with values, prints it, and then cleans up memory correctly. This demonstrates how row and column indexing works and reinforces the difference between a pointer-to-pointer and a contiguous block.

Goal

Create and use a runtime-sized numeric grid in C++ and free all allocated memory correctly.

Requirements

Requirement 1 Requirement 2 Requirement 3

Take Quiz

Keep learning

Related questions

View more