C言語編 第48章 数学関数

先頭へ戻る

この章の概要

この章の概要です。

数学関数

C言語の標準ライブラリには、数学的な処理を行うための関数(以下、数学関数と表記)が多数含まれています。開発するプログラムの分野によっては、非常に有用になることもありますし、反対にほとんど用がないこともあるかも知れません。

この章では、分野を問わず使用頻度が高そうなものに限定して紹介します。ただし、数学的な解説はしません(そこは専門ではないので控えます)。また、数学関数を使う上で知っておくべきことについて触れます。

数学関数のほとんどは、math.h で宣言されています。ここにある関数は、引数や戻り値に double型を使うようになっています。わずかですが、整数型が使える関数があり、そういったものは stdlib.h の方に分離されています。

gcc や clang で math.h の関数を使うときには、コンパイルオプションに「-lm」を追加してください。数学関連のライブラリが分けられているため、このように明示的にリンクの指示を与える必要があります。

C99 (complex.h)

C99 には、複素数を扱う型が追加されています。従来の math.h にある関数を、複素数に対して適用できるように、complex.h という標準ヘッダが追加されました。

C99 (math.h の強化)

C99 の math.h に宣言されている数学関数は、その種類が C95 に比べて大幅に増加しています。

また、C95 までの数学関数は、引数や戻り値が double型で統一されていますが、C99 では、float型や long double型が使える関数が追加されています。これらの関数の名前は、double型を扱う既存の関数の名前の末尾に、float型版なら f を、long double型版なら l を付加したものになっています。

double fabs(double x);
float fabsf(float x);  // C99 で追加
long double fabsl(long double x);  // C99 で追加

C99 (tgmath.h)

math.h や complex.h の数学関数は、型に応じた使い分けが必要です。特に、後から扱う型を変更した場合、呼び出す関数も修正しなければならないことに注意が必要です。暗黙的に型変換できてしまうため気付きづらく、意図しない精度で計算が行われてしまう可能性があります。

そこで、tgmath.h という新たな標準ヘッダが追加されました。ここには、型総称マクロと呼ばれる、特殊なマクロが定義されており、型の種類 (float、double、long double と、それぞれの複素数型のみ)を問わずに、同じ関数名が使えるようになっています。

例えば、次のサンプルプログラムのように、float、double、long double のいずれの型でも「fabs」という名前で使用できます。

#include <stdio.h>
#include <tgmath.h>

int main(void)
{
    printf( "%f\n", fabs(-12.34f) );
    printf( "%f\n", fabs(-12.34) );
    printf( "%Lf\n", fabs(-12.34L) );
    
    return 0;
}

実行結果:

12.340000
12.340000
12.340000

tgmath.h は、VisualStudio 2015/2017 では使用できません。

型総称マクロは、コンパイラがこれを特別扱いして「うまく処理」しているだけであり、我々がこの機能を真似することはできません。C11 からは、総称選択という新機能が追加されたことにより、真似ることができるようになりました。


エラー処理

数学関数のいくつかは、計算が不可能であったり、結果を表現不可能であったりする可能性を持っています。このようなエラーが起きていることを検出する方法を知っておく必要があります。

数学関数が発生させるエラーには種類があります。

実引数の指定が適切な範囲内にない場合、定義域エラーが発生します。実例としては、平方根を求める sqrt関数後で取り上げます)において、実引数を負数にした場合があります。

結果が表現できない場合、値域エラー(「ちいき」と読みます)が発生します。値域エラーが発生する原因には、結果が巨大すぎて表現できないオーバーフローと、微小すぎて表現できないアンダーフローがあります。
実例として、べき乗を求める pow関数後で取り上げます)は値域エラーを発生させることがあります。

一般に、定義域エラーは、実引数を注意して指定することで防ぐことができます。各関数ごとに、定義域エラーを発生させない有効な値の範囲があるはずなので、その範囲を逸脱していないかどうか確認することができるでしょう。例えば、sqrt関数では、実引数を負数にしなければ、定義域エラーの発生を防げます。

