9回目 中間のまとめ・復習

中間試験までに取り組んでほしい内容は,これまでに行った実習の全ての範囲である.
これまでの実習のうち,C言語の文法に関するもので特に頻繁に登場し,よく理解しておくべきものは,

などである.

今回は,これまでの実習を思い出しながら,整理してみよう.


1. 関数(ユーザ関数)

関数の定義方法

関数とは,ひとまとまりの処理の塊であり,処理したいデータを引数として受け取り,計算などの処理を行い,その結果を返すための手段である.
具体的に,足し算の関数で説明を行う.


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

この関数は,仮引数としてdouble型の変数 x, y を用意し,受け取った値を足したものを,関数add_calcとしての戻り値(返り値とも言う)として結果を返す.

この関数は値を返すので,関数名の前にdoubleという戻り値の型が宣言されている.
関数が値を返す必要が無い場合は,voidと記述する.

関数の宣言方法

関数の存在を他の関数に知らせ,利用可能とするためには,そのかたちを「宣言」する必要がある.正確にはプロトタイプ宣言という. (これにたいして,関数の処理内容を具体的に記述することを「定義」という.)

例えば,main関数内で上記の関数を呼び出したければ,main関数よりも上方の行に,


double add_calc(double x, double y);

のように,関数名と引数および戻り値の型を宣言しておく.(ここにはセミコロンが必要)
関数のプロトタイプ宣言さえあれば,関数の本体は別の.cppファイルにあっても良く,ファイルを分割したコンパイルでもエラーとならない. もちろん,コンパイルの後のリンク時に,関数が全て揃っていないとエラーになる.

関数の使い方

メイン関数や関数の中で,関数名および()で囲った引数を正しく記述すれば,関数の呼び出し(=関数への処理のジャンプ)ができる.
例えば,


#include <stdio.h>

int main(void)
{
    double a = 7.8;
    double b = 2.7;

    printf("a+b= %f", add_calc(a, b) );
    return 0;
}

となる.
もちろん,この記述中にはadd_calc関数の定義が書かれていないため,

の,いずれかを行う必要がある.
後述の分割コンパイルの章も参照のこと.



2. 配列と構造体

配列は「同じ型」の複数の変数を扱うための手段である.
例えば,要素数100個の倍精度実数(double)の配列は


double x[100];

のような形で表される.2次元配列であれば,


double y[100][100];

と書ける.
ちょうど1次元配列ならベクトル,2次元配列なら行列のようにみなすこともできるが, 注意しなければならないのは,配列は「変数の集合体」に過ぎず, 各要素が「1個の変数」と考えておく必要がある.
言い換えると,数学的な「ベクトル」や「行列」の性質は無く,一括での初期化や代入はできない. (従って,繰り返し文を使って,1つ1つ処理するのが基本である.)

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


/* 構造体の定義の例 */

/* 例1:住所録.型が異なる変数からなる.*/
struct address
{
    char name[20];
    int age;
    char address[100];
    int postal_code;
}

/* 例2:成績データ.すべて同じ型だが,意味が違う */
struct seiseki
{
    int kouriki;
    int math_a;
    int math_d;
    int english;
};

上記の例のように,異なる方を組み合わせて新しい方を作るか,または,同じ型であってもそれぞれに名前をつけて扱いたい場合にも便利である.
構造体はデータ変数群まとめるための方法と考えておけばよく,扱いたいデータの構造を設計するのに非常に重要である.

プログラム中では,構造体の各メンバ変数を個別に操作してもよいが,例えば,


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

のように書けば,構造体の配列dataを20個分,宣言と同時に「初期化」するといった使い方ができる.

また,構造体の構造体(入れ子の構造体)も利用可能であり,例えば,


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

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

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

    id[0].hyouka.kouriki = 100;  /* 有川君 の「hyouka」の「kouriki」を100にする. */

}

のように,構造体の中に別の構造体をメンバ変数として利用することも可能である.



3. ポインタ(アドレス参照)

一言で言えば,ポインタ変数は,「アドレス」を格納するための変数である.


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

    p = &x;  /* 変数xのポインタ(アドレス)を p に代入.変数の型とポインタの型はそろえる */
    *p = 100;    /* ポインタの中身(つまり変数x)を代入 */
    y = *p +50;  /* ポインタの中身に50を足したものを変数yに代入 */
}

ポインタ変数(ここではp)を宣言するとき,pの値はアドレスそのものであり,アスタリスク演算子*を用いて*pと記述すれば,ポインタの中身が参照できる.
ただし,予め p = &x として,ポインタ p が x を指すようにしておく必要がある.

上記の例では,初めx== 0 となっており,ポインタを使って*pに100を代入すると,pの指す変数 x も100となる.
逆に,x=100;と代入すれば,xの値は100となり,*pの値も100となる.

