M.Hiroi's Home Page

Linux Programming

お気楽C言語プログラミング超入門

[ PrevPage | Clang | NextPage ]

複素数

近年、多くのプログラミング言語で「複素数 (complex number)」がサポートされるようになりました。たとえば、C言語では 1999 年に発行された規格 C99 で複素数型が導入されました。Go 言語や Python でも複素数型をサポートしていますし、複素数用の標準ライブラリを用意している言語 (C++, Ruby, Haskell など) も多くあります。他の言語では、FORTRAN や Common Lisp が昔から複素数型をサポートしています。今回はC言語の複素数についてまとめてみました。

●C言語の数

C99 以前のC言語で使用できる数は「整数 (integer)」と「浮動小数点数 (floating point number)」の 2 種類しかありませんでした。下表に基本的な数を示します。

表 : 基本的な数 (64 ビット処理系 [clang version 10.0.0] の場合)
型名範囲
char-128 ~ 127 (1 バイト)
unsigned char0 ~ 255 (1 バイト)
short int-32768 ~ 32767 (2 バイト)
unsigned short int0 ~ 65535 (2 バイト)
int-2147483648 ~ 2147483647 (4 バイト)
unsigned int0 ~ 4294967295 (4 バイト)
long int-9223372036854775808 ~ 9223372036854775807 (8 バイト)
unsigned long int0 ~ 18446744073709551615 (8 バイト)
long long int-9223372036854775808 ~ 9223372036854775807 (8 バイト)
unsigned long long int0 ~ 18446744073709551615 (8 バイト)
float1.1754944E-38 ~ 3.4028235E+38 (32 bit 単精度)
double2.22507E-308 ~ 1.79769E+308 (64 bit 倍精度)
long double3.362103E-4932 ~ 1.189731e+4932 (拡張倍精度)

C言語の場合、数値の範囲は処理系によってかわります。規格で定められているのは最低限の条件だけです。一般に、32 bit 処理系では int, long が 4 バイトで、64 bit 処理系では int が 4 バイト、long が 4 バイトか 8 バイトになるようです。

浮動小数点数には IEEE 754 という標準仕様があり、近代的なプログラミング言語のほとんどは、IEEE 754 に準拠した浮動小数点数をサポートしています。浮動小数点数はすべての小数を正確に表現することはできないので、その値は近似的 (不正確) なものとなります。

IEEE 754 には通常の数値以外にも、負のゼロ (-0.0)、正負の無限大 (∞, -∞)、NaN (Not a Number, 非数) といった値が定義されています。これらの値はC言語 (C99) でも取り扱うことができます。

●無限大

一般に、無限大は値のオーバーフロー、ゼロ除算 (数値 / 0.0)、数学関数の計算結果 (たとえば log(0.0)) などで発生します。標準ヘッダファイル math.h には正の無限大を表すマクロ INFINITY が定義されています。書式付き出力関数の変換指示子 (e, f, g) では無限大を inf と表示します。

また、無限大を判定するマクロ isinf() や有限な数を判定するマクロ isfinite() も用意されています。isinf(x) は引数 x が正負の無限大であれば真を返します。isfinite(x) は x が正負の無限大ではなく非数でもなければ真を返します。簡単な使用例を示しましょう。

リスト : 無限大の使用例 (inf1.c)

#include <float.h>
#include <math.h>
#include <stdio.h>

int main()
{
  float a = INFINITY;
  double b = INFINITY;
  long double c = INFINITY;
  double d = DBL_MAX;
  double e = - DBL_MAX;
  printf("a = %f\n", a);
  printf("b = %f\n", b);
  printf("c = %Lf\n", c);
  printf("-a = %f\n", -a);
  printf("-b = %f\n", -b);
  printf("-c %Lf\n", -c);
  printf("d = %g\n", d);
  printf("e = %g\n", e);

  printf("isinf(b) = %d\n", isinf(b));
  printf("isinf(d) = %d\n", isinf(d));
  printf("isfinite(b) = %d\n", isfinite(b));
  printf("isfinite(d) = %d\n", isfinite(d));

  printf("d * 2 = %g\n", d * 2.0);
  printf("e * 2 = %g\n", e * 2.0);
  printf("1.0 / 0.0 = %f\n", 1.0 / 0.0);
  printf("log(0.0) = %f\n", log(0.0));
  return 0;
}
$ clang -lm inf1.c
$ ./a.out
a = inf
b = inf
c = inf
-a = -inf
-b = -inf
-c -inf
d = 1.79769e+308
e = -1.79769e+308
isinf(b) = 1
isinf(d) = 0
isfinite(b) = 0
isfinite(d) = 1
d * 2 = inf
e * 2 = -inf
1.0 / 0.0 = inf
log(0.0) = -inf

無限大は他の数値と比較したり演算することもできますが、結果が非数 (NaN, 書式付き出力関数では nan と表示) になることもあります。

リスト : 無限大の使用例 (inf2.c)

#include <float.h>
#include <math.h>
#include <stdio.h>

int main()
{
  double a = INFINITY;
  double b = - INFINITY;
  printf("a = %f\n", a);
  printf("b = %f\n", b);

  printf("a == a => %d\n", a == a);
  printf("a == b => %d\n", a == b);
  printf("a != a => %d\n", a != a);
  printf("a != b => %d\n", a != b);
  printf("a > b => %d\n", a > b);
  printf("a < b => %d\n", a < b);
  printf("a > 0.0 => %d\n", a > 0.0);
  printf("a < 0.0 => %d\n", a < 0.0);
  printf("b < 0.0 => %d\n", b < 0.0);

  printf("a + 10.0 = %g\n", a + 10.0);
  printf("a - 10.0 = %g\n", a - 10.0);
  printf("a * 10.0 = %g\n", a * 10.0);
  printf("a / 10.0 = %g\n", a / 10.0);

  printf("a + a = %g\n", a + a);
  printf("a * a = %g\n", a * a);
  printf("a - a = %g\n", a - a);
  printf("a * a = %g\n", a / a);
  printf("a + b = %g\n", a + b);
  printf("a * 0.0 = %g\n", a * 0.0);
  return 0;
}
$ clang inf2.c
$ ./a.out
a = inf
b = -inf
a == a => 1
a == b => 0
a != a => 0
a != b => 1
a > b => 1
a < b => 0
a > 0.0 => 1
a < 0.0 => 0
b < 0.0 => 1
a + 10.0 = inf
a - 10.0 = inf
a * 10.0 = inf
a / 10.0 = inf
a + a = inf
a * a = inf
a - a = -nan
a * a = -nan
a + b = -nan
a * 0.0 = -nan

●負のゼロ

負のゼロ (-0.0) は、計算結果が負の極めて小さな値でアンダーフローになったとき発生します。また、正の値を負の無限大で除算する、負の値を正の無限大で除算する、負の値と 0.0 を乗算しても -0.0 が得られます。書式付き出力関数では負のゼロを -0 と表示します。

IEEE 754 では、演算子 (C言語では ==) による 0.0 と -0.0 の比較は等しいと判定されます。数値の符号は標準モジュール math.h にある関数 signbit() で求めることができます。signbit(x) は引数 x が負ならば真を返します。これで 0.0 と -0.0 を区別することができます。

リスト : 負のゼロ (mzero1.c)

#include <math.h>
#include <stdio.h>

int main()
{
  double a = -1e-323;
  printf("a = %g\n", a);
  printf("a / 2 = %g\n", a / 2.0);
  printf("a / 4 = %g\n", a / 4.0);
  printf("1.0 / -inf = %g\n", 1.0 / (- INFINITY));
  printf("-1.0 / inf = %g\n", -1.0 / INFINITY);
  printf("-1.0 / 0.0 = %g\n", -1.0 * 0.0);
  double z1 = 0.0;
  double z2 = -0.0;
  printf("z1 = %g\n", z1);
  printf("z2 = %g\n", z2);
  printf("z1 == z2 => %d\n", z1 == z2);
  printf("z1 != z2 => %d\n", z1 != z2);
  printf("signbit(z1) => %d\n", signbit(z1));
  printf("signbit(z2) => %d\n", signbit(z2));
  return 0;
}
$ clang mzero1.c
$ ./a.out
a = -9.88131e-324
a / 2 = -4.94066e-324
a / 4 = -0
1.0 / -inf = -0
-1.0 / inf = -0
-1.0 / 0.0 = -0
z1 = 0
z2 = -0
z1 == z2 => 1
z1 != z2 => 0
signbit(z1) => 0
signbit(z2) => 1

