-
Section 1: Getting Started
-
Section 2: Core Syntax
-
Section 3: Object-Oriented C++
-
Section 4: Memory Management
-
Section 5: Templates and Generic Programming
-
Section 6: The Standard Template Library
-
Section 7: Modern C++ Features
-
Section 8: Error Handling
-
Section 9: Multithreading
-
Section 10: Operator Overloading and Type Conversion
-
Section 11: Advanced Topics
-
Section 12: Tooling and Build Systems
-
Section 13: Design Patterns in C++
-
Section 14: Interfacing with C and Systems Programming
-
Section 15: Networking and IPC Basics
-
Section 16: Graphics and Game Programming Basics
-
Section 17: The Boost Libraries
-
Section 18: Data Structures and Algorithms in C++
-
Section 19: Practical Projects
-
Section 20: More Concurrency Patterns
-
Section 21: More OOP and Design Practice
-
Section 22: File I/O and Streams
-
Section 23: More Standard Library
-
Section 24: Practice Exercises
-
Section 25: Interview and Algorithm Practice
-
Section 26: Compiler and Language Internals
-
Section 27: GUI and Application Frameworks Overview
-
Section 28: Testing and Quality Practices
-
Section 29: Numerics Library
-
Section 30: Concepts Library (C++20)
-
Section 31: Ranges Library (C++20) In Depth
-
Section 32: More Utility Library
208: Practice Exercise: Building a Simple Matrix Class with Operator Overloading
I've seen this mistake more times than I can count, even from developers who think they've mastered C++ memory management. When you start overloading operators for your own classes, it's tempting to try and "optimize" by returning references to avoid copies. But there's a very specific place where that logic will blow up in your face.
// BUGGY CODE: Don't do this!
class Matrix {
int rows, cols;
double** data;
// ... constructor and destructor implemented ...
Matrix& operator+(const Matrix& other) {
Matrix result(this->rows, this->cols);
for (int i = 0; i < rows; ++i) {
for (int j = 0; j < cols; ++j) {
result.data[i][j] = this->data[i][j] + other.data[i][j];
}
}
return result; // CRASH IMMINENT
}
};
The Dangling Reference Trap
Take a close look at that operator+ signature. It returns a Matrix&. Inside the function, we create a local object called result. This object lives on the stack. The moment the function hits that closing brace, result is destroyed. By returning a reference to it, you're handing the caller a pointer to a memory location that no longer holds a valid object. If you try to use the result of A + B, your program will likely segfault or, worse, give you garbage data that makes you question your sanity for three hours.
Returning by Value for New Objects
The fix is simple: return by value. When you use operator+, you aren't modifying the existing matrices; you're creating a brand new one. Therefore, the return type must be Matrix, not Matrix&.
// FIXED CODE
Matrix operator+(const Matrix& other) const {
Matrix result(this->rows, this->cols);
for (int i = 0; i < rows; ++i) {
for (int j = 0; j < cols; ++j) {
result.data[i][j] = this->data[i][j] + other.data[i][j];
}
}
return result; // The compiler uses RVO (Return Value Optimization) to make this efficient
}
I also added const to the end of the member function. This is crucial. It tells the compiler that adding two matrices doesn't change the original matrices. Without this, you won't be able to add two matrices if one of them is passed as a const reference or is a temporary object.
Making Operators Feel Native
If we want our Matrix class to feel like a first-class citizen in C++, we shouldn't rely on clunky methods like matrix.getValue(1, 2). Instead, we overload operator(). While operator[] only takes one argument, operator() allows us to pass both the row and column index, making the syntax feel much more like MATLAB or Python's NumPy.
Here is how I usually structure a lean, functional Matrix class for practice exercises:
#include <iostream>
#include <vector>
class Matrix {
int rows, cols;
std::vector<std::vector<double>> data;
public:
Matrix(int r, int c) : rows(r), cols(c), data(r, std::vector<double>(c, 0.0)) {}
// Overloading () for easy access: mat(r, c)
double& operator()(int r, int c) { return data[r][c]; }
double operator()(int r, int c) const { return data[r][c]; }
Matrix operator+(const Matrix& other) const {
Matrix res(rows, cols);
for(int i=0; i<rows; ++i)
for(int j=0; j<cols; ++j)
res(i, j) = (*this)(i, j) + other(i, j);
return res;
}
// Friend function for stream output
friend std::ostream& operator<<(std::ostream& os, const Matrix& m) {
for(int i=0; i<m.rows; ++i) {
for(int j=0; j<m.cols; ++j) os << m(i, j) << " ";
os << "\n";
}
return os;
}
};
Notice that I used std::vector here instead of raw pointers. In a real-world scenario, managing double** manually is a recipe for memory leaks. By using vectors, we let RAII handle the cleanup, allowing us to focus on the operator overloading logic itself.
📋 Practical Task
Exercise: Implementing Matrix-Scalar Multiplication and Chaining
Your task is to extend the Matrix class provided in the lesson to support scalar multiplication. This will test your ability to handle different operator signatures and ensure that operations can be chained together (e.g., Matrix result = (A + B) * 2.5;).
- Requirement 1: Overload
operator*as a member function to allow aMatrixto be multiplied by adouble(e.g.,mat * 2.0). - Requirement 2: Overload
operator*as a non-member function to allow adoubleto be multiplied by aMatrix(e.g.,2.0 * mat). This is essential because the left-hand operand is not aMatrixobject. - Requirement 3: Implement a
mainfunction that creates two 2x2 matrices, adds them together, multiplies the result by a scalar, and prints the final matrix usingstd::cout.
Verification: Ensure your scalar multiplication returns a new Matrix object by value to avoid the dangling reference issue discussed in the lesson.
There are no comments for now.