一方で、値域エラーは、防ぐことが難しいこともあります。値域エラーの発生の有無は、関数の実装次第な部分もあるためです。

いずれにしても、注意して防ぐことが困難であるならば、適切な仕組みをもって検出する必要があります。(C95 では)数学関数のエラーは、errno の仕組みを使って検出します。C99 以降に対応したコンパイラの場合、errno の仕組みを使わない可能性があることに注意して下さい。このことについては、以下で取り上げています

errno の仕組みを使うコンパイラの場合は、定義域エラーの発生時には errno に EDOM が、値域エラーの発生時は ERANGE が格納されます。

EDOM も ERANGE も、その置換結果は 0 ではない値なので、エラーを errno で報告する標準ライブラリ関数を呼び出す際には、その呼び出しの直前で 0 を代入しておき、呼び出しの直後で値を調べるようにします。実際のプログラムは、この後、各関数を説明する中で掲載します。

C99 (エラー処理)

C99 からは、エラーを通知する新たな仕組みとして、浮動小数点例外を使うことがあります。ただし、従来通り errno の仕組みを使うこともありますし、併用することもあります。

どの方法が使われるかは、コンパイラごとに異なっています。math.h にある math_errhandling というマクロがどう置換されるかで知ることができます。可能性は次の3通りです。

#define math_errhandling (MATH_ERRNO)
#define math_errhandling (MATH_ERREXCEPT)
#define math_errhandling (MATH_ERRNO | MATH_ERREXCEPT)

VisualStudio 2015/2017、clang 5.0.0 での置換結果はいずれも、(MATH_ERRNO | MATH_ERREXCEPT) になっています。

置換結果に MATH_ERRNO が含まれている場合は errno の仕組みが使われ、MATH_ERREXCEPT が含まれている場合は、浮動小数点例外の仕組みが使われます。

errno の仕組みが、数学関数以外でも使われることと同じく、浮動小数点例外の仕組みも数学関数以外でも使われます。例えば、無限大から無限大を減算するような、通常の演算でも使用される可能性があります。

浮動小数点例外を使う場合、数学関数がエラーを発生させたら、その内容に応じた浮動小数点例外を発生させます。浮動小数点例外が発生すると、浮動小数点状態フラグに、浮動小数点例外の種類に応じた値がセットされます。浮動小数点状態フラグは、直接的にはアクセスできないところにある変数のような存在です。

浮動小数点例外の種類に応じて、以下のマクロが fenv.h で定義されています。

マクロ 意味
FE_INVALID 定義域エラー。不正な演算を要求
FE_DIVBYZERO 値域エラー。結果が無限大になってしまう場合
FE_OVERFLOW 値域エラー。結果がオーバーフローしている
FE_UNDERFLOW 値域エラー。結果がアンダーフローしている
FE_INEXACT 結果が精度の問題で正確に表現できず、丸められている
FE_ALL_EXCEPT 上記のうち、処理系が対応しているものすべての組み合わせ

ただし、処理系がすべての浮動小数点例外に対応している保証はなく、一部のみの対応である可能性があります。いずれにしても、FE_ALL_EXCEPT は、対応しているすべての浮動小数点例外の組み合わせを意味します。

FE_INEXACT については、浮動小数点数の計算で丸めが起こるのはよくあることなので、割と至るところで発生します。エラーというよりは、報告という感覚で受け取った方がいいかも知れません。

実際にエラーの有無を調べる流れは、errno を使ったものに似ています。

手順 errno 浮動小数点例外
errno に 0 を代入 feclearexcept関数を呼び出して、浮動小数点状態フラグをクリアする
数学関数を呼び出す 数学関数を呼び出す
errno の値を調べる fetestexcept関数を呼び出して、浮動小数点状態フラグの状態を調べる

feclearexcept関数と fetestexcept関数は、fenv.h に以下のように宣言されています。

int feclearexcept(int excepts);
int fetestexcept(int excepts);

いずれも実引数には、先ほどのマクロのいずれか、あるいは組み合わせを指定します。普通は、feclearexcept関数の方は FE_ALL_EXCEPT を指定して、すべてのフラグをクリアすることになるでしょう。

