/*
* 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 $