HOWTO · C++

How to Declare 2D Array Using new in C++

This article explains how to declare a 2D array using the new operator in C++. Learn the step-by-step process, including memory management and accessing elements. Perfect for beginners and experienced programmers alike, this guide provides clear examples and detailed explanations to enhance your C++ skills.

On this page

A runtime-sized matrix in C++ can be represented in several ways, but new does not create a built-in multidimensional array. You choose how the elements are laid out and how their storage is owned. The examples below use two rows and three columns so you can see how indexing maps to the stored values. The row-allocation example was run successfully in the supplied execution environment.

How a dynamic two-dimensional array is represented

A built-in declaration such as int data[2][3] has a fixed size. For dimensions chosen at runtime, common options are an int ** whose rows are allocated separately, one flat allocation with a calculated index, or nested std::vector objects. These representations all support row and column access, but their storage layout and ownership differ. C++ does not check that your row and column indices are within their valid ranges.

Allocate rows separately for arr[i][j] access

An int ** points to an array of row pointers; it does not point to one rectangular block of elements. Allocate the pointer array first, then allocate each row. This preserves the familiar matrix[i][j] notation and also permits rows of different lengths, provided you track each row’s length yourself.

Run this example:

#include <iostream>

int main() {
    const int rows = 2;
    const int columns = 3;
    int **matrix = new int*[rows];

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

    for (int i = 0; i < rows; ++i) {
        for (int j = 0; j < columns; ++j) {
            matrix[i][j] = i * columns + j + 1;
            std::cout << matrix[i][j] << (j + 1 == columns ? '\n' : ' ');
        }
    }

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

Each new[] must be paired with delete[]: release the rows, then the pointer array. If allocation fails partway through construction, a raw-pointer implementation needs additional cleanup logic; for production code, prefer an owning container or smart pointers to avoid manual lifetime management.

Store elements in one contiguous allocation

A single new[] allocation stores every element together. For row-major layout, the element at row i and column j is at offset i * columns + j. The dimensions must be retained because the pointer alone does not record the number of rows or columns. Check that the multiplication used to compute the allocation size is representable for the types and dimensions in your program.

For example, after allocating rows * columns elements in int *data, access an element with data[i * columns + j]. Release the allocation once with delete[] data. This layout makes the storage contiguous, but it does not provide data[i][j] indexing directly; a helper function or a container abstraction can hide the offset calculation. No performance ranking follows from layout alone, so measure the complete workload if speed is a deciding factor.

Use std::vector for automatic memory management

For ordinary application code, nested vectors usually provide the clearest ownership. The inner vectors own each row, and their destructors release the elements automatically when the outer vector leaves scope. The following initialization creates two rows with three elements apiece:

std::vector<std::vector<int>> matrix(2, std::vector<int>(3, 0));

Use matrix[i][j] to access an element, or range-based loops to visit rows and values. Each row can be resized independently, which is useful for jagged data. Like raw arrays, vector indexing with operator[] does not perform bounds checks; use at() when checked access is appropriate. A nested vector also does not promise that all rows occupy one contiguous block.

Choose an approach and manage its limits

Choose separately allocated rows when rows may differ in length or the pointer-to-pointer form is required. Choose one flat allocation when a single owned block and explicit row-major mapping fit your interface. Prefer std::vector when automatic cleanup, resizing, and straightforward ownership matter; it is generally the safer starting point than owning raw new allocations.

A fixed-size built-in array is simple when dimensions are known at compile time, for example int matrix[2][3]. For jagged rows with manual allocation, store each row’s length and use that length for access and cleanup. With every representation, validate dimensions before indexing: an out-of-range access through raw pointers or operator[] has undefined behavior. Pair each array allocation made with new[] with its matching delete[], and avoid copying raw owning pointers unless ownership is deliberately transferred or shared.