Multidimensional arrays

A multidimensional array is an array that contains one or more arrays, where each array within the multidimensional array can be seen as an element of the outer array. In other words, a dd-dimensional array is an array of (d1)(d-1) dimensional arrays.

For instance, a 2D (two-dimensional) array can be visualized as an array of single-dimensional arrays.

Let’s learn how to make these multidimensional arrays in C++. For now, we’ll only focus on 2D arrays. We can easily generalize and extend the same syntax by adding further dimensions.

Creating a 2D array

A 2D array is often represented as a matrix with rows and columns. To declare a 2D array in C++, the following syntax is typically used:

DataType array_name[row_size][column_size];

For instance, consider the following example:

int TwoDArray[4][3];

This declares a 2D array named TwoDArray with 4 rows and 3 columns. We can visualize it as an array that has four elements, where each element is a 1D array of size three. In total, the array contains 12 values.

The TwoDArray array may be viewed logically as follows:

Press + to interact
2D array with four rows and three columns
2D array with four rows and three columns

Initializing a 2D array

Since a 2D array is an array of 1D arrays, we can use nested curly braces ({{...}}) to group the different levels of elements.

For instance, in the example below, we have a 2D array of 4 rows and 3 columns. We can see four elements inside the outer curly braces, which can be viewed as rows. Each row element contains three elements that can be viewed as the columns being initialized.

int TwoDArray[4][3] = { {0,1,2}, {1,6,4}, {2,9,5}, {3,9,6} };

To effectively utilize a 2D array, it is crucial to understand how to access its elements. This can be achieved by utilizing two subscript ([]) operators, where the first subscript represents the row number and the second represents the column number.

TwoDArray[r][c] ; // This will access the rth array and take the cth element within that array. 

Let’s look at an example.

#include <iostream>
using namespace std;
int main()
{
int TwoDArray[4][3] = { {0,1,2}, {1,6,4}, {2,9,5}, {3,9,6} };
for(int r = 0; r < 4; r++) // for each row
{
for(int c = 0; c < 3; c++) // for each column
cout << TwoDArray[r][c] << " "; // printing the values
cout << endl;
}
return 0;
}
2D array initialization

In the Example2D.cpp file:

  • The given code initializes and prints the values of a 2D array TwoDArray with dimensions 4 (rows) and 3 (columns). It uses nested for loops to iterate through each element of the array. The outer loop represents the rows, while the inner loop represents the columns. The cout statement is used to print each element, followed by a space. After printing all the elements in a row, the cout << endl; statement is used to move to the next line, creating a row-wise display of the array.

In the Example2DGoodPractice.cpp file:

  • The second code is an improved version of the first implementation because it uses constants (ROWS and COLS) to define the number of rows and columns in the 2D array. This approach enhances code flexibility and maintainability. By using constants, it becomes easier to modify the array size by simply changing the values of the constants without the need to modify the loop conditions or array declaration directly. This enables more efficient code modification and avoids potential errors when adjusting the array dimensions. Overall, the second code provides a more adaptable and scalable solution compared to the first implementation.

Accessing and mapping functions

Though we logically represent a 2D array in terms of rows and columns, a multidimensional array is physically stored (in RAM) like a 1D array.

For instance, say we have a 2D array with nn rows and mm columns; then, at the backend, this array will be stored as a 1D array with a total of n×mn \times m elements (where each element is of the size of the array data type).

Let’s see how multidimensional arrays are mapped to a single-dimensional array. There are two ways to map the elements:

  • Row major mapping (the first row, followed by the second row, the third row, and so on)

  • Column major mapping (the first column, the second column, the third column, and so on)

In C++, arrays are laid out in memory using a row-major layout. This means that the elements of a row are stored next to each other in memory, and accessing elements in row-major order can improve the cache performance.

To determine the address of a particular element in a 2D array, the following formula translation happens automatically:

&baseAddress[ri][ci]=baseAddress+(ri×cols+ci)×element_size\begin{equation} \&baseAddress[r_i][c_i] = baseAddress + (r_i \times cols + c_i) \times element\_size \end{equation}

Here, baseAddressbaseAddress is the address of the array (or the name of the array), colscols is the number of columns in the array, rir_i and cic_i are the row and column indexes of the element, respectively, and element_sizeelement\_size is the size of each element in bytes.

For example, consider the following 2D array:

int TwoDArray[3][4] = {
{1, 2, 3, 4},
{5, 6, 7, 8},
{9, 10, 11, 12}
};

To access the element at row ri, column ci in TwoDArray[ri][ci], we'll use the following formula:

accessed_address = &TwoDArray[0][0] + (ri * 4 + ci) * sizeof(int)

This formula calculates the address of the element using the base address of the array (&TwoDArray[0][0]). For example, if we access the element at TwoDArray[2][3], the formula will drive:

// singleDArray[] = {1,2,3,4,5,6,7,8,9,10,11,12};
accessed_address = &TwoDArray[0][0] + (2 * 4 + 3) * sizeof(int)
// Will access the 11th element of the singleDArray i.e. the last element.

Passing 2D arrays to functions

When we pass a 2D array to a function in C++, we need to specify the number of columns in the array in the function header because the function needs to know the size of each row to correctly calculate the address of each element.

We’ll write a print2DArray() function with these essential parameters:

  • The first parameter is a const char msg[] (a null terminating string, which will be a message, before printing the matrix).

  • M[][MAXCOLS] (the matrix) holds all the values that we need to process. Notice that we have written MAXCOLS in columns dimension (that is because the compiler needs to know which conversion formula it should be using to map it to the 1D map).

  • int rows and int cols represents the total number of rows and columns that we want to process.

Let’s write the function.

#include <iostream>
#include <iomanip>
using namespace std;
void print2DArray(const char msg[], int M[][4], int rows)
{
cout << msg << '\n';
for (int r = 0; r < rows; r++)
{
for (int c = 0; c < 4; c++)
cout << M[r][c] << " ";
cout << '\n';
}
}
void print2DArray(const char msg[], int M[][2], int rows)
{
cout << msg << '\n';
for (int r = 0; r < rows; r++)
{
for (int c = 0; c < 2; c++)
cout << M[r][c] << " ";
cout << '\n';
}
}
void print2DArray(const char msg[], int M[][3], int rows)
{
cout << msg << '\n';
for (int r = 0; r < rows; r++)
{
for (int c = 0; c < 3; c++)
cout << M[r][c] << " ";
cout << '\n';
}
}
int main()
{
int arr[4][4] = {{1, 2, 3, 4}, {1, 2, 3, 4}, {3, 3, 3, 3}, {4, 4, 4, 4}};
// Only 2x2 values are initialized the rest will automatically assigned to 0
print2DArray("4x4 Matrix:", arr, 4);
int arr2[2][2] = {{1, 2}, {3, 4}};
// Only 2x2 values are initialized the rest will automatically assigned to 0. Along with
// that we have taken two variables rows, cols holding how much of the sub matrix must be used
print2DArray("2x2 Matrix:", arr, 2);
int arr3[3][3] = {{1, 2, 3}, {3, 4, 5}, {5, 6, 7}};
// Only 2x2 values are initialized the rest will automatically assigned to 0
print2DArray("3x3 Matrix:", arr3, 3);
return 0;
}
Multiple functions with different cols

In the multipleFunctionsDifferentCols.cpp file:

  • Multiple print2DArray() functions are defined, each accepting a different column dimension of a 2D array. This is necessary because the compiler needs to determine the appropriate formula to map the 2D array onto a single dimension at compile-time. Since the column dimension affects the calculation of memory addresses, distinct functions are required to handle each specific column dimension. By providing separate functions, the program ensures that the correct mapping and printing of the 2D array elements are performed accurately for different column dimensions.

In the oneFunctionsDifferentDimensions.cpp file:

  • With the provided implementation of print2DArray(), the MAXCOLS parameter represents the fixed column dimension of the array, while the rows and cols parameters specify the subwindow or subarray of the larger 2D array that the function will manipulate. By using these variables, the function can dynamically handle different subwindow sizes without the need for separate functions. This approach provides flexibility by allowing the function to work with various column dimensions while maintaining a single function definition.

Let’s practice with more problem-solving exercises using 2D arrays to sharpen our skills and gain proficiency in handling them.

Example 1: Column-wise sum

In this example, we’ll write a sumOfAllColumns() function that takes in a 2D array M, the number of rows and columns to process, and an array colsSum to store the sum of each column.

void sumOfAllColumns(int M[][MAXCOLS], int rows, int cols, int colsSum[]);
// M is the 2D-matrix, rows and cols are the subwindow where we want to work and
// colsSum[] is passed as an array where the answers should be stored.

The function will compute the sum of each column in the 2D array M and store the results in the colsSum[] array. Let’s proceed with implementing and testing the function.

