Skip to the content of the web site.

Complex number class

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.

The basic class

We will start with two member variables:

  • The private member variables will be suffixed with an underscore to identify them as such.
  • The constructor of the class will take two parameters, each of which has a default value of 0.0, and these two member variables will be assigned the real and imaginary parts of the complex number.
  • Each member variable will be associated with two public member functions, one that retrieves the value, and the other that modifies the value.

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";
    }
}

Adding additional 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 ...
	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() };
}

Starting complex arithmetic

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:

  • We define a member function that has a special name; for example Complex::operator+( Complex const &z ).
  • The programmer has the choice of calling w.operator+( z ) to call this member function, just like any other member function.
  • The compiler, however, if it sees w + z where w is in the complex number class, it will convert this internally to w.operator+( z ) and call the corresponding member function.

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 };
}

Completing complex arithmetic

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()));
}

Trigonometric and exponential functions

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() ) };
}

Functional approach

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();
}

Printing

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();
}

Implementation

You can view this entire implementation at repl.it.

Other possible features

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() };
}

The Standard Template Library

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:

cplusplus.com.

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 ).