7回目


中間試験までに取り組む内容は,これまでに取り組んだ課題です.これまでにやったことは,復習を含め,

関数
配列と構造体
ポインタ(アドレス参照)
複数ソースファイルとヘッダファイル

本日は,これまでにやったことを思い出しながら,整理してみたいと思います.

1. 関数(ユーザ関数)
関数とは,処理したいデータを受け取り,計算を行い,その結果を返すための手段です.
具体的に,足し算の関数を作ってみます.

double add_calc(double x, double y){
 return x+y;
}

この関数は引数としてx, yを用意し,受け取ったxの値,yの値を足したものを関数add_calcとしての返り値として
結果を返します.結果を返すから関数名の前にdoubleという型が宣言されています.

宣言の方法
関数を関数であると定義するには,宣言する必要があります.main関数よりも前に,

double add_calc(double, double)

のように関数名と引数および戻り値の型を宣言しておきます.

関数の使い方
メイン関数や関数の中で,関数名および引数の中身を記述すれば利用できます.
例えば,上記の例でしたら,

main(){
 double a = 7.8;
 double b = 2.7;
 
 printf("a+b= %f", add_calc(a, b))

}

余談ですが,引数や戻り値を持たない関数もあります.単純ですが,画面に”あ”と表示させるだけの関数で
あれば,

void print_a(void){
 printf("あ");
}

となり,引数は無いのでvoid,戻り値もないので関数はvoid型です.

2. 配列と構造体
配列はすでにご存じのとおり,同じ型の複数の変数のデータを扱うための手段です.例を挙げれば,

double x[100]

のような形で示せますし,あるいは,

double y[100][100]

のように2次元配列以上も扱うことができます.ちょうど1次元配列ならベクトル,2次元配列なら行列のように
みなすこともできますが,注意しなければならないのは,配列は変数の集合体に過ぎず,y00,y01のような変数
のデータ列と考えておかないといけないのです.数学的なベクトルや行列の性質はありません.

さて,配列を使えば,同じ型のデータをひとまとめにできますが,データ処理を行う際には複数の型を使いたい
ことは往々にしてあります.そこで利用するのが,構造体です.例を挙げれば,

struct seiseki{
 int kouriki;
 int math_a;
 int math_d;
 int english;
}

のように,同じ型であっても異なる変数名で扱いたい場合にも便利です.変数群まとめるための方法と考えて
おけばよいかと思います.メイン関数の中で,そのまま呼び出してもよいですが,例えば,

main(){
 struct seiseki data[20] = {
  {80, 50, 60, 70},
  {60, 75, 68, 88},
  ・・・
  {60, 57, 90, 35}
 }
}

のように書けば,構造体seisekiのデータ構造を使って,構造体の配列dataを20個分確保するといった使い方が
できます.
また,構造体の構造体(入れ子の構造体)も利用可能であり,例えば,

struct seiseki{
 int kouriki;
 int math_a;
 int math_d;
 int english;
}

struct kojin{
 int id; /*学生番号*/
 char family_name; /*苗字*/
 char first_name; /*名前*/
 struct seiseki hyoka; /*成績の構造体*/
}

main(){
 struct kojin id[20] = {
  {1001, "有川","一哉", 80, 50, 60, 70},
  {1002, "川上","浩二",60, 75, 68, 88},
  ・・・
  {1020, "山本","修二",60, 57, 90, 35}
 }
}

と記述すれば,構造体の中に構造体を書き込んで利用することも可能です.

3. ポインタ(アドレス参照)
ポインタについては,その意義について諸説ありますが,メモリの節約と計算の高速化は確かにあると思います.
大きなメモリに高速のCPUを組み合わせれば,ポインタを使わなくてもいいと言えるかというとそうでもありません.
むしろ,変数や関数の汎用性を上げることができる,ともいえると思います.

main(){
 int x = 0, y = 0;
 int* p;

 p = &x; /*変数xのポインタ(アドレス)はpと定義する.変数とポインタの型はそろえる*/

 *p = 100; /*ポインタの中身(変数x)を代入*/

 y = *p +50; /*ポインタの中身に50を足したものを変数yに代入*/

}