なお、-0.0 は数学関数 (math.h の関数 atan2 など) や複素数の演算処理などで使われます。

リスト : 負のゼロ (mzero2.c)

#include <math.h>
#include <stdio.h>

int main()
{
  printf("sqrt(0.0) => %g\n", sqrt(0.0));
  printf("sqrt(-0.0) => %g\n", sqrt(-0.0));
  printf("atan2(0.0, -1.0) => %g\n", atan2(0.0, -1.0));
  printf("atan2(-0.0, -1.0) => %g\n", atan2(-0.0, -1.0));
  return 0;
}
$ clang -lm mzero2.c
$ ./a.out
sqrt(0.0) => 0
sqrt(-0.0) => -0
atan2(0.0, -1.0) => 3.14159
atan2(-0.0, -1.0) => -3.14159

●非数

NaN は数ではないことを表す特別な値 (非数) です。一般的には 0.0 / 0.0 といった不正な演算を行うと、その結果は NaN になります。標準ヘッダファイル math.h には NaN を表すマクロ NAN が定義されています。また、NaN を判定するマクロ isnan() も用意されています。

リスト : 非数 (nantest.c)

#include <math.h>
#include <stdio.h>

int main()
{
  printf("NaN = %g\n", NAN);
  printf("NaN / NaN = %g\n", NAN / NAN);
  printf("inf / inf = %g\n", INFINITY / INFINITY);
  printf("isnan(NaN) = %d\n", isnan(NAN));
  printf("isnan(inf) = %d\n", isnan(INFINITY));
  printf("isfinite(NaN) = %d\n", isfinite(NAN));
  printf("isfinite(1.234) = %d\n", isfinite(1.234));
  return 0;
}
$ clang nantest.c
$ ./a.out
NaN = nan
NaN / NaN = nan
inf / inf = nan
isnan(NaN) = 1
isnan(inf) = 0
isfinite(NaN) = 0
isfinite(1.234) = 1

●C言語の複素数

数学では複素数 \(z\) を \(x + yi\) と表記します。x を実部、y を虚部、i を虚数単位といいます。虚数単位は 2 乗すると -1 になる数です。実部と虚部の 2 つの数値を格納するデータ構造を用意すれば、プログラミング言語でも複素数を表すことができます。C言語で複素数を扱うときは標準ヘッダファイル complex.h をインクルードしてください。complex.h には複素数の計算に必要な定義、マクロ、関数が含まれています。

複素数のデータ型は '数値型 complex' で表します。数値型は実部と虚部のデータ型です。

double complex (実部 X と虚部 Y が double の数)
float complex (実部 X と虚部 Y が float の数)
long double complex (実部 X と虚部 Y が long double の数)

C言語は複素数 \(z = x + yi\) を x + y * I (または x + I * y) と表記します。I は虚数単位を表すマクロです。clang や gcc では 数値 + 数値i と記述することもできます。なお、規格 C11 では複素数型を生成するマクロ CMPLX(実部, 虚部) が complex.h に追加されました。

CMPLX(double, double)
CMPLXF(float, float)
CMPLXL(long double, long double)

なお、M.Hiroi が使用している clang (version 14.0.0) の場合、これらのマクロはサポートされていないようです。clang を使用するときは注意してください。

数値型が double の場合、実部は関数 creal() で、虚部は関数 cimag() で求めることができます。数値型が float の場合は crealf(), cimagf() に, long double の場合は creall(), cimagl() になります。複素数の虚部の符号を反転することを「複素共役」といいます。数値型が double の場合、複素共役は関数 conj() で、float の場合は conjf() で, long double の場合は conjl() で求めることができます。

簡単な例を示しましょう。

リスト : 複素数の簡単な使用例 (ctest1.c)

#include <stdio.h>
#include <complex.h>

// 複素数の表示
void cprint(double complex z)
{
  printf("%g%+gi\n", creal(z), cimag(z));
}

int main(void)
{
  double complex a = 1.0 + 2.0 * I;
  double complex b = 3.0 - 4.0i;
  double complex c = -5.0 + -6.0i;
  printf("a = "); cprint(a);
  printf("b = "); cprint(b);
  printf("c = "); cprint(c);
  printf("creal(a) = %g\n", creal(a));
  printf("cimag(a) = %g\n", cimag(a));
  printf("creal(b) = %g\n", creal(b));
  printf("cimag(b) = %g\n", cimag(b));
  printf("creal(c) = %g\n", creal(c));
  printf("cimag(c) = %g\n", cimag(c));
  printf("conj(a) = "); cprint(conj(a));
  printf("conj(b) = "); cprint(conj(b));
  printf("conj(c) = "); cprint(conj(c));
  return 0;
}
$ clang ctest1.c
$ ./a.out
a = 1+2i
b = 3-4i
c = -5-6i
creal(a) = 1
cimag(a) = 2
creal(b) = 3
cimag(b) = -4
creal(c) = -5
cimag(c) = -6
conj(a) = 1-2i
conj(b) = 3+4i
conj(c) = -5+6i

複素数は極形式 \(z = r (\cos \theta + i \sin \theta)\) で表すことができます。このとき、r を絶対値、\(\theta\) を偏角といいます。数値型が double の場合、絶対値は関数 cabs() で、float の場合は cabsf() で、long double の場合は cabsl() で求めることができます。

偏角は複素平面において正の実軸とベクトル (x, y) との角度を表します。数値型が double の場合、偏角は関数 carg() で求めることができます。float では cargf() を、long double では cargl() を使います。これらの関数の返り値 \(\theta\) の範囲は \(-\pi \leq \theta \leq \pi\) (\(\pi\) : 円周率) です。

リスト : 複素数の簡単な使用例 (ctest2.c)

#include <stdio.h>
#include <complex.h>

// 複素数の表示
void cprint(double complex z)
{
  printf("%g%+gi\n", creal(z), cimag(z));
}

int main(void)
{
  double x[9] = {-1.0, -1.0, 0.0, 1.0, 1.0,  1.0,  0.0, -1.0, -1.0};
  double y[9] = { 0.0,  1.0, 1.0, 1.0, 0.0, -1.0, -1.0, -1.0, -0.0};
  for (int n = 0; n < 9; n++) {
    double complex a = x[n] + y[n] * I;
    printf("a = ");
    cprint(a);
    printf("cabs(a) = %g\n", cabs(a));
    printf("carg(a) = %g\n", carg(a));
  }
  return 0;
}
$ clang -lm ctest2.c
$ ./a.out
a = -1+0i
cabs(a) = 1
carg(a) = 3.14159
a = -1+1i
cabs(a) = 1.41421
carg(a) = 2.35619
a = 0+1i
cabs(a) = 1
carg(a) = 1.5708
a = 1+1i
cabs(a) = 1.41421
carg(a) = 0.785398
a = 1+0i
cabs(a) = 1
carg(a) = 0
a = 1-1i
cabs(a) = 1.41421
carg(a) = -0.785398
a = 0-1i
cabs(a) = 1
carg(a) = -1.5708
a = -1-1i
cabs(a) = 1.41421
carg(a) = -2.35619
a = -1-0i
cabs(a) = 1
carg(a) = -3.14159

C言語の場合、\(-1.0+0.0i\) の偏角 \(\theta\) は \(\pi\) になり、\(-1.0-0.0i\) の偏角は \(-\pi\) になります。ゼロと負のゼロを区別しないプログラミング言語では、偏角 \(\theta\) の範囲を \(-\pi \lt \theta \leq \pi\) に制限して、\(-1.0 + 0.0i \ (= -1.0 - 0.0i)\) の偏角を \(\pi\) とします。

●複素数の四則演算

複素数の四則演算は次のようになります。

\( \begin{array}{l} (a + bi) + (c + di) = (a + c) + (b + d)i \\ (a + bi) - (c + di) = (a - c) + (b - d)i \\ (a + bi) \times (c + di) = (ac - bd) + (bc + ad)i \\ (a + bi) \div (c + di) = \dfrac{ac + bd + (bc - ad)i}{c^2 + d^2} \end{array} \)

