ホーム » スタッフ » 斉藤徹 » 講義録 » 情報制御基礎 (ページ 8)

情報制御基礎」カテゴリーアーカイブ

2025年5月
 123
45678910
11121314151617
18192021222324
25262728293031

検索・リンク

様々な移動平均・レポート-No.3

移動平均のレポートでは、表計算ソフトを用いて、移動平均の範囲のとり方などを変えながら、平均をとった結果に、どう影響するのかを考える。

Excel で様々な移動平均の式を入力

表計算ソフトに、移動平均の式を入力する際には、以下の資料を参考にせよ。

上図のB4〜E4にできた移動平均の式は、B5以下にコピーすればいい。

レポート内容

以下のような移動平均を、Excel にて計算し、その結果の違いについて考察せよ。
移動平均で用いる点の数は、自分の出席番号の3の余りによって、条件を与える。

計算方法 出席%3=0 出席%3=1 出席%3=2
単純移動平均(狭) n=3 n=4 n=5
単純移動平均(広) n=6 n=8 n=10
過去の値による単純移動平均(狭) n=3 n=4 n=5
過去の値による単純移動平均(広) n=6 n=8 n=10
加重移動平均(狭) n=3 n=4 n=5
加重移動平均(広) n=6 n=8 n=10
指数移動平均(基本) α=1/2 α=1/2 α=1/2
指数移動平均(広) α=1/3 α=1/4 α=1/5

平均に用いる値は、以下のデータとする。

    • 2018-06-05-wave.csv 時刻tとx(t)のコンマ区切りファイル
    • 時間の遅延がわかるような波形を用い、考察してあることが望ましい

1枚のグラフの中に、元波形+8波形=9本のグラフを記載すると、グラフの内容が分かりにくいので、複数のグラフ結果で図示すること。

プログラミングが得意な人は、上記をExcelで処理するのではなく、C言語にて移動平均を計算し、結果をExcelに取り込んでグラフとして表示することにチャレンジしてほしい。

提出レポートに、全データの計算結果は不要です。動作の確認の意味で、先頭10点ほどの計算結果をつけてください。

様々な移動平均

波形処理をハードウェアで行うかソフトウェアで行うか

組込み用の小型コンピュータが普及する以前は、このような波形に対する処理を行う場合には、電子回路的に波形のフィルタリングを行っていた。しかし電子回路的な方法では、回路の特性が変化してうまく処理ができなくなった場合に、回路の組み換えが発生するかもしれない。ただし回路の変更は基板の作り直しが必要であったりすることから、コストがかかる

一方、A/D変換機能を内蔵した組込み用小型コンピュータも極めて安価になってきた。

こういったコンピュータの普及で、最近ではアナログ値をコンピュータに取り込んでデジタルの数値的な処理で余計な情報を取り除く。この方法であれば、プログラムを変更するだけなので、安価に変更が可能となる。ただし、アナログ値をA/D変換するのには時間がかかることから、周波数の高い信号には不向きである。

単純移動平均

前回説明を行った単純移動平均は、時刻tの平均を、その前後のデータで平均を求めた。この方式は、実際には与えられた波形のデータを全部記録した跡に、単純移動平均をとる場合に有効である。

しかし、時々刻々変化する測定値の平均をその都度使うことを考えると、上記の方法は、未来の測定値を使っていることから、現実的ではない。

#define NS 3
int x[ SIZE ] ; // 入力値
int y[ SIZE ] ; // 出力値
for( int t = NS ; t < SIZE-NS ; t++ ) {
   int s = 0 ;
   for( int i = -NS ; i <= +NS ; i++ ) // 2*NS+1回の繰り返し
      s += x[t+i] ;
   y[t] = s / (2*NS + 1) ;
}

過去の値だけを使った移動平均

そこで、過去の値だけで移動平均をとることも考えられる。

この、単純移動平均と、過去の値だけを使う単純移動平均を、適当な測定値に対して適用した場合のグラフの変化を Excel によってシミュレーションした結果を以下に示す。

しかし、このグラフを見ると、波形後半の部分に注目するとよく分かるが、過去の値だけを使った移動平均では、測定値が立ち上がったのを追いかけて値が増えていく。これでは移動平均は時間的な遅れとなってしまう。