ポインタ演算子:& は変数のアドレスを取り出す演算子,* はそのアドレスに格納されているの変数の値を取り出す演算子.

ポインタ利用のメリット

処理の効率化

ポインタには数々のメリットがあり,C言語の最大の特長と言っても過言では無い.
例えば,配列データ内の順序の変更や並べ替え(例えばソートなど)では,データの格納順序だけが問題であって,データをいちいち移し替えたり移動することに意味は無い.
先の例を用いて,並べ替えの手順を考えてみよう.


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;
 }
}

関数の引数としての利用(アドレス参照)

ポインタの応用として,「関数」の引数をポインタとする意義をもう一度考えてみよう.
毎度おなじみの,2つの値を入れ替える関数 irekae を考える.
まず,ポインタを使わないと,


#include <stdio.h>

void irekae(int, int);       /* 関数のプロトタイプ宣言では,このように型名だけで変数名を省略することもできる. */

int main(void)
{
    int a, b;

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

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

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

このプログラムを実行して,例えば100, 50と入力し,irekae関数を呼び出しても入れ替えは行われず, a = 100, b = 50 と出力される.
これは,main関数内の変数a,bに値が入力されたとき,関数の引数にそのまま「コピー」され,irekae関数内で入れ替えてもmain関数内に反映されないためである.
main関数内の変数 a, bと,irekae関数内の変数 a, b は,ローカル変数なので,同じ変数名であっても全く別物である.

では,ポインタを使ってみよう.


#include <stdio.h>

void irekae(int*, int*);      /* 変数名は省略,型名のみ. */

int main()
{
    int a, b ;
    printf("整数2つ入力");
    scanf("%d", &a );
    scanf("%d", &b );
    irekae( &a, &b );      /* アドレスを渡す */
    printf("a = %d , b = %d \n", a, b );
    return 0;
}

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

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

  1. メイン関数でa, bが定義され,scanfで a,b に値が格納される.
  2. irekae関数を呼び出す際,関数の引数に値そのものではなく,main関数の変数a, bの「アドレス」を渡す.
  3. irekae関数の引数はポインタなので,ポインタの指す相手を操作できる.
  4. ポインタ演算子*を用いて入れ替えを行うと,main関数の変数が書き換えられる.
  5. その結果,returnで戻り値を設定しなくても,main関数の変数a,bが正しく書き換えられる.



4.グローバル変数

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

グローバル変数とは,ソースコードの中でどの関数からも共通して参照・代入が可能な変数である.
様々な解説書や記事に,「グローバル変数はなるべく使わないこと」と記述されていることが多い. プログラム言語仕様として用意されておきながら,何故このように言われるかは先に述べたような理由であるが,気を付けておけば便利な場合もある. 例えば,数学・物理定数など利用する場合などがそれにあたる.

円周率を考えてみましょう.プリプロセッサで#include <math.h>を宣言しておけば,M_PIは定数として使うことができるが, 本来であれば,プログラム上でどんな定数が与えられているのか知っておく必要がある.

そこで一つの方法としては,直接与える方法で,double型が約15桁の精度を持っているとすると,

const double pi = 3.14159265358979;

としてもよい.
もっと精度が欲しいからと言って,long double型を使いさらに多くの桁を宣言してもよいが,処理系によってはdoubleに丸められてしまう可能性もあるので,闇雲に増やしても意味がない. それよりも用意されている算術関数を用いて定数を作る方法もある.

const double pi = 4.0 * atan(1.0);

と書けばよい. atan(1)は45度,すなわちπ/4であるので,これを4倍すればπが算出できる.

先に述べたように,double型は約15桁の精度を有しており,値によっては多少保障される桁が変化する. 精度以上の定数を記述しても意味がない(=切り捨てられる)だけなら良いのだが, どのように扱うかは処理系依存で結果が厳密に保証できないので,その都度,有効な精度の桁数を確認しなければならない. 一般的にここまで心配する必要は無いが,数値シミュレーションを行う場合,繰り返し計算回数の多さから無視できないこともある.)

その点,予め用意されている関数を用いて定数を作れば,処理系の持っている精度で定数が定義できるため,上記のようなことを気にしなくてもよくなる.
グローバル変数の中身が書き換わらない,あるいは,他の動的な変数によって領域が壊されない事が担保されれば,使っても問題はない.



5. ヘッダファイルと複数ファイルを用いた分割コンパイル

main関数と同じソースファイルにすべての必要な関数を記述しておく方法は,小規模なソフトウェアであれば困ることはないが, 規模が大きくなるにつれて,見通しが悪くなり,不便になってくる. 理想的には,main関数は,プロトタイプ宣言と最低限の変数,処理内容を示した流れで関数を並べるだけ,にしたい. それ以外の関数は別名のソースファイル(実装ファイルという)とヘッダファイルを作っておけば,デバッグも容易となる.