これらの演算はC言語の演算子 +, -, * , / で行うことができます。複素数の場合、大小の比較演算は使えませんが、等値の判定は演算子 == や != で行うことができます。簡単な例を示しましょう。

リスト : 複素数の使用例 (ctest3.c)

#include <stdio.h>
#include <complex.h>

// 複素数の表示
void cprint(double complex z)
{
  printf("%g%+gi\n", creal(z), cimag(z));
}

int main(void)
{
  double complex a = 1.0 + 2.0i;
  double complex b = 3.0 + 4.0i;
  printf("a = "); cprint(a);
  printf("b = "); cprint(b);
  printf("a + b = "); cprint(a + b);
  printf("a - b = "); cprint(a - b);
  printf("a * b = "); cprint(a * b);
  printf("a / b = "); cprint(a / b);
  printf("a == a => %d\n", a == a);
  printf("a != a => %d\n", a != a);
  printf("a == b => %d\n", a == b);
  printf("a != b => %d\n", a != b);

  double complex c = CMPLX(1e300, 1e300);
  printf("c = "); cprint(c);
  printf("cabs(c) = %g\n", cabs(c));
  printf("1 / c = "); cprint(CMPLX(1.0, 0.0) / c);
  printf("c * c = "); cprint(c * c);
  return 0;
}
$ clang -lm ctest3.c
$ ./a.out
a = 1+2i
b = 3+4i
a + b = 4+6i
a - b = -2-2i
a * b = -5+10i
a / b = 0.44+0.08i
a == a => 1
a != a => 0
a == b => 0
a != b => 1
c = 1e+300+1e+300i
cabs(c) = 1.41421e+300
1 / c = 5e-301-5e-301i
c * c = -nan+infi

実部と虚部の値は inf や nan になることもあります。

●複素数の指数関数と対数関数

複素数を引数にとる指数関数はオイラー (Euler) の公式から導くことができます。