for( int t = NS ; t < SIZE ; t++ ) {
   int s = 0 ;
   for( int i = 0 ; i <= NS ; i++ ) // NS+1回の繰り返し
      s += x[t-i] ;
   y[t] = s / (NS+1) ;
}

加重移動平均

過去の値を使った移動平均では遅れが発生する。でも、平均を取る際に、「n回前の値」と「現在の値」を考えた時、「その瞬間の平均値」は「現在の値」の方が近い値のはず。であれば、平均を取る時に、「n回前の値は少なめ」「現在の値は多め」に比重をかけて加算する方法がある。

for( int t = 3 ; t < SIZE ; t++ ) {
   int s = x[t]   * 3
         + x[t-1] * 2
         + x[t-2] * 1 ; // 数個の移動平均だし、
   y[t] = s / (3+2+1) ; //  ループを使わずに書いてみる。
}

この様に、過去に遡るにつれ、平均をとる比重を直線的に小さくしながら移動平均をとる方法は、加重移動平均と呼ばれる。以下にその変化をExcelでシミュレーションしたものを示す。

指数移動平均

ここまで説明してきた、単純移動平均や、加重移動平均は、平均をとる範囲の「過去の値」を記憶しておく必要がある。広い時間にわたる移動平均をとる場合は、それに応じてメモリも必要となる。これは、組み込み型の小型コンピュータであれば、メモリが足りず平均処理ができない場合もでてくる。

そこで、荷重移動平均の重みを、は、100%,は50%,は25%… というように、過去に遡るにつれ、半分にして平均をとる。

しかし、以降の項で、 を使うと以下のように書き換えることができる。

for( int t = 1 ; t < SIZE ; t++ ) {
   y[t] = ( x[t] + y[t-1] ) / 2 ;
}

この方法であれば、直前の平均値を記録しておくだけで良い。このような移動平均を、指数移動平均と呼ぶ。

ここで示した指数移動平均は、過去を遡るにつれとなっているが、これをさらに一般化した指数移動平均は、以下の式で示される。前述の移動平均は、とみなすことができる。

#define ALPHA 0.5
for( int t = 1 ; t < SIZE ; t++ ) {
    y[t] = ALPHA * x[t] + (1.0 - ALPHA) * y[t-1] ;
}

以下のプログラムは、うまく動かない。理由を説明せよ。

#define RVA 4
for( int t = 1 ; t < SIZE ; t++ ) {
   // 以下はy[t]は全部ゼロになる。
   y[t] = 1/RVA * x[t] + (1.0 - 1/RVA) * y[t-1] ;

   // 以下は、整数型演算だけで、正しく動くだろう。
   // y[t] = ( x[t] + (RVA-1) * y[t-1] ) / RVA ;
}

移動平均のプログラム

移動平均のプログラム(ダサっ)

#include <stdio.h>
#define WIDTH 5
double xt[1000] ; // 元波形データ
double yt[1000] ; // 移動平均処理後のデータ
int main() {
    int    i ; 
    double t , x ;
    // 全データを読み込む(入力はコンマ区切りの2データ)
    for( i = 0 ; scanf( "%lf,%lf" , &t , &x ) == 2 ; i++ )
        xt[ i ] = x ;
    // データ xt[*] の移動平均を yt[*] に求める。
    for( i = WIDTH ; i < 1000 - WIDTH ; i++ ) {
        int j ;
        double s = 0.0 ; // 合計
        // 前後の値の合計を求める
        for( j = -WIDTH ; j <= WIDTH ; j++ )
            s = s + xt[ i + j ] ;
        // 合計をデータ数で割る
        yt[ i ] = s / (WIDTH * 2 + 1) ;
    }
    // 処理結果を出力する。
    for( i = 0 ; i < 1000 ; i++ )
        printf( "%d,%lf,%lf\n" , i , xt[i] , yt[i] ) ;
    return 0 ;
}

でも、このプログラムは、以下の点で問題がある。

  1. 範囲のデータを加算しているけど、加算の繰り返しが多い。
  2. 配列にデータを最初に全て読み込んでいるけど、長時間のデータならば大量のメモリが必要。
  3. 測定しながら移動平均を計算する場合、データはどうする?

