A tiny primer on Complex numbers184 stolen directly from Wikipedia:
A complex number is a number that can be expressed in the form \(a+bi\), where \(a\) and \(b\) are real numbers [i.e. floating point types in C], and \(i\) represents the imaginary unit, satisfying the equation \(i^2=−1\). Because no real number satisfies this equation, \(i\) is called an imaginary number. For the complex number \(a+bi\), \(a\) is called the real part, and \(b\) is called the imaginary part.
But that’s as far as I’m going to go. We’ll assume that if you’re reading this chapter, you know what a complex number is and what you want to do with them.
And all we need to cover is C’s faculties for doing so.
Turns out, though, that complex number support in a compiler is an optional feature. Not all compliant compilers can do it. And the ones that do, might do it to various degrees of completeness.
You can test if your system supports complex numbers with:
#ifdef __STDC_NO_COMPLEX__
#error Complex numbers not supported!
#endif
Furthermore, there is a macro that indicates adherence to the ISO 60559 (IEEE 754) standard for floating point math with complex numbers, as well as the presence of the _Imaginary
type.
#if __STDC_IEC_559_COMPLEX__ != 1
#error Need IEC 60559 complex support!
#endif
More details on that are spelled out in Annex G in the C11 spec.
To use complex numbers, #include <complex.h>
.
With that, you get at least two types:
_Complex
complex
Those both mean the same thing, so you might as well use the prettier complex
.
You also get some types for imaginary numbers if you implementation is IEC 60559-compliant:
_Imaginary
imaginary
These also both mean the same thing, so you might as well use the prettier imaginary
.
You also get values for the imaginary number \(i\), itself:
I
_Complex_I _Imaginary_I
The macro I
is set to _Imaginary_I
(if available), or _Complex_I
. So just use I
for the imaginary number.
One aside: I’ve said that if a compiler has __STDC_IEC_559_COMPLEX__
set to 1
, it must support _Imaginary
types to be compliant. That’s my read of the spec. However, I don’t know of a single compiler that actually supports _Imaginary
even though they have __STDC_IEC_559_COMPLEX__
set. So I’m going to write some code with that type in here I have no way of testing. Sorry!
OK, so now we know there’s a complex
type, how can we use it?
Since the complex number has a real and imaginary part, but both of them rely on floating point numbers to store values, we need to also tell C what precision to use for those parts of the complex number.
We do that by just pinning a float
, double
, or long double
to the complex
, either before or after it.
Let’s define a complex number that uses float
for its components:
float complex c; // Spec prefers this way
complex float c; // Same thing--order doesn't matter
So that’s great for declarations, but how do we initialize them or assign to them?
Turns out we get to use some pretty natural notation. Example!
double complex x = 5 + 2*I;
double complex y = 10 + 3*I;
For \(5+2i\) and \(10+3i\), respectively.
We’re getting there…
We’ve already seen one way to write a complex number:
double complex x = 5 + 2*I;
There’s also no problem using other floating point numbers to build it:
double a = 5;
double b = 2;
double complex x = a + b*I;
There is also a set of macros to help build these. The above code could be written using the CMPLX()
macro, like so:
double complex x = CMPLX(5, 2);
As far as I can tell in my research, these are almost equivalent:
double complex x = 5 + 2*I;
double complex x = CMPLX(5, 2);
But the CMPLX()
macro will handle negative zeros in the imaginary part correctly every time, whereas the other way might convert them to positive zeros. I think185 This seems to imply that if there’s a chance the imaginary part will be zero, you should use the macro… but someone should correct me on this if I’m mistaken!
The CMPLX()
macro works on double
types. There are two other macros for float
and long double
: CMPLXF()
and CMPLXL()
. (These “f” and “l” suffixes appear in virtually all the complex-number-related functions.)
Now let’s try the reverse: if we have a complex number, how do we break it apart into its real and imaginary parts?
Here we have a couple functions that will extract the real and imaginary parts from the number: creal()
and cimag()
:
double complex x = 5 + 2*I;
double complex y = 10 + 3*I;
("x = %f + %fi\n", creal(x), cimag(x));
printf("y = %f + %fi\n", creal(y), cimag(y)); printf
for the output:
x = 5.000000 + 2.000000i y = 10.000000 + 3.000000i
Note that the i
I have in the printf()
format string is a literal i
that gets printed—it’s not part of the format specifier. Both return values from creal()
and cimag()
are double
.
And as usual, there are float
and long double
variants of these functions: crealf()
, cimagf()
, creall()
, and cimagl()
.
Arithmetic can be performed on complex numbers, though how this works mathematically is beyond the scope of the guide.
#include <stdio.h>
#include <complex.h>
int main(void)
{
double complex x = 1 + 2*I;
double complex y = 3 + 4*I;
double complex z;
z = x + y;
printf("x + y = %f + %fi\n", creal(z), cimag(z));
z = x - y;
printf("x - y = %f + %fi\n", creal(z), cimag(z));
z = x * y;
printf("x * y = %f + %fi\n", creal(z), cimag(z));
z = x / y;
printf("x / y = %f + %fi\n", creal(z), cimag(z));
}
for a result of:
x + y = 4.000000 + 6.000000i
x - y = -2.000000 + -2.000000i
x * y = -5.000000 + 10.000000i x / y = 0.440000 + 0.080000i
You can also compare two complex numbers for equality (or inequality):
#include <stdio.h>
#include <complex.h>
int main(void)
{
double complex x = 1 + 2*I;
double complex y = 3 + 4*I;
printf("x == y = %d\n", x == y); // 0
printf("x != y = %d\n", x != y); // 1
}
with the output:
x == y = 0 x != y = 1
They are equal if both components test equal. Note that as with all floating point, they could be equal if they’re close enough due to rounding error186.
But wait! There’s more than just simple complex arithmetic!
Here’s a summary table of all the math functions available to you with complex numbers.
I’m only going to list the double
version of each function, but for all of them there is a float
version that you can get by appending f
to the function name, and a long double
version that you can get by appending l
.
For example, the cabs()
function for computing the absolute value of a complex number also has cabsf()
and cabsl()
variants. I’m omitting them for brevity.
Function | Description |
---|---|
ccos() |
Cosine |
csin() |
Sine |
ctan() |
Tangent |
cacos() |
Arc cosine |
casin() |
Arc sine |
catan() |
Play Settlers of Catan |
ccosh() |
Hyperbolic cosine |
csinh() |
Hyperbolic sine |
ctanh() |
Hyperbolic tangent |
cacosh() |
Arc hyperbolic cosine |
casinh() |
Arc hyperbolic sine |
catanh() |
Arc hyperbolic tangent |
Function | Description |
---|---|
cexp() |
Base-\(e\) exponential |
clog() |
Natural (base-\(e\)) logarithm |
Function | Description |
---|---|
cabs() |
Absolute value |
cpow() |
Power |
csqrt() |
Square root |
Function | Description |
---|---|
creal() |
Return real part |
cimag() |
Return imaginary part |
CMPLX() |
Construct a complex number |
carg() |
Argument/phase angle |
conj() |
Conjugate187 |
cproj() |
Projection on Riemann sphere |