NivaarExam PrepOfficial exam papers ↗

25-Comp-A4 Program Design and Data Structures · December 2014

Question 4 of 9: Object-Oriented Design — A C++ Complex Number Class

Nivaar worked solution (AI-drafted; not reviewed by a licensed engineer)

Notes on this paper

Paper format. 98-Comp-A4 Program Design and Data Structures, December 2014 — 3 hours, closed book, no calculator permitted. Nine questions, each of equal weight (20 marks); candidates answer any six, so a complete paper is 120 marks. Pseudocode or any high-level language is accepted, and the examiner's note states explicitly that marking emphasises the operation of the program, not syntactic details. All nine questions are answered below, because the whole set is the more useful revision resource. Answers are given in C or C++ as the question dictates; each is compilable as written, but a clear, correctly reasoned pseudocode answer would earn the same marks.

Reference texts for this subject.

  • Cormen, Leiserson, Rivest & Stein, Introduction to Algorithms, 4th ed. — tree traversals (ch. 12), partitioning (ch. 7), asymptotic analysis (ch. 3).
  • Weiss, Data Structures and Algorithm Analysis in C, 2nd ed. — linked lists (ch. 3), binary search trees (ch. 4).
  • Deitel & Deitel, C++ How to Program, 10th ed. — class design and operator overloading (ch. 9–10), file streams (ch. 14).
  • Kernighan & Ritchie, The C Programming Language, 2nd ed. — character I/O idioms (ch. 1, 7), pointers and structures (ch. 5–6).
  • Stroustrup, The C++ Programming Language, 4th ed. — value semantics and const-correctness (ch. 16–18).

The Computer Engineering citation list is built around architecture and networking texts (Patterson & Hennessy, Tanenbaum, Mano); this subject is programming and data structures, so the works above are cited instead.

Question 4: Object-Oriented Design — A C++ Complex Number Class (20 marks)

Question text not reproduced: the examination questions are © Engineers and Geoscientists BC. Open the official past paper (linked at the top of this page) to read the question, then follow the worked solution below.

Given. A required class name, a required split into a header and an implementation file, and a required feature list: construction with and without initialisation, read and write access to both parts, addition, subtraction, multiplication, and printing.

Find. The two files, with an interface that supports every listed operation and an implementation whose arithmetic is correct for all signs.