移動平均のプログラム(ちょっと改良)

全部のデータを覚えるのはメモリの無駄なので、移動平均する直近のデータだけを覚えるように改良する。
しかし、データを保存する度に、配列をずらす処理も無駄なので、データを保存する場所(以下の例ではbp)を保存したら次の場所を示すように記述してみる。

#include <stdio.h>

#define WIDTH 10
double buff[ WIDTH ] ; // 直近のWIDTH個だけ保存
int    bp = 0 ;        // 最新データの場所
double bs = 0.0 ;      // 直近のWIDTH個の合計

int main() {
    int i ;
    double t , x ;
    for( i = 0 ; scanf( "%lf,%lf" , &t , &x ) == 2 ; i++ ) {
        // WIDTH個前のデータを捨てるために合計から引く
        bs = bs - buff[ bp ] ;

        buff[ bp ] = x ;      // 最新データを保存
        bs = bs + x ;         // 最新のデータで合計
        // 直近のデータを覚える場所を移動
        bp++ ;
        if ( bp >= WIDTH )
            bp = 0 ;
        // 移動平均を出力
        printf( "%d %lf\n" , i , bs / WIDTH ) ;
    }
    return 0 ;
}

移動平均の処理

前回の授業で説明したようなA/D変換した数値データを読み取った場合、どのようなことが発生するか考える。

例えば、以下に示すような測定値があったとする。

このデータの一部をグラフ化してみると、次のような波形であった。

この波形をみると、大きく見ればsinカーブだが、細かい点を見るとデータにブレがある。

誤差の原因

このような測定結果が得られた場合、本来コンピュータで処理したいデータは何であろうか?

原因は様々なものが考えられるが、
(1) 回路のノイズ対策が不十分で、外部の電気的な影響が混入してしまうこともある。
(2) 一方で、D/A 変換を行う場合には、量子化誤差もある。

青線:元波形、赤線:4段階で量子化

この例は、-1〜1の範囲を、4段階に量子化することで、本来の波形とは異なった値になっている。

例えば、最初の波形が、加速度センサーの値であったとして、船の上で揺れているために、大きな周期で加速度が変化しているかもしれない。一方で、船自体がエンジンによる揺れで加速度が変化しているかもしれない。

船の中で波の揺れと、エンジンの揺れが観測されている加速度センサーの情報で、船の揺れの大きさ・揺れの周期を知りたい場合、どうすればいいだろうか?単純なsinカーブの波形であれば、波形の最大値・最小値・0との交点の場所を探せば、振幅や周期が求めることができるが、このようなノイズが入った波形では、正しく振幅・周期が求まらない。

移動平均

このデータを見ると、10個のデータまでの間で、波形が上下に変動している。船の揺れとエンジンの揺れが原因であれば、10個ぐらいのデータのゆらぎが、エンジンによる揺れと考えられる。では、この10個ぐらいの範囲で値が上下の影響を減らしたければ、どうすればいいか?一番簡単な方法は、前後10個のデータで平均を取ればいいだろう。まずは、Excel で前後データの平均をとってみよう。

Excelで前後11点の平均を求める式をセルに入れる

青線:元波形データ(B列)、赤線:前後11点の平均(C列)

このように、データの前後の決められた範囲の平均を平均する処理は、移動平均(単純移動平均)と呼ぶ。

時間tにおけるデータをとした場合、前後5点の移動平均は、以下のような式で表せるだろう。

自宅学習の課題(レポート提出は不要)

表計算ソフトで、移動平均を計算させてみよう。  

  • 元波形
  • 前後5点で移動平均
  • 前後11点で移動平均
  • 前後51点で移動平均

をとるような表計算の式を書き込んで、その結果の波形がどんなグラフになるのか確認しておくこと。

移動平均の応用

例えば、以下のような心電図のデータがあったとする。心電図では、波の高さで心臓が正常か判断するが、この青のグラフでは、大きな周期の変動が含まれるため、波の高さを正確に測れない。この波形から診断するときに、移動平均が使えないだろうか?

このような場合には、測定値の波形の移動平均をとり心拍データの変動を取り除き、測定値から移動平均の値を引くことで、心拍データだけを取り出すことが可能となる。

D/A変換回路とA/D変換回路

