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:
-
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. -
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.
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:
-
int** ary = new int*[rows];- Allocate space for
rowspointers. arynow points to an array that can hold row addresses.
- Allocate space for
-
ary[i] = new int[cols];- For each row, allocate an array of
colsintegers. - Now each
ary[i]points to one row.
- For each row, allocate an array of
-
ary[0][0] = 10;- Go to row
0. - Then go to column
0inside that row. - Store
10.
- Go to row
-
ary[0][1] = 20;- Same row, next column.
-
ary[1][2] = 30;- Go to row
1. - Then column
2.
- Go to row
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.
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 anint**- 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
deletefor a single object allocated withnew - use
delete[]for arrays allocated withnew[]
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
| Approach | Syntax | Memory layout | Pros | Cons |
|---|---|---|---|---|
| Fixed 2D array | int a[r][c] | Contiguous | Simple, fast indexing | Size usually must be known in advance |
| Pointer-to-pointer | int** a with row allocations | Separate rows | Flexible row allocation | More complex cleanup, not truly contiguous |
| Flat dynamic array | int* a = new int[r*c] | Contiguous | Efficient, simple allocation | Manual index math |
std::vector<std::vector<int>> | vector<vector<int>> | Usually separate rows | Safe, readable | May be less cache-friendly |
Flat std::vector<int> | vector<int> | Contiguous | Safe and efficient | Manual index math |
int** vs true 2D array
| Feature | int** | int[row][col] or new int[rows][cols] |
|---|---|---|
| Same type? | No | No |
Supports a[i][j] | Yes, if rows are allocated | Yes |
| Contiguous memory | Usually no | Yes |
| Requires per-row allocation | Yes | No |
| Easy cleanup | No | More straightforward |
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.
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
Keep learning
Related questions
Advantages of Brace Initialization in C++
Learn why C++ brace initialization is often clearer and safer than other object initialization styles, with examples and common pitfalls.
Basic Rules and Idioms for Operator Overloading in C++
Learn the core rules, syntax, and common idioms for operator overloading in C++, including member vs non-member operators.
C++ Aggregates, Trivial Types, Trivially Copyable Types, and PODs Explained
Learn what aggregates, trivial types, trivially copyable types, and PODs mean in C++, how they differ, and why they matter.