ポインタ変数(例えばp)を宣言するとき,pはポインタ(アドレス)そのものであり,*pと記述すれば,ポインタの中身が
参照できます.ただし,予めp = &□として,ポインタの中身を宣言しておかないと何も処理ができなくなってしまいます.
上記の例では,初めx=0となっており,ポインタの中身が100とされたとき,xも100となっています.もちろんx=100と
書いてしまえば,xは100ですし,*pを呼び出せば100となります.
つまり,&は変数のアドレス(ポインタ)を示しますし,*はポインタの中身(変数)を示しています.
余談ですが,*(&x)はxのアドレス(ポインタ)の中身ですのでxそのものを示しています.

さて,これでは手続きばかりで,あまりメリットが見えないと思います.
順番の並べ替え,例えばソートなど,そのデータの順番だけが問題であって,データをいちいち移し替えたり移動する
ことに意味はありません.
先の例を用いて,並べ替えの手順を考えてみましょう.構造体のメイン関数内を見てください.

struct kojin id[20]=・・・; /*構造体の配列を宣言,初期化する.*/
struct kojin* idp[20]; /*構造体の配列のポインタを宣言する.*/
struct kojin* workidp; /*並べ替えのテンポラリの置き場のポインタ*/
int n;

for ( i = 0; i < 20; i ++ ) {/*構造体の配列id[20]のポインタをidp[20]と定義する.*/
 idp[ i ] = &id[ i ];
}

ことで準備ができます.あとは以下のような処理を行えば,メモリ内のデータの位置を変えることなく,アドレスだけ
入れ替えることができるので,処理が高速になってくれます.

for( i = 0; i < 20; i++){
 if(並べ替え条件){
  workidp = idp[ 入替先(例えばn1) ];
  idp[ 入替先(例えばn1) ] = idp[ 入替元(例えばn+1) ];
  idp[ 入替元(例えばn+1) ] = workidp;
 }
}

関数間のやり取りをポインタで示すことも考えてみましょう.
まず,ポインタを使わないと,

void irekae(int, int)

main( ){
 int a, b ;

 printf("整数2つ入力") ;
 scanf("%d %d ", &a, &b ) ;
 irekae( a, b );
 printf("a = %d , b = %d \n", a, b ) ;
}

void irekae( int a, int b) {
 int temp;

 temp = a;
 a = b;
 b = temp;
}

入力する際,100, 50の順としてもa = 100, b = 50が出力されてしまいます.変数a, bに値が入力されたとき,
関数の引数にそのままコピーされ,関数内で入れ替えても関数の止まりです.returnは一つしか返せません.
結果として,scanfで取り込んだメイン関数のa, bが保持されるだけです.
なお,メイン関数内のa, bと関数内のa, bは文字は同じでも別物です.
では,ポインタを使ってみましょう.

void irekae( int *, int *)

main(){
 int a, b ;

 printf("整数2つ入力") ;
 scanf(" %d %d ",&a, &b) ;
 irekae (&a, &b) ;
 printf ("a = %d , b = %d \n ", a, b ) ;
}

void irekae ( int* a , int* b){
 int temp;
 temp = *a;
 *a = *b;
 *b = temp;
}

一見,ポインタに*をつけて中身をやり取りしているから同じことをしているように見えるかと思います.ですが
先に述べたとおり,メイン関数内のa, bと関数内のa, bは文字は同じでも別物ですから,ポインタの意味合いを
考える必要があります.

1)メイン関数でa, bが定義され,scanfでa, bの具体的な値が格納される.
2)関数を呼び出す際,関数のポインタa, bにメイン関数の値a, bを設定します.
3)関数はポインタを引数としているのでそのポインタへのやり取りを行います.
4)ポインタのやり取りで*を付けた中身の演算を行い,ポインタのつけられたメイン関数の変数は書き換えられます.
5)結果,returnで戻り値を設定しなくても,4)にてすでにメイン関数の変数a, bは書き換えられています.

つまり,

関数の呼び出し:関数のポインタa, b = メイン関数の値&a, &bとして定義する.
関数:関数のポインタの中身の計算*a, *bを実行,結果はそのポインタであるメイン関数の値a, bに格納される

と考えてください.すなわち,以下のように記述しても,

void irekae( int *, int *)

main(){
 int a, b ;

 printf("整数2つ入力") ;
 scanf(" %d %d ",&a, &b) ;
 irekae (&a, &b) ;
 printf ("a = %d , b = %d \n ", a, b ) ;
}

void irekae ( int* x , int* y){
 int temp;
 temp = *x;
 *x = *y;
 *y = temp;
}

先ほどの流れの代わりに,

関数の呼び出し:関数のポインタx, y = メイン関数の値&a, &bとして定義する.
関数:関数のポインタの中身の計算*x, *yを実行,結果はそのポインタであるメイン関数の値a, bに格納される

