Last Updated:

Multidimensional Arrays in C++ - How to

The first article described the methods of working with the simplest type of arrays - a one-dimensional (linear) array. In this second article, we will look at multidimensional arrays. Basically, we will talk about two-dimensional arrays. But these examples are easily extrapolated to arrays of any dimension. As in the first article, only C/C++ style arrays will be considered, without using STL features.

This article assumes the reader's basic knowledge of one-dimensional and multidimensional arrays, pointers, and address arithmetic. You can learn this knowledge from any C/C++ tutorial.

Classics of the genre

If we open the classic work "Programming Language C" by Brian Kernigan and Dennis Ritchie, we read that "In the C language, it is possible to work with multidimensional rectangular arrays, although in practice they are used much less often than arrays of pointers." C++ has almost completely inherited the work with multidimensional arrays of its predecessor.

Defining Automatic Multidimensional Arrays

In this section, I will sometimes use the term "matrix" as a synonym for the term "two-dimensional array". In C/C++, a rectangular two-dimensional array of numbers does implement the mathematical concept of a matrix. However, in general, a two-dimensional array is a much broader concept than a matrix, since it can be neither rectangular nor numeric.

The definition of automatic multidimensional arrays is almost identical to the definition of one-dimensional arrays (as discussed in the first article), except that instead of one size, several can be specified:

const unsigned int DIM1 = 3;
const unsigned int DIM2 = 5;

int ary[DIM1][DIM2];

This example defines a two-dimensional array of 3 lines with 5 type values per row. A total of 15 values of type .intint

const unsigned int DIM1 = 3;
const unsigned int DIM2 = 5;
const unsigned int DIM3 = 2;

int ary[DIM1][DIM2][DIM3];

In the second example, a three-dimensional array is defined containing 3 matrices, each of which consists of 5 rows of 2 type values in each row.int

It is understood that the type of data contained in a multidimensional array can be anything.

In the following presentation, the term "C-array" will be used for such multidimensional arrays to distinguish them from arrays of other kinds.

Initialization

In static (compile-time) initialization, the values of the C array are listed in the order in which the sizes (indexes) in the array definition are specified. Each level (index), except for the youngest, multidimensional array is enclosed in its own pair of curly braces. The values of the lowest index are indicated separated by commas:

const unsigned int DIM1 = 3;
const unsigned int DIM2 = 5;

int ary[DIM1][DIM2] = {
    { 1, 2, 3, 4, 5 },
    { 2, 4, 6, 8, 10 },
    { 3, 6, 9, 12, 15 }
};

The example shows the static initialization of a rectangular array. The entire list of initializing values is enclosed in curly braces. The values for each of the 3 rows are enclosed in its own pair of curly braces, the values for each of the 5 columns for each row are listed separated by commas.

If you have an initializer, the leftmost size of the array can be omitted. In this case, the compiler will determine this size based on the initialization list.

Populate an array with values

A multidimensional array is populated with values using nested loops. And, as a rule, the number of cycles coincides with the dimension of the array:

#include <iostream>
#include <iomanip>

using namespace std;

const unsigned int DIM1 = 3;
const unsigned int DIM2 = 5;

int ary[DIM1][DIM2];

int main() {

    for (int i = 0; i < DIM1; i++) {
        for (int j = 0; j < DIM2; j++) {
            ary[i][j] = (i + 1) * 10 + (j + 1);
        }
    }

    // ...

In this example, each element of the array is assigned a value whose first digit indicates the row number and the second digit indicates the column number for that value (numbering from 1).

Output array values to the console

As a continuation of the previous example, we can write:

    for (int i = 0; i < DIM1; i++) {
        for (int j = 0; j < DIM2; j++) {
            cout << setw(4) << ary[i][j];
        }
        cout << endl;
    }

    return 0;
}

As a result, we get the following output to the console:

  11  12  13  14  15
  21  22  23  24  25
  31  32  33  34  35

For a three-dimensional array, you can write code that uses the same techniques:

#include <iostream>
#include <iomanip>

using namespace std;


const unsigned int DIM1 = 3;
const unsigned int DIM2 = 5;
const unsigned int DIM3 = 2;

int ary[DIM1][DIM2][DIM3];

int main() {

    for (int i = 0; i < DIM1; i++) {
        for (int j = 0; j < DIM2; j++) {
            for (int k = 0; k < DIM3; k++) {
                ary[i][j][k] = (i + 1) * 100 + (j + 1) * 10 + (k + 1);
                cout << setw(4) << ary[i][j][k];
            }
            cout << endl;
        }
        cout << endl;
    }

    return 0;
}

