| 目录 |

36 复数

下面是关于复数184的一点入门介绍,直接抄自维基百科:

复数是可以表示为 \(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 中有详细说明。

36.1 复数类型

要使用复数,需 #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 类型了,可它怎么用呢?

36.2 给复数赋值

既然复数有实部和虚部,而这两部分都依赖浮点数来存储数值,我们就还得告诉 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\)。

36.3 构造、分解与打印

我们这就快讲到了……

我们已经见过一种书写复数的方式:

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()。

36.4 复数的算术运算与比较

复数上可以进行算术运算,不过其数学原理超出了本指南的范围。

#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。

36.5 复数数学

等等!复数可不止简单的算术运算!

下面这张汇总表列出了处理复数时可用的全部数学函数。

我只列出每个函数的 double 版本,但它们全都有 float 版本(在函数名后加上 f)和 long double 版本(在函数名后加上 l)。

例如,用于计算复数绝对值的 cabs() 函数也有 cabsf() 和 cabsl() 两个变体。为简洁起见,我把它们省略了。

36.5.1 三角函数

函数 说明
ccos() 余弦
csin() 正弦
ctan() 正切
cacos() 反余弦
casin() 反正弦
catan() 玩《卡坦岛》
ccosh() 双曲余弦
csinh() 双曲正弦
ctanh() 双曲正切
cacosh() 反双曲余弦
casinh() 反双曲正弦
catanh() 反双曲正切

36.5.2 指数与对数函数

函数 说明
cexp() 以 \(e\) 为底的指数函数
clog() 自然对数(以 \(e\) 为底)

36.5.3 幂与绝对值函数

函数 说明
cabs() 绝对值
cpow() 幂
csqrt() 平方根

36.5.4 操作函数

函数 说明
creal() 返回实部
cimag() 返回虚部
CMPLX() 构造一个复数
carg() 辐角/相位角
conj() 共轭187
cproj() 黎曼球面上的投影


| 目录 |