ここでは制御シミュレーションを例として,実用上必要なテクニックを解説する.
コンピュータを動作させるためには,必ずといって良いほどプログラミングによるソフトウエア作成が必要となる.
とくにC言語は,スーパーコンピュータ(スパコン)を用いた大規模な並列計算機から,自動車や家電製品に組み込まれているマイクロコントローラ(マイコン)まで,非常に幅広いスケールのプロセッサをターゲットとしたソフトウエア開発で利用されている.
マイクロコントローラとは,コンピュータの心臓部であるCPUとメモリに加えて,アナログ入出力,ディジタル入出力などが一体となった小さなコンピュータのことである.
これに加えて,WiFiや5G通信などの通信モジュールが一体となったものもあり,IoT (Internet Of Things)の中核をなす機器である.
マイコンは,自動車,家電製品,ドローン,ロボットなど,何らかの機械・機器に組み込んで使用されるため,通常のPCで使用されるようなディスプレイ,キーボード,マウスなどを接続することは少ない.
したがって,ソフトウエアの開発(ソースコードの編集や,コンパイル作業)を通常のPC上で行い,作成した実行ファイルをUSBやシリアル通信経由でマイコンに転送(書き込む)する方法が一般的である.
このような開発手法をクロス開発と呼び,使われるコンパイラをクロスコンパイラと呼ぶ.
Arduino (アルデュイーノ)は,教育用に開発されたマイコンである.
安価(数千円)で入手しやすく,Arduino IDE と呼ばれる統合開発環境(エディタ+コンパイラ)を無料でダウンロードすることができる.
プログラミング言語は C/C++ 言語であり,作成したプログラムはUSBケーブル経由で簡単にマイコンに転送することができる.
以下に開発の例を示す.
プログラムは基本的にC言語であり,ボタンクリックのみでコンパイルから実行ファイルの転送,実行が可能である.
コンパイルから実行までの動画.見慣れない関数が並んでいるが,これはマイコンボードに固有の機能.また,ソースファイルにmain()が無いが,別の場所で定義されている.
動作に必要な関数のみを記述すればよいので,簡単に開発を始められる.
コンピュータを使った計算シミュレーションでは,様々な定数・パラメータを少しづつ変えながら,結果を比較する作業が多い.
そのたびにソースコードを編集してコンパイルし直すのは面倒でもあり,また,間違いが発生することもある.
そこで,ソースコード中に定数を直接,書き込んでしまうのではなく,ターミナルから実行ファイルを実行する際に,数値や文字を渡す方法がある.
この方法を用いれば,ソースファイルを書き換えて何度もコンパイルし直す必要がなく,効率的である.
例えば,コマンドプロンプト(Windows)やターミナル(Mac, UNIX)などで,以下のように実行ファイル名に続いて文字・数値などを入力したとします.
(Win)c:\> a.exe 1.52 6.44 abc (Mac) % ./a.out 1.52 6.44 abc
この 1.52 6.44 abc という値や文字列は,main()関数の引数として,プログラム内で利用することができます.
main()関数の正式な記述法は,以下のようになります.
int main(int argc,char *argv[])
argc は整数型で,引数の個数.argv はポインタの配列(!)で,各引数を表す文字列の先頭を指すポインタの配列.(変数名は「argc」や「argv」である必要はないのですが,慣例ではこう書きます)
先ほどの実行例 a.exe 1.52 6.44 aaa は,プログラム中で以下のように扱われます.
| 変数名 | 意味 | 例 |
|---|---|---|
argc |
引数の個数 → ポインタ配列 argv[] の要素数 | 4 |
argv[] |
各引数(文字列)を指すポインタの配列 argv[0]には,実行ファイル名が入る. |
argv[0] = "a.exe" argv[1] = "1.52" argv[2] = "6.44" argv[3] = "aaa" |
以下は,実行ファイル名に続いて入力された文字を,そのまま表示するサンプルプログラムです.
#include <stdio.h>
int main(int argc, char *argv[])
{
int i;
for(i=0; i<argc; i++) {
printf("argv[%d] : %s\n",i, argv[i]);
}
return 0;
}
上のプログラムを作成し,コンパイル・実行して動作を確認してみよう.
実行時に,実行ファイル名に続いて数値や文字を入力してみよう.
>a.exe 111 222 333 argv[0]=??? argv[0]=??? ...
argv[]の数値への変換
argv[]は文字列として渡されるので,そのままでは数値として計算には使えない.
そこで,以下のような文字列→数値変換関数を用いて,文字列を整数や浮動小数点に変換する.
ここでは,文字列→実数の変換を行うatof()関数を示す.
これ以外にも,整数に変換するatoi()や,類似の関数strtol(), strtod()がある.
//
// atofの例 ascii to float
// コマンドラインで渡された引数を合計して出力する.
//
#include <stdio.h>
#include <stdlib.h> // atof関数用ヘッダファイル
int main(int argc,char *argv[])
{
double x;
double S = 0.0; // 合計の計算用
for(int i=1; i<argc; i++) { // 添え字は 1 から開始.argv[0]は処理しない.
x = atof(argv[i]); // 文字列を数値に変換
printf("argv[%d] : %lf\n", i, x);
S += x;
}
printf("S = %lf\n", S);
return 0;
}
実行結果
% ./a.out 1.1 2.5 3.9
argv[1] : 1.100000
argv[2] : 2.500000
argv[3] : 3.900000
S = 7.500000
scanf()で,実行の都度,キーボード入力していた数値などを,実行時開始時に1行でまとめて指定することが可能.ヒストリー機能を使えば,さらに楽.
コマンドライン引数が便利な場合もあれば,毎度引数を入力する必要があるため煩わしい場合もある.
そのために,予め決めた名前のテキストファイル(設定ファイル,データファイルなど)を作成しておき,これを読み込ませる方法もある.
例えば,sim.txtとかsim.iniのような名前をつけ,ここに設定情報や計算パラメータなどを記録しておく方法である.
この場合は,コマンドライン引数にその設定ファイル名を指定すると汎用性を高めることができる.
| 方法 | メリット | デメリット |
|---|---|---|
| コマンドライン引数 | 一括して変数を入力でき,軽快な操作となる. | 実行のたびに,値を入力しなければならない. |
scanf()で対話的に変数を入力 |
1変数ごとに確認しながらキーボード入力できる | 変数が多い場合に面倒.入力ミスを招く可能性がある. |
| 設定ファイルの読み込み | 計算条件を予め検討して準備できる.設定を保管・記録できる. | テキストエディタで編集の必要がある.スペース,タブ,改行,文字コードでエラーを起こす可能性がある. |
main()関数に直接,引数を渡す方法では,実行ファイルを呼び出す際に計算パラメータをダイレクトに渡すことができた.
これをもう少し便利に使うために,WindowsOSにはバッチファイルという方法がある.
(MacOS, Linuxではシェルスクリプトと呼ばれる同様の機能がある.)
テキストエディタ(VSCodium,さくらエディタなど)で,以下のようなテキストファイルを新規作成し,.bat という拡張子のファイルとして保存しよう.
例えば,以下の内容が書かれたテキストファイルを「exec.bat」という名前で保存しておく.
ここでは,数値は計算に必要な何かのパラメータとする.
a.exe 2.0 0.1 0.05 a.exe 2.0 0.3 0.07 a.exe 1.2 0.12 0.1
バッチファイルと同じフォルダ内にa.exeがあることを確認して,コマンドラインからこのバッチファイル名を指定してみよう.
c:\> exec.bat
こうすると,バッチファイルに書かれた3行が連続して実行される.
バッチファイルは何行でも書くことができ,行の上から順に1行ごとに実行ファイルが呼ばれ実行される.
これを使えば,キーボードからいちいちコマンドを入力する必要がなく,全自動で様々な条件のシミュレーションが行える.
さらに,プログラムを改良して,main関数の引数に「出力ファイル名」を指定するようにしておけば,様々な条件でのシミュレーションをコマンド一発で処理,結果を保存できるため,非常に有効な方法となる.
これをバッチ処理という.
rem rem 先頭にremと書かれている行はコメント.無視される. rem rem exec.bat 引数に出力ファイル名を追加 rem a.exe 2.0 0.1 0.05 001.csv a.exe 2.0 0.1 0.07 002.csv a.exe 2.0 0.1 0.1 003.csv ・・・(パラメータを様々に変えた数多くのコマンド) a.exe 5.0 0.5 0.5 100.csv
main()関数と同じソースファイルにすべての必要な関数を記述しておく方法は,小規模なソフトウェアであれば困ることはないが,
規模が大きくなるにつれて,見通しが悪くなり,不便になってくる.
また,複数人でプログラムを開発したり,自分が過去に作成したソースコードの一部を再利用したい場合などは,処理の機能別にファイルを分けておくと便利である.
通常は,main() 関数には最低限の変数の宣言と,処理内容を記述した関数を順にコール(呼び出す)するだけ,にしたい.
このように,機能を部品として「モジュール化」しておき,部品として使用する関数を別名のソースファイル(実装ファイルという)とヘッダファイルとして分けて作っておけば,デバッグも容易となる.
//
// 典型的な main 関数の例
// もちろんこのままでは動作しない
//
int main(int argc, char *argv[])
{
Init(); /* 変数などの初期化処理 */
ReadData(); /* データの読み込み,設定など */
DoSomething(); /* 何か処理をする */
DoOtherthing(); /* 何か処理をする */
Output(); /* 結果の出力.画面やファイルなど */
return 0;
}
例として,消費税込み金額の計算をする関数calc_tax()を別ファイルに記述し,main()関数から呼び出す例を示す.
ファイル名を sub.cpp,sub.h とする.
| sub.cpp | sub.h |
|---|---|
|
|
これらを用いるmain関数を含むソースファイルは
| main.cpp |
|---|
|
となる.
ヘッダファイル内の#ifndef _SUB_H, #define _SUB_H, #endifの部分はインクルードガードと呼ばれ,このヘッダファイルが別のファイルに重複してインクルードされるのを防ぐ役割を果たす.
(ソースファイルが増えてくると,関数の宣言が複数includeされ,多重定義でコンパイルエラーとなる場合がある.)
コンパイル方法は,必要な実装ファイル(.cppファイル)を並べてコンパイラに渡す.
c++ main.cpp sub.cpp
と,全ての実装ファイルを同時に指定(正確にはコンパイルとリンク)する必要がある.
試しに,main.cpp のみコンパイルしてみよ.どのようなエラーが出るか.
変数のアドレスを格納し,別の変数を指し示すことができるポインタ変数をに学んでいるが,実は関数もポインタを利用して指すことができる.
これを関数ポインタと呼ぶ.
また,関数ポインタを使って引数として関数を渡すこともできる.
#include <stdio.h>
void func(double x) // 通常の関数
{
printf("Hello! x = %lf\n", x);
return;
}
int main(void)
{
double x = 5.5;
printf("address of func = %p\n", func); // 関数名=アドレス
void (*p)(double); // 関数を指すポインタ p を宣言する.独特な書き方.
p = func; // 関数 func のアドレスを,ポインタ p に代入.
(*p)(x); // 関数ポインタを使用した関数呼び出し.
return 0;
}
関数を指すポインタは,何に利用するかというと,
関数ポインタは,既に作成した複数の類似の関数をモジュールとして利用できるので,関数内のアルゴリズムを変えて処理する際に便利である. また,何かの「数値を算出するため」の関数ではなく,「計算するための仕組み」としての関数にでき,汎用性,再利用性が格段に上がります.
以下の例を見て下さい.
calc()の引数として,関数ポインタを用いています.
main()関数内では,calc関数の引数としてsqr, cubeのいずれかを渡しています.
すなわち,calc関数は「計算する数値データ」に加えて「どのような計算をするか」自体を引数として取ることができます.
このような機能を利用すれば,例えば微分方程式の解法に使う計算手法(オイラー法やルンゲ・クッタ法)を切り替えたり,P制御とPID制御をプログラム中で自由に切り替えたりすることが容易にできます.
#include <stdio.h>
//
// 引数として,関数(処理法)とデータをとる関数.
// 呼び出し元の引数次第で,処理する関数を切り替えられる
//
double calc(double (*p)(double), double a)
{
return (*p)(a);
}
// (1) 二乗する
double sqr(double a)
{
return a*a;
}
// (2) 三乗する
double cube(double a)
{
return a*a*a;
}
int main(void)
{
double x = 2.0;
printf("sqr of %lf is %lf\n", x, calc(sqr, x)); // 引数に関数をとる関数calcの呼び出し
printf("cube of %lf is %lf\n", x, calc(cube, x));
return 0;
}
calc()内の関数ポインタの値pを表示してみよう.
コマンドライン引数からファイル名(.cpp, .txtなどのテキストファイルとする)を指定すると,
各行の先頭に行番号を追加して,その中身を画面に表示するプログラムを作成せよ.
存在しないファイル名を指定した場合はエラーを出力すること.
実行例:コマンドラインから以下のようにファイル名を指定する
例1:
> a.exe hello.cpp (存在するファイルを指定)
0000 : #include <stdio.h>
0001 :
0002 : int main(int argc, char* argv[])
0003 : {
...
0050 : }
例2:
> a.exe Non-exist.cpp (存在しないファイルを指定)
err! file not found!
フーリエ変換の実習で作成した,sin_waves() などの波形をDFT処理するプログラムを,
以下のように分割してコンパイルできるようにせよ.
なお,ファイルは以下の計3つとする.(000000の部分は各自の学籍番号)
すでに各ファイルを作成済みで持っている場合も,ファイル名を下記の通り変更して分割コンパイルを確認すること.
コンパイル方法は以下の通り,.cppファイルを順に並べてコンパイラに渡す.
c++ 153R000000-13-2.cpp 153R000000-13-dft.cpp
エラーがなければ,単一の a.exe が生成される.
(注:Microsoftやbccコンパイラでは,最初に渡した.cppファイル名の拡張子をexeに変えた 153R000000-13-2.exeが生成される.)
ソースファイル・ヘッダファイルをすべて提出すること.