Difficulty: Intermediate | Prerequisites: C++ classes, pointers, dynamic memory (new/delete), C++20 modules (see Part 1)
This material covers the internal design of a class that represents a 2D image as a dynamically allocated grid of RGB pixels. It pulls together constructors (including chaining and copy construction), manual memory management with new/delete, deep copying, range validation, and PPM image output. This is where object-oriented programming meets low-level resource management. You should already be comfortable with pointers, new/delete, and basic class syntax.
The UJImage class manages a 2D grid of UJPixel values using raw pointers and manual memory allocation. It provides multiple constructors (default, parameterised, copy) that chain into each other via delegating constructors, and a destructor that frees all allocated memory. Three private helper functions handle allocation (alloc), deep copying (clone), and deallocation (dealloc), keeping the public interface clean.
Delegating constructor (constructor chaining)
A constructor that calls another constructor of the same class in its member initialiser list, using the syntax ClassName() : ClassName(args) { }. Avoids duplicating initialisation logic.
Think of it as one constructor saying "do what that other constructor does first, then I will add my own steps."
Copy constructor
A constructor that takes a const reference to another object of the same class and creates a new object as a copy. Signature: UJImage(const UJImage& objOriginal). Called when you pass an object by value, return an object, or explicitly construct from another.
Deep copy
Copying an object by allocating new memory and duplicating the contents of every pointed-to element, so the copy is fully independent. Contrast with a shallow copy, which copies only the pointer values (both objects would then point to the same memory).
In simple terms, a deep copy gives you two completely separate objects. Change one, the other stays the same.
Dynamic memory allocation
Using new to request memory from the heap at runtime and delete (or delete[] for arrays) to free it. The UJImage class allocates a 2D array of UJPixel values this way.
Destructor
A special member function (~UJImage()) called automatically when an object goes out of scope or is explicitly deleted. Responsible for releasing any resources the object owns, especially dynamically allocated memory.
Accessor (getter)
A const member function that returns a private member variable without modifying the object's state. Examples: getHeight(), getWidth(), getPixel().
Mutator (setter)
A member function that modifies a private member variable, typically with validation. Example: setPixel() checks that row, column, and pixel values are all within valid ranges.
PPM (Portable Pixmap)
A simple, plain-text image format. The header consists of the magic number P3, the image dimensions (width then height), and the maximum colour value (255). The body is a grid of RGB triples separated by whitespace.
enforceRange()
A private helper that validates whether a given integer falls within a specified [min, max] range. If it does not, the function prints an error to cerr and terminates the program with exit(ERROR_RANGE).
Grid _image; is the 2D pixel array. Grid is an alias for Row*, which is UJPixel**: a pointer to an array of pointers, each pointing to an array of UJPixel.
int _rows; and int _cols; store the image dimensions.
The underscore prefix is a naming convention for private members.
DEFAULT_ROWS = 2 and DEFAULT_COLS = 2: dimensions for the default constructor.
DEFAULT_PIXEL = {255, 255, 255}: white, using aggregate initialisation.
MAX = 256: used for modular arithmetic when adding pixel values (keeps them in the 0 to 255 range).
These are static constexpr, meaning they belong to the class (not any one object) and are evaluated at compile time.
The default constructor delegates to the two-parameter constructor: UJImage() : UJImage(DEFAULT_ROWS, DEFAULT_COLS) { }
The two-parameter constructor delegates to the three-parameter version: UJImage(int, int) : UJImage(int, int, DEFAULT_PIXEL) { }
The three-parameter constructor does the real work by calling alloc(intRows, intCols, recPixel).
This chain avoids repeating allocation logic in every constructor.
Signature: UJImage(const UJImage& objOriginal)
Delegates to the two-parameter constructor first (which allocates memory of the right size via alloc), then calls clone(objOriginal) to deep-copy pixel data.
The member initialiser list reads : UJImage(objOriginal._rows, objOriginal._cols), which is how a copy constructor can access the private members of the source object (objects of the same class can see each other's private members).
Sets _rows and _cols, then allocates the grid:
_image = new Row[_rows]; allocates an array of row pointers.
For each row: _image[r] = new UJPixel[_cols]; allocates an array of pixels.
Each pixel is set to recPixel.
This is a two-level allocation: the outer array holds pointers, each inner array holds actual pixel data.
Asserts that _rows and _cols match the source object. This is a safety check, not user-facing validation.
Iterates through every [r][c] position and copies pixel data element by element. This is the deep copy: each pixel value is duplicated, not just the pointer.
Asserts that _image is not nullptr (guards against double-free).
Deletes each row array with delete [] _image[r], then deletes the outer array with delete _image.
The order matters: you must free the inner arrays before the outer one, or the inner pointers are lost (memory leak).
Simply calls dealloc(). This runs automatically when a UJImage object goes out of scope or is explicitly deleted.
getHeight() returns _rows. getWidth() returns _cols. Both are marked const because they do not modify the object.
getPixel(int intRow, int intCol) returns a copy of the pixel at the given position. It calls enforceRange() on both the row and column before accessing the array.
setPixel(int intRow, int intCol, UJPixel recPixel) validates five things before writing:
Row within [0, _rows - 1]
Column within [0, _cols - 1]
Red, green, and blue each within [0, 255]
Only after all five checks pass does it assign _image[intRow][intCol] = recPixel.
This is defensive programming: the object protects its own invariants rather than trusting the caller.
Signature: void enforceRange(int intValue, int intMin, int intMax) const
If the value falls outside [intMin, intMax], it prints an error message to std::cerr (not cout, because error output should go to a separate stream) and calls exit(ERROR_RANGE) to terminate immediately.
This is a hard stop, not an exception. The program ends rather than continuing with invalid data.
Builds a PPM-format string using a std::stringstream.
The header is: P3, then _cols _rows (width first, height second, which is a common PPM gotcha), then 255.
The body iterates row by row, column by column, writing each pixel's red, green, and blue values separated by spaces.
Returns the entire image as a std::string, which can be streamed to a file or to cout.
The PPM format is used in graphics courses and research because it requires no external libraries to read or write. The same principles (pixel grids, colour channels, serialisation) apply to every image format, just with compression and metadata added on top.
"The copy constructor just copies the pointer." If it did, both objects would share the same pixel grid, and deleting one would corrupt the other. The copy constructor allocates fresh memory and copies every pixel value individually (a deep copy).
"You can delete the outer array first and then the inner arrays." You cannot. Once you delete the outer array of row pointers, those pointers are gone. You lose access to the inner arrays and leak their memory. Always free inner allocations first.
"assert() and enforceRange() do the same thing." They serve different purposes. assert() is for catching programming errors during development (it can be compiled out in release builds). enforceRange() is runtime validation that always runs and terminates the program with a meaningful error message.
"The PPM header writes height then width." The PPM specification requires width (columns) first, then height (rows). The code writes _cols << ' ' << _rows. Getting this backwards produces a valid-looking file that renders incorrectly.
⚠️ Be able to trace through the constructor chaining: default calls two-param, two-param calls three-param, three-param calls alloc(). Know which constructor body runs and in what order.
⚠️ Explain why the copy constructor needs a deep copy. This is a classic exam question: "What would happen with a shallow copy?"
⚠️ Know the deallocation order: inner arrays (delete[]) first, outer array (delete) second. Be prepared to explain why reversing this causes a memory leak.
⚠️ Understand why getPixel() returns a copy (by value) while setPixel() writes directly to the array. The accessor protects the internal state; the mutator validates before modifying it.
⚠️ The PPM format header order (width before height) is a detail that has caught students before.
True or false: The default UJImage constructor allocates a 2x2 grid of white pixels. (True. It chains to the three-param constructor with DEFAULT_ROWS, DEFAULT_COLS, and DEFAULT_PIXEL.)
Fill in the blank: The clone() function performs a ______ copy of the pixel array. (deep)
True or false: enforceRange() throws an exception when a value is out of range. (False. It calls exit(ERROR_RANGE), terminating the program immediately.)
Fill in the blank: In the PPM header, the order is magic number, then ______ then ______, then max colour value. (width (cols), height (rows))
True or false: getPixel() returns a reference to the internal pixel. (False. It returns a copy by value.)
Q: Walk through what happens when you write UJImage objImage_2(45, 45, {250, 150, 50});.
A: The three-parameter constructor is called. It invokes alloc(45, 45, {250, 150, 50}), which sets _rows = 45 and _cols = 45, allocates an array of 45 row pointers, then for each row allocates an array of 45 UJPixel values, setting every pixel to {250, 150, 50}.
Q: Why does the copy constructor call clone() instead of alloc() with the source's pixel data?
A: The copy constructor delegates to the two-parameter constructor first, which calls alloc() to set up the grid at the correct size (filled with default white pixels). Then clone() overwrites every pixel with the source's data. This separation keeps allocation and copying as distinct responsibilities.
Q: What happens if you forget to implement a destructor for UJImage?
A: The dynamically allocated pixel arrays are never freed. Every time a UJImage object goes out of scope, its memory stays allocated but unreachable. This is a memory leak. Over time or in a loop, the program's memory usage grows without bound.
Q: Why does setPixel() validate the RGB values but alloc() does not?
A: alloc() is a private helper called only by the class's own constructors, which pass known-safe values (constants or validated parameters). setPixel() is public and takes values from the caller, who might pass anything. The validation boundary sits at the public interface.
Q: Explain the two-level allocation in alloc(). Why not allocate one contiguous block?
A: The design allocates an array of row pointers (new Row[_rows]), then for each row allocates an array of pixels (new UJPixel[_cols]). This gives a _image[r][c] access pattern. A single contiguous block would be more cache-friendly but would require manual index arithmetic (r * _cols + c) everywhere, which this design avoids by using the pointer-to-pointer structure.
This material connects to operator overloading (Part 3 of these notes), where the same alloc, clone, and dealloc helpers are reused in the assignment operator. It also connects to the Rule of Three: if a class needs a custom destructor, it almost certainly needs a custom copy constructor and copy assignment operator too, which is exactly what UJImage implements.
UJImage class, C++ constructors, delegating constructor, constructor chaining, copy constructor, deep copy vs shallow copy, dynamic memory allocation, new delete C++, destructor, two-level array allocation, pointer to pointer, Grid Row UJPixel, accessor getter, mutator setter, enforceRange validation, PPM image format P3, stringstream, constexpr static, Rule of Three, memory leak, assert vs validation