Here, assigning a value to an array element and output to the console occur in the same group of loops.

Memory Location

For a multidimensional C array, a single block of memory of the required size is allocated: .размер_массива1 * размер_массива2 * ... * размер_массиваN * sizeof(тип_элемента_массива)

Values are arranged sequentially. The leftmost index changes the slowest. That is, for a three-dimensional array, the values for the first (index 0) matrix are first arranged, then for the second, etc. The values for matrices are arranged line by line (cf. with the static initialization of the array above).

The name (identifier) of a multidimensional C array is a pointer to the first element of the array (as for one-dimensional arrays)

If the code from the last example changes slightly:

    int cnt = 1;

    for (int i = 0; i < DIM1; i++) {
        for (int j = 0; j < DIM2; j++) {
            for (int k = 0; k < DIM3; k++) {
                ary[i][j][k] = cnt++;
                cout << setw(4) << ary[i][j][k];
            }
            cout << endl;
        }
        cout << endl;
    }

    return 0;    // 

if you set a breakpoint on and look under the debugger, the memory allocated under the variable , you will see that the values located in memory increase sequentially:returnary

0x00A9F218  01 00 00 00 02 00 00 00 03 00 00 00 04 00 00 00  ................
0x00A9F228  05 00 00 00 06 00 00 00 07 00 00 00 08 00 00 00  ................
0x00A9F238  09 00 00 00 0a 00 00 00 0b 00 00 00 0c 00 00 00  ................
0x00A9F248  0d 00 00 00 0e 00 00 00 0f 00 00 00 10 00 00 00  ................
0x00A9F258  11 00 00 00 12 00 00 00 13 00 00 00 14 00 00 00  ................
0x00A9F268  15 00 00 00 16 00 00 00 17 00 00 00 18 00 00 00  ................
0x00A9F278  19 00 00 00 1a 00 00 00 1b 00 00 00 1c 00 00 00  ................
0x00A9F288  1d 00 00 00 1e 00 00 00 00 00 00 00 00 00 00 00  ................

Khaki

№1

Since all the values of a multidimensional C array are arranged sequentially, using address arithmetic, you can make the following hack:

const unsigned int DIM1 = 3;
const unsigned int DIM2 = 5;

int ary[DIM1][DIM2];

int main() {

    for (int i = 0; i < DIM1; i++) {
        for (int j = 0; j < DIM2; j++) {
            ary[i][j] = (i + 1) * 10 + (j + 1);
        }
    }

    int *ptr = (int *)ary;    //  ;-)

    for (int i = 0; i < DIM1; i++) {
        for (int j = 0; j < DIM2; j++) {
            cout << setw(4) << *(ptr + i * DIM2 + j);
        }
        cout << endl;
    }

    return 0;
}

Or even like this:


    int *ptr = (int *)ary;

    for (int i = 0; i < DIM1 * DIM2; i++) {
        cout << setw(4) << ptr[i];
    }
    cout << endl;

    return 0;
}

In the last fragment, the values of the two-dimensional array are accessed as a one-dimensional array. A civilized solution is implemented through .union

№2

From the two examples given above, it follows that working with a two-dimensional or multidimensional array (as understood at a higher level of abstraction) can technically be organized by means of a one-dimensional array of the appropriate size:

const unsigned int DIM1 = 3;
const unsigned int DIM2 = 5;

int ary[DIM1 * DIM2];

int main() {

    for (int i = 0; i < DIM1; i++) {
        for (int j = 0; j < DIM2; j++) {
            *(ary + i * DIM2 + j) = (i + 1) * 10 + (j + 1);
        }
    }

    for (int i = 0; i < DIM1; i++) {
        for (int j = 0; j < DIM2; j++) {
            cout << setw(4) << *(ary + i * DIM2 + j);
        }
        cout << endl;
    }

    return 0;
}

This technique is quite common. Its benefit is that the array does not necessarily have to be allocated automatically. It can also be selected dynamically. But at the same time logically consider as a C-array.ary[DIM1 * DIM2]

The above code is written in the spirit of pure C. In C++, such things are usually hidden in the classroom, leaving a concise interface on the outside without any traces of address arithmetic.

Non-native twins

Now consider working with "dynamic" multidimensional arrays, i.e. with arrays for which memory is allocated dynamically.

Creating and Destroying Dynamic Multidimensional Arrays