小型コンピュータを使った制御では、外部回路に指定した電圧を出力(D/A変換)したり、外部の電圧を入力(A/D変換)したりすることが多い。以下にその為の回路と動作について説明する。

D/A変換回路

ラダー抵抗回路によるD/A変換の仕組みを引用

このような回路で、D0,D1,D2 は、デジタル値の0=0[V] , 1=5[V] であった場合、Output 部分の電圧は、(D0,D1,D2)の値が、(0,0,0),(0,0,1),…(1,1,1)と変化するにつれ、5/8[V]づつ増え、(1,1,1)で 5*(7/8)=4.4[V]に近づいていく。Output が出力によって電圧が変化しないように、アンプで増幅する。

DCモータをアナログ量で制御しないこと

このように、電圧をコンピュータから制御するようになると、ロボットで模型用の直流モータの回転速度をこれで制御したい…と考えるかもしれない。
しかし、直流モータは、コイル(電磁石)は単なる導線である。例えば、小さい電流で遅い速度でモータを回転させようとすると、小さい電圧でも導線(抵抗はほぼ0[Ω])には大量の電流が流れる。


PWM変調

こういう場合には、PWM変調(Pulse Width Modulation) を行う。

このような波形であれば、低速度でも高トルクが得られる。

A/D変換回路

D/A変換とは逆に、アナログ量をデジタル値に変換するには、どのようにするか?

このような場合には、A/D変換回路を用いる。一般的な回路では、以下のような逐次比較型A/D変換を用いる。

この回路では、変換開始と共に入力値をサンプル保持回路でアナログ量を保存する。
その後、Registerの中のデジタル値を、D/A 変換回路でアナログ量に変換した結果を、比較器(Comparator)でどちらが大きいか判断し、その結果に応じて2分探索法とかハイアンドローの方式のように、比較を繰り返しながらデジタル値を入力値に近づけていく。

ハイアンドロー(数あてゲーム)

数あてゲームで、デタラメな0〜127までの整数を決めて、ヒントを元にその数字を当てる。回答者は、数字を伝えると、決めた数よりHighかLowのヒントをもらえる。
最も速い回答方法は…

例えば決めた数が55だとすると
・64 - Low   0------  0..63
・32 - High  01----- 32..63
・48 - High  011---- 48..63
・56 - Low   0110--- 48..55
・52 - High  01101-- 52..55
・54 - High  011011- 54..55
・55 - Bingo 0110111 55確定
どんな値でも、7回(27=127)までで当てることができる。

入出力と変数・レポートNo.2

以下のような、位相が 0°,15°,30°,45° ずれた sin(x) のグラフを描くためのプログラムを作りたい。

Excel で式入力すりゃいいじゃん…というのはナシ。

0 =A1+15 =B1+15
=A1+5 =sin((A2+B1)/180*3.141592) =sin((A2+C1)/180*3.141592)

// 以下のプログラムは、初心者なら書きそうなボケが沢山入ってます。
// 正しく直してください。
#include <stdio.h>

#define PI = 3.1415926535 ;
int th ;

void phase( int x ) {
    printf( "%d" , x ) ;
    // 位相を0度から45度まで15度ずつ変化させる
    for( th = 0 ; th <= 45 ; th += 15 ) {
        printf( " %d" , sin( (x + th) / 180 * PI ) ) ;
    }
    printf( "¥n" ) ;
}

int main() {
    // 角度を 0..360度 の範囲で表示
    for( th = 0 ; th != 360 ; th += 5 ) {
        phase( th ) ;
    }
    return 0 ;
}

このプログラムを正しく修正し、Excel に値を取り込んで、上図に示すようなグラフにしてください。

レポートでは、プログラムリスト、プログラムの説明、実行結果、感想・考察を記載し提出してください。

入出力リダイレクト

プログラムの動作を確認する場合、指定された値を使って計算したり、その結果を他のプログラムで使いたい場合が多い。そこで、入出リダイレクトについて説明する。

入力リダイレクト

以下に上げるような Excel のデータで平均を計算してみよう。