実引数に複数のマクロを指定するときにはビット和演算(第49章)を行います。

fetestexcept関数は、指定した浮動小数点例外のうちのいずれか1つでも発生していたら、0以外の値を返します。

正確にいえば、戻り値は、浮動小数点状態フラグと実引数とのビット積演算(第49章)を行った結果です。

なお、ここまでに取り上げた浮動小数点例外に関する機能を使うには、FENV_ACCESS という標準プラグマを、次のように定義する必要があります。

#pragma STDC FENV_ACCESS on

ただし、VisualStudio 2015/2017 は標準プラグマに対応していないため、次のように VisualStudio の独自のプラグマを定義します。

#pragma fenv_access (on)

以下、sqrt関数(後述)で定義域エラーの発生を調べる例です。

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

#if defined(_MSC_VER)  /* VisualStudio であるか? */
#pragma fenv_access (on)
#else
#pragma STDC FENV_ACCESS on
#endif

void sqrt_test(double x)
{
    double result;

    feclearexcept( FE_ALL_EXCEPT );
    result = sqrt( x );
    if( fetestexcept(FE_INVALID) ){
        puts( "定義域エラーが発生しました。" );
    }
    else{
        printf( "%f\n", result );
    }
}

int main(void)
{
    sqrt_test( 9.0 );    /* OK */
    sqrt_test( -9.0 );   /* 定義域エラー */
    sqrt_test( 0.0 );    /* OK */

    return 0;
}

実行結果:

3.000000
定義域エラーが発生しました。
0.000000

C11 (極エラー)

C11 では、極エラー (Pole Error) が追加されています。これは、実引数が有限ではあるものの極限に近いために、計算結果が無限大になることで発生します。

極エラーの扱いは、値域エラーと同様です。エラー処理に errno を用いるのなら ERANGE がセットされ、浮動小数点例外を用いるのなら、FE_DIVBYZERO で表される例外が発生します。

絶対値

絶対値を求めるには、fabs関数を使います。

double fabs(double x);

C99 には、float型版の fabsf関数、long double型版の fabsl関数があります。

fabs関数は、実引数に指定した値の絶対値を返します。非常に単純な関数であり、エラーを発生させることもありません。

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

int main(void)
{
    printf( "%f\n", fabs(1.0) );
    printf( "%f\n", fabs(-1.0) );
    printf( "%f\n", fabs(0.0) );

    return 0;
}

実行結果:

1.000000
1.000000
0.000000

また、stdlib.h に、整数型を扱う abs関数labs関数があります。

int abs(int i);
long int labs(long int i);

C99 には、long long int型版の llabs関数があります。

こちらも、実引数に指定した値の絶対値を返します。

整数型で表現できる値の範囲の都合上、最も小さい負数の絶対値を表現できない可能性があります。この場合に返される結果は未定義です。

#include <limits.h>
#include <stdio.h>
#include <stdlib.h>

int main(void)
{
    printf( "%d\n", abs(1) );
    printf( "%d\n", abs(-1) );
    printf( "%d\n", abs(INT_MIN) );      /* 未定義の結果を返す可能性がある */
    printf( "%d\n", abs(INT_MIN + 1) );

    return 0;
}

具体的には、数の表現に、2の補数表現を使う環境で問題になります。この表現方法では、負数の方が正数よりも 1つだけ多くの数を扱えます(例えば -128~+127 のように)。2の補数表現は非常に一般的であり、ほとんどの環境がこの表現方法を使っています(第18章)。

べき乗

べき乗を求めるには、pow関数を使います。

double pow(double x, double y);

C99 には、float型版の powf関数、long double型版の powl関数があります。

x の y乗を計算して返します。

pow関数は、以下のようにエラーを発生させることがあります。

  1. x が負数で、y が整数でない場合、定義域エラー
  2. x が 0 で、y が 0 以下の場合、定義域エラー。あるいは値域エラー
  3. 結果が表現できない場合、値域エラー

