Overloading the Loading/Printing Operators (<< and >>)

Before diving into the operators, let’s see a couple of ways we can create a matrix in C++. There are two approaches that are commonly used:

  1. Using 2D Arrays

  2. Using pointers

Using a 2D static array

Here’s a simple code to create a 3×33\times3 matrix using a 2D array.

Press + to interact
#include <iostream>
using namespace std;
const int MAX_ROWS = 100, MAX_COLUMNS = 100;
int main()
{
int rows = 3,cols = 3; // you may take these dimensions as input from the user or file
int arr[MAX_ROWS][MAX_COLUMNS] = {{7, 6, 5},{3, 4, 9},{2, 8, 1}};
cout<<"Matrix Using 2D Array:"<<endl;
for(int i=0;i<rows;i++)
{
for(int j=0;j<cols;j++)
cout<<arr[i][j]<<" ";
cout<<endl;
}
return 0;
}

Let’s review the code:

This is a C++ program that initializes a 2D array of dimensions 3×33\times3, where the number of rows and columns is defined using constants MAX_ROWS and MAX_COLUMNS, respectively. These constants set the maximum size of the array, which can hold up to 100 rows and 100 columns. The program utilizes only a small portion of the array’s capacity by initializing the array with only three rows and columns. However, using these constants allows for the generalization of the array’s size to support larger arrays if required.

Pros and cons of using 2D arrays

A question that we might ask is: Why are we using MAX_ROWS and MAX_COLUMNS?

One advantage of defining the maximum number of rows MAX_ROWS and columns MAX_COLUMNS as constants allow for a generalized approach to defining the size of the 2D array. For instance, by changing the values of MAX_ROWS and MAX_COLUMNS, the program can easily create larger arrays without modifying the code for every new array size. However, this approach also has some drawbacks. The program cannot allocate memory for the larger array if the number of rows and columns required for a particular task exceeds the defined maximum capacity. Moreover, if the program initializes an array with smaller dimensions than the maximum capacity, it would waste considerable memory that could be used for other tasks. Therefore, while using constants to define the maximum size of the array provides flexibility and generality, it still requires careful consideration of memory utilization and allocation.

Press + to interact
Matrix with the maximum size
Matrix with the maximum size

As we can see in the illustration above, we have reserved the space for a 100×100100\times 100 matrix, where we’ve only used the space for a 3×33\times3 matrix.

Using pointers and dynamic memory allocation

In this section, we’ll explore a more sophisticated approach to working with matrices by utilizing pointers and dynamic memory allocation. You might have noticed that specifying the number of rows MAX_ROWS and columns MAX_COLUMNS in the function declaration can limit its flexibility. However, we can overcome this limitation and create a more versatile matrix implementation by employing pointers to pointers. In the following example, we’ll explore this concept and learn how to create dynamic arrays to represent matrices of various dimensions.

Press + to interact
#include <iostream>
using namespace std;
void printMatrix(int ** matrix, int rows, int cols)
{
for(int i=0;i<rows;i++)
{
for(int j=0;j<cols;j++)
cout<<matrix[i][j]<<"\t";
cout<<endl;
}
}
int main()
{
int rows = 3, cols = 3;
int **matrix = new int*[rows], val = 10;
for (int i = 0; i < rows; ++i)
{
matrix[i] = new int[cols];
for(int j =0;j<cols;j++)
matrix[i][j]=(val--)*100;
}
cout<<"Matrix Using Pointers:"<<endl;
printMatrix(matrix, rows, cols);
}

This code defines a function called printMatrix() that takes a two-dimensional integer array (matrix) along with its number of rows and columns, and prints out its elements. The main function creates a two-dimensional integer array with 3 rows and 3 columns using dynamic memory allocation and initializes its elements with values starting from 1,000 (goes down by 100). It then calls the printMatrix() function to print out the matrix.

Note: Now, we don’t have the limitations of memory wastage and writing separate functions for different dimensions.

Certainly, this is a better implementation than a fixed-size 2D array. Let’s take it a step further by transforming it into a distinct data type called Matrix class. By leveraging the power of operator overloading, we’ll enable the Matrix class with comprehensive matrix arithmetic capabilities, making it a more versatile and powerful library.

Header file of the Matrix

Let’s create a header file of our Matrix class where we’ll incorporate the double pointers to create the matrix.

Press + to interact
#include <iostream>
#include <fstream>
using namespace std;
class Matrix
{
int** Vs;
int rows, cols;
public:
// member functions
Matrix();
Matrix(const int R,const int C);
void LoadMatrix(ifstream& Rdr);
void Allocate2D(const int R,const int C);
// friend functions
friend ostream& operator<<(ostream&, const Matrix& M);
friend ifstream& operator>>(ifstream& rdr, Matrix& M);
};