A6 に =AVERAGE(A1:A5) を入力…
というのは、ナシ。Visual Basic で組むのもナシ。(^_^;

Excelで、「ファイル-名前をつけて保存」、「ファイル形式」に「スペース区切りテキスト(.prn)」で保存する。

// 入力値の平均を求める
//   ファイルは、Z:¥foo¥bar¥avg.c にあるとする。
#include <stdio.h>

int main() {
    int count = 0 , sum = 0 ;
    int x ;
    while( scanf( "%d" , &x ) == 1 ) {
        count++ ;
        sum += x ;
    }
    printf( "%lf¥n" , sum / count ) ; // この行は間違い。修正せよ。
    return 0 ;
}

このプログラムを普通に実行したら、キーボードから 83,95,92,95,77 と毎回入力しなければならない。めんどくさくない?

Excel の空白区切りのデータを読み込む

先程の Excel で保存したファイルを同じディレクトリにコピーする。(Z:¥foo¥bar¥avg.prnとする)

コンパイラで、avg.c をコンパイルし、avg.exe ができていることを確認し、
cmd.exe を起動

Z:¥> cd Z:¥foo¥bar
Z:¥foo¥bar> avg.exe < avg.prn

このように、プログラムを起動する時に、通常はキーボードから入力するプログラムに対し、起動時に “< ファイル名” を付けて起動し、ファイル入力に切り替えることを、入力リダイレクトと言う。

出力リダイレクトとグラフ化

前回の授業での sin(x) のプログラムの実行結果を、Excel で確認してみよう。

// sin の値を出力
// ファイルは、Z:¥foo¥bar¥sin.c にあるとする。
#include <stdio.h>
#include <math.h>

int main() {
    double th , y ;
    for( th = 0.0 ; th <= 360.0 ; th += 5.0 ) {
        y = sin( th / 180.0 * 3.1415926535 ) ;
        printf( "%lf %lf¥n" , th , y ) ;
    }
    return 0 ;
}

プログラムを実行する時に、

コンパイラで sin.c をコンパイルし、sin.exe ができていることを確認し
cmd.exe を起動

Z:¥> cd Z:¥foo¥bar             プログラムのディレクトリに移動
Z:¥foo¥bar> sin.exe > sin.csv  プログラムを起動し出力を sin.csv に保存

このように、プログラムを起動する時に、通常は結果を画面に出力するプログラムに対し、起動時に “> ファイル名” を付けて起動し、結果をファイル出力に切り替えることを、出力リダイレクトと言う。

Excelにインポートしてグラフ化

Excel を起動し、「ファイル-インポート」より、「テキストファイル」を選び、「区切り文字」-「スペース区切り」でデータを取り込む。

あとは、取り込まれたデータ範囲を選択し、「挿入」-「グラフ」で好きなグラフ形式を選ぶ。

実数の扱い・レポート-No.1

プログラムの制御構造と実数の取扱いの確認として、以下のレポートを次回講義までに提出せよ。

No.1-1 制御構造

以下の3つ(No.1-1-1,No.1-1-2,No1-1-3)の問題から、No.1-1-「(自分の出席番号 % 3)+1」について、プログラムのフローチャートを描き、その実行順序を20ステップめまで答えよ。

レポートには、

  • 元プログラム
  • フローチャート
  • 実行順序
  • 変数の変化がわかる内容

を明記すること。

No.1-1-1

No.1-1-2

switch-case 文は説明していませんが、挙動をよく調べて回答してください。

No.1-1-3

No.1-2 実数の扱い

自分の携帯電話番号(もしくは家の電話番号)の末尾4桁のうち、2桁を整数部、末尾2桁を小数部として、その値を2進数に変換した結果を、計算方法が判るように答えよ。ただし、結果の2進数の小数部は8桁まで答えよ。

例えば、0778628278 ならば、82.78 )10を、xxxxxxx.xxxxxxxx )2 のように答えること。

実数の注意点・回答編

回答がすぐに見えないように、別記事に分ける

数値の精度に注意

// case-1 ---- プログラムが止まらない
#define PI 3.1415926535
int main() {
    double th , y ;
    // 0〜πまで100分割でsinを求める
    for( th = 0.0 ; th != PI ; th += PI / 100.0 ) {
        y = sin( th ) ;
        printf( "%lf %lf¥n" , th , y ) ;
    }
    return 0 ;
}

