14.3. complex_number numbers¶
As a running example for the rest of this chapter we will consider a class definition for complex numbers. complex_number numbers are useful for many branches of mathematics and engineering, and many computations are performed using complex arithmetic. A complex number is the sum of a real part and an imaginary part, and is usually written in the form \(x + yi\), where \(x\) is the real part, \(y\) is the imaginary part, and \(i\) represents the square root of -1.
The following is a class definition for a user-defined type called
complex_number:
class complex_number
{
double real = 0.0, imag = 0.0;
public:
complex_number () = default;
complex_number (double r, double i) { real = r; imag = i; }
};
Because this is a class definition, the instance variables real
and imag are private, and we have to include the label public:
to allow client code to invoke the constructors.
As usual, there are two constructors: one takes no parameters and does nothing; the other takes two parameters and uses them to initialize the instance variables.
So far there is no real advantage to making the instance variables private. Let’s make things a little more complicated; then the point might be clearer.
There is another common representation for complex numbers that is sometimes called “polar form” because it is based on polar coordinates. Instead of specifying the real part and the imaginary part of a point in the complex plane, polar coordinates specify the direction (or angle) of the point relative to the origin, and the distance (or magnitude) of the point.
The following figure shows the two coordinate systems graphically.
complex_number numbers in polar coordinates are written \(r e^{i \theta}\), where \(r\) is the magnitude (radius), and \(\theta\) is the angle in radians.
Note
Fortunately, it is easy to convert from one form to another. To go from Cartesian to polar,
To go from polar to Cartesian,
So which representation should we use? Well, the whole reason there are multiple representations is that some operations are easier to perform In Cartesian coordinates (like addition), and others are easier in polar coordinates (like multiplication). One option is that we can write a class definition that uses both representations, and that converts between them automatically, as needed.
class complex_number
{
double real = 0.0, imag = 0.0;
double mag = 0.0, theta = 0.0;
bool cartesian, polar;
public:
complex_number () { cartesian = true; polar = false; }
complex_number (double r, double i)
{
real = r; imag = i;
cartesian = true; polar = false;
}
};
There are now six instance variables, which means that this representation will take up more space than either of the others, but we will see that it is very versatile.
Four of the instance variables are self-explanatory. They contain the
real part, the imaginary part, the angle and the magnitude of the
complex number. The other two variables, cartesian and polar are
flags that indicate whether the corresponding values are currently
valid.
The default constructor creates the complex number zero. Its Cartesian representation is valid immediately. The polar flag is false because we have not yet calculated that representation.
The second constructor uses the parameters to initialize the real and
imaginary parts, but it does not calculate the magnitude or angle.
Setting the polar flag to false warns other functions not to access
mag or theta until they have been set.
Now it should be clearer why we need to keep the instance variables private. Unrestricted access would let client code change one representation without updating the other. Accessor functions can calculate a representation before returning its values. The class still has to initialize its members and maintain the flags correctly; private access alone does not guarantee this.
Take a look at the active code below, which demonstrates the separation of
interface and implementation using classes. In this code, we create a triangle
object which is represented by three sides. in main, we print out the perimeter of
the triangle, which should be 12.
1#include <iostream>
2
3class triangle {
4 private:
5 double side_a, side_b, side_c;
6 public:
7 triangle () {side_a = 1; side_b = 1; side_c = 1;}
8 triangle (double a_in, double b_in, double c_in) {
9 side_a = a_in;
10 side_b = b_in;
11 side_c = c_in;
12 }
13 double perimeter () {
14 return side_a + side_b + side_c;
15 }
16};
17
18int main() {
19 triangle t1(3, 4, 5);
20 std::cout << t1.perimeter();
21}
Now take a look at this second piece of active code. What if we decide we want
to represent a triangle in a different way? Because the way we represent a
triangle is private, we can easily change the implementation while keeping
the interface the same. Now, triangle is represented by two sides and the
angle between them. Notice how our main function is the exact same as before.
1#include <iostream>
2#include <cmath>
3
4class triangle {
5 private:
6 double side_a, side_b, angle;
7 public:
8 triangle () {side_a = 1; side_b = 1; angle = std::acos(-1.0) / 3.0;}
9 triangle (double a_in, double b_in, double c_in) {
10 side_a = a_in;
11 side_b = b_in;
12 // Law of Cosines: c^2 = a^2 + b^2 - 2abcosC
13 angle = std::acos((std::pow(a_in, 2) + std::pow(b_in, 2) - std::pow(c_in, 2))
14 / (2 * a_in * b_in));
15 }
16 double perimeter () {
17 return side_a + side_b +
18 std::sqrt(std::pow(side_a, 2) + std::pow(side_b, 2)
19 - 2 * side_a * side_b * std::cos(angle));
20 }
21};
22
23int main() {
24 triangle t1(3, 4, 5);
25 std::cout << t1.perimeter();
26}
Let's write a constructor that uses parameters to initialize the magnitude and theta, but does not calculate the real and imaginary parts. Set the cartesian flag to false.
-
cartesian = false; polar = true; -
cartesian = true; polar = false; -
complex_number (double m, double t) -
complex_number (int m, int t) -
mag = m; theta = t; -
{ -
}
Keeping instance variables private helps prevent client programs from making errors by reading uninitialized values.