复数是可以表示为 \(a+bi\) 形式的数,其中 \(a\) 和 \(b\) 是实数 [即 C 中的浮点类型],而 \(i\) 表示虚数单位,满足方程 \(i^2=−1\)。因为没有实数满足这个方程,所以 \(i\) 被称为虚数。对于复数 \(a+bi\),\(a\) 称为实部,\(b\) 称为虚部。
不过我最多也就讲到这儿。我们假定,你既然在读这一章,就已经知道复数是什么、以及你想拿它做什么了。
而我们要讲的,只是 C 为此提供的那些能力。
然而,编译器对复数的支持其实是一项可选特性。并非所有符合标准的编译器都能做到。而能做到的那些,完整程度也参差不齐。
你可以这样测试你的系统是否支持复数:
#ifdef __STDC_NO_COMPLEX__
#error Complex numbers not supported!
#endif此外,还有一个宏,用来指示是否遵循 ISO 60559(IEEE 754)关于复数浮点运算的标准,以及是否存在 _Imaginary 类型。
#if __STDC_IEC_559_COMPLEX__ != 1
#error Need IEC 60559 complex support!
#endif更多细节在 C11 规范的附录 G 中有详细说明。
要使用复数,需 #include <complex.h>。
这样一来,你就至少得到两种类型:
_Complex
complex这两者含义相同,所以你不妨用更漂亮的 complex。
如果你的实现符合 IEC 60559,还会得到一些用于虚数的类型:
_Imaginary
imaginary这两者也含义相同,所以你不妨用更漂亮的 imaginary。
你还会得到虚数 \(i\) 本身的值:
I
_Complex_I
_Imaginary_I宏 I 被设为 _Imaginary_I(如果可用)或 _Complex_I。所以表示虚数时直接用 I 就行。
顺便说一句:我说过,如果某个编译器把 __STDC_IEC_559_COMPLEX__ 设为 1,那它就必须支持 _Imaginary 类型才算符合标准。这是我对规范的理解。然而,我知道的编译器里,没有一个在设置了 __STDC_IEC_559_COMPLEX__ 的情况下真正支持 _Imaginary。所以下面我会写一些用到该类型的代码,而这些代码我根本没法测试。抱歉!
好,现在我们知道有 complex 类型了,可它怎么用呢?
既然复数有实部和虚部,而这两部分都依赖浮点数来存储数值,我们就还得告诉 C:复数的这两部分要使用什么精度。
做法很简单:把 float、double 或 long double 贴在 complex 前面或后面即可。
我们来定义一个用 float 作为其组成部分的复数:
float complex c; // 规范更推荐这种写法
complex float c; // 一回事——顺序无关紧要这对声明来说当然很好,可我们该怎么初始化它们或给它们赋值呢?
结果是,我们可以使用一种相当自然的记法。看例子!
double complex x = 5 + 2*I;
double complex y = 10 + 3*I;分别对应 \(5+2i\) 和 \(10+3i\)。
我们这就快讲到了……
我们已经见过一种书写复数的方式:
double complex x = 5 + 2*I;用其他浮点数来构造它也没有问题:
double a = 5;
double b = 2;
double complex x = a + b*I;此外还有一组宏可以帮助构造它们。上面的代码可以用 CMPLX() 宏写成这样:
double complex x = CMPLX(5, 2);据我查证所能确定的,这两者是几乎等价的:
double complex x = 5 + 2*I;
double complex x = CMPLX(5, 2);但 CMPLX() 宏每次都能正确处理虚部中的负零,而另一种写法则可能把它们变成正零。我认为185这似乎意味着:只要虚部有可能为零,就应该用这个宏……不过如果我搞错了,希望有人能纠正我!
CMPLX() 宏作用于 double 类型。另外还有两个宏分别用于 float 和 long double:CMPLXF() 和 CMPLXL()。(这些“f”和“l”后缀几乎出现在所有与复数相关的函数中。)
现在来试试反方向:如果我们有一个复数,怎么把它拆成实部和虚部?
这里有两个函数,可以从一个数中提取实部和虚部:creal() 和 cimag():
double complex x = 5 + 2*I;
double complex y = 10 + 3*I;
printf("x = %f + %fi\n", creal(x), cimag(x));
printf("y = %f + %fi\n", creal(y), cimag(y));输出如下:
x = 5.000000 + 2.000000i
y = 10.000000 + 3.000000i注意,我在 printf() 格式字符串里写的 i 是一个会被原样打印出来的字面 i——它不是格式说明符的一部分。creal() 和 cimag() 的返回值都是 double。
和往常一样,这些函数也有 float 和 long double 版本:crealf()、cimagf()、creall() 和 cimagl()。
复数上可以进行算术运算,不过其数学原理超出了本指南的范围。
#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));
}结果如下:
x + y = 4.000000 + 6.000000i
x - y = -2.000000 + -2.000000i
x * y = -5.000000 + 10.000000i
x / y = 0.440000 + 0.080000i你还可以比较两个复数是否相等(或不等):
#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
}输出为:
x == y = 0
x != y = 1如果两个分量的比较结果都相等,那么这两个复数就相等。注意,和所有浮点数一样,由于舍入误差,只要两者足够接近,它们也可能被判为相等186。
等等!复数可不止简单的算术运算!
下面这张汇总表列出了处理复数时可用的全部数学函数。
我只列出每个函数的 double 版本,但它们全都有 float 版本(在函数名后加上 f)和 long double 版本(在函数名后加上 l)。
例如,用于计算复数绝对值的 cabs() 函数也有 cabsf() 和 cabsl() 两个变体。为简洁起见,我把它们省略了。
| 函数 | 说明 |
|---|---|
ccos() |
余弦 |
csin() |
正弦 |
ctan() |
正切 |
cacos() |
反余弦 |
casin() |
反正弦 |
catan() |
玩《卡坦岛》 |
ccosh() |
双曲余弦 |
csinh() |
双曲正弦 |
ctanh() |
双曲正切 |
cacosh() |
反双曲余弦 |
casinh() |
反双曲正弦 |
catanh() |
反双曲正切 |
| 函数 | 说明 |
|---|---|
cexp() |
以 \(e\) 为底的指数函数 |
clog() |
自然对数(以 \(e\) 为底) |
| 函数 | 说明 |
|---|---|
cabs() |
绝对值 |
cpow() |
幂 |
csqrt() |
平方根 |
| 函数 | 说明 |
|---|---|
creal() |
返回实部 |
cimag() |
返回虚部 |
CMPLX() |
构造一个复数 |
carg() |
辐角/相位角 |
conj() |
共轭187 |
cproj() |
黎曼球面上的投影 |