NOTE:

These are a little sketchy...I'll fill in details 'soon'.

A Tale of Two Approaches

There are two basic ways to approach the creation of 2D arrays on the heap: tedious and annoying. That's not to say you shouldn't do so, just that you need to examine each to decide which is best for you/your application.

Tedious

This approach is the most 'obvious'. It uses an array of pointers where each element points to an array of data:

   arr1
    +---+
    | , |
    +-|-+    +---+---+---+---+
      |   +->|   |   |   |   |
      |   |  +---+---+---+---+
      |   |
      |   |     +---+---+---+---+
      |   | +-->|   |   |   |   |
     \ /  | |   +---+---+---+---+
      ,   | |
    +---+ | |     +---+---+---+---+
    | , |-+ |  +->|   |   |   |   |
    +---+   |  |  +---+---+---+---+
    | , |---+  |
    +---+      |    
    | , |------+    
    +---+           +---+---+---+---+
    | , |---------->|   |   |   |   |          
    +---+           +---+---+---+---+
    | , |--+
    +---+  |  +---+---+---+---+
           +->|   |   |   |   |
              +---+---+---+---+

In code terms, we'd have:

    type **arr1 = nullptr;
    arr1 = new(nothrow) type* [ROWS];
    if (arr1 != nullptr)
    {
        r = 0;
        arr1[r] = new(nothrow) type [COLS];
        while (arr[r] != nullptr              // we succeeded in last allocation
            && r+1 < ROWS)             // and there is room for the ~next~ allocation
        {
            arr1[++r] = new(nothrow) type [COLS];   // allocate into the next spot
        }
        num_rows = r+1;  // adjust for 0-start
    }
    else
    {
        num_rows = 0;    // we got no space at all!
    }

Note how the allocation stops when a row fails to allocate or when we've allocated all the rows. But we keep track of how many rows were successfully allocated for later with the num_rows helper variable.

Once allocated, we can use this array normally:

    for (r = 0; r < num_rows; r++)
    {
        for (c = 0; c < COLS; c++)
        {
            // use arr1[r][c] like normal
        }
    }

But, care must be taken when we're done to avoid leaving allocated but inaccessible fragments of memory:

    for (r = 0; r < num_rows; r++)  // delete each row
    {
        delete [] arr1[r];
        arr1[r] = nullptr;
    }
    delete [] arr1;                 // then delete the outer array
    arr1 = nullptr;

Annoying

The other way, less obvious to the novice, but less 2D friendly, is to map 2D coordinates into a 1D array. The standard formula is:

    arr2D[r][c] == arr1D[r*COLS+c]

This is how statically allocated 2D arrays are actually stored, BTW. Instead of our traditional idea of:

       C    O    L    S
       0    1    2    3
    +----+----+----+----+
   0|    |    |    |    |
 R  +----+----+----+----+
   1|    |    |    |    |
 O  +----+----+----+----+
   2|    |    |    |    |
 W  +----+----+----+----+
   3|    |    |    |    |
 S  +----+----+----+----+
   4|    |    |    |    |
    +----+----+----+----+

A 2D array is actually stored like so:

                         R                   O                  W                   S
       C    O    L    S    C    O    L    S    C    O    L    S    C    O    L    S    C    O    L    S
    +----+----+----+----+----+----+----+----+----+----+----+----+----+----+----+----+----+----+----+----+
    |    |    |    |    |    |    |    |    |    |    |    |    |    |    |    |    |    |    |    |    |
    +----+----+----+----+----+----+----+----+----+----+----+----+----+----+----+----+----+----+----+----+
     0,0  0,1  0,2  0,3  1,0  1,1  1,2  1,3  2,0  2,1  2,2  2,3  3,0  3,1  3,2  3,3  4,0  4,1  4,2  4,3

Notice how, for instance, the first element of the second row ([1][0]) is the fifth element of the linearized (1D) diagram: 1*COLS+0. And similarly for the third element of the fifth row ([4][2]): 4*COLS+2.

