There are quite a number of more fundamental problems in your code:
int rows;
int columns;
int mat[rows][columns];
is an illegal definition of a dynamic array in C++. Array sizes need to be compile time constants in C++, but rows
and columns
are not (and that wouldn't even change if you made both of const
, as different instances of the class might use different values.
Even if it was legal, in your default constructor you would access the arrays out of bounds in your constructor:
mat[rows][columns] = ...
With array sizes of 0 (which again are illegal in C++) there is no accessible index 0 in arrays.
So before we can go on we first need to fix these issues. There are several options for, depending on your needs.
A pretty simple approach is maintaining the data in a std::vector
of std::vectors
:
class Matrix
{
std::vector<std::vector<int>> mat;
public:
// ...
};
Vectors implicitly store information about their internal sizes, so the former rows
and columns
members (for which the correct types would have been size_t
anyway, not int
) are redundant and can and should get dropped in favour of functions using the information from the vectors:
size_t rows() { return mat.size(); }
size_t columns() { return mat.size() == 0 ? 0 : mat[0].size(); }
I assume here all rows being of same size, if not (i.e. a jagged array) we couldn't apply that for the columns.
The vector of vector is a really convenient way to implement a dynamic matrix, however comes with the cost of double indirection on matrix element access, need of an additional array (transparent to you, but you need it for storing the rows) and having to allocate the columns individually (again transparent to you).
You get more efficient if you implement your matrix based on a 1D-array:
size_t m_columns;
std::vector<int> mat;
Now you need to store columns separately, and you need to calculate the correct offsets into the matrix explicitly:
size_t rows() { return mat.size() / m_columns; }
size_t columns() { return m_columns; }
int& at(size_t x, size_t y) { return mat[x * m_columns + y]; }
Note that you still can provide the m[x][y]
syntax, replacing the at
function, but that gets quite a bit more complicated so I'll leave that out for now. Advantages, though, are single indirection and thus faster access, no need of additional memory and one single allocation for all memory, thus faster again.
Yet another approach is providing constant array sizes in form of template parameters:
template<size_t Rows, size_t Columns>
class Matrix
{
int mat[Rows][Columns];
};
This comes with fastest access to array elements and highest type safety, as you can provide operators for transposing, addition, multiplication, ... in a way such that you cannot pass matrices of bad dimensions to. On the other hand you lose much of flexibility, e.g. you cannot place matrices of different dimensions into the same container (vectors, lists or others) directly as templates with different parameters form different types (there are ways around, though, but less convenient, based on polymorphism) and need to specify sizes in the type already in code, i. e. you cannot calculate matrix sizes dynamically.
There are use cases for both approaches (based on dynamic allocation and on statically typed matrices), you need to select according to your concrete requirements. For a very first go I'd recommend the double vector approach for its simplicity, despite of its other disadvantages. Once it works you might switch to the 1D array approach...
Now about your actual problem:
You can always overload constructors, i.e. provide multiple ones that differ in their number and/or types of parameters; the most appropriate one will then be selected according to the arguments you provide (though in some specific cases ambiguities can arise).
In your case you'd provide:
Matrix();
Matrix(size_t rows, size_t columns); // not needed in the template variant,
// dimensions are given via
// template parameters
You should, though, use the constructor's initialiser list (not to be confused with std::initialiser list), which might look as follows:
Matrix() : /*rows(0), columns(0),*/ mat() { }
Remember that I dropped the members for rows and columns, thus they won't appear in the constructor any more either, I left them in comments to illustrate how it would have looked like if I hadn't. Note that mat()
calls the vector's default constructor which creates an empty one. Calling it explicitly is optional, you could have left it out, then it would have been called implicitly.
With std::vector
you are lucky: It provides a constructor with the number of elements to be initialised with as well as an optional default value that will be used for all elements; sowe can simply write:
Matrix(size_t rows, size_t columns) : mat(rows, std::vector<int>(columns)) { }
That's it...
Your complete class then might look as follows (combining all the information from above):
class Matrix
{
std::vector<std::vector<int>> mat;
public:
Matrix() = default; // short hand syntax...
Matrix(size_t rows, size_t columns) : mat(rows, std::vector<int>(columns)) { }
size_t rows() { return mat.size(); }
size_t columns() { return mat.size() == 0 ? 0 : mat[0].size(); }
// still providing `at` function as returning a reference to
// a row vector would allow to modify that one e. g. in size
// and thus create an invalid matrix!
int& at(size_t x, size_t y) { return mat[x][y]; }
};
You might stop at here for now, but actually we are not yet at the end. Once you get above working (please try to implement on your own, don't just copy/paste, you won't learn anything from), you can get back to here for the next step:
class Matrix
{
std::vector<std::vector<int>> mat;
public:
Matrix() = default; // short hand syntax...
Matrix(size_t rows, size_t columns) : mat(rows, std::vector<int>(columns)) { }
size_t rows() const { return mat.size(); }
size_t columns() const { return mat.size() == 0 ? 0 : mat[0].size(); }
// still providing `at` function as returning a reference to
// a row vector would allow to modify that one e. g. in size
// and thus create an invalid matrix!
int at(size_t x, size_t y) const { return mat[x][y]; }
int& at(size_t x, size_t y) { return mat[x][y]; }
};
Have you noticed the const
keywords? They allow to access instances that are declared const
, i.e. unmodifiable. You should declare all member functions const
that do not modify the object.
For the at
function I provided both versions – the const one just returns a value, this way no (illegal) modification of the matrix is possible, while the non-const one reference such that the matrix element can be changed.
Overload resolution is pretty simple in such a case: const
overload on const
matrices, non-const
overload on non-const
matrices:
Matrix m(1, 1);
Matrix const& mr = m; // const reference!
m.at(0, 0); // non-const at
mr.at(0, 0); // const at
m.rows(); // can call const functions on non-const objects, if there's
// no non-const overload – but not the other way round
So far the basics. If you are yet interested in the m[x][y]
syntax for the Matrix
class leave a comment, I might add a few extra lines ;)