Let’s review the code:

  • Lines 7–8: We declare the private attributes of the class, naming them as follows:

    • Vs is a double-pointer.

    • rows and cols, indicate the number of rows and columns, respectively.

  • Lines 11–14: We declare our public attributes, naming them as follows:

    • Matrix(): The default constructor.

    • Matrix(const int R,const int C): The parametrized constructor.

    • void LoadMatrix(ifstream& Rdr): This function assumes that the Rdr is pointing to the stream from where the matrix should be loaded.

    • void Allocate2D(const int R,const int C): This function should allocate the matrix with dimensions R and C.

  • Lines 16–17: We have written two friend functions prototypes (which are global operators and will have access to the private member attributes of the matrix object M passed as the parameter).

    • friend ostream& operator<<(ostream&, const Matrix& M): We’ll overload the << operator for printing the matrix.

    • friend ifstream& operator>>(ifstream& rdr, Matrix& M): We’ll overload the >> operator for reading the matrices from the file.

Note: Friend functions are functions that are not member functions of the class and have access to private data members of the class objects. A friend function's prototype is written inside the class.

The header file Matrix.h can’t do much as we haven’t implemented the Matrix class yet. Let’s create a Matrix.cpp file and add the implementations of four member functions.

Press + to interact
#include "Matrix.h"
#include <iostream>
#include <fstream>
using namespace std;
Matrix::Matrix()
{
this->cols = 0 = this->rows;
this->Vs = nullptr;
}
Matrix::Matrix(const int R,const int C):Vs(nullptr)
{
Allocate2D(R,C);
}
void Matrix::Allocate2D(const int R,const int C)
{
if (this->Vs != nullptr)
this->DeleteMatrix();
this->rows = R;
this->cols = C;
this->Vs = new int* [R];
for (int i = 0; i < this->rows; i++)
this->Vs[i] = new int[C]{};
}
void Matrix::LoadMatrix(ifstream& Rdr)
{
for (int i = 0; i < this->rows; i++)
for (int j = 0; j < this->cols; j++)
Rdr >> this->Vs[i][j];
}

Let’s discuss the code:

  • Lines 6–10: We create the default constructor.

  • Lines 11–14: We create the parameterized constructor.

Here, we’re missing the implementation of the << and >> operators. That’s because we first need to understand the structure of the file containing our matrices.

Press + to interact
1
3 3
4 7 5
9 8 5
3 7 2

Let’s understand the file structure:

  • Line 1: The number 1 indicates the number of matrices in the file. For the sake of simplicity, we only have one matrix.

  • Line 3: The two numbers indicate the number of rows and columns.

  • Lines 4–6: We have a 3×33\times3 matrix.

Overloading the file reading operator (>>)

Let’s implement the matrix using the overloaded fstream operator.

Press + to interact
// Notice no Matrix:: is written here because operator>>() is a global function
ifstream& operator>>(ifstream& Rdr, Matrix& M)
{
int R, C;
Rdr >> R >> C;
M.Allocate2D(R,C); // We may call any member function of M
for (int i = 0; i < M.rows; i++) // To access any private attribute of M, like M.rows
for (int j = 0; j < M.cols; j++) // and M.cols
Rdr >> M.Vs[i][j]; // and M.Vs
return Rdr; // This is just for cascading
}

Let’s review the code:

  • Lines 4–5: We declare the variables R and C depicting the number of rows and columns in our matrix and initialize them.

  • Line 6: We reserve the memory for our matrix using the Allocate2D() function.

  • Lines 7–9: We populate our matrix using the data read from the file.

  • Line 10: We return Rdr to enable cascading, enabling multiple inputs in a single line.

Note: Cascading enables the chaining of input operations, allowing multiple inputs to be read in a single statement, such as file >> variable1 >> variable2;. This concept applies to various operators, like cout << variable1 << variable2;, (i+=2)+=5, and more, streamlining code for concise and expressive operations.

Overloading the printing operator (<<)

Let’s implement the ostream operator.

Press + to interact
// Notice no Matrix:: is written here because operator<<() is a global function
ostream& operator<<(ostream&, const Matrix& M)
{
for (int i = 0; i < M.rows; i++)
{
for (int j = 0; j < M.cols; j++)
cout <<M.Vs[i][j] << " ";
cout << endl;
}
return cout ; // for cascading
}

We used the nested for loops for the printing of the matrices.

Note: We don’t use Matrix:: before the function declaration, unlike the other member functions of the Matrix class. The reason is that both operator<<() and operator>>() are global friend functions. We design them this way to grant them access to the private member attributes of the received matrix M.

Demo of the Matrix class

Click the “Run” button to execute the code.

4

2 2
1 2 
4 5

2 2
7 8 
2 2

3 3
4 7 5
9 8 5
3 7 2

4 3 
4 6 5
3 3 4
5 8 9
4 5 3
Demo of loading and displaying matrices

Tip: Play around with the demo, change the number of matrices and their dimensions, and feel free to become comfortable with this implementation.

We’ll be building on this implementation by adding further matrix arithmetic functionalities to this class.