という点で結果は同じとなります.関数の独立性を確保しつつ,メイン関数内の変数を操作できることが分かる
かと思います. この点は,次の章に挙げるような,ヘッダファイルの利用にとても有効です.

4.グローバル変数
プログラムを作る際,関数や変数の独立性を意識することが重要です.
独立と思っていたらいつの間にか書き換えられてしまっても,もっともらしい結果を出してしまうことがあります.
プログラムとしては厄介なバグであり,シミュレーションの危険性と言っても過言ではありません.
文法的なエラーではないので発見しにくいからです.

グローバル変数とは対象としているプログラムのプロジェクト(ソースコード群)の中でどの関数からも共通して
呼び出すことが可能な変数です.

さて,様々な解説書や記事に,グローバル変数はなるべく使わないこと,と記述されています.プログラム言語
仕様として用意されておきながら何故このように言われるかは先に述べたような理由ですが,気を付けておけば
便利な場合もあります.例えばですが,数学・物理定数など利用する場合です.

円周率を考えてみましょう.プリプロセッサで#include <math.h>を宣言しておけば,M_PIは定数として使うことが
できますが,本来であれば,プログラム上でどんな定数が与えられているのか知っておく必要があります.
そこで一つの方法としては,直接与える方法で,double型が15桁の精度を持っているとすると,
const double pi = 3.14159265358979;
としてもよいのです.もっと精度が欲しいからと言って,long double型を使いさらに多くの桁を宣言してもよいが
処理系によって丸められてしまう可能性もあるので,闇雲に増やしても意味がありません.それよりも用意されて
いる関数を用いて定数を作る方法もあることを覚えておいて下さい.
const double pi = 4.0 * atan(1.0);
と書けばいいのです.アークタンジェント1であれば45度すなわちπ/4ですのでこれを4倍すれば180度すなわち
πの値が算出できます.
何故,このような面倒なことをしているかというと,先に述べたように,double型は約15桁の精度を有しているので
あって,値によっては多少保障される桁が変化します.精度以上の定数を記述しても意味がないだけ(切り捨て
られる)だけならいいのですが,どのように扱うかは処理系に依存されるため,結果が厳密には保証できないか,
その都度,有効な精度の桁数を確認しなければならないのです.
(ここまで心配する必要は特にありませんが,数値シミュレーションを行う場合,繰り返し計算回数の多さから無視
できないこともあります.)
その点,予め用意されている関数を用いて定数を作れば,処理系の持っている精度そのものとなるため,上記の
ようなことを気にしなくてもよくなると考えられるのです.

上記の点は,細かい点ですが,演算結果の正確さを考える上でとても重要なことを説明しています.
本題に戻りますが,グローバル変数の中身が書き換わらない,あるいは,他の動的な変数によって領域が壊され
ない事が担保されれば,使っても問題はないと言えます.静的に運用し,実行中は一度も書き換わらないことが
必要な条件となります.

5. ヘッダファイルと複数ファイルを用いたプログラミングと分割コンパイル
main関数と同じソースファイルに関数を記述しておけば,小規模なソフトウェアであれば困ることはありませんが,
規模が大きくなるにつれて,不便になっていきます.理想的には,メイン関数は,プロトタイプ宣言と最低限の変数,
処理内容を示した流れで関数を並べるだけ,にしたいものです.それ以外の関数は別名のソースファイルとヘッダ
ファイルを作っておけば,

ではここで,二つのソースファイルに入った関数2つを呼び出してみたいと思います.
例として,jisaku1.c,jisaku1.h,jisaku2.c,jisaku2.hのようなソースファイル,ヘッダファイルを作成します.

jisaku1.c jisaku1.h
#include <stdio.h>
#include <math.h>
#include "jisaku1.h"

void calc_add(double* a, double* b, double* c){
 double x, y, z;
 x = *a;
 y = *b;
 z = *c;
 z = (x+y);
}
void calc_add(double*, double*, double*)
jisaku2.c jisaku2.h
#include <stdio.h>
#include <math.h>
#include "jisaku2.h"

double calc_tax(double* p){
 double tp;
 tp = 1.05 * (*p);
 return tp;
}
double calc_tax(double*)

簡単な例ですが,これらを用いたメイン関数のソースファイルは

cmain.c
#include <stdio.h>
#include <math.h>
#include "jisaku1.h"
#include "jisaku2.h"