Approach. Treat Complex as a value type: two double members, no dynamic memory, compound assignment as members and binary operators as free functions built on them, with the multiplication identity written out explicitly.

  1. Fix the representation. A complex number is an ordered pair of reals, $z = a + bi$, so two double members suffice. Because the class owns no pointer, no file handle and no other resource, the compiler-generated copy constructor, assignment operator and destructor are all correct as they stand and none should be written — the “rule of zero”. This is a deliberate design decision, not an omission.
  2. Provide both forms of declaration the question asks for. A default constructor covers Complex z; and sets the number to $0 + 0i$; a two-argument constructor with a default for the imaginary part covers both Complex z(3.0, 4.0); and Complex z(3.0);. Leaving that constructor implicitly convertible is what makes 2.0 + z legal, matching how the standard library's own std::complex behaves.
  3. Separate interface from implementation. The header declares; the implementation file defines. Include guards prevent double declaration when the header is pulled in more than once in a translation unit.
    /* ---------- Complex.h ---------- */
    #ifndef COMPLEX_H
    #define COMPLEX_H
    
    #include <iostream>
    
    class Complex {
    public:
        /* Declaration WITHOUT initialisation: Complex z;  -> 0 + 0i */
        Complex();
    
        /* Declaration WITH initialisation: Complex z(3.0, 4.0);  or  Complex z(3.0);
           Deliberately NOT marked explicit, so that a real number converts to a
           Complex and expressions such as  2.0 * z  compile. */
        Complex(double re, double im = 0.0);
    
        /* Read access */
        double real() const;
        double imag() const;
    
        /* Write access */
        void setReal(double re);
        void setImag(double im);
    
        /* Compound arithmetic: members, because they modify the left operand */
        Complex &operator+=(const Complex &rhs);
        Complex &operator-=(const Complex &rhs);
        Complex &operator*=(const Complex &rhs);
    
        void print(std::ostream &os = std::cout) const;
    
    private:
        double re_;
        double im_;
    };
    
    /* Binary arithmetic as FREE functions so that the left operand converts too:
       2.0 + z works here, but would not if these were member functions. */
    Complex operator+(Complex lhs, const Complex &rhs);
    Complex operator-(Complex lhs, const Complex &rhs);
    Complex operator*(Complex lhs, const Complex &rhs);
    
    std::ostream &operator<<(std::ostream &os, const Complex &z);
    
    #endif  /* COMPLEX_H */
  4. Derive the multiplication rule before coding it. Expanding and using $i^{2} = -1$, $$(a+bi)(c+di) = ac + adi + bci + bd\,i^{2}$$ $$\boxed{(a+bi)(c+di) = (ac-bd) + (ad+bc)\,i}$$ Addition and subtraction are componentwise, which needs no derivation.
  5. Write the implementation, guarding the multiplication. The real part must be computed from the old values of both members; saving $a$ and $b$ into local constants before assigning is what prevents the new real part from contaminating the imaginary part.
    /* ---------- Complex.cc ---------- */
    #include "Complex.h"
    
    Complex::Complex() : re_(0.0), im_(0.0) {}
    
    Complex::Complex(double re, double im) : re_(re), im_(im) {}
    
    double Complex::real() const { return re_; }
    double Complex::imag() const { return im_; }
    
    void Complex::setReal(double re) { re_ = re; }
    void Complex::setImag(double im) { im_ = im; }
    
    Complex &Complex::operator+=(const Complex &rhs)
    {
        re_ += rhs.re_;
        im_ += rhs.im_;
        return *this;
    }
    
    Complex &Complex::operator-=(const Complex &rhs)
    {
        re_ -= rhs.re_;
        im_ -= rhs.im_;
        return *this;
    }
    
    Complex &Complex::operator*=(const Complex &rhs)
    {
        /* (a + bi)(c + di) = (ac - bd) + (ad + bc)i.
           a and b MUST be saved first: writing re_ before im_ is computed would
           feed the new real part into the imaginary part. */
        const double a = re_, b = im_;
        const double c = rhs.re_, d = rhs.im_;
    
        re_ = a * c - b * d;
        im_ = a * d + b * c;
        return *this;
    }
    
    void Complex::print(std::ostream &os) const
    {
        os << re_;
        if (im_ < 0.0) os << " - " << -im_ << "i";
        else           os << " + " <<  im_ << "i";
    }
    
    /* Take lhs BY VALUE: the copy is the result, so no temporary is named. */
    Complex operator+(Complex lhs, const Complex &rhs) { return lhs += rhs; }
    Complex operator-(Complex lhs, const Complex &rhs) { return lhs -= rhs; }
    Complex operator*(Complex lhs, const Complex &rhs) { return lhs *= rhs; }
    
    std::ostream &operator<<(std::ostream &os, const Complex &z)
    {
        z.print(os);
        return os;
    }
  6. Check the arithmetic on a signed example. With $z_1 = 3+4i$ and $z_2 = 1-2i$: $z_1+z_2 = 4+2i$; $z_1-z_2 = 2+6i$; and $z_1 z_2 = \big(3(1)-4(-2)\big) + \big(3(-2)+4(1)\big)i = 11 - 2i$. Testing with a negative imaginary part is essential — a sign error in the $-bd$ or $+bc$ term is invisible when every input is positive.
    /* ---------- example use ---------- */
    #include "Complex.h"
    
    int main()
    {
        Complex z0;                 /* no initialisation -> 0 + 0i */
        Complex z1(3.0, 4.0);
        Complex z2(1.0, -2.0);
    
        z0.setReal(-1.5);           /* write access */
        std::cout << z1.real() << '\n';          /* read access  -> 3 */
    
        std::cout << z1 + z2 << '\n';            /*  4 + 2i */
        std::cout << z1 - z2 << '\n';            /*  2 + 6i */
        std::cout << z1 * z2 << '\n';            /* 11 - 2i */
        std::cout << 2.0 + z1 << '\n';           /*  5 + 4i, via the converting ctor */
        return 0;
    }
Question 4 — delivered interface and verification
RequirementProvided byCheck
Declaration without initialisationComplex()Complex z; → 0 + 0i
Declaration with initialisationComplex(double, double = 0.0)Complex z(3,4); → 3 + 4i
Read accessreal() const, imag() constreturns 3 and 4
Write accesssetReal(), setImag()mutates in place
Additionoperator+ via operator+=(3+4i) + (1−2i) = 4 + 2i
Subtractionoperator- via operator-=(3+4i) − (1−2i) = 2 + 6i
Multiplicationoperator* via operator*=(3+4i)(1−2i) = 11 − 2i
Printingprint() and operator<<prints “11 - 2i”
File splitComplex.h / Complex.ccdeclaration and definition separated