Solution
You could do it in row-major order as below:
class Image {
int rows, cols;
unsigned char *buffer;
public:
// ...
unsigned char* operator[](int const i) { return &(buffer[i * cols]); }
unsigned char const* operator[](int const i) const { return &(buffer[i * cols]); }
};
Live Demo
Discussion
Your buffer is a contiguous block of memory. Of size N = rows * cols
.

However the notion of an image is a set of pixels arranged in a 2D construct/matrix:

What we want is to arrange this 2D construct in the 1D buffer in computer's memory. We can do this in two ways:
- Row major order.
- Column major order.
In row major order we store each row of the image one after the other. That is, for the following size 2x2
image

the respective buffer storage would look like:

In column major order we store each column of the image one after the other. That is, the buffer storage for the same size 2x2
image would look like:

Programming languages that support multi-dimensional arrays either implement row-major or column-major storage order for them. In C and C++ row-major order is used.
In the code showed above we implement row-major order. We define an overloaded subscript operator that takes as input the row index (e.g., i
) we want to access. Multiplying this i
with the number of columns we get the starting index of the ith
row in the image. We return the address of the element that lies in this specific address. This address marks the beginning of a sub-array of the original buffer as well as the beginning of the ith
row. To clarify further see the following code example:
Image I(2, 3);
I[1][2] = 42;
calling I[1][2] = 42
is like calling:
(I.operator[](1))[2];
sub-call I.operator[](1)
returns a pointer to the address where the second row of the image starts. Then we use this returned pointer as an ordinary dynamic allocated array. In this specific example we add 2 to this pointer (i.e., that is what [2]
does) and thus we get the element of the row that lies 2 positions after the first element of the row (i.e., the third element of the second row of the image).
Alternative Solution For C++11
If your compiler supports C++11 and smart pointers you could do the following scheme in order to avoid memory management. Furthermore I would overload operator()(std::size_t const, std::size_t const)
because overloading as above you expose the buffer and thus you're hurting encapsulation:
class Image {
int rows, cols;
std::unique_ptr<unsigned char[]> buffer;
public:
Image(int const rows_ = 0, int const cols_ = 0)
: rows(rows_), cols(cols_), buffer(new unsigned char[rows * cols]) {}
Image(Image const &other)
: rows(other.rows), cols(other.cols), buffer(new unsigned char[rows * cols]) {
std::copy(other.buffer.get(), other.buffer.get() + (rows * cols), buffer.get());
}
Image(Image &&other)
: rows(other.rows), cols(other.cols), buffer(std::move(other.buffer)) {
other.rows = 0;
other.cols = 0;
}
unsigned char& operator()(std::size_t const i, std::size_t const j) {
return buffer[cols * i + j];
}
unsigned char const& operator()(std::size_t const i, std::size_t const j) const {
return buffer[cols * i + j];
}
Image& operator=(Image const &other) {
rows = other.rows;
cols = other.cols;
buffer.reset(new unsigned char[rows * cols]);
std::copy(other.buffer.get(), other.buffer.get() + (rows * cols), buffer.get());
return *this;
}
Image& operator=(Image &&other) {
rows = other.rows;
cols = other.cols;
buffer.swap(other.buffer);
other.rows = 0;
other.cols = 0;
return *this;
}
// ...
};
Live Demo