Overloading the Loading/Printing Operators (<< and >>)
Learn to load matrices from files and display them on the console using operators.
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:
Using 2D Arrays
Using pointers
Using a 2D static array
Here’s a simple code to create a
#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 fileint 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 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.
As we can see in the illustration above, we have reserved the space for a
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.
#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.
#include <iostream>#include <fstream>using namespace std;class Matrix{int** Vs;int rows, cols;public:// member functionsMatrix();Matrix(const int R,const int C);void LoadMatrix(ifstream& Rdr);void Allocate2D(const int R,const int C);// friend functionsfriend 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
andcols
, 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 theRdr
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 dimensionsR
andC
.
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.
#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.
13 34 7 59 8 53 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
matrix.
Overloading the file reading operator (>>
)
Let’s implement the matrix using the overloaded fstream
operator.
// Notice no Matrix:: is written here because operator>>() is a global functionifstream& operator>>(ifstream& Rdr, Matrix& M){int R, C;Rdr >> R >> C;M.Allocate2D(R,C); // We may call any member function of Mfor (int i = 0; i < M.rows; i++) // To access any private attribute of M, like M.rowsfor (int j = 0; j < M.cols; j++) // and M.colsRdr >> M.Vs[i][j]; // and M.Vsreturn Rdr; // This is just for cascading}
Let’s review the code:
Lines 4–5: We declare the variables
R
andC
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, likecout << variable1 << variable2;
,(i+=2)+=5
, and more, streamlining code for concise and expressive operations.
Overloading the printing operator (<<
)
Let’s implement the ostream
operator.
// Notice no Matrix:: is written here because operator<<() is a global functionostream& 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 theMatrix
class. The reason is that bothoperator<<()
andoperator>>()
are global friend functions. We design them this way to grant them access to the private member attributes of the received matrixM
.
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
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.