main(){
 double s, t, u;

 s=3980;
 t=5980;
 u=0;

 calc_add(&s, &t, &u);

 printf("total price = %d yen", (int)calc_tax(&u));
}

となります.

6.クラス
詳しくは4回目をご覧ください.ごく簡単な例を示しておくと,

jisaku.cpp jisaku.h
#include "jisaku.h"

void keisan::calc_add(double* a, double* b, double* c){
 double x, y, z;
 x = *a;
 y = *b;
 z = *c;
 z = (x+y);
}

void keisan::calc_dif(double* a, double* b, double* c){
 double x, y, z;
 x = *a;
 y = *b;
 z = *c;
 z = (x+y);
}
#ifndef _INC_JISAKU
#define _INC_JISAKU
#include <stdio.h>
#include <math.h>

class keisan{
 public:
  calc_add(double* a, double* b, double* c);
  calc_dif(double* a, double* b, double* c);
}

#endif

メイン関数は,以下のような形になります.

cmain.cpp
#include <stdio.h>
#include <math.h>
#include "jisaku.h"

main(){
 double s, t, u;
 double v, w, x;

 s=20;
 t=30;
 v=10;
 w=5;

 keisan::calc_add(&s, &t, &u);
 keisan::calc_def(&v, &w, &x);

 printf("wa = %d, sa = %d", (int)u, (int)x);
}

本授業では,cとc++を特に分けて話してはいませんが,現在取り組んでいる課題はC言語の範囲で対応
可能です.もちろんc++で書いていただいてもかまいませんし,現在の範囲でしたらcppであってもエラーは
出ないと思います.

7.DFTについて
周波数解析を行うためには既に話したように,FFTがあるが,もう少しシンプルで分かりやすいDFTについて
扱います.以下の手順でコーディングを行えば,DFTが実行できますので,取り組みながら理解していって下さい.

プリプロセッサ(ソースの一番初めの処理)
手順1:complex.hをインクルードする
手順2:定数piををグローバル変数として定義する

DFT本体の関数
手順1:関数はvoid dft(double* in, Complex* out, int N)とする
  in : もとデータが入っている.これは変更しない.実数の配列.
  out : 計算結果を入れる.こちらは複素数.
  N : データ個数.
手順2:複素数を並べ替えに利用するのでComplexの変数を何か定義する.
手順3:出力用の配列を初期化i=0〜Nで複素数の実部,虚部とも0にする.
out[i].re, out[i].rmとすれば呼び出すことができる.
手順4:DFT演算を行う.例えば変数omega, kを置くことにする.アルゴリズムは以下を参照
for(omega=0; omega<N; omega++) {
 for(k=0; k<N; k++) {
  // 積分の中身の計算
  tmp = Cexp( ToComplex(0.0 , 2.0 * pi * omega * k / N) );
  tmp = Cmul( ToComplex(in[k], 0.0), tmp);
  // Sigmaの計算
  out[omega].re += tmp.re;
  out[omega].im += tmp.im;
 }
}

手順5:出力用の配列の各値を要素数Nで割る

波形テータの生成
手順1:すでに先週,先々週に作成済み
 void saw_wave(double* x, int n)であれば,データ数nで波形データxのポインタで出力

メイン関数
手順1:変数を宣言する
 const int n=20など;20にこだわらなくてよい.
 double f[n]; // フーリエ変換「前」のデータ.c++では,配列の要素数は const int 型ならば,変数でもOK.
 Complex F[n]; // フーリエ変換「後」のデータ
手順2:配列に波形データの値を格納
いろいろな波形で実験すること.例えばsaw_wave(f, n);
手順3:波形元データを表示してみる.i=0-nまで表示すればよい.
手順4:DFTを行う.dft(f, F, n);と呼び出せばよい.
手順5:フーリエ変換後のデータを表示してみる.i=0-nまで表示すればよい.
手順6:フーリエ変換後のデータをパワースペクトル(=振幅の2乗)にして表示.i=0-n/2まで.
 振幅は単純にCabs(F[i]) * Cabs(F[i])とすればよい

DFTの中の手順4がコアとなる演算です.


課題1 上記DFTのソースを作成せよ.なお,以前作成したcomplex.hをインクルードすること.

課題2 上記ソースの波形データを変更して実行せよ.

課題3 上記ソースの手順4を説明せよ.テキストファイルまたはワードファイルにてレポートを作成せよ.
ヒント: DFTに関する書籍を読めばわかると思います.