Press + to interact
#include <iostream>
#include <iomanip>
using namespace std;
const int MAXROWS=2, MAXCOLS = 2; // Global Variable
// Assume the following implementations are available
void print(const char Msg[], int A[], int size); // for printing a 1D-array
void print2DArray(const char msg[], int M[][MAXROWS], int rows, int cols); // for printing a 2D-array
void sumOfAllColumns(int M[][MAXCOLS], int rows, int cols, int colsSum[])
{
for (int c = 0; c < cols; c++)
{
colsSum[c] = 0; // Initialize the summation at colsSum[c] to 0.
for (int r = 0 ; r < rows; r++)
colsSum[c] = colsSum[c]+ M[r][c];
}
}
int main()
{
int arr[MAXROWS][MAXCOLS] = {{1, 2}, {3, 4}};
int rows = 2, cols = 2;
int arr1[MAXCOLS]={0}, size = cols;
print2DArray("2x2 Matrix:", arr, rows, cols);
sumOfAllColumns(arr, rows, cols, arr1);
print("Sum of the columns: ", arr1, size);
return 0;
}
  • In the sumOfAllColumns() function, the loop iterates through each column, and then for each column, it iterates through each row to calculate the sum of elements in that column. This approach is chosen because it allows us to calculate the sum of each column separately.

  • By iterating through each column first, we can focus on a single column at a time and calculate its sum by iterating through the corresponding elements in each row. This ensures that we calculate the sum of each column accurately without mixing the elements from different columns. This approach is necessary because calculating column sums requires accessing elements column-wise rather than row-wise. Therefore, it is more logical and efficient to iterate through each column and then iterate through each row within that column to perform the summation.

Tip: Change the values of the 2D array M and test it on several inputs to better understand how the program is executed.

Practice exercise: Row-wise summation

Change the above code so that it now calculates row-wise summation. What changes do we need to make? Think and update the code and test it.

Let’s practice with another example of sorting matrix rows and columns.

Example 2: Row-wise sorting

Let’s implement the rowWiseSorting() function, which sorts a 2D array twoDarr[] with a specific number of rows and columns. The function sorts odd-numbered rows in ascending order and even-numbered rows in descending order. To enhance reusability, we’ll also include asc() and dsc() functions for sorting a single dimension in ascending and descending order, respectively. Here’s the implementation:

Press + to interact
#include <iostream>
using namespace std;
const int MAXROWS=5, MAXCOLS = 5;
void print2DArray(const char msg[], int M[][MAXCOLS], int rows, int cols);
void asc(int row[], int size)
{
for(int i = 0; i<size - 1; i++)
for(int j = 0; j < size - i - 1; j++)
if(row[j]>row[j+1])
swap(row[j], row[j+1]);
}
void dsc(int row[], int size)
{
for(int i = 0; i<size-1; i++)
for(int j = 0; j < size - i - 1; j++)
if(row[j]<row[j+1])
swap(row[j], row[j+1]);
}
void rowWiseSort(int twoDarr[][MAXCOLS], int rows, int cols)
{
for(int i = 0; i < rows ;i++)
{
if(i % 2)
asc(twoDarr[i], cols); // passing twoDarr[i] is sending a row to asc function
else
dsc(twoDarr[i], cols); // passing twoDarr[i] is sending a row to dsc function
}
}
int main() {
int twoDarr[MAXROWS][MAXCOLS]= {{13, 16, 18 , 15, 12 },{24, 29, 19, 12, 10},{10, 34, 22 , 18 , 11}, {11, 51, 17, 41, 32},{22, 61, 43, 11, 91}};
int row = 5;
int col = 5;
print2DArray("Before sorting", twoDarr, row, col);
rowWiseSort(twoDarr, row, col);
print2DArray("After sorting", twoDarr, row, col);
return 0;
}
  • The provided code contains the rowWiseSort() function, which sorts a 2D array twoDarr row-wise. The function iterates through each row of the array using a for loop. For odd-numbered rows, it calls the asc() function and passes the corresponding row (twoDarr[i]) as a single dimension array, along with the number of columns (cols), to sort it in ascending order. Similarly, for even-numbered rows, it calls the dsc() function to sort the row in descending order.

  • By passing a single dimension array representing a row to the sorting functions, the code achieves reusability because the same functions (asc() and dsc()) can be used to sort any single-dimension array. This approach avoids the need for separate sorting logic for odd and even rows within the rowWiseSort() function, making the code more concise and modular.

Have we achieved the best 2D array?

Question

What is the problem with using MAXCOLS as a global variable in the above implementation? Take a moment to consider the issue.

Show Answer

The subsequent lessons will discuss dynamic memory allocation, offering insights into the aforementioned question.