C99 以降では、コンパイラが IEC 60559 の仕様に準拠している場合、さらに細かな規定が追加されています。準拠の状況は、__STDC_IEC_559__マクロが定義されているかどうかで判定できます。

2番目が曖昧ですが、結果が無限大になってしまうようなケースで値域エラーになります。また、この条件を見る限りでは、x と y がともに 0 の場合もエラーになるはずですが、実際にはエラーにならず、1.0 を返す実装もあります(VisualStudio、clang はともに 1.0 を返します)。

C11 では、x が 0 で、y も 0 の場合は定義域エラー。x が 0 で、y が 0未満の場合は、定義域エラーまたは極エラーになるとされています。

動作を確認してみましょう。

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

void pow_test(double x, double y)
{
    double result;

    errno = 0;
    result = pow( x, y );
    if( errno == EDOM ){
        puts( "定義域エラーが発生しました。" );
    }
    else if( errno == ERANGE ){
        puts( "値域エラーが発生しました。" );
    }
    else{
        printf( "%f\n", result );
    }
}

int main(void)
{
    pow_test( 2.0, 3.0 );       /* OK */
    pow_test( 2.0, 0.0 );       /* OK (0以外を 0乗すると、常に 1.0) */
    pow_test( 0.0, 2.0 );       /* OK (0 を n乗すると、常に 0.0) */
    pow_test( 2.0, -3.0 );      /* OK */
    pow_test( -2.0, 3.0 );      /* OK */
    pow_test( 0.0, 0.0 );       /* 定義域エラー (1.0 のこともある) */
    pow_test( 0.0, -2.0 );      /* 値域エラー */
    pow_test( -2.0, 1.5 );      /* 定義域エラー */
    pow_test( DBL_MAX, 2.0 );   /* 値域エラー */
    pow_test( DBL_MIN, -2.0 );  /* 値域エラー */

    return 0;
}

実行結果:

8.000000
1.000000
0.000000
0.125000
-8.000000
1.000000
値域エラーが発生しました。
定義域エラーが発生しました。
値域エラーが発生しました。
値域エラーが発生しました。

平方根

平方根を求めるには、sqrt関数を使います。

double sqrt(double x);

C99 には、float型版の sqrtf関数、long double型版の sqrtl関数があります。

x の平方根を返します。例えば、9 の平方根には +3 と -3 がありますが、常に正の平方根が返されます。

sqrt関数は、実引数が負数の場合、定義域エラーを発生させます。

動作を確認してみましょう。

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

void sqrt_test(double x)
{
    double result;

    errno = 0;
    result = sqrt( x );
    if( errno == EDOM ){
        puts( "定義域エラーが発生しました。" );
    }
    else{
        printf( "%f\n", result );
    }
}

int main(void)
{
    sqrt_test( 9.0 );    /* OK */
    sqrt_test( -9.0 );   /* 定義域エラー */
    sqrt_test( 0.0 );    /* OK */

    return 0;
}

実行結果:

3.000000
定義域エラーが発生しました。
0.000000

近い整数を得る(丸め関数)

小数点以下を切り上げたり、切り捨てたりした結果が欲しい場面があります。このような場合、ある浮動小数点数から、一番近い整数を得る関数が利用できます。

こういった関数には、C95 時点では ceil関数floor関数の2つがあります。また、C99 で種類が大幅に増えています

double ceil(double x);
double floor(double x);

C99 には、float型版の ceilf関数floorf関数が、long double型版の ceill関数floorl関数があります。

ceil関数は、実引数の値の小数点以下を切り上げた結果を返します。floor関数は、切り捨てた結果を返します。結果の型は整数型ではなく、double型のまま返されます。

これらの関数はエラーを発生させることはありません。

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

int main(void)
{
    printf( "%f\n", ceil(10.1) );
    printf( "%f\n", ceil(10.9) );
    printf( "%f\n", ceil(-10.1) );
    printf( "%f\n", ceil(-10.9) );

    printf( "%f\n", floor(10.1) );
    printf( "%f\n", floor(10.9) );
    printf( "%f\n", floor(-10.1) );
    printf( "%f\n", floor(-10.9) );

    return 0;
}

