/* * 20211008-02-QQQQ.c * * 関数 sin(x) の区間 [0,π/4] の定積 * * コンパイル : * cc -c 20211008-02-QQQQ.c * cc -o 20211008-02-QQQQ.exe 20211008-02-QQQQ.o -lm * 実行 : * ./20211008-02-QQQQ.exe * * 定積分を、数値的にやる * !! sin(x) の不定積分は解析的にできる ( -cos(x) + C ) * !! => 本来ならば、それを利用すべき * !! 不定積分が解っていない場合どでも近似値がえられる * */ #include <stdio.h> #include <math.h> /* 数学的関数 sin を利用するので.. */ /* C 言語で数学関数(sin,cos,sqrt)を利用する場合 1. .c ファイルで #include <math.h> をする 2. リンクする時に -lm をつける ( math ライブラリを利用する ) */ /* * リーマン積分を利用する * * ∫_a^b f(x) dx = lim_{h->0} Σ f(a+i*n*h)*h * * => * 関数のグラフを縦切りにして、短冊をつくり * その短冊を長方形で近似して、その長方形の面積の和 * を積分値の近似値にする * */ #define FRACTIONAL 10000.0 /*1000.0*/ /* 区間の等分数 */ /* #define A B 以下、A があらわれると B におきかえられる => 定数値に名前をつける場合に利用される cf. PI : 円周率 ( 3.14 => 3.14159265 ) #define PI 3.14 */ /* * f(x)=sin(x) */ double f(double x) { /* * 引数 x に対して、x の 正弦値 sin(x) を値として返す関数 */ return sin(x); /* math ライブラリの sin 関数をそのまま利用 */ } /* reman_sum ( double min, double max, double delta ) S_1 〜 S_{n}^1 の和を計算する */ double reman_sum ( double min, double max, double delta ) { double sum = 0.0; /* 総和の計算結果 */ double x = min; /* 区分点の x 座標 ( 最初は min の値 ) */ while ( x < max ) { /* x の値が区間内ならば.. */ /* 短冊の面積を計算し、加える */ sum = sum + f(x)*delta; /* リーマン積分では、極限を取っている ( delta -> 0 ) 数値計算では、delta を適当なところで、止めている => 計算量(計算回数)と精度(誤差の大きさ)がトレードされている */ /* x の座標を次の点に移動させる */ x = x + delta; } /* for 構文なら for ( x = min; x < max; x = x + delta ) { } */ /* この時点で sum には積分値が入っている */ return sum; } /* * リーマン積分 * * 関数の積分値が、小さな幅の短冊の面積の和で近似できる事を利用 * * solove_reaman ( double min, double max ) * */ double solve_reman ( double min, double max ) { /* min から max までを積分 基本は reman_sum に任せる */ return reman_sum ( min, max, (max-min)/FRACTIONAL ); } /* * main 関数 */ int main ( void ) { printf ( "関数 f(x)=sin(x) を区間[0,π/4]で数値定積分する。\n" ); printf ( "リーマンの定義に従って計算した答えは %f になりました。\n", solve_reman ( 0.0, M_PI/4.0 ) /* M_PI は、 math.h の中で #define されている円周率 */ ); printf ( "解析的な計算の結果は 1-√2/2 なので、誤差は %f になり、答えに近い事が分ります\n", solve_reman ( 0.0, M_PI/4.0 ) - (1.0-sqrt(2.0)/2.0) /* sqrt : 平方根を計算する math ライブラリ関数 */ ); return 0; } /* リーマン積分という数学的な知識を利用し、 それを離散化 ( 連続なものを、[区分により] 飛び飛び[離散]の値にする ) することにより、「近似計算」が可能になる 数学 : 無限(極限/連続)なものを扱う => 細かいところを無視して、本質的な部分を抜き出す => 抽象化 数値計算 : 有限で処理をする => 細かい本質的でない(誤差の原因)が結果に紛れ込む !! 数学の結果 : 数学的性質 + 極限 !! 数値計算の結果 : 数学的性質 + 近似計算 */
/* * 20211015-01-QQQQ.c * * 0 〜 99 の偶数を出力する (for 構文 版) * * コンパイル : * cc ' -c 20211015-01-QQQQ.c * cc -o 20211015-01-QQQQ.exe 20211015-01-QQQQ.o * 実行 : * ./20211015-01-QQQQ.exe * */ #include <stdio.h> /* * 二つのアプローチを考える * a) 0 〜 99 の整数を考え、その中から偶数だけを出力する * 課題: 0 〜 99 の偶数を出力する (for 構文 版) * => 「0 〜 99」「偶数」 * 問題の条件 ( 0 〜 99, 偶数 ) をそれぞれ別に考える * 「0 〜 99」: for 構文 * 「偶数」 : 条件判断でやろう * 基本的な発想で、まず、これができるようにする * すでに知っている内容を組みあわせて、問題を解く * * b) はじめから 0 〜 99 の偶数だけを考えるようにする * 効率的だが、汎用性に欠ける */ /* * print_even_a() * a) 実直版: * for 構文で、パラメータ変数を 0 から 99 の整数値とする * それが偶数だったら、出力する (if 構文を使う) */ /* a-1) 0 ? 99 整数 1. 制御変数を用意し、その値 0 から 99 まで繰り返す 制御変数 : n for ( n = 0; n < 100; n++ ) { } 2. n が偶数の時だけ出力 n が偶数になる条件 n % 2 == 0 ( n を 2 割った余り[%]が 0 ) これを if 構文で判断し、偶数の時だけ出力する */ void print_even_a ( void ) { int n; /* for 構文のパラメータ */ for ( n = 0; n < 100; n++ ) { /* n を 0 〜 99 の値にする */ /* n <= 99 でもよいが.. */ if ( n % 2 == 0 ) { /* 偶数だったら.. */ printf ( "%d\n", n ); } } } /* * a) 実直版: * 繰り返す命令の中に条件文が、実行されことがある * n が奇数の場合は、何もしない * n を奇数にする事には意味がない( <= 無駄な作業 ) * => n が常に偶数になるようにすれば、 * 無駄(も、条件式[if 構文]も..)なくなる * <= 無駄を省くには、工夫が必要 */ /* * print_even_b() * b) 効率版: * for 構文で、パラメータ変数を 0 から 99 の偶数値をだすようにする */ /* #if 0 ? #endif の間は、無視される */ /* #if 0 */ void print_even_b ( void ) { int n; /* for 構文のパラメータ */ /* n が、偶数の小さいものの値に順になるようにしたい n=0,2,4,6,...,98 今の n の値が偶数として、 その次に大きな偶数にするにはどうするか (工夫) +2 すればよい n = n + 2 !! n += 2 <-> n = n + 2 */ for ( n = 0; n < 100; n = n + 2 ) { /* n を 0 〜 99 の偶数値にする */ /* n が偶数である事が保証されている */ /* => 数学的に証明ができる */ printf ( "%d\n", n ); /* 無条件に出力 */ } } /* #endif */ /* * main */ int main ( int argc, char *argv[] ) { /* #if 0 */ printf ( "a) 実直版:\n" ); print_even_a(); /* #endif */ /* #if 0 */ printf ( "b) 効率版:\n" ); print_even_b(); /* #endif */ return 0; }
#include <stdio.h> int main(int argc, char *argv[]) { int i = 0; /* 変数 i の 0 に初期化 int i; i = 0; */ while ( i < 10 ) { /* 「i<10」:繰り返し条件 */ printf ( "%d\n", i ); /* 7 行目と 8 行目の両方が「繰り返し命令」 */ i = i + 1; } /* i:0 0) 「i<10」のチェック => 成立する(真になる) 1) 次の命令を実行する printf ( "%d\n", i ); => 「0[改行]」が表示される i = i + 1; => i : 1 2) 0) に戻る i:1 0) 「i<10」のチェック => 成立する(真になる) 1) 次の命令を実行する printf ( "%d\n", i ); => 「1[改行]」が表示される i = i + 1; => i : 2 2) 0) に戻る ... i:0 -> 0[改行], i:1 i:1 -> 1[改行]. i:2 ... i:9 -> 9[改行], i:10 i:10 0) 「i<10」のチェック => 成立しない(偽になる) => while 構文を終了(して、次の命令に行く) */ return 0; /* main 関数での return は、(main 関数の終了なので)、プログラムの終了 */ }
#include <stdio.h> int main(int argc, char *argv[]) { int i = 0; while ( 0 < 10 ) { /* 条件式を変えた */ printf ( "%d\n", i ); i = i + 1; } return 0; }
#include <stdio.h> /* キーボードから、改行が入力されるまで、 二度ずつ、エコーバックする */ /* 文字を二度、表示して、それを値として返す */ int echo_two_times( int ch ) { /* 引数としては、「文字コード」を取る char 型ではなく、小さい絶対値をもつ int 型として、 扱うと、色々べんり char 型と int 型の値は、「自動的に変換」されるので、 あまり気を遣う必要はない */ putchar ( ch ); /* 一度目の出力 */ putchar ( ch ); /* 二度目の出力 */ return ch; /* 変数[代入]を利用しない工夫 */ } /* * */ int main(int argc, char *argv[]) { while ( echo_two_times ( getchar() ) != '\n' ) { /* なにしなくてもよい */ /* 実際にやる作業は、 echo_two_times がやってくれる */ } return 0; }
#include <stdio.h> int main(int argc, char *argv[]) { int i = 0; while ( i < 10 ) { /* ループ制御変数 i が含まれている */ printf ( "%d\n", i ); i = 100; /* 代入文により、ループ制御変数 i の値を変更 */ } /* i:0 0) 条件「i < 10」をチェック 真になるので、1)へ 1) 次の命令を実行 printf ( "%d\n", i ); -> 「0[改行]」 i = 100; -> i:100 2) 0) に戻る i:100 0) 条件「i < 10」をチェック 偽になるので、終了 */ return 0; /* main 関数での return は、(main 関数の終了なので)、プログラムの終了 */ }
#include <stdio.h> int main(int argc, char *argv[]) { int i = 0; while ( i < 10 ) { printf ( "%d\n", i ); i = 3; } /* i:0 0) 条件「i < 10」をチェック 真になるので、1)へ 1) 次の命令を実行 printf ( "%d\n", i ); -> 「0[改行]」 i = 3; -> i:3 2) 0) に戻る i:3 0) 条件「i < 10」をチェック 真になるので、1)へ 1) 次の命令を実行 printf ( "%d\n", i ); -> 「3[改行]」 i = 3; -> i:3 (結果的になにもしていないのと同じ) 2) 0) に戻る => このまま、無限ループ */ return 0; /* main 関数での return は、(main 関数の終了なので)、プログラムの終了 */ }
#include <stdio.h> int main(int argc, char *argv[]) { int i; for ( i = 0; i < 10; i = i + 1 ) { /* 初期化式: i = 0; 最初に、一度だけ、必ず実行される 通常の目的: ループ制御変数の初期化を行う 継続条件式: i < 10 while 構文の「繰り返し条件」と同じ役割 再初期化式: i = i + 1 繰り返し条件が成立した時に、「繰り返し命令」が実行され、 その後に、再び、繰り返し条件をチェックする事になるが、 「繰り返し命令」の後で、「条件チェック」の前に実行される命令 */ printf ( "%d\n", i ); /* 繰返し文: while 構文と同じ */ } return 0; }
#include <stdio.h> int main(int argc, char *argv[]) { int i; for ( i = 0; i < 10; i++ ) { /* 「i++」インクリメント演算子: i = i + 1 */ printf ( "%d\n", i ); } return 0; } /* i++; 1. もともと C 言語が開発されたコンピュータの基本命令(機械語)に、 インクリメント演算子があるので それを利用できるようにした 2. 配列と組み合わせると、色々便利な事がわかっている !! C 言語が開発されたコンピュータで、その便利機能が多用されている => それを C 言語でもつかえるようにしたかった プログラミング言語は、(機械語/アセンブラー:低級に対し)、高級言語とよばれるが C 言語は、中級言語と揶揄されることが多く その理由の一つが、これ(C 言語が開発されたコンピュータ用と記述がある) */
/* sin 関数のグラフを書きたい => excel を活用 <<具体的>> 1. C 言語のプログラムで、データを作る 2. 1. のデータを利用して Excel でグラフを作る !! CSV 形式を利用する */ #include <stdio.h> #include <math.h> /* 数学的関数 sin を利用するので.. */ #define FRACTIONAL 100.0 /*1000.0*/ /* 区間の等分数 */ /* * f(x)=sin(x) */ double f(double x) { /* * 引数 x に対して、x の 正弦値 sin(x) を値として返す関数 */ return sin(x); /* math ライブラリの sin 関数をそのまま利用 */ } /* plot ( double min, double max, double delta ) 区間 [min,max] の間、 delta 間隔で、y=f(x) の値を計算し、 それを表示する 表示形式は、一行に 「x, y」の形で表示をする */ void plot ( double min, double max, double delta ) { double x = min; /* 区分点の x 座標 ( 最初は min の値 ) */ while ( x < max ) { /* x の値が区間内ならば.. */ printf ( "%f, %f\n", x, f(x) ); x = x + delta; } } /* * plot_main */ void plot_main ( double min, double max ) { plot ( min, max, (max-min)/FRACTIONAL ); } /* * main 関数 */ int main ( void ) { printf ( "関数 f(x)=sin(x) を区間[0,π/4]で表示する。\n" ); plot_main ( 0.0, M_PI ); return 0; } /* リーマン積分という数学的な知識を利用し、 それを離散化 ( 連続なものを、[区分により] 飛び飛び[離散]の値にする ) することにより、「近似計算」が可能になる 数学 : 無限(極限/連続)なものを扱う => 細かいところを無視して、本質的な部分を抜き出す => 抽象化 数値計算 : 有限で処理をする => 細かい本質的でない(誤差の原因)が結果に紛れ込む !! 数学の結果 : 数学的性質 + 極限 !! 数値計算の結果 : 数学的性質 + 近似計算 */
前回の内容 while 構文 繰り返しを行う構文 構文: while ( 繰り返し条件 ) { 繰り返す命令 } 意味: 「繰り返し条件」が成立している限り、 「繰り返し命令」を繰り返す ( 同じ命令を何度も行う ) 実行の流れ: 0) 「繰り返し条件」をチェック => 偽(成立しない場合): 何もせず、次の命令に 真(成立した場合): 1) へ 1) 「繰り返す命令」を実行する 2) へ 2) 0) に戻る 0) -> 1) -> 2) -> 0) .. -> 0) -> 終了 真 実行 真 . 偽 プログラムを書く時の表現 「繰り返し条件」: 同じ表現のものを毎回チェックする 同じ「表現」なので、同じ意味(真/偽)になりそう その中に「変化するもの(変数)」が含まれていれば、 少なくても、条件をチェックする場合に、 値が変化する可能性がでてくる !! 変化するものが含まれていなければ、 !! 常に偽 ( 一切なにもしない.. ) !! 常に真 ( 無限に繰り返し、終了 ) => while 構文の「繰り返し条件」のところには、 変化するもの(変数)を含めるとよい !! 変数以外の「変化するもの」 !! 例 : 入力を(関数値として)、式に含めればよい さらに、 実際に、その変化を保証する必要がある !! 入力の場合は、入力するひとが、考える => 繰り返し命令の中で、「変数の値」を「更新」する 「更新」によって、変数の値が色々と変化し、 いつかは、「繰り返し条件」が不成立になるようにする必要がある 「繰り返し命令」を繰り返す事により、 最終的に、「繰り返し条件」が何時かは、「不成立(偽)になる」 ように「while 構文の条件式と命令を組み合わせる」必要がある => 数学的に示す while 構文の応用 収束値を計算するプログラム while 構文と相性がよい 二分法を利用して、方程式の解を求める 二分法 : 答え(方程式の解)が入る区間の範囲を 半分ずつにしてゆく 区間の範囲が、もとめる誤差内に入るまで、 繰り返す # 数学的には、 # a_n = (1/2)^n (n -> 無限 ) 0 に収束 # 任意 ε > 0 に対して # 十分に大きな n_0 において # |a_{n_0} - 0| < ε 数値計算 (解析的発想:答の近似値の精度を高めて行く) while 構文の数値計算の上での考え方 while ( 答えの精度が不十分 ) { 答えの精度を高める } <= while 構文の終了時には、 十分に精度の高い答え(の候補)が得られている needs: 厳密に答えがえられない場合がある 解析的な手法が存在しない ( 例: 5 次元以上代数方程式 ) => そうゆう場合でも、「答え」が欲しい seeds: ある程度の誤差が許される応用がある => 現実の世界では、「誤差の精度はいろいろあるが..」誤差が許される事がほとんど [2021/10/15] for 構文 for 構文とは 繰返しを記述する構文規則 ( cf. while ) 構文: for ( <初期化式>; <継続条件式>; <再初期化式> ) { <繰返し文> } 初期化式 : 最初に一度だけ、必ず行われる文 継続条件式 : 毎回、繰返し文の実行「前」に評価されこれが偽の場合は終了になる 繰返し文 : for 文によって繰り返される命令 再初期化式 : 繰返し文の実行の後に毎回実行される 流れ: <*> 0) 「初期化式」が実行される(必ず、一度だけ実行される) 1) 「継続条件式」をチェック 偽: for 構文は終了 真: 2) へゆく 2) 「繰り返し文」を実行して 3) へ <*> 3) 「再初期化式」を実行して 4) へ 4) 1) に戻る while 構文と for 構文は、互いに、 変換できる(事が多い..) <簡易版の変換規則> for 構文 for ( A; B; C ) { D; } => while (構文) A; while ( B ) { D; C; } # 実は、 # for 構文の括弧(パーレン「(,)」)の中身は # 省略(表現を空っぽにできる)可能 # 「初期化式」の省略 => 空文(なにもしない) # 「再初期化式」の省略 => 空文(なにもしない) # 「継続条件式」の省略 => 常に「真」となる ( 1==1 となると考えてもよい ) # while (構文) while ( A ) { B; } => for 構文 for ( ; A; ) { B; } for 構文の良いところ (while 構文と比較して..) 制御変数があり、 その制御変数への操作が、ひとまとまりにできるならば、 for 構文の括弧になかに一度に記述する事により、 「解かりやすく」なる 特に 固定回数繰り返しパターンは、一目でわかるので、 その場合は for 構文の利用を推奨 !! 明示的なループ制御変数がなかったり、 !! あるいは、それをまとめて記述できない場合 !! => 無理に for 構文にするより、 !! 直に、while 構文で表現した方が !! (思い込みを避ける事ができるので)望ましい for 構文は、ループ制御変数がある場合に用いるが、 そのループ制御変数への操作を工夫する ( 例: 例題 20211015-01 の場合は、 効率版で、「偶数」だけ生成するように工夫している ) ことにより、色々な機能が、すきっりとできる場合がある # 実直版をつくり、それから、効率版を作る(リファクタリング)が、 # 良い( 正しく動き、かつ、効率よい )プログラムの基本 インクリメント/デクリメント演算子 変数の値を 1 だけ増やしたり減らしたりする演算子 「++」: 変数を 1 だけ増やす => 「i++」 は、「i = i + 1 」と同じと思って良い 「--」: 変数を 1 だけ減らす => 「i--」 は、「i = i - 1 」と同じと思って良い # 10:40 ? 11:20 までの講義録画に失敗して、抜けがある 先週の課題 = おまけ: Excel との連携 C 言語で、数学的関数が扱える ( 例: sin 関数 ) 関数の値の値の計算ができるように => 図示化したい ! C 言語から、直接グラフを書く事も可能 ! <=> Excel を利用する事を考える 具体例: sin 関数をグラフ化したい
課題プログラム内の「/*名前:ここ*/」の部分を書き換え「/*この部分を完成させなさい*/」の部分にプログラムを追加して、プログラムを完成させます。
Download : 20211015-01.c
/* * 20211015-01-QQQQ.c * * 0 〜 99 の偶数を出力する (for 構文 版) * * コンパイル : * cc ' -c 20211015-01-QQQQ.c * cc -o 20211015-01-QQQQ.exe 20211015-01-QQQQ.o * 実行 : * ./20211015-01-QQQQ.exe * */ #include <stdio.h> /* * 二つのアプローチを考える * a) 0 〜 99 の整数を考え、その中から偶数だけを出力する * 問題の条件 ( 0 〜 99, 偶数 ) をそれぞれ別に考える * 基本的な発想で、まず、これができるようにする * b) はじめから 0 〜 99 の偶数だけを考えるようにする * 効率的だが、汎用性に欠ける */ /* * print_even_a() * a) 実直版: * for 構文で、パラメータ変数を 0 から 99 の整数値とする * それが偶数だったら、出力する (if 構文を使う) */ void print_even_a ( void ) { int n; /* for 構文のパラメータ */ for ( n = 0; /* p:ここ */; n++ ) { /* n を 0 〜 99 の値にする */ if ( /* q:ここ */ ) { /* 偶数だったら.. */ printf ( "%d\n", n ); } } } /* * print_even_b() * b) 効率版: * for 構文で、パラメータ変数を 0 から 99 の偶数値だすようにする */ void print_even_b ( void ) { int n; /* for 構文のパラメータ */ for ( n = 0; /* p:ここ */; /* r:ここ */ ) { /* n を 0 〜 99 の偶数値にする */ printf ( "%d\n", n ); } } /* * main */ int main ( int argc, char *argv[] ) { printf ( "a) 実直版:\n" ); print_even_a(); printf ( "b) 効率版:\n" ); print_even_b(); return 0; }
$ ./20211015-01-QQQQ.exe a) 実直版: 0 2 4 6 8 10 12 14 16 18 20 22 24 26 28 30 32 34 36 38 40 42 44 46 48 50 52 54 56 58 60 62 64 66 68 70 72 74 76 78 80 82 84 86 88 90 92 94 96 98 b) 効率版: 0 2 4 6 8 10 12 14 16 18 20 22 24 26 28 30 32 34 36 38 40 42 44 46 48 50 52 54 56 58 60 62 64 66 68 70 72 74 76 78 80 82 84 86 88 90 92 94 96 98 $