例として,2つに分割されたソースファイルに入った関数2つを呼び出す例を示す.
ファイル名を jisaku1.cpp,jisaku1.h,jisaku2.cpp,jisaku2.h とする.

jisaku1.cpp jisaku1.h
#include <stdio.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.cpp jisaku2.h
#include <stdio.h>
#include "jisaku2.h"

/* 関数の定義 */
double calc_tax(double* p)
{
 double tp;
 tp = 1.08 * (*p); /* 消費税8%,2015年現在 */
 return tp;
}
/* 関数の宣言 */
double calc_tax(double*);

これらを用いるmain関数を含むソースファイルは

cmain.cpp

#include <stdio.h>
#include "jisaku1.h"
#include "jisaku2.h"

int 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));
    return 0;
}

となる.
コンパイル方法は,

bcc32 cmain.cpp jisaku1.cpp jisaku2.cpp

と,全ての実装ファイルを同時にコンパイル(正確にはコンパイルとリンク)する必要がある.




6.クラス

C++言語では,構造体に「変数」だけで無く「関数」も含めることができる「クラス」という新しい概念が追加された.
詳しい文法の説明はここでは行わないが,ごく簡単な例を示しておくので,その概念は理解しておくのが望ましい.


#include <stdio.h>   /* c の標準入出力ヘッダファイル */
#include <iostream.h>   /* c++ の標準入出力ヘッダファイル.この例では無くても可 */

class Ckeisan      /* struct の代わりに class キーワードを使用する. */
{
public:    /* <-C++固有のキーワード.変数や関数を「公開」するという意味. */

    /* メンバ「変数」.これは構造体と同じ */
    int a;
    int b;

    /* メンバ「関数」. 構造体には無く,クラス特有のもの */
    int add(void)
    {
        return a+b;
    }
    int sub(void)
    {
        return a-b;
    }
};

int main(void)
{
    Ckeisan k;    /* 計算クラス.頭文字のCはクラスの意 */

    k.a = 10;   /* 構造体と同様,Ckeisanクラスのメンバ変数に値を代入 */
    k.b = 5;

    printf("add = %d\n", k.add() );   /* k.add()は,Ckeisanクラスのメンバ関数呼び出し */
    printf("sub = %d\n", k.sub() );   /* k.sub()は,Ckeisanクラスのメンバ関数呼び出し */
}

授業では,CとC++を特に分けて説明していないが,実習課題は class を使用する必要は無く,C言語の範囲で対応可能である.
もちろん,自分でC++を学び,それで課題提出用のソースコードを書いても構わない.



7.離散フーリエ変換(DFT)について

周波数解析を行うためにはDFTやFFTが用いられるが,シンプルで分かりやすいDFTについて考える.
復習になるが,以下の手順でソースコード作成を行えばDFT処理が実現できる.
なるべく多くのプログラミング課題に取り組みながら,理解を深めて確実なものとしてもらいたい.

プリプロセッサ(ソースの一番初めの処理)

  1. 複素数型構造体や関連する関数が定義されているヘッダファイル(complex.hなど)をインクルードする
  2. 定数 pi をconst なグローバル変数として定義する
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. 手順5:出力用の配列の各値を要素数Nで割る

波形テータの生成
  1. すでに先週,先々週に作成済み
    例えば,void saw_wave(double* x, int n)は,データ数nで波形データ配列xにデータを書き込む関数.
main関数
  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まで.
     振幅の二乗は, ライブラリ関数(math.h)のpow関数を用いて,pow(Cabs(F[i]), 2),または,単にCabs(F[i]) * Cabs(F[i])とすればよい

main関数中の手順4が,変換のコア(核)となるdft関数呼び出しである.



実習課題

課題1

上記DFTの解説および第7?8回を参考に,DFTを行うプログラムを再度作成せよ.
この際,以下に示す機能ごとにソースファイルを分割しコンパイルできるようにせよ.
なお,ファイルは以下の計7つとする.(000000の部分は各自の学生番号)

すでに各ファイルを作成済みで持っている場合も,ファイル名を下記の通り変更して分割コンパイルを確認すること.

コンパイル方法は以下の通りとなる.

bcc32 153R000000-10-1.cpp 153R000000-10-complex.cpp 153R000000-10-wave.cpp 153R000000-10-dft.cpp

エラーがなければ,最初のファイル名を元に拡張子をexeとした 153R000000-10-1.exeが生成される.


課題2

上記ソースのうち,波形データを様々に変更してスペクトルを計算し,グラフを書け.
配列サイズおよび波形は各自で決めて良い.
過去の実習では正弦波,矩形波のDFT処理を行ったので,それ以外で例えば

のスペクトルを求めてみよ.(少なくとも2つ程度)


課題2は,Excelブックファイル(.xlsx)のみ提出すること.元の波形とスペクトルの双方をグラフ化すること.