実行結果:

11.000000
11.000000
-10.000000
-10.000000
10.000000
10.000000
-11.000000
-11.000000

実引数が負数の場合でも規則は同じです。ceil関数は切り上げるので、元の数より大きい値が返され、floor関数は切り捨てるので、元の数より小さい値が返されます。

ところで、四捨五入をしたければどうすればよいのでしょうか? C99 には round関数があるので、これを使えばよいですが、C95 までは自力で計算するしかありません。この話題は、「逆引き 四捨五入する」で扱っているので、そちらを参照して下さい。

C99 (関数の追加)

C99 で、ceil関数や floor関数の float型版、long double型版も追加されていますが、それ以外にも、以下のように多くの関数が追加されています。ここでは、float型版、long double型版は省略しますが、すべての関数に存在します。

double round(double x);
long int lround(double x);
long long int llround(double x);
double trunc(double x);
double nearbyint(double x);
double rint(double x);
long int lrint(double x);
long long int llrint(double x);

round関数は、実引数の値を小数点以下で四捨五入した結果を返します。一番近い2つの整数値の丁度中間の値(つまり、4.5 とか -4.5 のような値)の場合は、0 から遠い方の値を返すことになっています(つまり、4.5 なら 5.0 が、-4.5 なら -5.0 が返されます)。

lround関数llround関数も round関数と同じことをしますが、結果を整数型で返します。一見便利そうな気もしますが、double型で表現できた値が、整数型で表現できるとは限らないことに注意が必要です。戻り値の型で表現できない場合の結果は未規定です。また、実引数の絶対値が大きすぎると、値域エラーを発生させます。

trunc関数は、小数点以下を捨て去った結果を返します(規格通りにいうと、絶対値が実引数の絶対値よりも大きくない値を返します)。例えば、floor関数では -4.5 に対して、-5.0 を返しますが、trunc関数は -4.0 を返します。

nearbyint関数は、現在の丸め方向に従って、小数点以下を丸めた整数値を返します。丸め方向は、浮動小数点数の演算の際、精度上表現できない値になったときに、どのような処置を取るかを決定するものです。これは、fenv.h にある関数やマクロによって変更・取得することができます。

rint関数は、nearbyint関数と同じことをしますが、結果が実引数の値と一致することを前提としています。つまり、実引数がすでに整数値になっていなければならず、そうでなければ、FE_INEXACT 浮動小数点例外を発生させることがあります。

lrint関数llrint関数は、nearbyint関数と同じことをしますが、結果を整数型で返します。戻り値の型で表現できない場合の結果は未規定です。また、実引数の絶対値が大きすぎると、値域エラーを発生させます。

対数

対数を求める標準ライブラリ関数は(C95 には)2つあります。1つは、自然対数(底がネイピア数 e の対数)を求める log関数、もう1つは、常用対数(底が 10 の対数)を求める log10関数です。

double log(double x);
double log10(double x);

C99 には、float型版の logf関数log10f関数、long double型版の logl関数log10l関数があります。

実引数の自然対数あるいは常用対数を求めて返します。

実引数が負数の場合、定義域エラーが発生します。また、実引数が 0 の場合には、値域エラーが発生することがあります。

C99 以降では、コンパイラが IEC 60559 の仕様に準拠している場合、さらに細かな規定が追加されています。準拠の状況は、__STDC_IEC_559__マクロが定義されているかどうかで判定できます。

log10関数の使用例として、10進数の桁数を調べる関数を作ってみます。

#include <assert.h>
#include <errno.h>
#include <math.h>
#include <stdio.h>
#include <stdlib.h>

int get_digits(int n)
{
    double result;

    if( n == 0 ){
        return 1;
    }
    
    errno = 0;
    result = log10( abs(n) );
    assert( errno == 0 );

    return (int)result + 1;
}

int main(void)
{
    printf( "%d\n", get_digits(0) );
    printf( "%d\n", get_digits(1) );
    printf( "%d\n", get_digits(100) );
    printf( "%d\n", get_digits(10000000) );
    printf( "%d\n", get_digits(-1) );
    printf( "%d\n", get_digits(-100) );
    printf( "%d\n", get_digits(-10000000) );

    return 0;
}