このプログラムの問題点は、終了条件が th != PI で書かれている点。

コンピュータの中では、データはすべて2進数で扱い、データを保存する桁数も有限である。

このため、0.0314159265 を 100 回 加えても、桁末尾の誤差のため、3.14159265 != 3.1415926535となってしまう。(ただしこの説明は10進数で説明しているけど、実際は2進数で同じような現象が発生している。)

int型とdouble型での暗黙の型変換

// case-2 ---- y の値が全てゼロ
int main() {
    int    th ;
    double y ;
    for( th = 0 ; th <= 360 ; th += 5 ) {
        y = sin( th / 180 * 3.1415926535 ) ;
        printf( "%d %lf¥n" , th , y ) ;
    }
    return 0 ;
}

このプログラムの問題点は、th / 180 。これがゼロになる原因。

コンピュータの中では、整数型 int と、実数 double 型では、計算の仕方が異なる。特に、実数型は、小数点の位置や指数を考えながら計算を行う必要があるため、処理に時間がかかる。このため、大量のデータを扱う場合にはコンピュータにとって簡単な計算となるように書く必要がある。

整数型は、小数点以下の値を持たない。このためコンピュータの中では、th = 5 の時、th / 180 を計算すると、5/180 = 0.0277 ではなく、5/180 = 0 で計算を行う(小数点以下は切り捨て)。

C言語の原則: 暗黙の型変換
    同じ精度で計算できるのなら同じ精度の型で計算する。
    精度が異なる場合は、精度の高い型に変換してから計算する。
        int 演算子 int    = int
        int 演算子 double = double

このようなことが発生するので、y=sin(th…)の行は、以下のように書くべきである。

y = sin( th / 180.0 * 3.1415926535 ) ;  // 180.0 は double 型
y = sin( (double)th / 180 * 3.1415926535 ) ; // 型キャストで double 型
y = sin( double( th ) / 180 * 3.1415926535 ) ; // C++の型変換関数
y = sin( (double)th / 180.0 * 3.1415926535 ) ; // 暗黙の型変換を排除

数値の範囲に注意

// 16bit コンピュータの時代に、
//   画面上のマウスとターゲットの距離を計算したかった
int distance( int x0 , int y0 , int x1 , int y1 ) {
    int dx = x1 - x0 ;
    int dy = y1 - y0 ;
    return sqrt( dx * dx + dy * dy ) ;
}

例えば、このプログラムで (x0,y0)=(0,0) , (x1,y1)=(200,200) 出会った場合、sqrt( 40000 + 40000 ) という計算が出て来る。

ところで、16bit コンピュータにおける int 型は、どうやって覚えているのか?

符号あり整数型

コンピュータの中では、負の数を扱う必要から、2の補数表現が用いられる。

         x = 0000,0000,0000,1010(2)    = 10(10)
        ~x = 1111,1111,1111,0101      1の補数
    ~x + 1 = 1111,1111,1111,0110      1の補数に+1 ⇒ -10(10)
x + ~x     =   1111,1111,1111,1111
x + ~x + 1 = 1,0000,0000,0000,0000 ≒ 0 // 16bit目ははみ出るからzero

このため、16bit コンピュータの int 型で扱える数は、

   max = 0111,1111,1111,1111(2) = 32767(10)
   min = 1000,0000,0000,0000(2) = -32768(10)

40000 は、16bit コンピュータでは、扱える範囲を越えている。

ということで、前述のプログラムは、

// 16bit コンピュータなら、long int 型は 32bit 
int distance( int x0 , int y0 , int x1 , int y1 ) {
    long int dx = x1 - x0 ;
    long int dy = y1 - y0 ;
    return (int)sqrt( dx * dx + dy * dy ) ;
}
// スピードを気にしないのなら(sqrtがdouble型だし...)
double distance( double x0 , double y0 , double x1 , double y1 ) {
    double dx = x1 - x0 ;
    double dy = y1 - y0 ;
    return sqrt( dx * dx + dy * dy ) ;
}

実数の注意点

C言語でプログラムを作成していて、簡単な数値計算のプログラムでも動かないと悩んだことはないだろうか?解らなくて友達のプログラムを真似したら動いたけど、なぜ自分のプログラムは動かなかったのか深く考えたことはあるだろうか?