As a rule, work with such arrays is carried out as follows:

const unsigned int DIM1 = 3;
const unsigned int DIM2 = 5;

int main() {

     int **ary; // (one)

     // creation
     ary = new int * [DIM1]; // array of pointers (2)
     for (int i = 0; i < DIM1; i++) { // (3)
         ary[i] = new int [DIM2]; // initialization of pointers
     }

     // work with an array
     for (int i = 0; i < DIM1; i++) {
         for (int j = 0; j < DIM2; j++) {
             ary[i][j] = (i + 1) * 10 + (j + 1);
         }
     }

     for (int i = 0; i < DIM1; i++) {
         for (int j = 0; j < DIM2; j++) {
             cout << setw(4) << ary[i][j];
         }
         cout << endl;
     }

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

     return 0;
}

(1) To access a two-dimensional array, a variable of type is declared a pointer to a type pointer (in this case, a pointer to a pointer to a pointer to ).aryint

(2) The variable is initialized by the operator , which allocates memory for the array of pointers to .newint

(3) In a loop, each element of the pointer array is initialized by the operator , which allocates memory to an array of type .newint

Memory is freed strictly in reverse order: first, arrays of values of type , are destroyed, and then the array of pointers is destroyed.int

Working with a dynamic multidimensional array is syntactically identical to working with a multidimensional C array.

Sample code for a three-dimensional array:

const unsigned int DIM1 = 3;
const unsigned int DIM2 = 5;
const unsigned int DIM3 = 2;

int main() {

     int *ary;

     int cnt = 1;

     // creation
     ary = new int ** [DIM1];
     for (int i = 0; i < DIM1; i++) {
         ary[i] = new int * [DIM2];
         for (int j = 0; j < DIM2; j++) {
             ary[i][j] = new int [DIM3];
         }
     }

     // work with an array
     for (int i = 0; i < DIM1; i++) {
         for (int j = 0; j < DIM2; j++) {
             for (int k = 0; k < DIM3; k++) {
                 ary[i][j][k] = cnt++;
                 cout << setw(4) << ary[i][j][k];
             }
             cout << endl;
         }
         cout << endl;
     }

     // destruction
     for (int i = 0; i < DIM1; i++) {
         for (int j = 0; j < DIM2; j++) {
             delete[] ary[i][j];
         }
         delete[] ary[i];
     }
     delete[]ary;

     return 0;
}

Where the Dog Rummaged

Working with a dynamic multidimensional array is syntactically identical to working with a multidimensional C array. (I quote the previous section.) Syntactically, yes, but there is a profound difference between these arrays that novice programmers often forget.

First, a different amount of memory is allocated to the dynamic array.

If you calculate how much memory will be allocated for a two-dimensional array from the example above, you will get: the first operator allocated memory for 3 pointers, the second operator in the loop three times allocated memory for 5 elements of the type . That is, it turned out that they allocated memory for 15 type values and for 3 values of type pointer to . For the C array, the compiler allocated memory for only 15 values of the . (All sorts of alignments and other optimizations are not taken into account!)newnewintintintint

Second, the memory allocated to the dynamic array is not continuous. Therefore, hack #1 (treating a two-dimensional array as a one-dimensional array) will not work.

Third, passing multidimensional arrays to functions and working with them will be different for dynamic arrays and C arrays.

A dynamic multidimensional array is implemented as an array of pointers to arrays, the values of which, in turn, can also be pointers to arrays. The last link in this chain will always be arrays with values of the target type.

A dynamic multidimensional array is NOT a C array.

Paradoxically, it is a fact that the closest relative for these non-native twins is hack #2, which implements work with a multidimensional array through a one-dimensional array (see Hacks). All three of the above differences are irrelevant to him.

It is worth noting that an array of pointers to arrays is a more flexible structure than a two-dimensional C array. For example, for an array of array pointers to arrays, the array sizes may be different, or some array may not exist at all. The most common example is an "array of strings", i.e. an array of pointers to arrays of type (see the next section for an example).char

One more time about precaution

From the foregoing, it follows that it is necessary to clearly distinguish multidimensional C-arrays of the form

type id[size1]...[sizeN]; 

from arrays pointers to arrays.

Sometimes the external differences are very minor. For example, a C-string is a one-dimensional array of elements of type , ending with a null byte. How do I implement an array of strings?char

You could like this:

char month[12][10] = { 
    "January", "February", "March", 
    "April", "May", "June", 
    "July", "August", "September", 
    "October", "November", "December" 
};