\( \begin{array}{l} e^{i\theta} = \cos \theta + i \sin \theta \quad (Euler's formula) \\ \begin{eqnarray} e^{x+iy} &=& e^x e^{iy} \\ &=& e^x (\cos y + i \sin y) \end{eqnarray} \end{array} \)

複素数の対数関数は複素数 z を絶対値 \(|z|\) と偏角 \(\theta\) を使って導くことができます。

\( \begin{array}{l} x + iy = |z| e^{i\theta} \\ \begin{eqnarray} \log_e (x + iy) &=& log_e |z| e^{i\theta} \\ &=& log_e |z| + log_e e^{i\theta} \\ &=& log_e |z| + i\theta, \quad (-\pi \leq \theta \leq \pi) \end{eqnarray} \end{array} \)

複素数 x, y のべき乗 \(x^y\) は次式で求めることができます。

\(x^y = e^{y\log x}\)

標準ヘッダファイル complex.h には、複素数に対応した関数 cexp, clog, cpow が定義されています。

指数関数: cexp(z)
double complex cexp(double complex z);
float complex cexpf(float complex z);
long double complex cexpl(long double complex z);
対数関数: clog(z)
double complex clog(double complex z);
float complex clogf(float complex z);
long double complex clogl(long double complex z);
べき乗: cpow(x, y) = xy
double complex cpow(double complex x, double complex y);
float complex cpowf(float complex x, float complex y);
long double complex cpowl(long double complex x, long double complex y);

簡単な使用例を示します。

リスト : 複素数の指数関数、対数関数、べき乗

#include <complex.h>
#include <stdio.h>

#define PI 3.141592653589793

// 複素数の表示
void cprint(double complex z)
{
  printf("%g%+gi\n", creal(z), cimag(z));
}

int main()
{
  printf("cexp(0 + i pi/4) = "); cprint(cexp(0.0 + I * PI / 4.0));
  printf("cexp(0 + i pi/2) = "); cprint(cexp(0.0 + I * PI / 2.0));
  printf("cexp(0 + i pi) = "); cprint(cexp(0.0 + I * PI));
  printf("cexp(0 - i pi) = "); cprint(cexp(0.0 - I * PI));
  printf("cexp(1 + i) = "); cprint(cexp(1.0 + I * 1.0));
  printf("\n");

  printf("clog(1 + i) = "); cprint(clog(1.0 + I * 1.0));
  printf("clog(1 + 0i) = "); cprint(clog(1.0 + I * 0.0));
  printf("clog(0 + i) = "); cprint(clog(0.0 + I * 1.0));
  printf("clog(1 - i) = "); cprint(clog(1.0 - I * 1.0));
  printf("clog(1e300 + i 1e300) = "); cprint(clog(1e300 + I * 1e300));
  printf("\n");

  double complex a = 1.0 + I * 1.0;
  printf("cpow(1+i, 0) = "); cprint(cpow(a, 0.0 + I * 0.0));
  printf("cpow(1+i, 1) = "); cprint(cpow(a, 1.0 + I * 0.0));
  printf("cpow(1+i, 2) = "); cprint(cpow(a, 2.0 + I * 0.0));
  printf("cpow(1+i, 3) = "); cprint(cpow(a, 3.0 + I * 0.0));
  printf("cpow(1+i, 1+i) = "); cprint(cpow(a, a));
  printf("cpow(1+2i, 3+4i) = "); cprint(cpow(1.0+I*2.0, 3.0+I*4.0));
  printf("\n");

  printf("clog(-1 + 0i) = "); cprint(clog(-1.0 + I * 0.0));
  printf("clog(-1 - 0i) = "); cprint(clog(-1.0 - I * 0.0));
  printf("clog(-1e300 + 0i) = "); cprint(clog(-1e300 + I * 0.0));
  printf("clog(-1e300 - 0i) = "); cprint(clog(-1e300 - I * 0.0));
  return 0;
}
$ clang -lm ctest4.c
$ ./a.out
cexp(0 + i pi/4) = 0.707107+0.707107i
cexp(0 + i pi/2) = 6.12323e-17+1i
cexp(0 + i pi) = -1+1.22465e-16i
cexp(0 - i pi) = -1-1.22465e-16i
cexp(1 + i) = 1.46869+2.28736i

clog(1 + i) = 0.346574+0.785398i
clog(1 + 0i) = 0+0i
clog(0 + i) = 0+1.5708i
clog(1 - i) = 0.346574-0.785398i
clog(1e300 + i 1e300) = 691.122+0.785398i

cpow(1+i, 0) = 1+0i
cpow(1+i, 1) = 1+1i
cpow(1+i, 2) = 1.22465e-16+2i
cpow(1+i, 3) = -2+2i
cpow(1+i, 1+i) = 0.273957+0.583701i
cpow(1+2i, 3+4i) = 0.12901+0.0339241i

clog(-1 + 0i) = 0+3.14159i
clog(-1 - 0i) = 0-3.14159i
clog(-1e300 + 0i) = 690.776+3.14159i
clog(-1e300 - 0i) = 690.776-3.14159i

最後の 4 つの例を見てください。関数 \(\log z \ (z = x + iy)\) は負の実軸 (\(-\infty \lt x \lt 0\)) において、\(x + 0.0i\) と \(x - 0.0i\) では値が異なります。数学では値を一つ返す関数を「一価関数」、複数の値を返す関数を「多価関数」といいます。ここで、定義域を制限することで多価関数を一価関数にみなすことを考えます。関数 \(\log z\) の場合、負の実軸を定義域から取り除けば、\(\log z\) を一価関数とみなすことができるわけです。

参考 URL 8 によると、この取り除いた領域を branch cut と呼ぶそうです。プログラミングでは branch cut を定義域から取り除くのではなく、その領域では不連続な関数とするそうです。参考文献 1 では「分枝切断線」、Python のドキュメントでは「分枝切断」と記述されています。本稿では branch cut を「分枝切断」と記述することにします。

プログラミング言語の場合、0.0 と -0.0 を区別する処理系であれば、C言語のように 2 つの値を区別することができます。0.0 と -0.0 を区別しない処理系では、偏角 \(\theta\) の範囲を \(-\pi \lt \theta \leq \pi\) に制限することで、\(\log z\) の返り値を (\(-\pi\) を取り除いて) 一つにすることができます。

●複素数の三角関数

複素数の三角関数の定義は、オイラーの公式から導かれる式の \(\theta\) を複素数 \(z\) に変えたものになります。

\(\sin -\theta = -\sin \theta, \ \cos -\theta = \cos \theta \) より
\( \begin{eqnarray} e^{i(-\theta)} &=& \cos -\theta + i \sin -\theta \\ &=& \cos \theta - i \sin \theta \end{eqnarray} \)

\( \begin{array}{l} e^{i\theta} + e^{-i\theta} = 2 \cos \theta \\ \cos \theta = \dfrac{e^{i\theta} + e^{-i\theta}}{2} \\ e^{i\theta} - e^{-i\theta} = 2i \sin \theta \\ \sin \theta = \dfrac{e^{i\theta} - e^{-i\theta}}{2i} \end{array} \)

\(\theta\) を複素数 z に置き換えた式が三角関数の定義になる

\( \begin{eqnarray} \sin z = \dfrac{e^{iz} - e^{-iz}}{2i} \\ \cos z = \dfrac{e^{iz} + e^{-iz}}{2} \end{eqnarray} \)

\(\sin z, \cos z\) に純虚数 \(ix\) を与えると双曲線関数 (\(\sinh x, \cosh x\)) になります。

双曲線関数の定義
\(\begin{eqnarray} \sinh x = \dfrac{e^x - e^{-x}}{2} \\ \cosh x = \dfrac{e^x + e^{-x}}{2} \end{eqnarray}\)
\(\begin{eqnarray} \sin ix &=& \dfrac{e^{iix} - e^{-iix}}{2i} \\ &=& \dfrac{e^{-x} - e^x}{2i} \times \dfrac{-i}{-i} \\ &=& i \dfrac{e^x - e^{-x}}{2} \\ &=& i \sinh x \\ \cos ix &=& \dfrac{e^{iix} + e^{-iix}}{2} \\ &=& \dfrac{e^{-x} + e^x}{2} \\ &=& \cosh x \end{eqnarray}\)

これに三角関数の加法定理 [*1] を使うと次の式が導かれます。

\(\begin{eqnarray} \sin (x + iy) &=& \sin x \cos iy + \cos x \sin iy \\ &=& \sin x \cosh y + i \cos x \sinh y \\ \cos (x + iy) &=& \cos x \cos iy - \sin x \sin iy \\ &=& \cos x \cosh y - i \sin x \sinh y \\ \tan (x + iy) &=& \dfrac{\sin 2x + \sin 2iy}{cos 2x + cos 2iy} \\ &=& \dfrac{\sin 2x + i \sinh 2y}{\cos 2x + \cosh 2y} \end{eqnarray}\)

標準ヘッダファイル complex.h に定義されている三角関数 (csin, ccos, ctan) は複素数に対応しています。

三角関数:
double complex csin(double complex z);
float complex csinf(float complex z);
long double complex csinl(long double complex z);

double complex ccos(double complex z);
float complex ccosf(float complex z);
long double complex ccosl(long double complex z);

double complex ctan(double complex z);
float complex ctanf(float complex z);
long double complex ctanl(long double complex z);

簡単な使用例を示します。

リスト : 複素数の三角関数 (ctest5.c)

#include <complex.h>
#include <stdio.h>

// 複素数の表示
void cprint(double complex z)
{
  printf("%g%+gi\n", creal(z), cimag(z));
}

int main()
{
  double complex c[4] = {0.0+I*1.0, 0.0-I*1.0, 1.0+I*1.0, 1.0-I*1.0};
  for (int n = 0; n < 4; n++) {
    double complex a = c[n];
    double complex b = csin(a);
    printf("a = "); cprint(a);
    printf("csin(a) = "); cprint(b);
    printf("cabs(csin(a)) = %g\n", cabs(b));
  }
  printf("\n");
  for (int n = 0; n < 4; n++) {
    double complex a = c[n];
    double complex b = ccos(a);
    printf("a = "); cprint(a);
    printf("ccos(a) = "); cprint(b);
    printf("cabs(ccos(a)) = %g\n", cabs(b));
  }
  printf("\n");
  for (int n = 0; n < 4; n++) {
    double complex a = c[n];
    double complex b = ctan(a);
    printf("a = "); cprint(a);
    printf("ctan(a) = "); cprint(b);
    printf("cabs(ctan(a)) = %g\n", cabs(b));
  }
  return 0;
}
$ clang -lm ctest5.c
$ ./a.out
a = 0+1i
csin(a) = 0+1.1752i
cabs(csin(a)) = 1.1752
a = 0-1i
csin(a) = 0-1.1752i
cabs(csin(a)) = 1.1752
a = 1+1i
csin(a) = 1.29846+0.634964i
cabs(csin(a)) = 1.4454
a = 1-1i
csin(a) = 1.29846-0.634964i
cabs(csin(a)) = 1.4454

a = 0+1i
ccos(a) = 1.54308-0i
cabs(ccos(a)) = 1.54308
a = 0-1i
ccos(a) = 1.54308+0i
cabs(ccos(a)) = 1.54308
a = 1+1i
ccos(a) = 0.83373-0.988898i
cabs(ccos(a)) = 1.29345
a = 1-1i
ccos(a) = 0.83373+0.988898i
cabs(ccos(a)) = 1.29345

a = 0+1i
ctan(a) = 0+0.761594i
cabs(ctan(a)) = 0.761594
a = 0-1i
ctan(a) = 0-0.761594i
cabs(ctan(a)) = 0.761594
a = 1+1i
ctan(a) = 0.271753+1.08392i
cabs(ctan(a)) = 1.11747
a = 1-1i
ctan(a) = 0.271753-1.08392i
cabs(ctan(a)) = 1.11747
-- note --------
[*1] 三角関数の公式は引数が複素数でも成り立ちます。ただし、\(|\sin x| \leq 1, |\cos x| \leq 1\) という関係式は、x が実数だと成立しますが複素数では成立しません。

●複素数の双曲線関数

複素数の双曲線関数の定義は、実数の定義で引数 x を複素数 z に変えたものになります。

双曲線関数の定義 (z は複素数)
\(\begin{eqnarray} \sinh z = \dfrac{e^{z} - e^{-z}}{2} \\ \cosh z = \dfrac{e^{z} + e^{-z}}{2} \end{eqnarray}\)

sinh z, cosh z に純虚数 ix を与えると三角関数 (sin x, cos x) になります。

\(\begin{eqnarray} \sinh ix &=& \dfrac{e^{ix} - e^{-ix}}{2} \\ &=& \dfrac{e^{ix} - e^{-ix}}{2} \times \dfrac{i}{i} \\ &=& i \dfrac{e^{ix} - e^{-ix}}{2i} \\ &=& i \sin x \\ \cosh ix &=& \dfrac{e^{ix} + e^{ix}}{2} \\ &=& cos x \end{eqnarray}\)

これに双曲線関数の加法定理を使うと、次の式が導かれます。

双曲線関数の加法定理
\(\begin{eqnarray} \sinh(x + y) &=& \sinh x \cosh y + \cosh x \sinh y \\ \cosh(x + y) &=& \cosh x \cosh y + \sinh x \sinh y \\ \tanh(x + y) &=& \dfrac{\sinh(x + y)}{\cosh(x + y)} \\ &=& \dfrac{\sinh 2x + \sinh 2y}{\cosh 2x + \cosh 2y} \end{eqnarray}\)
\(\begin{eqnarray} \sinh(x + iy) &=& \sinh x \cos y + i \cosh x \sin y \\ \cosh(x + iy) &=& \cosh x \cos y + i \sinh x \sin y \\ \tanh(x + iy) &=& \dfrac{\sinh(x + iy)}{\cosh(x + iy)} \\ &=& \dfrac{\sinh 2x + i \sin 2y}{\cosh 2x + \cos 2y} \end{eqnarray}\)

標準ヘッダファイル complex.h に定義されている双曲線関数 (csinh, ccosh, ctanh) は複素数に対応しています。

双曲線関数:
double complex csinh(double complex z);
float complex csinhf(float complex z);
long double complex csinhl(long double complex z);

double complex ccosh(double complex z);
float complex ccoshf(float complex z);
long double complex ccoshl(long double complex z);

double complex ctanh(double complex z);
float complex ctanhf(float complex z);
long double complex ctanhl(long double complex z);

簡単な使用例を示します。

リスト : 複素数の双曲線関数

#include <complex.h>
#include <stdio.h>

// 複素数の表示
void cprint(double complex z)
{
  printf("%g%+gi\n", creal(z), cimag(z));
}

int main()
{
  double complex c[5] = {0.0+I*1.0, 0.0-I*1.0, 1.0+I*1.0, 1.0-I*1.0, 0.0+I*0.0};
  for (int n = 0; n < 5; n++) {
    double complex a = c[n];
    double complex b = csinh(a);
    printf("a = "); cprint(a);
    printf("csinh(a) = "); cprint(b);
  }
  printf("\n");
  for (int n = 0; n < 5; n++) {
    double complex a = c[n];
    double complex b = ccosh(a);
    printf("a = "); cprint(a);
    printf("ccosh(a) = "); cprint(b);
  }
  printf("\n");
  for (int n = 0; n < 5; n++) {
    double complex a = c[n];
    double complex b = ctanh(a);
    printf("a = "); cprint(a);
    printf("ctanh(a) = "); cprint(b);
  }
  return 0;
}
$ clang -lm ctest6.c
mhiroi@DESKTOP-FQK6237:~/clang$ ./a.out
a = 0+1i
csinh(a) = 0+0.841471i
a = 0-1i
csinh(a) = 0-0.841471i
a = 1+1i
csinh(a) = 0.634964+1.29846i
a = 1-1i
csinh(a) = 0.634964-1.29846i
a = 0+0i
csinh(a) = 0+0i

a = 0+1i
ccosh(a) = 0.540302+0i
a = 0-1i
ccosh(a) = 0.540302-0i
a = 1+1i
ccosh(a) = 0.83373+0.988898i
a = 1-1i
ccosh(a) = 0.83373-0.988898i
a = 0+0i
ccosh(a) = 1+0i

a = 0+1i
ctanh(a) = 0+1.55741i
a = 0-1i
ctanh(a) = 0-1.55741i
a = 1+1i
ctanh(a) = 1.08392+0.271753i
a = 1-1i
ctanh(a) = 1.08392-0.271753i
a = 0+0i
ctanh(a) = 0+0i

●複素数の平方根

複素数 z の平方根は次の式で求めることができます。

\(z = x + iy, \ |z| = \sqrt{x^2 + y^2}, \ \theta = \arg z \ (-\pi \leq \theta \leq \pi)\) とすると

\( \sqrt{x + iy} = \begin{cases} \sqrt{|z| e^{i\theta}} = \sqrt{|z|} e^{i\theta/2} & (1) \\ \sqrt{|z| e^{i\theta + 2\pi}} = \sqrt{|z|} e^{i\theta/2 + \pi} & (2) \end{cases} \)

式 (1) を平方根の主値といいます。角度は \(2\pi\) を足すと同じ角度になるので、式 (2) がもう一つの解になります。三角関数の半角の公式を使うと、式 (1) から次の式が導かれます。

三角関数の半角の公式
\(\begin{eqnarray} \sin^2{\left(\frac{\theta}{2}\right)} = \dfrac{1 - \cos \theta}{2} \\ \cos^2{\left(\frac{\theta}{2}\right)} = \dfrac{1 + \cos \theta}{2} \end{eqnarray}\)
\(y \geq 0\) の場合
\(\begin{eqnarray} \sqrt{|z|} e^{i\theta/2} &=& \sqrt{|z|} \left(\cos{\frac{\theta}{2}} + i \sin{\frac{\theta}{2}}\right) \\ &=& \sqrt{|z|} \left(\sqrt{\frac{1 + \cos \theta}{2}}) + i \sqrt{\frac{1 - \cos \theta}{2}}\right) \\ &=& \dfrac{\sqrt{|z| + |z|\cos \theta}}{2} + i \sqrt{\frac{|z| - |z| \cos \theta}{2}} \\ &=& \sqrt{\frac{|z| + x}{2}} + i \sqrt{\frac{|z| - x}{2}}, \quad (|z|\cos \theta = x) \end{eqnarray}\)

\(y \lt 0\) の場合、虚部の符号が \(-\) になる
\( \sqrt{|z|} e^{i\theta/2} = \sqrt{\dfrac{|z| + x}{2}} - i \sqrt{\dfrac{|z| - x}{2}} \)

標準ヘッダファイル complex.h に定義されている csqrt は複素数に対応しています。

平方根: csqrt(z)
double complex csqrt(double complex z);
float complex csqrtf(float complex z);
long double complex csqrtl(long double complex z);

簡単な実行例を示します。

リスト : 複素数の平方根 (ctest7.c)

#include <complex.h>
#include <stdio.h>

// 複素数の表示
void cprint(double complex z)
{
  printf("%g%+gi\n", creal(z), cimag(z));
}

int main()
{
  double complex c[6] = {2.0+I*0.0, -2.0+I*0.0, 0.0+I*2.0, 0.0-I*2.0, 2.0+I*2.0, -2.0-I*2.0};
  for (int n = 0; n < 6; n++) {
    double complex a = c[n];
    double complex b = csqrt(a);
    printf("a = "); cprint(a);
    printf("csqrt(a) = "); cprint(b);
    printf("csqrt(a) * csqrt(a) = "); cprint(b * b);
  }
  printf("csqrt(-2+0i) = "); cprint(csqrt(-2.0 + I * 0.0));
  printf("csqrt(-2-0i) = "); cprint(csqrt(-2.0 - I * 0.0));
  printf("csqrt(-1e300+0i) = "); cprint(csqrt(-1e300 + I * 0.0));
  printf("csqrt(-1e300-0i) = "); cprint(csqrt(-1e300 - I * 0.0));
  return 0;
}
$ clang -lm ctest7.c
$ ./a.out
a = 2+0i
csqrt(a) = 1.41421+0i
csqrt(a) * csqrt(a) = 2+0i
a = -2+0i
csqrt(a) = 0+1.41421i
csqrt(a) * csqrt(a) = -2+0i
a = 0+2i
csqrt(a) = 1+1i
csqrt(a) * csqrt(a) = 0+2i
a = 0-2i
csqrt(a) = 1-1i
csqrt(a) * csqrt(a) = 0-2i
a = 2+2i
csqrt(a) = 1.55377+0.643594i
csqrt(a) * csqrt(a) = 2+2i
a = -2-2i
csqrt(a) = 0.643594-1.55377i
csqrt(a) * csqrt(a) = -2-2i
csqrt(-2+0i) = 0+1.41421i
csqrt(-2-0i) = 0-1.41421i
csqrt(-1e300+0i) = 0+1e+150i
csqrt(-1e300-0i) = 0-1e+150i

\(\sqrt x\) は \(\log x\) と同じ分枝切断を持っています。x を負の整数とすると sqrt(x) の解は \(i \sqrt x\) になりますが、もうひとつ \(-i \sqrt x\) という解があります。

●逆三角関数

三角関数の逆関数を「逆三角関数 (inverse trigonometric function)」といいます。以下に標準ヘッダファイル math.h に定義されている逆三角関数を示します。

ここでは引数 x を実数とします。asin x は引数 x が与えられたとき sin w = x となる角度 w を求めます。同様に acos x は cos w = x となる角度 w を、atan x は tan x = w となる角度 w を求めます。三角関数には周期性があるので、上式を満たす角度 w は無数に存在します。つまり、逆三角関数の返り値は無数にあることになりますが、通常は一つの値を返すように範囲を制限します。これを「主値」といいます。

逆三角関数の主値を以下に示します。

\(\begin{array}{lcc} \arcsin x = w & -1 \leq x \lt 1 & -\pi/2 \leq w \leq \pi/2 \\ \arccos x = w & -1 \leq x \lt 1 & 0 \leq w \leq \pi \\ \arctan x = w & x \in \mathbb{R} & -\pi/2 \leq w \leq \pi/2 \end{array}\)

本ページでは、数式の表示に JavaScript のライブラリ MathJax を使っています。MathJax では、逆三角関数を arcsin, arccos, arctan と表示します。

簡単な実行例を示します。

リスト : 逆三角関数 (ctest8.c)

#include <math.h>
#include <stdio.h>

int main()
{
  for (double x = -1.0; x <= 1.0; x += 0.5) {
    printf("asin(%g) = %g\n", x, asin(x));
  }
  for (double x = -1.0; x <= 1.0; x += 0.5) {
    printf("acos(%g) = %g\n", x, acos(x));
  }
  for (double x = -2.0; x <= 2.0; x += 1.0) {
    printf("atan(%g) = %g\n", x, atan(x));
  }
  return 0;
}
$ clang -lm ctest8.c
$ ./a.out
asin(-1) = -1.5708
asin(-0.5) = -0.523599
asin(0) = 0
asin(0.5) = 0.523599
asin(1) = 1.5708
acos(-1) = 3.14159
acos(-0.5) = 2.0944
acos(0) = 1.5708
acos(0.5) = 1.0472
acos(1) = 0
atan(-2) = -1.10715
atan(-1) = -0.785398
atan(0) = 0
atan(1) = 0.785398
atan(2) = 1.10715

math.h には関数 atan2 も用意されています。

double atan2(double y, double x);

引数 x, y は実数です。atan2 は直交座標系においてベクトル (x, y) と x 軸との角度を求める関数です。複素平面で考えると、複素数 \(x + iy\) の偏角 \(\theta\) を求めることと同じです。返り値 (角度 \(\theta\)) の範囲は \(-\pi \leq \theta \leq \pi\) になります。

簡単な使用例を示します。

リスト : atan2() の使用例 (ctest81.c)

#include <math.h>
#include <stdio.h>

int main(void)
{
  double x[9] = {-1.0, -1.0, 0.0, 1.0, 1.0,  1.0,  0.0, -1.0, -1.0};
  double y[9] = { 0.0,  1.0, 1.0, 1.0, 0.0, -1.0, -1.0, -1.0, -0.0};
  for (int n = 0; n < 9; n++) {
    printf("atan2(%g, %g) = %g\n", y[n], x[n], atan2(y[n], x[n]));
  }
  return 0;
}
$ clang -lm ctest81.c
$ ./a.out
atan2(0, -1) = 3.14159
atan2(1, -1) = 2.35619
atan2(1, 0) = 1.5708
atan2(1, 1) = 0.785398
atan2(0, 1) = 0
atan2(-1, 1) = -0.785398
atan2(-1, 0) = -1.5708
atan2(-1, -1) = -2.35619
atan2(-0, -1) = -3.14159

●複素数の逆三角関数

複素数の逆三角関数の定義は、複素数の三角関数の定義から導くことができます。asin z の定義は次のようになります。

\(z, w\) は複素数とする

\(\begin{array}{l} \arcsin z = w \\ z = \sin w = \dfrac{e^{iw} - e^{-iw}}{2i} \\ z \times 2ie^{iw} = \dfrac{e^{iw} - e^{-iw}}{2i} \times 2ie^{iw} \\ 2iz(e^{iw}) = (e^{iw})^2 - 1 \\ (e^{iw})^2 - 2iz(e^{iw}) - 1 = 0 \end{array}\)

\(e^{iw}\) の二次方程式と考えて解くと

\(e^{iw} = iz \pm \sqrt{1 - z^2}\)

両辺の対数をとって \(-i\) を掛け算し、平方根の主値 \(+\) を選ぶ

\(w = \arcsin z = -i \log{\left(iz + \sqrt{1 - z^2}\right)} \)

\(\arccos z, \arctan z\) は定義だけを示します。

\(\begin{array}{l} \arccos z = \dfrac{\pi}{2} - \arcsin z \\ \arctan z = i \dfrac{\log{\left(1 - iz\right)} - \log{\left(1 + iz\right)}}{2} \end{array}\)

asin, acos, atan は次に示す分枝切断を持っています。

標準ヘッダファイル complex.h に定義されている逆三角関数 (casin, cacos, catan) は複素数に対応しています。

逆三角関数:
double complex casin(double complex z);
float complex casinf(float complex z);
long double complex casinl(long double complex z);

double complex cacos(double complex z);
float complex cacosf(float complex z);
long double complex cacosl(long double complex z);

double complex catan(double complex z);
float complex catanf(float complex z);
long double complex catanl(long double complex z);

簡単な実行例を示します。

リスト : 複素数の逆三角関数 (ctest9.c)

#include <complex.h>
#include <stdio.h>

// 複素数の表示
void cprint(double complex z)
{
  printf("%g%+gi\n", creal(z), cimag(z));
}

int main()
{
  double complex c[6] = {2.0+I*0.0, -2.0+I*0.0, 0.0+I*2.0, 0.0-I*2.0, 2.0+I*2.0, -2.0-I*2.0};
  for (int n = 0; n < 6; n++) {
    double complex a = c[n];
    double complex b = casin(a);
    printf("a = "); cprint(a);
    printf("casin(a) = "); cprint(b);
    printf("csin(casin(a)) = "); cprint(csin(b));
  }
  printf("\n");
  printf("casin(1e300+0.0i) = "); cprint(casin(1e300 + I * 0.0));
  printf("casin(1e300-0.0i) = "); cprint(casin(1e300 - I * 0.0));
  printf("casin(-1e300+0.0i) = "); cprint(casin(-1e300 + I * 0.0));
  printf("casin(-1e300-0.0i) = "); cprint(casin(-1e300 - I * 0.0));
  printf("\n");

  for (int n = 0; n < 6; n++) {
    double complex a = c[n];
    double complex b = cacos(a);
    printf("a = "); cprint(a);
    printf("cacos(a) = "); cprint(b);
    printf("ccos(cacos(a)) = "); cprint(ccos(b));
  }
  printf("\n");
  printf("cacos(1e300+0.0i) = "); cprint(cacos(1e300 + I * 0.0));
  printf("cacos(1e300-0.0i) = "); cprint(cacos(1e300 - I * 0.0));
  printf("cacos(-1e300+0.0i) = "); cprint(cacos(-1e300 + I * 0.0));
  printf("cacos(-1e300-0.0i) = "); cprint(cacos(-1e300 - I * 0.0));
  printf("\n");

  for (int n = 0; n < 6; n++) {
    double complex a = c[n];
    double complex b = catan(a);
    printf("a = "); cprint(a);
    printf("catan(a) = "); cprint(b);
    printf("ctan(catan(a)) = "); cprint(ctan(b));
  }
  printf("\n");
  printf("catan(0+2i) = "); cprint(catan(0.0 + I * 2.0));
  printf("catan(-0+2i) = "); cprint(catan(-0.0 + I * 2.0));
  printf("catan(0-4i) = "); cprint(catan(0.0 - I * 4.0));
  printf("catan(-0-4i) = "); cprint(catan(-0.0 - I * 4.0));
  printf("\n");
  return 0;
}
$ clang -lm ctest9.c
$ ./a.out
a = 2+0i
casin(a) = 1.5708+1.31696i
csin(casin(a)) = 2+1.06058e-16i
a = -2+0i
casin(a) = -1.5708+1.31696i
csin(casin(a)) = -2+1.06058e-16i
a = 0+2i
casin(a) = 0+1.44364i
csin(casin(a)) = 0+2i
a = 0-2i
casin(a) = 0-1.44364i
csin(casin(a)) = 0-2i
a = 2+2i
casin(a) = 0.754249+1.73432i
csin(casin(a)) = 2+2i
a = -2-2i
casin(a) = -0.754249-1.73432i
csin(casin(a)) = -2-2i

casin(1e300+0.0i) = 1.5708+691.469i
casin(1e300-0.0i) = 1.5708-691.469i
casin(-1e300+0.0i) = -1.5708+691.469i
casin(-1e300-0.0i) = -1.5708-691.469i

a = 2+0i
cacos(a) = 0-1.31696i
ccos(cacos(a)) = 2+0i
a = -2+0i
cacos(a) = 3.14159-1.31696i
ccos(cacos(a)) = -2+2.12115e-16i
a = 0+2i
cacos(a) = 1.5708-1.44364i
ccos(cacos(a)) = 1.3692e-16+2i
a = 0-2i
cacos(a) = 1.5708+1.44364i
ccos(cacos(a)) = 1.3692e-16-2i
a = 2+2i
cacos(a) = 0.816547-1.73432i
ccos(cacos(a)) = 2+2i
a = -2-2i
cacos(a) = 2.32505+1.73432i
ccos(cacos(a)) = -2-2i

cacos(1e300+0.0i) = 0-691.469i
cacos(1e300-0.0i) = 0+691.469i
cacos(-1e300+0.0i) = 3.14159-691.469i
cacos(-1e300-0.0i) = 3.14159+691.469i

a = 2+0i
catan(a) = 1.10715+0i
ctan(catan(a)) = 2+0i
a = -2+0i
catan(a) = -1.10715+0i
ctan(catan(a)) = -2+0i
a = 0+2i
catan(a) = 1.5708+0.549306i
ctan(catan(a)) = 1.83697e-16+2i
a = 0-2i
catan(a) = 1.5708-0.549306i
ctan(catan(a)) = 1.83697e-16-2i
a = 2+2i
catan(a) = 1.31122+0.238878i
ctan(catan(a)) = 2+2i
a = -2-2i
catan(a) = -1.31122-0.238878i
ctan(catan(a)) = -2-2i

catan(0+2i) = 1.5708+0.549306i
catan(-0+2i) = 1.5708+0.549306i    // 実部の符号が逆
catan(0-4i) = 1.5708-0.255413i
catan(-0-4i) = -1.5708-0.255413i

clang (ver 14.0.0) では、catan() の分枝切断に不具合があるようです。

●逆双曲線関数

双曲線関数の逆関数を「逆双曲線関数 (inverse hyperbolic function)」といいます。標準ヘッダファイル math.h には逆双曲線関数 asinh, acosh, atanh が定義されています。MathJax では逆双曲線関数を \(\sinh^{-1} x, \cosh^{-1} x, \tanh^{-1} x \) で表します。双曲線関数と逆双曲線関数の定義域と値域を示します。

\(\begin{array}{lcc} y = \sinh x & -\infty \lt x \lt \infty & -\infty \lt y \lt \infty \\ y = \cosh x & -\infty \lt x \lt \infty & 1 \leq y \lt \infty \\ y = \tanh x & -\infty \lt x \lt \infty & -1 \lt y \lt 1 \\ y = \sinh^{-1} x & -\infty \lt x \lt \infty & -\infty \lt y \lt \infty \\ y = \cosh^{-1} x & 1 \leq x \lt \infty & 0 \leq y \lt \infty \\ y = \tanh^{-1} x & -1 \lt x \lt 1 & -\infty \lt y \lt \infty \end{array}\)

x と y は実数です。x = cosh y の逆関数 y = acosh x を満たす y の値は 2 つありますが、ここでは \(y \geq 0\) を主値として選ぶことにします。

逆双曲線関数の定義は双曲線関数の定義から導くことができます。

\(\begin{array}{l} \sinh^{-1} x = y \\ y = \sinh x = \dfrac{e^{x} - e^{-x}}{2} \\ y \times 2e^{x} = \dfrac{e^{x} - e^{-x}}{2} \times 2e^{x} \\ 2ye^{x} = e^{2x} - 1 \\ e^{2x} - 2ye^{x} - 1 = 0 \end{array}\)

\(e^{x}\) の 2 次方程式として解く

\(e^{x} = \dfrac{2y \pm \sqrt{4y^2 + 4}}{2} = y \pm \sqrt{y^2 + 1} \)

\(e^{x} \gt 0\) だから平方根の符号に \(+\) を選んで対数をとる

\(x = \log \left(y + \sqrt{y^2 + 1}\right)\)
\(\begin{array}{l} \cosh^{-1} x = y \\ y = \cosh x = \dfrac{e^{x} + e^{-x}}{2} \\ y \times 2e^{x} = \dfrac{e^{x} + e^{-x}}{2} \times 2e^{x} \\ 2ye^{x} = e^{2x} + 1 \\ e^{2x} - 2ye^{x} + 1 = 0 \end{array}\)

\(e^{x}\) の 2 次方程式として解く

\(e^{x} = \dfrac{2y \pm \sqrt{\left(4y^2 - 4\right)}}{2} = y \pm \sqrt{y^2 - 1} \)

\(e^{x} \gt 0\) だから平方根の符号に \(+\) を選んで対数をとる

\(x = \log \left(y + \sqrt{y^2 - 1}\right), \quad (y \geq 1) \)
\(\begin{array}{l} \tanh^{-1} x = y \\ \begin{eqnarray} y &=& \tanh x \\ &=& \dfrac{\sinh x}{\cosh x} \\ &=& \dfrac{e^{x} - e^{-x}}{e^{x} + e^{-x}} \\ &=& \dfrac{e^{2x} - 1}{e^{2x} + 1} \end{eqnarray} \\ y(e^{2x} + 1) = (e^{2x} - 1) \\ (1 - y)e^{2x} = 1 + y \\ e^{2x} = \dfrac{1 + y}{1 - y} \\ x = \dfrac{1}{2} \log{\dfrac{1 + y}{1 - y}} \end{array}\)

関数 \(\tanh^{-1} x\) を使うと、次の関係式から \(\sinh^{-1} x, \ \cosh^{-1} x\) を求めることができます。

\(\sinh^{-1} x = \tanh^{-1}{\dfrac{x}{\sqrt{x^2 + 1}}} \)

\(\cosh^{-1} x = \tanh^{-1}{\dfrac{\sqrt{1 - x^2}}{x}} \)

簡単な実行例を示します。

リスト : 逆双曲線関数 (ctest10.c)

#include <math.h>
#include <stdio.h>

int main()
{
  for (double x = -2.0; x <= 2.0; x += 0.5) {
    printf("asinh(%g) = %g\n", x, asinh(x));
    printf("sinh(asinh(%g)) = %g\n", x, sinh(asinh(x)));
  }
  printf("\n");

  for (double x = 1.0; x <= 4.0; x += 0.5) {
    printf("acosh(%g) = %g\n", x, acosh(x));
    printf("cosh(acosh(%g)) = %g\n", x, cosh(acosh(x)));
  }
  printf("\n");

  for (double x = -0.75; x <= 0.75; x += 0.25) {
    printf("atanh(%g) = %g\n", x, atanh(x));
    printf("tanh(atanh(%g)) = %g\n", x, tanh(atanh(x)));
  }
  return 0;
}
$ clang -lm ctest10.c
$ ./a.out
asinh(-2) = -1.44364
sinh(asinh(-2)) = -2
asinh(-1.5) = -1.19476
sinh(asinh(-1.5)) = -1.5
asinh(-1) = -0.881374
sinh(asinh(-1)) = -1
asinh(-0.5) = -0.481212
sinh(asinh(-0.5)) = -0.5
asinh(0) = 0
sinh(asinh(0)) = 0
asinh(0.5) = 0.481212
sinh(asinh(0.5)) = 0.5
asinh(1) = 0.881374
sinh(asinh(1)) = 1
asinh(1.5) = 1.19476
sinh(asinh(1.5)) = 1.5
asinh(2) = 1.44364
sinh(asinh(2)) = 2

acosh(1) = 0
cosh(acosh(1)) = 1
acosh(1.5) = 0.962424
cosh(acosh(1.5)) = 1.5
acosh(2) = 1.31696
cosh(acosh(2)) = 2
acosh(2.5) = 1.5668
cosh(acosh(2.5)) = 2.5
acosh(3) = 1.76275
cosh(acosh(3)) = 3
acosh(3.5) = 1.92485
cosh(acosh(3.5)) = 3.5
acosh(4) = 2.06344
cosh(acosh(4)) = 4

atanh(-0.75) = -0.972955
tanh(atanh(-0.75)) = -0.75
atanh(-0.5) = -0.549306
tanh(atanh(-0.5)) = -0.5
atanh(-0.25) = -0.255413
tanh(atanh(-0.25)) = -0.25
atanh(0) = 0
tanh(atanh(0)) = 0
atanh(0.25) = 0.255413
tanh(atanh(0.25)) = 0.25
atanh(0.5) = 0.549306
tanh(atanh(0.5)) = 0.5
atanh(0.75) = 0.972955
tanh(atanh(0.75)) = 0.75

●複素数の逆双曲線関数

複素数の逆双曲線関数の定義は、実数の定義で引数 x を複素数 z に変えたものになります。

\(\begin{array}{l} \sinh^{-1} z = \log \left(z + \sqrt{z^2 + 1}\right) \\ \begin{eqnarray} \cosh^{-1} z &=& \log \left(z + \sqrt{z^2 - 1}\right) &(1)& \\ &=& \log \left(z + \sqrt{z + 1} \sqrt{z - 1}\right) \quad &(2)& \end{eqnarray} \\ \tanh^{-1} z = \dfrac{1}{2} \log\left(\dfrac{1 + z}{1 - z}\right) = \dfrac{1}{2} \left( \log {\left( 1 + z \right)} - \log {\left(1 - z\right)} \right) \end{array}\)

参考文献, URL 8 によると、\(\cosh^{-1} z\) を式 (1) でプログラムすると「分枝切断線」が複雑になるため、他の式 (たとえば (2) など) でプログラムする処理系が多いようです。ちなみに、ANSI Common Lisp では \(\cosh^{-1} z\) を次の式で定義しています。

\( \cosh^{-1} z = 2 \log \left( \sqrt{\dfrac{z + 1}{2}} + \sqrt{\dfrac{z - 1}{2}} \right) \)

asinh, acosh, atanh は次に示す分枝切断を持っています。

標準ヘッダファイル complex.h の逆双曲線関数 (casinh, cacosh, catanh) は複素数に対応しています。

逆双曲線関数:
double complex casinh(double complex z);
float complex casinhf(float complex z);
long double complex casinhl(long double complex z);

double complex cacosh(double complex z);
float complex cacoshf(float complex z);
long double complex cacoshl(long double complex z);

double complex catanh(double complex z);
float complex catanhf(float complex z);
long double complex catanhl(long double complex z);

簡単な実行例を示します。

リスト : 複素数の逆双曲線関数 (ctest11.c)

#include <complex.h>
#include <stdio.h>

// 複素数の表示
void cprint(double complex z)
{
  printf("%g%+gi\n", creal(z), cimag(z));
}

int main()
{
  double complex c[6] = {2.0+I*0.0, -2.0+I*0.0, 0.0+I*2.0, 0.0-I*2.0, 2.0+I*2.0, -2.0-I*2.0};
  for (int n = 0; n < 6; n++) {
    double complex a = c[n];
    double complex b = casinh(a);
    printf("a = "); cprint(a);
    printf("casinh(a) = "); cprint(b);
    printf("csinh(casinh(a)) = "); cprint(csinh(b));
  }
  printf("\n");
  printf("casinh(0.0+2.0i) = "); cprint(casinh(0.0 + I * 2.0));
  printf("casinh(-0.0+2.0i) = "); cprint(casinh(-0.0 + I * 2.0));
  printf("casinh(0.0-4.0i) = "); cprint(casinh(0.0 - I * 4.0));
  printf("casinh(-0.0-4.0i) = "); cprint(casinh(-0.0 - I * 4.0));
  printf("\n");

  for (int n = 0; n < 6; n++) {
    double complex a = c[n];
    double complex b = cacosh(a);
    printf("a = "); cprint(a);
    printf("cacosh(a) = "); cprint(b);
    printf("ccosh(cacosh(a)) = "); cprint(ccosh(b));
  }
  printf("\n");
  printf("cacosh(0+0i) = "); cprint(cacosh(0.0 + I * 0.0));
  printf("cacosh(0-0i) = "); cprint(cacosh(0.0 - I * 0.0));
  printf("cacosh(-4+0i) = "); cprint(cacosh(-4.0 + I * 0.0));
  printf("cacosh(-4-0i) = "); cprint(cacosh(-4.0 - I * 0.0));
  printf("\n");

  for (int n = 0; n < 6; n++) {
    double complex a = c[n];
    double complex b = catanh(a);
    printf("a = "); cprint(a);
    printf("catanh(a) = "); cprint(b);
    printf("ctanh(catanh(a)) = "); cprint(ctanh(b));
  }
  printf("\n");
  printf("catanh(2+0i) = "); cprint(catanh(2.0 + I * 0.0));
  printf("catanh(2-0i) = "); cprint(catanh(2.0 - I * 0.0));
  printf("catanh(-4+0i) = "); cprint(catanh(-4.0 + I * 0.0));
  printf("catanh(-4-0i) = "); cprint(catanh(-4.0 - I * 0.0));
  printf("\n");
  return 0;
}
$ clang -lm ctest11.c
$ ./a.out
a = 2+0i
casinh(a) = 1.44364+0i
csinh(casinh(a)) = 2+0i
a = -2+0i
casinh(a) = -1.44364+0i
csinh(casinh(a)) = -2+0i
a = 0+2i
casinh(a) = 1.31696+1.5708i
csinh(casinh(a)) = 1.06058e-16+2i
a = 0-2i
casinh(a) = 1.31696-1.5708i
csinh(casinh(a)) = 1.06058e-16-2i
a = 2+2i
casinh(a) = 1.73432+0.754249i
csinh(casinh(a)) = 2+2i
a = -2-2i
casinh(a) = -1.73432-0.754249i
csinh(casinh(a)) = -2-2i

casinh(0.0+2.0i) = 1.31696+1.5708i
casinh(-0.0+2.0i) = 1.31696+1.5708i   // 実部の符号が逆
casinh(0.0-4.0i) = 2.06344-1.5708i
casinh(-0.0-4.0i) = -2.06344-1.5708i

a = 2+0i
cacosh(a) = 1.31696+0i
ccosh(cacosh(a)) = 2+0i
a = -2+0i
cacosh(a) = 1.31696+3.14159i
ccosh(cacosh(a)) = -2+2.12115e-16i
a = 0+2i
cacosh(a) = 1.44364+1.5708i
ccosh(cacosh(a)) = 1.3692e-16+2i
a = 0-2i
cacosh(a) = 1.44364-1.5708i
ccosh(cacosh(a)) = 1.3692e-16-2i
a = 2+2i
cacosh(a) = 1.73432+0.816547i
ccosh(cacosh(a)) = 2+2i
a = -2-2i
cacosh(a) = 1.73432-2.32505i
ccosh(cacosh(a)) = -2-2i

cacosh(0+0i) = 0+1.5708i
cacosh(0-0i) = 0-1.5708i
cacosh(-4+0i) = 2.06344+3.14159i
cacosh(-4-0i) = 2.06344-3.14159i

a = 2+0i
catanh(a) = 0.549306+1.5708i
ctanh(catanh(a)) = 2+1.83697e-16i
a = -2+0i
catanh(a) = -0.549306+1.5708i
ctanh(catanh(a)) = -2+1.83697e-16i
a = 0+2i
catanh(a) = 0+1.10715i
ctanh(catanh(a)) = 0+2i
a = 0-2i
catanh(a) = 0-1.10715i
ctanh(catanh(a)) = 0-2i
a = 2+2i
catanh(a) = 0.238878+1.31122i
ctanh(catanh(a)) = 2+2i
a = -2-2i
catanh(a) = -0.238878-1.31122i
ctanh(catanh(a)) = -2-2i

catanh(2+0i) = 0.549306+1.5708i
catanh(2-0i) = 0.549306-1.5708i
catanh(-4+0i) = -0.255413+1.5708i
catanh(-4-0i) = -0.255413-1.5708i

clang (ver 14.0.0) の場合、casinh() の分枝切断に不具合があるようです。

●おわりに

簡単なプログラムを作りながらC言語のプログラミングについて説明してきましたが、今回で一区切りとさせていただきます。本稿でC言語のすべての機能を説明するのは不可能ですが、C言語の基本からソート、連結リスト、二分木、ヒープ、ハッシュ法など、基本的なアルゴリズムやデータ構造はひととおり説明することができたのではないかと思っております。最後に、本稿がC言語に関心を持たれている皆様の参考になれば幸いです。

●参考文献, URL

  1. Guy L. Steele Jr., 『COMMON LISP 第 2 版』, 共立出版, 1991
  2. 奥村晴彦,『C言語による最新アルゴリズム事典』, 技術評論社, 1991
  3. IEEE 754 -- Wikipedia
  4. IEEE 754における負のゼロ - Wikipedia
  5. NaN - Wikipedia
  6. 逆三角関数 - Wikipedia
  7. 逆双曲線関数 - Wikipedia
  8. 逆双曲線関数と逆三角関数の branch cut | 雑記帳

初版 2021 年 11月 13 日
改訂 2023 年 4 月 8 日

Copyright (C) 2021-2023 Makoto Hiroi
All rights reserved.

[ PrevPage | Clang | NextPage ]