まずは動く例

以下のプログラムは、見れば判るけど、th を 0度〜360度まで5度刻みで変化させながら、y = sin(th) の値を表示するプログラム。

// sin の値を出力
#include <stdio.h>
#include <math.h>

int main() {
    double th , y ;
    for( th = 0.0 ; th <= 360.0 ; th += 5.0 ) {
        y = sin( th / 180.0 * 3.1415926535 ) ;
        printf( "%lf %lf¥n" , th , y ) ;
    }
    return 0 ;
}

動かないプログラム

では、以下のプログラムはどうだろうか?

// case-1 ---- プログラムが止まらない
#define PI 3.1415926535
int main() {
    double th , y ;
    // 0〜πまで100分割でsinを求める
    for( th = 0.0 ; th != PI ; th += PI / 100.0 ) {
        y = sin( th ) ;
        printf( "%lf %lf¥n" , th , y ) ;
    }
    return 0 ;
}
// case-2 ---- y の値が全てゼロ
int main() {
    int    th ;
    double y ;
    for( th = 0 ; th <= 360 ; th += 5 ) {
        y = sin( th / 180 * 3.1415926535 ) ;
        printf( "%d %lf¥n" , th , y ) ;
    }
    return 0 ;
}

どちらも、何気なく読んでいると、動かない理由が判らないと思う。そして、元のプログラムと見比べながら、case-1 では、「!=」を「<=」に書き換えたり、case-2 では、「int th ;」を「double th ;」に書き換えたら動き出す。

では何が悪かったのか…
回答編

数値の範囲に注意

前節の回答編で示したが、数値の扱える値の範囲に注意すべきである。
私自身が自分で書いたプログラムで悩んだ例を以下に示す。

// 16bit コンピュータの時代に、
//   画面上のマウスとターゲットの距離を計算したかった
int distance( int x0 , int y0 , int x1 , int y1 ) {
    int dx = x1 - x0 ;
    int dy = y1 - y0 ;
    return sqrt( dx * dx + dy * dy ) ;
}

このプログラムを実行した時、通常はうまく動くのだけど、時々「sqrt は、負の値では計算できません」というエラーを表示する。

なぜだろうか?
回答編

2進数への変換(補助資料)

10進数で 123.45 は、1*102 + 2*101 + 3*100 + 4*10-1 + 5*10-2 を意味する。(あたりまえ)

2進数に変換する場合、整数部と小数部に分けて考えると、

10進数なら、それぞれを 10 で割る、10 をかけると
  123 / 10 = 12.3 小数部に3 が出てくる。
  0.45 * 10 = 4.5 整数部に4 が出てくる。

2進数なら、それぞれを 2 で割る、2をかけると...
123.45 を 2進数に変換

 2)123 )10 = 1111011 )2
    ̄ ̄ ̄ ̄
 2) 61 ... 1    次々と2で割って、余りを求める
    ̄ ̄ ̄ ̄
 2) 30 ... 1
    ̄ ̄ ̄ ̄
 2) 15 ... 0
    ̄ ̄ ̄ ̄
 2)  7 ... 1
    ̄ ̄ ̄ ̄
 2)  3 ... 1
    ̄ ̄ ̄ ̄
 2)  1 ... 1
    ̄ ̄ ̄ ̄
     0 ... 1

✕2)0.45 )10 = 0.01110011001100...)2
    ̄ ̄ ̄ ̄ ̄
✕2)0.9 ... 0    次々と2倍して、整数部を求める
    ̄ ̄ ̄ ̄ ̄
✕2)1.8  ... 1 ※
    ̄ ̄ ̄ ̄ ̄
✕2)1.6 ... 1
    ̄ ̄ ̄ ̄ ̄
✕2)1.2 ... 1
    ̄ ̄ ̄ ̄ ̄
✕2)0.4 ... 0
    ̄ ̄ ̄ ̄ ̄
✕2)0.8 ... 0 ※の繰り返し
    ̄ ̄ ̄ ̄ ̄
✕2)1.6 ... 1
    ̄ ̄ ̄ ̄ ̄
   :
よって、123.45 )10 = 1111011 .011100110011...)2

システム

最新の投稿(電子情報)

アーカイブ

カテゴリー