This is an example of defining and initializing a two-dimensional C array

Each C-line occupies exactly 10 bytes, including the final zero (consider the type to be 1 byte). Unused bytes for short strings like "May" contain "garbage" (or zeros, if the compiler has taken care of it). The entire array takes up one contiguous block of 120 bytes of memory (12 lines of 10 characters).char

char *month[12] = { 
    "January", "February", "March", 
    "April", "May", "June", 
    "July", "August", "September", 
    "October", "November", "December" 
};

And here a one-dimensional (!) array of pointers to arrays of elements of type .char

All information available through the variable , occupies 13 blocks of memory: an array of 12 pointers and 12 memory blocks, the addresses of which are stored in pointers containing C-strings with month names. And there is no guarantee that 12 blocks of memory with C-strings will be arranged in memory sequentially and in the order corresponding to the enumeration in the initializer.month

But in both cases, the character b in the string "February" will be accessed by the expression .month[1][2]

And, in conclusion, one more warning.

Because multidimensional C arrays typically take up a large amount of memory, they must be declared with extreme caution inside functions, including in . And with caution to the nth degree in recursive functions. You can easily get a stack overflow and, as a result, a program crash.main()

Multidimensional arrays when working with functions

 

Since multidimensional C arrays and multidimensional dynamic arrays are completely different data types, the approaches will be different when working with functions.

Passing to a Multidimensional C Array Function

 

A function that takes a C array as a parameter might look like this:

const unsigned int DIM1 = 3;
const unsigned int DIM2 = 5;

void func(int ary[DIM1][DIM2]) { ... }    // (1)
void func(int ary[][DIM2]) { ... }        // (2)

Form (1) is the most common.

Form (2). When passing a multidimensional C array to a function, you can omit the length of the leftmost dimension. The compiler does not need this information to calculate access to the elements of the array.

As always in C/C++, a parameter is passed to a function by value. That is, a copy of the actual parameter is available in the function. Since the name of the C-array is a pointer to its first element (i.e., the address of the first element), a copy of the address of the beginning of the array is passed to the function. Therefore, within the function, you can change the values of the elements of the array, because they are accessed through the transmitted address, but you can not change the address of the beginning of the array passed as a parameter, because this is a copy of the actual parameter.

You cannot return a multidimensional C array from a function as a result by standard means.

Passing to a Multidimensional Dynamic Array Function

 

Since a multidimensional dynamic array is implemented as a one-dimensional array of pointers, the same approaches are used when working with functions as for a one-dimensional array, described in the first article, with precision to the data types.

For example, this is the complete code of a program that demonstrates how to work with a two-dimensional dynamic array using functions.

#include <iostream>
#include <iomanip>

using namespace std;

const unsigned int DIM1 = 3;
const unsigned int DIM2 = 5;

int **array_generator(unsigned int dim1, unsigned int dim2) {
     int **ptrary = new int * [dim1];
     for (int i = 0; i < dim1; i++) {
         ptrary[i] = new int[dim2];
     }
     return ptrary;
}

void array_destroyer(int **ary, unsigned int dim1) {
     for (int i = 0; i < dim1; i++) {
         delete[] ary[i];
     }
     delete[]ary;
}

int main() {

     int **matrix;

     // array creation
     matrix = array_generator(DIM1, DIM2);

     // usage
     for (int i = 0; i < DIM1; i++) {
         for (int j = 0; j < DIM2; j++) {
             matrix[i][j] = (i + 1) * 10 + (j + 1);
             cout << setw(4) << matrix[i][j];
         }
     }

     // destruction
     array_destroyer(matrix, DIM1);
     return 0;
}

In the first article, I wrote that "Allocating memory in one function and releasing in another is a bad idea fraught with errors." Therefore, consider this example only as a demonstration of working with functions and arrays of pointers.

Although on the other hand... On the other hand, a very similar approach is ubiquitous in classes, where a resource (in this case, memory) is captured in one function (constructor) and released in another (destructor). But in the case of classes, security is provided by encapsulating critical data and maintaining a consistent state of the class instance by class methods.

An array of pointers is used in every program that can receive input from the command line (or when it is called from the operating system). One of the classic forms of the function has the form:main()

int main(int argc, char **argv) { ... }

The arguments to the function are the number of rows (the size of the array of pointers) and the array of pointers to the strings - . I.e. is an array of pointers to arrays of values of type .argcargvargvchar

Perhaps this is all I wanted to tell in this article. I hope that someone will find it useful for themselves.