(Isn't it cool how this scheme takes into account that each row is COLS elements long and takes advantage of our 0-based indices?)

With this approach, you can allocate your array in one fell swoop:

    type * arr2 = nullptr;
    arr2 = new(nothrow) type [ROWS*COLS];

(Note that there is no chance to have lost desired rows — we get the whole array or nothing.)

And use it:

    for (r = 0; r < ROWS; r++)
    {
        for (c = 0; c < COLS; c++)
        {
            // use arr2[r*COLS+c]
        }
    }

And the deallocation is simple, too:

    delete [] arr2;
    arr2 = nullptr;

The annoying thing is the need to map the coordinates during use. Some folks try to mask it with a concoction such as:

    inline type & map2D(type * arr, size_t row, size_t col, size_t COLS)
    {
        return arr[row*COLS+col];
    }

But this is just a bit icky — it still doesn't hide enough details. If you are going to use this approach, the coolest way would have to be to embed the array in a class and overload operator() to do your 2D indexing:

    class Arr2D
    {
        type * arr2;
        size_t ROWS, COLS;
        void alloc(void) { arr2 = new(nothrow) type [ROWS*COLS]; return; }
    public:
        Arr2D(void) : arr2(nullptr), ROWS(0), COLS(0) { }
        Arr2D(size_t R, size_t C) : arr2(nullptr), ROWS(R), COLS(C) { alloc(); }
            // copy constructor left to reader!!!
            // operator= left to reader!!!
        ~Arr2D(void) { delete [] arr2; }
        type & operator() (size_t r, size_t c) { return arr2[r*COLS+c]; }
            // *ALL* error checking left to reader!!!
    };

Now you can do something like this:

    Arr2D ar(ROWS,COLS);
    
    for (r = 0; r < ROWS; r++)
    {
        for (c = 0; c < COLS; c++)
        {
            // use ar(r,c)
        }
    }

Still not completely normal, but you've hidden the allocation details, the deallocation details, and, of course, the mapping details inside the class.

Another possibility would be to use a helper position class

Other Properties to Consider

A Merger with a Happy Ending?

But, for those out for the ultimate best of both worlds (and willing to pay the costs of both worlds), try out this variation. In addition to the 1D mapping, we make an array of pointers to the beginning of each row within the linearized space:

            0,0  0,1  0,2  0,3  1,0  1,1  1,2  1,3  2,0  2,1  2,2  2,3  3,0  3,1  3,2  3,3  4,0  4,1  4,2  4,3
           +----+----+----+----+----+----+----+----+----+----+----+----+----+----+----+----+----+----+----+----+
           |    |    |    |    |    |    |    |    |    |    |    |    |    |    |    |    |    |    |    |    |
           +----+----+----+----+----+----+----+----+----+----+----+----+----+----+----+----+----+----+----+----+
             '                   '                   '                   '                   '
            / \                 / \                 / \                 / \                 / \
   +---+     |                   |                   |                   |                   |
   | , |-----+                   |                   |                   |                   |
   +---+                         |                   |                   |                   |
   | , |-------------------------+                   |                   |                   |
   +---+                                             |                   |                   |
   | , |---------------------------------------------+                   |                   |
   +---+                                                                 |                   |
   | , |-----------------------------------------------------------------+                   |
   +---+                                                                                     |
   | , |-------------------------------------------------------------------------------------+
   +---+ 

In code this might look something like this:

    // declaration
    type * data = nullptr, ** arr2D = nullptr;

    // allocation
    data = new(nothrow) type [ROWS*COLS];    // allocate linearized space
    if (data != nullptr)
    {
        arr2D = new(nothrow) type* [ROWS];   // allocate row pointers
    }
    if (arr2D != nullptr)
    {
        for (size_t r = 0; r != ROWS; r++)    // aim row pointers
        {
            arr2D[r] = data + r*COLS;
        }

        // access (in a pair of for loops)
        arr2D[r][c]
    }

    // deallocation
    delete [] arr2D;
    delete [] data;

Although we have the potential of failing to allocate the row-start pointers array separately from the data's allocation, this method can give nicer access patterns for general use. (You can even place a conditional in your class' accessor function to take advantage of the [][] when arr2D allocated or perform the mapping calculation when it failed. *bounce*)