Looking at how to define a complex number in the computer, we get something like this:
double imag, real; char symbol;
Unfortunately, we'd have to have duplicates of each of these three variables for any complex numbers we wanted to define. So if we had two complex numbers a and b, we'd need to declare:
double a_imag, b_imag, a_real, b_real; char a_symbol, b_symbol;
Not a happy situation! Think if you had 10 or 20 or more! The best we could hope for would be parallel vector's or something.
To alleviate this, C added a new way to conglomerate many values which could be of different types (vector's, recall, would need to all have the same exact type for all elements). This mechanism is known as the structure (struct for short — short-hand, that is...um...yeah...). To define a complex number as a struct type, we'd have to do something like this in C:
typedef struct { double imag, real; char symbol; } Complex;
But C++ shortened this complex syntax (pardon the pun) to:
struct Complex { double imag, real; char symbol; };
Note that you still need the ; after the close brace. (Even though it is no longer necessary to use typedef on the structure, there are optional parts that could follow the } but that we rarely use. The ; signifies to the compiler that we are done defining this structure and won't be using the optional parts.) Also note that the type name (Complex) has moved from the end of the definition to right after the keyword struct.
In either case, we can now declare variables of the type Complex as:
Complex a, b, c;
That's much simpler than what we did above to declare complex variables for a and b — and this time we got a c, too! Each of these variables has a distinct copy of the three values making up a complex number (imag, real, and symbol). Each of these values is known as a field or member of the structure (or, when looking at a particular variable's copies, as a field or member of the structured variable).
We'll often visualize such structured variables like this:
a b +-----------------------+ +-----------------------+ | | | | | real | | | | real | | | | imag | | | | imag | | | | symbol | | | | symbol | | | | | | | +-----------------------+ +-----------------------+ c +-----------------------+ | | | real | | | | imag | | | | symbol | | | | | +-----------------------+
See how each one has a place to store a real part, an imag part, and a symbol for the square root of -1?
So, how do we get to these values to manipulate them? Well, do you recall that cin owns a function named fail (as well as others named get, peek, ignore, etc.)? Recall that to access this function you had to ask cin's permission using the dot (.) operator: cin.fail(). Access to structure members is similar. To access the fields of a, for instance, you might type:
cin >> a.real; // read real part from user a.imag = -4.2; // assign imaginary part if (a.symbol == 'j') // see if we're using the normal sqrt(-1) symbol { cout << "I'm a double-E!!" << endl; }
Access to the members of the other variables would be similar — simply replace the variable name a with that of the variable whose fields you wish to access.
Let's look at how you might implement an operation for a complex number. Obviously we'll have to define how all operations occur to a complex number since the computer didn't even know what one looked like till a couple of minutes ago when we defined our structure. So, we'll need to define a function for each of the operations in our list:
Let's start with an easy one: addition. A prototype for this function might look something like:
// add x to y and return result Complex Add(Complex x, Complex y); // add x to y and store in z --- z = x + y inline void Add(Complex x, Complex y, Complex & z) { z = Add(x, y); return; }
Notice that it isn't required to do anything but list the new type like any other type. Two value arguments and a return type for the first overload and two value arguments and a reference argument to change for the second overload. This gives our caller flexibility just like when we allowed both whole container and size_type bounded versions of vector and string functions.
So, what does the body look like? Well, if we were adding two complex numbers in math, it might look like:
x_a + x_b i y_a + y_b j ------------------------- (x_a+y_a) + (x_b+y_b) i \___/ \___/ z_a z_b
So we should do the same for our C++ code:
// add x to y and return result Complex Add(Complex x, Complex y) { Complex z; z.imag = x.imag + y.imag; z.real = x.real + y.real; z.symbol = x.symbol == y.symbol ? x.symbol : 'i'; return z; }
Note how the imaginary part of the x argument is added to the imaginary part of the y argument and this result is stored in the imaginary part of the z argument. Since the imag field of a Complex structure is of type double, the computer already knows how to add and store these values. What we are defining with our function is how to add an entire complex number to another and store the result. To do this, we have to add each of the imaginary parts and the real parts and store them. Then we must pick a symbol (I've chosen x's symbol rather arbitrarily here) and store that. Until all of these parts is done, the complex number result is still incomplete.
Now we can call these functions from the main (or elsewhere) like:
Add(a,b,c); // adds a and b and stores the result in c --- c = a + b // OR: c = Add(a,b); // same thing, different way to view it...
Cool!
However, while the language designers made sure that simple variables would transfer to functions in a (relatively) fast and efficient manner, they completely ignored structures (and their kin: string's and vector's)! So, we often see programmers take action to alleviate this inefficiency. Since we know that references are fast and efficient, we could pass all structure arguments as references. But this might allow for accidental changes to arguments we meant to leave alone. Protecting arguments from accidental change, though, is what const is all about! So, we can make any structure arguments to functions that should be value arguments into constant references to make them transfer to the function quickly and efficiently while maintaining data security. For the Add functions above this might look like:
// add x to y and return result Complex Add(const Complex & x, const Complex & y); // add x to y and store in z --- z = x + y inline void Add(const Complex & x, const Complex & y, Complex & z) { z = Add(x, y); return; }
A call to these functions would look the same as before. The only difference between these constant references and those we've seen before is that when the type being constantly referenced was a built-in type or string, we could have a literal or even expression as the actual argument: with structures (as with vector's) we can't have literal or expression actual arguments. Not a big loss since you can't have literal or expression structures anyway. *smile*
But addition isn't all we have to define! The computer makes NO assumptions, you'll recall. That means that not only does it not know how to add two complex numbers, it doesn't even know how to input or output complex numbers! The only thing the compiler can figure out for itself is assignment. (It will simply copy all the bits from one memory location to the other like it always does...)
So, let's look at the implementation of these tedious details here.
We still have to teach the computer how to print a complex number on the screen! Our first guess might be something like:
void Output(const Complex & x) { cout << x.real << x.imag << x.symbol; return; }
Let's look at a few cases here:
Complex Number Output on Screen ------------------------------------------------ 9-4i 9-4i 9+4i 94i 9+0i 90i
Not too glorious. Let's try this:
void Output(const Complex & x) { cout << x.real << '+' << x.imag << x.symbol; return; } // Complex Number Output on Screen // ------------------------------------------------ // 9-4i 9+-4i // 9+4i 9+4i // 9+0i 9+0i
*ich* Not a whole lot better. Let's think about this some more. We have a value that may be positive, zero, or negative. Didn't I see something like that earlier? Yeah...it was an example of the ?: operator! Let's try that:
void Output(const Complex & x) { cout << x.real << (x.imag >= 0 ? "+" : "") << x.imag << x.symbol; return; } // Complex Number Output on Screen // ------------------------------------------------ // 9-4i 9-4i // 9+4i 9+4i // 9+0i 9+0i
Cool! Now we have it working for any value! Yippee! Oh, sorry...got carried away in the moment.
BTW, note that this output function doesn't output anything but the complex number. It doesn't print spacing before, after, or in between the values. It doesn't print any labels. It doesn't even print a newline (endl) or anything! Why is that? Well, look at the way the standard data types print. They print their value and nothing more or less. Somewhere in the iostream library, there are functions defining that this is how it should be. So, when we define our own data types, we should follow suit: print the value and nothing more or less.
Now we can add to our program:
Complex a, b = { 9, 12, 'i' }, // initialize b with list of values c; a = b; // make a a copy of b c = Add(a,b); // add 'em up (like a cheap multiply...hmm...) Output(c); // print result!
So they can see the answer! But...what if they want to:
How do we read a complex number from the keyboard? Well, how might the user like to type a complex number? Several ways are:
9+4j 9 +4j 9+ 4j 9+4 j 9 + 4j 9+ 4 j 9 +4 j 9 + 4 j
Hmm...so a complex number might actually have spaces inside it. Okay. We need to read the stuff, possibly space-separated possibly not, into our fields. Let's try this:
void Input(Complex & x) { cin >> x.real >> x.imag >> x.symbol; return; }
How will this react to the above inputs? Recall that spaces between data are ignored and characters after data are ignored but characters IN data cause cin to lock up and die — never reading anything else again. Let's see:
Input real imag symbol left ------------------------------------------------------------------- 9+4j 9 4 j 9 +4j 9 4 j 9+ 4j 9 --- --- + 4j (cin is dead) 9+4 j 9 4 j 9 + 4j 9 --- --- + 4j (cin is dead) 9+ 4 j 9 --- --- + 4 j (cin is dead) 9 +4 j 9 4 j 9 + 4 j 9 --- --- + 4 j (cin is dead)
Not great, but not bad. We got half of them working right off the bat! It's that plus sign that causes a problem (it might also be a minus sign: 9 - 4j). So, let's read that in separately:
void Input(Complex & x) { char plusminus; cin >> x.real >> plusminus >> x.imag >> x.symbol; return; }
How's that do? Well:
Input real plusminus imag symbol left ------------------------------------------------------------------- 9+4j 9 + 4 j 9 +4j 9 + 4 j 9+ 4j 9 + 4 j 9+4 j 9 + 4 j 9 + 4j 9 + 4 j 9+ 4 j 9 + 4 j 9 +4 j 9 + 4 j 9 + 4 j 9 + 4 j
WooHoo!! But:
Input real plusminus imag symbol left ------------------------------------------------------------------- 9-4i 9 - 4 i 9 -4i 9 - 4 i 9- 4i 9 - 4 i 9-4 i 9 - 4 i 9 - 4i 9 - 4 i 9- 4 i 9 - 4 i 9 -4 i 9 - 4 i 9 - 4 i 9 - 4 i
And shouldn't our imaginary part be negative here? After all, our math format is: a + bj. So the imaginary part should be negative since we have: 9 + -4i. (It just looks icky to do it that way and so we type 9 - 4j, instead. BTW, are you getting that the symbol is pretty irrelevant here. It stands for the square root of -1 — which doesn't truly exist. It is only a placeholder.)
So, we need to fix this a bit:
void Input(Complex & x) { char plusminus; cin >> x.real >> plusminus >> x.imag >> x.symbol; if (plusminus == '-') { x.imag = -x.imag; } return; }
Thus, if plusminus is a minus sign, we'll negate that positive value stored in our imaginary part to reflect it.
Note again that this code reads a complex number and nothing more. It doesn't print a newline afterwards. It doesn't prompt for the value. It merely reads the value and stores it. This reflects how cin handles the standard data types.
Now we can make our program more useful looking:
Complex a, b = { 9, 12, 'i' }, c; cout << "Enter a complex number: "; Input(a); c = Add(a,b); Output(a); cout << " + "; Output(b); cout << " = "; Output(c);
Now we can input, assign, output, and add complex numbers! Pretty snazzy!
This tedium applies to not just complex numbers, but to any data type you might choose to define. You'll have to teach the computer to do everything to each one separately. For instance, if you were going to implement the rational numbers described above as an ADT, you'd have to define addition, input, output, and assignment (among other operations) all over again — for rational numbers.
The main difficulty here is representing these various data types on the screen. For instance, rational numbers are normally written as:
a --- b
A format fine for pencil and paper, but not easily done with character output on a screen. So we sometimes make compensations to the medium of display. Rational numbers, for instance, are most often displayed (and input) as:
a / b (or a/b or a /b or a/ b)
So watch yourself as you implement ADTs for various things. Remember to always include input, output, and assignment for any ADT you define. Watch the I/O format to make sure it's not too difficult to type or understand. Then add any other operations needed for the type.
Since a data type is generally used in many programs (we use short and char all the time, right?), we'd like to place every ADT in a library as well. So, we'll put the type definitions and operation function prototypes in the header and the operation function definitions in the implementation and just re-use it in different applications.
More to come...