This tutorial describes how to create a complex number class.
In your studies, you have already used complex numbers, and currently, you would have to have two local variables to represent every complex number; e.g.,
#include <iostream> int main(); int main() { // z = 3.2 - 2.5j int zr{ 3.2}; int zi{-2.5}; // w = -0.7 + 1.9j int wr{-0.7}; int wi{ 1.9}; return 0; }
You might also realize, therefore, that we would have to define addition and multiplication functions that pass the result by reference:
void add( double xr, double xi, double yr, double yi, double &zr, double &zi ) { zr = xr + yr; zi = xi + yi; } void multiply( double xr, double xi, double yr, double yi, double &zr, double &zi ) { zr = xr*yr - xi*yi; zi = xr*yi + xi*yr; }
Consequently, you may consider using a vector to represent complex numbers:
#include <iostream> #include <array> int main(); int main() { // z = 3.2 - 2.5j std::array<double, 2> z{ 3.2, -2.5}; // w = -0.7 + 1.9j std::array<double, 2> w{-0.7, 1.9}; return 0; }
Now you could create a more standard function:
std::array<double, 2> add( std::array<double, 2> const&x, std::array<double, 2> const&y ) { return std::array<double, 2>{x[0] + y[0], x[1] + y[1]}; } std::array<double, 2> add( std::array<double, 2> const&x, std::array<double, 2> const&y ) { return std::array<double, 2>{x[0]*y[0] - x[1]*y[1], x[0]*y[1] + x[1]*y[0]}; }
Still, this is not very satisfying. consequently, a class sounds much more reasonable.
We will start with two member variables:
Here is this basic class. The class definition declares all member functions and all member variables. The constructor initializes the
#include <string> #include <cmath> // Class declaration class Complex; // Class definition class Complex { public: // Default values of parameters must be // declared in the class definition Complex( double real = 0.0, double imag = 0.0 ); double real() const; double imag() const; std::string to_string() const; void real( double real ); void imag( double imag ); private: double real_; double imag_; }; // Member function definitions // Constructor // - do not repeat the default values here Complex::Complex( double real, double imag ): real_{real}, imag_{imag} { // Empty constructor } // Access the real part double Complex::real() const { return real_; } // Access the imaginary part double Complex::imag() const { return imag_; } // The complex number should be written in standard form, either // 3.54 + 2.91j // 3.54 - 2.53j // and not something odd like the following if the imaginary part is negative // 3.54 + -2.53j std::string Complex::to_string() const { if ( imag() >= 0.0 ) { return std::to_string( real() ) + " + " + std::to_string( imag() ) + "j"; } else { return std::to_string( real() ) + " - " + std::to_string( -imag() ) + "j"; } } // Assign the real part void Complex::real( double real ) { real_ = real; } // Assign the imaginary part void Complex::imag( double imag ) { imag_ = imag; }
You will note that Complex::to_string() uses real() and imag() instead of real_ and imag_ directly. Nothing prevents you from having written a function
std::string Complex::to_string() const { if ( imag() >= 0.0 ) { return std::to_string( real() ) + " + " + std::to_string( imag() ) + "j"; } else { return std::to_string( real() ) + " - " + std::to_string( -imag() ) + "j"; } }
class Complex { public: // Default values of parameters must be // declared in the class definition Complex( double real = 0.0, double imag = 0.0 ); // ...other accessing member functions ... double abs() const; double arg() const; Complex conj() const; // ...other modifying member functions ... private: double real_; double imag_; }; // Member function definitions double Complex::abs() const { return std::sqrt( real()*real() + imag()*imag() ); } double Complex::arg() const { return std::atan2( imag(), real() ); } Complex Complex::conj() const { return Complex{ real(), -imag() }; }
Next, we must define complex addition, subtraction, multiplication and division. Fortunately, C++ allows operator overloading. For example, we would want all of the following to work:
Complex result{}; Complex w{ 3.2, -5.4}; Complex z{-7.1, 0.6}; result = w + z; // Add two complex numbers result = w - z; // Subtract the second complex number from the first result = w*z; // Multiply two complex numbers result = w/z; // Divide the first complex number by the second result = +z; // Does nothing... result = -z; // Returns the additive inverse result = w + 8.9; // Same as z + (8.9 + 0.0j) result = w - 8.9; // Same as z - (8.9 + 0.0j) result = w * 8.9; // Same as z*(8.9 + 0.0j) result = w / 8.9; // Same as z/(8.9 + 0.0j)
Operator overloading is performed as follows:
None of these operators should modify any argument, so the member functions (its best to think of them as member functions) should be declared const and the argument should be passed by constant reference. Again, in all cases, we could have accessed real_ and imag_, but it is preferable to use the accessing and modifying member functions.
class Complex { public: // Default values of parameters must be // declared in the class definition Complex( double real = 0.0, double imag = 0.0 ); // ...other accessing member functions ... Complex operator+( Complex const &z ) const; Complex operator-( Complex const &z ) const; Complex operator*( Complex const &z ) const; Complex operator/( Complex const &z ) const; Complex operator+() const; Complex operator-() const; Complex operator+( double r ) const; Complex operator-( double r ) const; Complex operator*( double r ) const; Complex operator/( double r ) const; // ...other modifying member functions ... private: double real_; double imag_; }; // Member function definitions Complex Complex::operator+( Complex const &z ) const { return Complex{ real() + z.real(), imag() + z.imag() }; } Complex Complex::operator-( Complex const &z ) const { return Complex{ real() - z.real(), imag() - z.imag() }; } Complex Complex::operator*( Complex const &z ) const { return Complex{ real()*z.real() - imag()*z.imag(), real()*z.imag() + imag()*z.real() }; } Complex Complex::operator/( Complex const &z ) const { double absz2{ z.real()*z.real() + z.imag()*z.imag() }; return Complex{ (real()*z.real() - imag()*z.imag())/absz2, (real()*z.imag() + imag()*z.real())/absz2 }; } Complex Complex::operator+() const { return Complex{ real(), imag() }; } Complex Complex::operator-() const { return Complex{ -real(), -imag() }; } Complex Complex::operator+( double r ) const { return Complex{ real() + r, imag() }; } Complex Complex::operator-( double r ) const { return Complex{ real() - r, imag() }; } Complex Complex::operator*( double r ) const { return Complex{ real()*r, imag()*r }; } Complex Complex::operator/( double r ) const { return Complex{ real()/r, imag()/r }; }
Now, you may be wondering, we've defined z + r and z*r; how about r + z and r*z. Both addition and multiplication are commutative, so r + z == z + r and r*z == z*r, but r/z does not equal z/r.
To create these, we must define global functions, but these global functions will put a complex number first, so it will call one of the above operators:
// Global function declarations Complex operator+( double r, Complex const &z ); Complex operator-( double r, Complex const &z ); Complex operator*( double r, Complex const &z ); Complex operator/( double r, Complex const &z ); // Global function definitions Complex operator+( double r, Complex const &z ) { return z + r; } Complex operator-( double r, Complex const &z ) { return (-z) + r; } Complex operator*( double r, Complex const &z ) { return z*r; } Complex operator/( double r, Complex const &z ) { return z.conj()*(r/(z.real()*z.real() + z.imag()*z.imag())); }
Any serious engineer using this class will need functions that return both exponential functions and trigonometric functions evaluated at complex numbers.
class Complex { public: // Default values of parameters must be // declared in the class definition Complex( double real = 0.0, double imag = 0.0 ); // ...other accessing member functions ... Complex sqrt() const; Complex cos() const; Complex sin() const; Complex tan() const; Complex acos() const; Complex asin() const; Complex atan() const; Complex cosh() const; Complex sinh() const; Complex tanh() const; Complex acosh() const; Complex asinh() const; Complex atanh() const; Complex exp() const; Complex log() const; // ...other modifying member functions ... private: double real_; double imag_; }; ///////////////////////////////// // Member function definitions // ///////////////////////////////// Complex Complex::sqrt() const { return Complex{ std::sqrt( (abs() + real())/2.0 ), std::copysign( std::sqrt( (abs() - real())/2.0 ), imag() ) }; } Complex Complex::cos() const { return Complex{ std::cos(real())*std::cosh(imag()), - std::sin(real())*std::sinh(imag()) }; } Complex Complex::sin() const { return Complex{ std::sin(real())*std::cosh(imag()), std::cos(real())*std::sinh(imag()) }; } Complex Complex::tan() const { double cosr{ std::cos( real() ) }; double sinhi{ std::sinh( imag() ) }; double denom{ cosr*cosr + sinhi*sinhi }; return Complex{ std::sin(real())*cosr/denom, std::cosh(imag())*sinhi/denom }; } Complex Complex::acos() const { double realp2{ (real() + 1.0)*(real() + 1.0) }; double realm2{ (real() - 1.0)*(real() - 1.0) }; double imagz2{ imag()*imag() }; double p{0.5*std::sqrt(realp2 + imagz2)}; double m{0.5*std::sqrt(realm2 + imagz2)}; double pm{p + m}; return Complex{ M_PI - std::acos( m - p ), std::copysign( std::log( pm + std::sqrt(pm*pm - 1.0)), -imag() ) }; } Complex Complex::asin() const { double realp2{ (real() + 1.0)*(real() + 1.0) }; double realm2{ (real() - 1.0)*(real() - 1.0) }; double imagz2{ imag()*imag() }; double p{0.5*std::sqrt(realp2 + imagz2)}; double m{0.5*std::sqrt(realm2 + imagz2)}; double pm{p + m}; return Complex{ -std::asin( m - p ), std::copysign( std::log( pm + std::sqrt(pm*pm - 1.0)), imag() ) }; } Complex Complex::atan() const { double realz2{ real()*real() }; double imagp2{ (imag() + 1.0)*(imag() + 1.0) }; double imagm2{ (imag() - 1.0)*(imag() - 1.0) }; return Complex{ 0.5*( std::atan2( real(), 1.0 - imag() ) - std::atan2(-real(), imag() + 1.0 ) ), 0.25*std::log( (realz2 + imagp2)/(realz2 + imagm2) ) }; } Complex Complex::cosh() const { return Complex{ std::cosh(real())*std::cos(imag()), std::sinh(real())*std::sin(imag()) }; } Complex Complex::sinh() const { return Complex{ std::sinh(real())*std::cos(imag()), std::cosh(real())*std::sin(imag()) }; } Complex Complex::tanh() const { double sinhr{ std::sinh( real() ) }; double cosi{ std::cos( imag() ) }; double denom{ sinhr*sinhr + cosi*cosi }; return Complex{ std::cosh(real())*sinhr/denom, std::sin(imag())*cosi/denom }; } Complex Complex::acosh() const { double realp2{ (real() + 1.0)*(real() + 1.0) }; double realm2{ (real() - 1.0)*(real() - 1.0) }; double imagz2{ imag()*imag() }; double p{0.5*std::sqrt(realp2 + imagz2)}; double m{0.5*std::sqrt(realm2 + imagz2)}; double pm{p + m}; return Complex{ std::log( pm + std::sqrt(pm*pm - 1.0)), std::copysign( M_PI - std::acos( m - p ), imag() ) }; } Complex Complex::asinh() const { double realz2{ real()*real() }; double imagp2{ (imag() + 1.0)*(imag() + 1.0) }; double imagm2{ (imag() - 1.0)*(imag() - 1.0) }; double p{0.5*std::sqrt(realz2 + imagp2)}; double m{0.5*std::sqrt(realz2 + imagm2)}; double pm{p + m}; return Complex{ std::copysign( std::log( pm + std::sqrt(pm*pm - 1.0)), real() ), -std::asin( m - p ) }; } Complex Complex::atanh() const { double realp2{ (real() + 1.0)*(real() + 1.0) }; double realm2{ (real() - 1.0)*(real() - 1.0) }; double imagz2{ imag()*imag() }; return Complex{ 0.25*std::log( (realp2 + imagz2)/(realm2 + imagz2) ), 0.5*( std::atan2( imag(), real() + 1.0 ) - std::atan2(-imag(), 1.0 - real() ) ) }; } Complex Complex::exp() const { double r{ std::exp( real() ) }; return Complex{ r*std::cos( imag() ), r*std::sin( imag() ) }; } Complex Complex::log() const { return Complex{ std::log( abs() ), std::atan2( imag(), real() ) }; }
One problem with the objected-oriented approach is that the notation is not standard with any text book. Consequently, it is undesirable to have to write, for example,
Complex z{-7.1, 0.6}; std::cout << (z.cos()*z.cos() + z.sin()*z.sin()) << std::endl;
Instead, the average user would like to use:
std::cout << (cos( z )*cos( z ) + sin( z )*sin( z )) << std::endl;
To facilitate this, we will also include global functions:
// Global function declarations double real( Complex const &z ); double imag( Complex const &z ); double abs( Complex const &z ); double arg( Complex const &z ); Complex conj( Complex const &z ); Complex cos( Complex const &z ); Complex sin( Complex const &z ); Complex tan( Complex const &z ); Complex cosh( Complex const &z ); Complex sinh( Complex const &z ); Complex tanh( Complex const &z ); Complex exp( Complex const &z ); // Global function definitions double real( Complex const &z ) { return z.real(); } double imag( Complex const &z ) { return z.imag(); } double abs( Complex const &z ) { return z.abs(); } double arg( Complex const &z ) { return z.arg(); } Complex conj( Complex const &z ) { return z.conj(); } Complex cos( Complex const &z ) { return z.cos(); } Complex sin( Complex const &z ) { return z.sin(); } Complex tan( Complex const &z ) { return z.tan(); } Complex cosh( Complex const &z ) { return z.cosh(); } Complex sinh( Complex const &z ) { return z.sinh(); } Complex tanh( Complex const &z ) { return z.tanh(); } Complex exp( Complex const &z ) { return z.exp(); }
Finally, it would be nice to use
std::cout << z << std::endl;
instead of
std::cout << z.to_string() << std::endl;
After all, it is already possible to print many other objects and primitive data types, why not this class? For this, we must define another global function that implements the << operator when the left-hand argument is an output-stream object (std::ostream) and the second is a complex object:
// Global function declarations std::ostream &operator<<( std::ostream &out, Complex const &z ); // Global function definitions std::ostream &operator<<( std::ostream &out, Complex const &z ) { // The ostream operator already knows how to print a string // - the ostream object is passed by reference and returned by reference // - this make it possible to use std::cout << w << " + " << z // << " = " << (w + z) << std::endl; return out << z.to_string(); }
You can view this entire implementation at repl.it.
Other features that can be added:
Adding member functions and global functions for comparison operators:
// Global function declarations bool operator==( double r, Complex const &z ); bool operator!=( double r, Complex const &z ); // Member functions bool operator==( Complex const &z ) const; bool operator==( double r ) const; bool operator!=( Complex const &z ) const; bool operator!=( double r ) const; // Member function definitions bool Complex::operator==( Complex const &z ) const { return (real() == z.real()) && (imag() == z.imag()); } bool Complex::operator==( double r ) const { return (real() == r) && (imag() == 0.0); } bool Complex::operator!=( Complex const &z ) const { return (real() != z.real()) || (imag() != z.imag()); } bool Complex::operator!=( double r ) const { return (real() != r) || (imag() != 0.0); } // Global function definitions bool operator==( double r, Complex const &z ) { return z == r; } bool operator!=( double r, Complex const &z ) { return z != r; }
One cannot universally define what it means for z < w to be either true or false, so these will not be implemented.
Adding member functions for auto-assignment and auto-increment operators:
// Member functions Complex &operator+=( Complex const &z ); Complex &operator+=( double r ); Complex &operator-=( Complex const &z ); Complex &operator-=( double r ); Complex &operator*=( Complex const &z ); Complex &operator*=( double r ); Complex &operator/=( Complex const &z ); Complex &operator/=( double r ); Complex &operator++(); // For ++z; Complex operator++( int ); // For z++; Complex &operator--(); // For --z; Complex operator--( int ); // For z--; // Member function definitions Complex &Complex::operator+=( Complex const &z ) { real_ += z.real(); imag_ += z.imag(); return *this; } Complex &Complex::operator+=( double r ) { real_ += r; return *this; } Complex &Complex::operator-=( Complex const &z ) { real_ -= z.real(); imag_ -= z.imag(); return *this; } Complex &Complex::operator-=( double r ) { real_ -= r; return *this; } // We must be careful here: // - if we are multiplying z by itself, then we are // updating z.real() with the first statement, too Complex &Complex::operator*=( Complex const &z ) { // Just in case we are multiplying z by itself, we will save // both real_ and z.real_ and use these in calculating imag_ double r{ real() }; double zr{ z.real() }; real_ = real()*z.real() - imag()*z.imag(); imag_ = r*z.imag() + imag()*zr; return *this; } // We must be careful here: // - if we are multiplying z by itself, then we are // updating z.real() with the first statement, too Complex &Complex::operator*=( double r ) { real_ *= r; imag_ *= r; return *this; } Complex &Complex::operator/=( Complex const &z ) { double absz2{ z.real()*z.real() + z.imag()*z.imag() }; // Just in case we are multiplying z by itself, we will save // both real_ and z.real_ and use these in calculating imag_ double r{ real() }; double zr{ z.real() }; real_ = (real()*z.real() - imag()*z.imag())/absz2; imag_ = ( r*z.imag() + imag()*zr )/absz2; return *this; } Complex &Complex::operator/=( double r ) { real_ /= r; imag_ /= r; return *this; } // This must first increment the real part, and // then return a reference to the updated object Complex &Complex::operator++() { ++real_; return *this; } // This must increment the real part, but // the object returned must be the original complex number // - real_++ returns the original value of 'real_' Complex Complex::operator++( int ) { return Complex{ real_++, imag() }; } // This must first decrement the real part, and // then return a reference to the updated object Complex &Complex::operator--() { --real_; return *this; } // This must decrement the real part, but // the object returned must be the original complex number // - real_-- returns the original value of 'real_' Complex Complex::operator--( int ) { return Complex{ real_--, imag() }; }
Fortunately, the Standard Template Library (STL) already has a complex number library. You are welcome to look at it at it and use it as appropriate:
You will note that unlike the example above, the Complex-number class in the STL is templated; thus, you must indicate the type you want for the member variables in your declarations:
#include <complex> #include <iostream> int main(); int main() { std::complex<double> z{0.6, 0.8}; // Demonstrate that the Taylor series of the // exponential function still works for complex // values of the exponent. std::cout.precision( 16 ); std::cout << "exp( 0.6 + 0.8j ) = " << std::exp( z ) << std::endl; std::complex<double> approx{1.0}; double factorial{1.0}; std::complex<double> power{1.0}; std::cout << " Order 0\tapproximation: " << approx << std::endl; for ( int k{1}; k < 20; ++k ) { power *= z; factorial *= k; approx += power/factorial; std::cout << " Order " << k << "\tapproximation: " << approx << std::endl; } std::cout << "Approximation: " << approx << std::endl; return 0; }
You can run this example on repl.it. As you should note, it is as easy to use complex numbers in C++ as it is to use double. You will note, however, that all trigonometric and other functions are only implemented in a functional approach, meaning, you cannot, for example, call z.exp(), and instead you must use the notation of std::exp( z ).