This lab will help you practice with classes and libraries.
Write a class for a complex number ADT. To refresh your memory, complex numbers have the form: a + bi. Where a is the real part, b is the imaginary part, and i represents the square root of -1 (which doesn't exist and is therefore imaginary).
Standard mathematical operations are defined on complex numbers:
a+bi + c+di = (a+c) + (b+d)i a+bi - c+di = (a-c) + (b-d)i a+bi * c+di = (a*c-b*d) + (a*d+b*c)i // i*i == -1 (a*c+b*d) - (a*d-b*c)i a+bi / c+di = ---------------------- c*c + d*d -(a+bi) = (-a) + (-b)i
That last one is negation (aka opposite), of course.
And special operations are also defined:
___________ |a+bi| = \/ a*a + b*b // magnitude ____ a+bi = a-bi // conjugate
(For the curious:
2 ____ |a+bi| = a+bi * a+bi ____ a+bi a+bi * c+di ------ = ------------- c+di 2 |c+di|
If you really wanted to know...)
Define these operations (along with constructors, input/output, and accessors/mutators) for your ADT/class. Place your ADT in a library.
Write a driver to test the ADT behaviors thoroughly.
Why do your class methods take fewer arguments than you would expect?
Does the compiler change y when you have 'x + y' in your program? So should your addition method change the other Complex number (the argument object)? How can you tell the compiler this in the most efficient way?
Does this phenomenon extend to the other operations?
What kind of value should be returned from the standard math operations (i.e. what TYPE of value)? From conjugate? From magnitude?
Does your input method prompt the user? Why should it not?
Does your output method print anything besides the Complex number (even an endl)? Why should it not?
This assignment is (Level 3).
Add (Level 2) to properly apply inline'ing and const-ness to ALL of your class methods. (This includes proper use of initializer lists on constructors!)
Should any of your methods be inlined for speed? Which ones will you inline? How does one do this for a class method?
If you were to end up inline'ing all of your methods, would you still need an implementation file for your library? Why/Why not?
Do the accessors, output, or math operation methods alter the calling object in any way? How do you specify such a situation to the compiler so it can better handle/enforce your decision?
What needs to be in your constructor initializer lists? Is there a certain order to the list? Does it go on the prototype or the definition?
Add (Level 2) for overloading your math operation methods to allow mixing Complex numbers with built-in floating point types. (Hint: What should be the resulting TYPE of such operations?)
Add (Level 3) for putting in comparison functions. At first this may seem easy since you can't even compare two Complex objects to see if one is less than or greater than the other. That eliminates four operations right off the bat! But equality and non-equality are really hard. Since we have double members for the real and imaginary parts, we can't simply implement these operations with a pair of anded == comparisons! We'll have to use the absolute difference trick — and that requires an tolerance value and that can vary from application to application! We can't tie our class to a specific tolerance!
Your first thought might be making a simple member variable for the tolerance. But that would let each object change its tolerance independently of other Complex objects in the application. That'd be pretty creepy, wouldn't it? I mean, what would you do if the caller of your equality function had tolerance 1e-4 and the argument object had a tolerance of 1e-9? Whose is more important? Isn't [non-]equality supposed to be symmetric?!
That's where a static member variable can come in so handy! A static member variable is shared by all objects (instances) of a class. That way, all Complex objects would observe the same tolerance in their comparison attempts. To make such a member, just mark its declaration, accessor, and mutator with the static keyword. But you can't initialize it in a constructor — then every object created would reinitialize it — even after it had been mutated! Instead, you make what appears to be a redeclaration of the member with initialization outside the class definition. Of course you have to specify that it is the class scope member variable for this to work. For example, here's a class with a static member:
class StaticMemberExample { static TYPE member; };
Now, later, when you define the non-inline parts of this class, you'd put before or between them or somewhere nearby this line:
TYPE StaticMemberExample::member = VALUE;
And that would initialize it for the whole class — every object ever declared of this type would not only share it, but have that same value for that member. Here's a full example. We'll come back to this idea later for more details...
Add (Level 3) for making your math operations actually overloaded operators. (If you did the built-in mix-in option, you can make those operator overloads for another (Level 1). But don't forget that operator functions are not commutative by default — they have to be taught to be commutative... Also a hint: reverse subtraction and division — these aren't commutative — have different formulas. Division is easily implemented with conjugate and magnitude, but subtraction would really benefit from unary - — negation/opposite.)
If you did all above options, this lab could be worth as much as (Level 11).