実行結果:

1
1
3
8
1
3
8

常用対数を求めることは、すなわち「ある数が 10 の何乗であるか」ということですから、この性質を利用して、10進数の桁数を得られます。単純に使うと 1 小さい数が得られるので +1 する必要はあります(例えば、10 が 10 の 1乗であることを考えれば、意味が分かるでしょう)。

log10関数の実引数は 1以上にしないとエラーが発生してしまう可能性があるため、0以下の数の桁数を得たいときに対する備えが必要です。負数への備えとしては、絶対値を渡すようにしておけばよいです。0 の場合は直接 1(桁) を返すことにしています。

三角関数

三角関数についても用意されています。正弦を sin関数で、余弦を cos関数で、正接を tan関数で求められます。

double sin(double x);
double cos(double x);
double tan(double x);

C99 には、float型版の sinf関数cosf関数、long double型版の sinl関数cosl関数があります。

引数 x には、角度をラジアン単位で指定します。

「度」と「ラジアン」を変換するような関数やマクロは、標準にはありません。必要があれば、自前で作っておくとよいでしょう。このとき、円周率の定義についても、標準には存在していないので、自前での定義が必要です。

円周率に関して、M_PI という名前の定義をよく見かけますが、これは標準のものではありません。

次のサンプルプログラムでは、45°刻みで、sin、cos、tan の結果を出力しています。

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

#define PI 3.14159265358979323846
#define DEG_TO_RAD(deg)  ((deg) / 180.0 * (PI))  /* 度からラジアンへの変換 */

int main(void)
{
    double deg;
    int i;

    deg = -180.0;
    for( i = 0; i <= 8; ++i ){
        printf( "sin(%.1f) = %f\n", deg, sin(DEG_TO_RAD(deg)) );
        deg += 45.0;
    }

    deg = -180.0;
    for( i = 0; i <= 8; ++i ){
        printf( "cos(%.1f) = %f\n", deg, cos(DEG_TO_RAD(deg)) );
        deg += 45.0;
    }

    deg = -180.0;
    for( i = 0; i <= 8; ++i ){
        printf( "tan(%.1f) = %f\n", deg, tan(DEG_TO_RAD(deg)) );
        deg += 45.0;
    }
    
    return 0;
}

実行結果:

sin(-180.0) = -0.000000
sin(-135.0) = -0.707107
sin(-90.0) = -1.000000
sin(-45.0) = -0.707107
sin(0.0) = 0.000000
sin(45.0) = 0.707107
sin(90.0) = 1.000000
sin(135.0) = 0.707107
sin(180.0) = 0.000000
cos(-180.0) = -1.000000
cos(-135.0) = -0.707107
cos(-90.0) = 0.000000
cos(-45.0) = 0.707107
cos(0.0) = 1.000000
cos(45.0) = 0.707107
cos(90.0) = 0.000000
cos(135.0) = -0.707107
cos(180.0) = -1.000000
tan(-180.0) = 0.000000
tan(-135.0) = 1.000000
tan(-90.0) = -16331239353195370.000000
tan(-45.0) = -1.000000
tan(0.0) = 0.000000
tan(45.0) = 1.000000
tan(90.0) = 16331239353195370.000000
tan(135.0) = -1.000000
tan(180.0) = -0.000000

90°のときの正接は定義できないため、おかしな結果になっていますが、エラーとはなりません。


練習問題

問題① 2つの浮動小数点数が、どれだけ離れているかを計算するプログラムを作成して下さい。

問題② 2次元平面上にある2つの点の間の距離を計算するプログラムを作成して下さい。


解答ページはこちら

参考リンク



更新履歴

'2018/5/25 新規作成。



前の章へ(第47章 ワイド文字)

次の章へ(第49章 ビット演算)

C言語編のトップページへ

Programming Place Plus のトップページへ


このエントリーをはてなブックマークに追加
rss1.0 取得ボタン RSS