Difficulty: Intermediate–Advanced | Prerequisites: UJImage class structure (see Part 2), pointers, references, const correctness
Operator overloading lets you define what =, ==, +, [], (), --, and << mean for your own classes. This practical applies all of them to the UJImage class, giving image objects natural syntax for comparison, arithmetic, pixel access, and output. You should understand the UJImage class internals (constructors, alloc, clone, dealloc) before working through this material.
The UJImage class overloads seven operators. The assignment operator (=) deallocates, reallocates, and deep-copies. The equality operator (==) compares dimensions and every pixel. The addition operator (+) creates a new image with pixel values added modulo 256. The function call operator (()) and subscript operator ([]) provide two ways to access individual pixels. The post-decrement operator (--) decrements all pixel channels with wraparound. The stream insertion operator (<<) outputs the image in PPM format.
Operator overloading
Defining a custom meaning for a built-in operator (like + or ==) when it is used with objects of your class. The operator function has the keyword operator followed by the symbol.
Think of it as teaching C++ what "adding two images" or "comparing two images" means.
Self-assignment check
The test if(this != &objRHS) at the start of operator=. Prevents an object from deallocating its own memory and then trying to copy from it when someone writes obj = obj.
friend function
A function declared inside a class with the friend keyword that is not a member of the class but has access to its private members. The operator<< for stream insertion is declared as a friend because its left operand is an ostream, not a UJImage.
In simple terms, a friend function lives outside the class but has a backstage pass to its private data.
Post-decrement operator
The operator--(int) overload. The dummy int parameter distinguishes post-decrement (obj--) from pre-decrement (--obj). Post-decrement returns the original value and then modifies the object.
Modular arithmetic (% MAX)
Used in the + operator to keep pixel values within the valid 0 to 255 range. Adding two pixel channels and taking the result modulo 256 wraps overflow back to 0.
Row-major indexing
Converting a flat index to a 2D position using row = index / cols and col = index % cols. This is what operator[] does to map a single integer to a pixel in the grid.
lvalue reference return
Returning UJPixel& (a reference) from operator() and operator[] allows the caller to both read and write the pixel. Returning by value would only allow reading.
Signature: const UJImage& operator=(const UJImage& objRHS);
First checks for self-assignment: if(this != &objRHS). If the left-hand side and right-hand side are the same object, it skips the body and returns *this.
If they differ:
Calls dealloc() to free the current object's memory.
Calls alloc() to allocate new memory matching the source's dimensions.
Calls clone() to deep-copy the pixel data.
Returns *this to support chaining (a = b = c).
This follows the standard pattern for overloading = when the class manages dynamic memory.
Signature: const bool operator==(const UJImage& objRHS) const;
Returns false early if _rows or _cols differ.
Then compares every pixel in both grids. If any channel (red, green, or blue) differs at any position, returns false.
Returns true only if dimensions match and every single pixel value is identical.
Marked const because comparing two images should not modify either one.
Signature: const UJImage& operator+(const UJImage& objRHS);
Requires both images to have the same dimensions. If they do not, the program exits with ERROR.
Creates a new UJImage on the heap with new UJImage(_rows, _cols).
For each pixel, adds the corresponding channel values from both images and applies % MAX (modulo 256) to handle overflow. A red value of 200 plus 100 becomes 44 (300 % 256).
Returns a reference to the heap-allocated object. Note: this design leaks memory because the caller has no way to delete the returned object. In production code you would return by value instead.
Signature: UJPixel& operator()(int intRow, int intCol);
Provides 2D access to pixels using the syntax objImage(row, col).
Calls enforceRange() on both the row and column before returning a reference.
Because it returns UJPixel&, you can use it to both read and write: objImage(20, 30) = {1, 1, 1}; assigns a new pixel value at row 20, column 30.
Signature: UJPixel& operator[](int intIndex);
Provides flat, one-dimensional access using row-major indexing.
Converts the flat index to 2D coordinates: intRow = intIndex / _cols; and intCol = intIndex % _cols;.
For a 45-column image, index 620 maps to row 13 (620 / 45 = 13), column 35 (620 % 45 = 35).
Also returns a reference, so objImage[620] = {5, 5, 5}; works for writing.
Signature: UJImage& operator--(int); (the int parameter is unused, it just signals post-decrement).
Creates a copy of the current object on the heap: UJImage* temp = new UJImage(*this);
Then decrements every channel of every pixel in the current object. If a channel drops below 0, it wraps to 255.
Returns *temp, the pre-decrement copy. This matches post-decrement semantics: the expression objImage-- evaluates to the old value, and the object itself holds the decremented values afterwards.
The wrapping logic uses the ternary operator: --value < 0 ? 255 : value.
Note: like operator+, this leaks the heap-allocated temp object.
Signature: friend std::ostream& operator<<(std::ostream& osLHS, const UJImage& objRHS);
Declared as a friend because the left operand is ostream, not UJImage. A member function's left operand is always the object itself.
Calls objRHS.toPPM() and streams the result to osLHS.
Returns the ostream& to support chaining: cout << img1 << img2;.
Operator overloading is widely used in graphics, linear algebra, and scientific computing libraries. Overloading + for images is how libraries like OpenCV express pixel-wise arithmetic. The << pattern is standard for making any class printable with cout.
"operator<< should be a member function." It cannot be, because the left operand of << is an ostream, not a UJImage. Member operator functions always have the object as the left operand. That is why operator<< is a friend free function.
"Post-decrement and pre-decrement have the same signature." They do not. Post-decrement takes a dummy int parameter: operator--(int). Pre-decrement takes no parameters: operator--(). The int is never used; it is purely a syntactic marker.
"operator= does not need a self-assignment check." Without it, obj = obj would call dealloc() first, destroying the data it is about to copy from. The object would try to read freed memory.
"Returning a reference from operator+ is fine." Returning a reference to a heap-allocated object creates a memory leak because the caller receives a reference, not a pointer, and has no clean way to delete it. Best practice is to return by value, which lets the compiler use move semantics or copy elision.
⚠️ Be able to write the self-assignment check for operator= from memory and explain why it is necessary.
⚠️ Know the difference between operator--(int) (post) and operator--() (pre). Be ready to explain the return value semantics of each.
⚠️ Understand why operator<< must be a friend function, not a member.
⚠️ Be able to convert a flat index to 2D coordinates using row-major order: row = index / cols, col = index % cols. Exam questions may give you specific numbers.
⚠️ Know what % MAX (modulo 256) does in the addition operator and why it is needed.
True or false: operator<< for UJImage is a member function. (False. It is a friend free function because the left operand is ostream.)
Fill in the blank: The self-assignment check in operator= compares this with ______. (&objRHS, the address of the right-hand-side object)
True or false: operator--(int) returns the decremented object. (False. It returns the pre-decrement copy. The current object is decremented in place.)
Fill in the blank: For a 45-column image, flat index 630 maps to row ______ and column ______. (row 14, column 0: 630 / 45 = 14, 630 % 45 = 0)
True or false: Adding pixel channels 200 and 100 with operator+ gives 300. (False. It gives 44, because 300 % 256 = 44.)
Q: Trace through what happens when you write objImage_1 = objImage_2; where both are existing UJImage objects with different dimensions.
A: operator= is called. The self-assignment check (this != &objRHS) passes because they are different objects. dealloc() frees objImage_1's current pixel grid. alloc() creates a new grid matching objImage_2's dimensions, filled with default white pixels. clone() copies every pixel value from objImage_2 into the new grid. The function returns *this.
Q: Why does operator== check dimensions before comparing pixels?
A: If the images have different dimensions, they cannot be equal, and comparing pixels would access out-of-bounds memory (the smaller image's grid does not have as many elements). The dimension check is both a logical shortcut and a safety guard.
Q: In operator+, what is the result of adding pixel {200, 150, 100} and {100, 200, 200}?
A: Red: (200 + 100) % 256 = 44. Green: (150 + 200) % 256 = 94. Blue: (100 + 200) % 256 = 44. The result pixel is {44, 94, 44}.
Q: What is the difference between objImage(20, 30) and objImage[620] on a 45-column image?
A: objImage(20, 30) uses the function call operator and directly accesses row 20, column 30. objImage[620] uses the subscript operator with row-major indexing: 620 / 45 = 13 (row), 620 % 45 = 35 (column). So they access different pixels. To access row 20, column 30 via operator[], you would need index 20 * 45 + 30 = 930.
Q: Explain why operator-- (post-decrement) creates a copy on the heap before decrementing.
A: Post-decrement must return the value before the change. The function saves a copy of the current state (new UJImage(*this)), then decrements every pixel channel in the current object. The saved copy, which still holds the original values, is what gets returned. The caller sees the old value; the object itself now holds the decremented values.
This connects back to the Rule of Three (Part 2): the copy constructor, assignment operator, and destructor form a set. If you write one, you should write all three. It also connects to the C++11 move semantics topic: the memory leaks in operator+ and operator-- are exactly what move constructors and move assignment operators were designed to solve.
C++ operator overloading, assignment operator, copy assignment, self-assignment check, equality operator, operator==, operator+, pixel arithmetic, modular arithmetic, modulo 256, function call operator, operator(), subscript operator, operator[], row-major indexing, flat index to 2D, post-decrement operator, operator--(int), pre-decrement vs post-decrement, friend function, operator<<, ostream insertion, stream overloading, lvalue reference return, UJImage, UJPixel, Rule of Three, memory leak operator overloading