= C++演習(2014年4~5月) = <> == 目標 == 素粒子物理の分野で良く使われるデータ解析用のソフトウェア[[http://root.cern.ch|ROOT]]を使うのに必要なC++プログラミングの知識を習得する。 == 内容と進め方 == プログラミングは文法を正確に覚えることよりも、とにかくコードを書いて慣れることが重要である。したがって、文法事項を順番に説明することはせず、ソースコード例を用いる。用意されたソースコードは、コンパイルして実行すること。できれば、ソースコード中の変数の値や論理を少し変更して、コンパイル・実行してプログラムの動作を検証するということを自分でやってみること。 小さな文法的なミスのせいでコンパイルができない、ということは頻繁にあるが、それを解決するには経験を積むのが一番である。上達するにはとにかく自分でコードを書いて、コンパイル、実行するということを何度も繰り返すことが重要である。 === 日程 === 4/23(水)、4/30(水)、5/7(水)、5/21(水)、5/28(水)、6/4(水)、6/18(水)、 (<>) スライド: [[attachment:201404-CppTutorial.pdf]] [[attachment:201404-CppTutorial.pptx]] == 事前準備 == 演習を進めていく上で、環境変数をいくつか設定する必要がある。これは、システム全体で共用され、通常ソフトウェアのインストール先、その他のディレクトリ名、外部サーバー名等を設定して様々なプログラムで参照できるようにするものである。 ログインした際に自動的に初期設定を行うファイル(~/.zshrc)を編集して、以下の行を加える。(これは共通アカウントに対しては設定済み) {{{ export ROOTSYS=/nfs/opt/root-v5-34-07 a=$(pwd); cd $ROOTSYS; source ./bin/thisroot.sh; cd $a; export OCHA_SVN=svn+ssh://hpx.phys.ocha.ac.jp/var/svn/repos alias start_ssh_agent=`eval $(ssh-agent)` }}} 環境変数が設定されているかどうかは、 {{{ echo $ROOTSYS; }}} のように、環境変数名の先頭に'$'を付け加えてその値を書き出させることで確認できる。または、{{{printenv}}}で全ての環境変数を表示することができる。 == ソースコード == 演習で使うプログラムのソースコードは、 [[http://hpx.phys.ocha.ac.jp/websvn/|Subversionレポジトリ]] においてある。 {{{ svn co $OCHA_SVN/Lab/Tutorials }}} 環境変数{{{OCHA_SVN}}}が設定されていない場合は、以下のコマンドを実行する。コマンドで実行した場合、次回ログインした際には、もう一度行う必要があるため、初期設定ファイルで行っておくと良い。 {{{ export OCHA_SVN=svn+ssh://hpx.phys.ocha.ac.jp/var/svn/repos }}} == Subversionの使い方 == Subversionはソースコード管理に使用するためのソフトウェアで、ファイル一括して管理するため複数の人でコードを共有する場合に便利である。また、更新履歴も管理してくれるため、間違ったコードをアップロードしても容易に以前のバージョンに戻すことができる。使用するには、{{{svn}}}というコマンドに引数でサブコマンドや他の情報を指定する。良く使うものは、 ||svn co ||レポジトリ(サーバー)のURLを指定して、コードをcheckoutする(=ダウンロード) || ||svn update ||サーバー上の変更をローカルに反映させる || ||svn status -u ||ローカルなファイルとサーバー上のファイルの違いを調べる || ||svn diff ||ローカルとサーバー上のファイルの中身を一行ずつ比較する || ||svn co -m <コメント> ||ローカルなファイルへの変更をサーバー上に反映させる(=アップロード) || svnコマンドを使用して、サーバーに接続する際にパスワードを聞かれます。毎回聞かれるのが面倒な場合は {{{ start_ssh_agent ssh-add }}} を一度前もって実行しておくと、以後パスワードは聞かれなくなります。 前回からソースコードを少し修正したため、Subversionサーバーから最新版をダウンロードする。そのためには、~/.../Tutorialsに移動して、 {{{ svn update }}} を実行する。 = 講習内容 = == 第1回 == * Linuxシステムへのログイン * Subversionレポジトリからソースコードをダウンロード * 環境設定 * emacsでソースファイルを閲覧、編集 * プログラムのコンパイル * Makefile(コンパイルの自動化) * プログラムの実行 == 第2回 == === 環境設定 === 前回、ソースコードをダウンロードして、環境設定、プログラムのコンパイル、実行ができるようになった。但し、改めてログインするため、各自の作業ディレクトリ等の環境設定をする必要がある。 {{{ cd ~/<自分のディレクトリ>/Tutorials; # <自分のディレクトリ>の部分は適切なディレクトリ名に置き換える source ./TutorialSetup/setup.sh }}} 今回は共通アカウントを使っているため、これはログインする度に行う必要がある。自分のアカウントを使っている場合は初期設定ファイル(~/.zshrc)に加えても良い。 もう一つ、svnコマンドを使うたびにパスワードを聞かれるが、これを避けたい場合、 {{{ start_ssh_agent ssh-add }}} を実行する。こうすると、Subversionレポジトリのあるマシンに接続する際に使用するsshコマンドにパスワードを登録することができ、毎回パスワードを入力する必要が無くなる。 === CppTutorial1のプログラムの中身の確認 === ||'''プログラム名''' ||'''実行例''' ||'''内容''' || ||Add_1plus2.exe ||{{{ ./Add_1plus2.exe }}} ||1+2=3に特化したプログラム || ||Add_Aplus2.exe ||{{{ ./Add_AplusB.exe 12 34 }}} ||実行時に2つの数値を指定できる。コマンドライン引数 || ||Calculate_AandB.exe ||{{{ ./Calculate_AandB.exe - 100 31 }}} ||実行時に四則演算も指定できる。プログラム内で条件分岐 || それぞれのプログラムのソースコードをファイルを見て確認する。 === プログラム作成(1から100までの和を計算する) === プログラムを一から作成する練習として1から100までの整数を全て足して和を求めてみる。答えは5050になるはずである。以下の手順は、プログラムを書く際に、途中で何度かコンパイル・実行をしてコードに誤りが無い事を確認しながら進めている。この通りにする必要はないが、一般にもっと大きなプログラムを書く際は、全体をいくつかの部分に分けそれぞれが正しく動作していることを確認しながら書くことになるので、ここでやる方法が少しは参考になるであろう。 1. main関数を用意する。int main(int argc, char* argv[]) { } 1. main関数内にforループを用意する。for (int i=1; i<=100; i++i) { } 1. forループ内にprintf("i=%d\n", i);を入れて、この時点でコンパイル・実行して、"i=1", "i=2", ...という出力が得られることを確認する 1. forループの前にint sum=0;、ループ内にsum += i;、ループの後にprintf("sum=%d\n", sum);を追加する。 1. 再度、コンパイルして実行する * {{{ g++ -o myprogram.exe myprogram.cxx}}} 1. Makefileにプログラムを追加する。例に倣ってPROG_SRCS = ...の最後に新しいソースファイル名を追加する 1. main関数 {{{ #include using namespace std; int main(int argc, char* argv[]) { std::printf("Start of the program\n"); return 0; } }}} 2. forループを入れる {{{ #include using namespace std; int main(int argc, char* argv[]) { std::printf("Start of the program\n"); int i; for (i=1; i<=100; ++i) { std::printf("Inside the loop, i=%d\n", i); } return 0; } }}} 3. ループ内で和を計算する {{{ #include using namespace std; int main(int argc, char* argv[]) { std::printf("Start of the program\n"); int i; int sum=0; for (i=1; i<=100; ++i) { // std::printf("Inside the loop, i=%d\n", i); sum += i; } std::printf("Sum of numbers from 1 to 100 is %d\n", sum); return 0; } }}} == 第3回 == 前回の1~100までの和を計算するプログラムを完成させる。その後、課題1にあるように少し修正してみる。 === 準備 === サーバーにログインした後、以下の作業をして最新のコードのダウンロードと環境設定を行う。これは、ログインする度に行う。 {{{ cd ~/<自分のディレクトリ>/Tutorials; # <自分のディレクトリ>の部分は適切なディレクトリ名に置き換える source ./TutorialSetup/setup.sh start_ssh_agent ssh-add svn update }}} === プログラム作成 === 繰り返し、条件分岐、浮動小数点数の扱いに慣れる。そのために、一からプログラムを作成する。 * 楕円の面積の計算、(x/3)^2^+(y/2)^2^<1で囲まれる面積を計算する。y>0にある領域の面積を計算して2倍するのが簡単(?) * x:[-3,3]を1000分割して、dx=6.0/1000とする * x=-3+dx*iとxを少しずつ動かしながら、y(x)*dxを足し合わせていく * Poisson分布([[http://ja.wikipedia.org/wiki/ポアソン分布|ポアソン分布]]) * 平均2のPoisson分布に対して、P(0), P(1), P(2), P(3), P(4), P(5), ...を求める * 平均30のPoisson分布に対して、∫,,0,,^x^P(x)dx<0.95となるxを求める == 第4回 == これまでに、いくつかプログラムのソースコードを扱ってきた。ここで、コードや文法の内容を簡単に説明する。プログラムを構成する要素としては、 * main関数、<> * プログラムに対して必ず一つ必要で、プログラムの中身はその中に記述する。main関数に記述されたコードが順番に実行される。 * 変数の利用 * 変数の宣言: < <変数名>=<初期値>;", col=blue)>> という形式を取る。初期値は省略しても良いが、その場合初期値は不定となる。 * 値の代入: a = 3; や a = b + c; * 文字列の出力 * <, <変数1>, <変数2>, ...)", col=blue)>> となる(C言語で使われていたもの) * または、< << std::endl;", col=blue)>> を利用(C++で導入されたもの) * 繰り返し * <; <条件式>; <ループ毎の処理>) { ... }", col=blue)>> * { } は複数の文をまとめるために利用する。コードブロックを定義して、繰り返し行う処理の範囲を明確にする * 条件分岐 * 条件式: 1<3(-> true); 1>3(-> false); a==3; など結果が真偽値となる文 * <) { (条件が満たされた場合に実行)... } else { (条件が満たされなかった場合に実行)... }", col=blue)>> * 条件式を評価した後に実行する文は、{ }内にまとめる。 * 関数 * 他の場所で定義されたコードを呼び出して実行する。 * 関数は、< <関数名>(<引数>);", col=blue)>> という形をしていて、呼び出す際は必要とされる引数(数および型)を与える必要がある。 * 例えば、double sqrt(double x) という平方根を計算する関数は、double型の引数を一つ与えるとdouble型の値を返すように定義されている。 * 数学関数は典型的な例であるが、printf(...)のようにある処理をするもの一般を関数と呼ぶ 繰り返し、条件分岐、関数を使えれば数10行のプログラムで少し役に立つプログラムも書ける。後は、文字列の扱いや配列を使えるようになれば、基本的なC++プログラムは書けるはずである。 !CppTutorial2に因数分解を行うプログラムを載せてある。 === CppTutorial2にあるプログラム === ||'''プログラム名''' ||'''実行例''' ||'''内容''' || ||!CheckPrime1.exe ||{{{ ./CheckPrime1.exe 101 }}} ||与えられた整数が素数かどうかを判定 || ||Factorize.exe ||{{{ ./Factorize.exe 1000 }}} ||与えられた整数を素因数分解する || ||!ListPrime.exe ||{{{ ./ListPrime.exe 100 }}} ||1~与えられた整数までの素数を全て書き出す || == 第5回 == クラスについて学ぶ。最近は、ほとんどのプログラミング言語でオブジェクト指向という考え方が導入されている。次回からデータ解析用のツールであるROOTの使い方に移りたいので、クラスについて最低限必要なことを説明する。 まず、もう少し複雑なコードを書く。その後クラスを利用するとプログラムがどう変わるかを見る。 * 運動方程式を解いて粒子の軌跡をたどる * Runge-Kutta法(常微分方程式を解く標準的なアルゴリズム) * ボールの衝突を計算 * 多数のボールの運動 === CppTutorial3にあるプログラム === ||'''プログラム名''' ||'''実行例''' ||'''内容''' || ||classicalMotion.exe ||./classicalMotion.exe 10 20 > a.dat ||粒子の初速度(v,,x,,, v,,y,,)を与えて、時刻毎の座標、速度を書き出す || ||collisionOfBalls.exe ||./collisionOfBalls.exe 10 -20 0.1 > coll.dat ||2つの粒子の衝突を含めた軌跡を求める。所属度と微妙な位置調整をパラメータとして渡す || ライブラリに含まれているもの ||'''ファイル名''' ||'''関数''' ||'''内容''' || ||!RungeKutta.hxx (.cxx) ||evolveStepRK(...) ||1次元のRungeKutta法。時間dt経過後の位置、速度を更新する || ||!CollisionTool.hxx (.cxx) ||checkCollision(...) ||有限の半径を持つ2つのディスクが衝突するかどうかチェック || ||!CollisionTool.hxx (.cxx) ||processCollision(...) ||2つのディスクが衝突した場合、衝突後の速度を計算する || これらの関数は単独で実行するのではなく、上のmainプログラムから呼び出せれることを想定して作成されている。 === 分割コンパイル === プログラムのコード量が多くなってくると、全てを一つのファイルに記述すると読みづらくなる。ここでは、昨日に合わせてプログラム全体をいくつかの関数に分割しており、別々のファイルに記述し、コンパイルしてある。このような状況で実行可能形式のファイルを生成するには、以下の手順が必要である。 1. それぞれのファイルを別々にコンパイルして、オブジェクト・ファイル(*.o)ファイルを生成する * '''.cxx --> .o''' * 各ファイルでは、後で使う個別の関数やクラスを定義するだけでmain関数を含まないかもしれない。 1. 複数のオブジェクトファイルをまとめてライブラリ(lib*.so)を生成する * '''A.o, B.o, C.o, ... --> libABC.so''' * ライブラリにはmain関数は含ませない。main関数を含むファイルを除く必要がある 1. main関数の定義されている.oファイルとライブラリをリンクして、実行可能形式のファイルを生成する * '''MAIN.o(mainを含む), libABC.so --> MAIN.exe''' プログラムを分割してライブラリを生成することで、同じ関数を複数のプログラムで共有することが可能である。これは、その関数を修正したい場合、それが定義されているファイルの一ヶ所だけ修正すれば良いので、プログラムの整合性を保つ上でも便利である。 === オブジェクト指向とクラス === プログラムの記述を簡潔に読みやすくするための技法としてクラスを紹介する。最近のプログラミングでは、オブジェクト指向という手法が導入されており、そこで中心となる要素がクラスというものである。ある程度プログラミングの経験を積まないとクラスの有用性は認識しづらいかもしれないけど、他のソフトウェア(ROOT等)をライブラリとして利用する場合、クラスの使用方法を知っておく必要が これまでに扱ったプログラムは、基本的にmain関数の中に実行したいコードを順番に記述したものであった。繰り返しや関数呼び出しによって実行が一直線ではなく、ループしたり関数の内部に飛んだりということはあったが、プログラム実行の流れは分かりやすいもので、開発する上でも四則演算、代入、関数呼び出し等を必要に応じて組み合わせれば良かった。 しかし、プログラムが複雑になるとこのやり方では、徐々に収拾がつかなくなってくる。箱の中で、1000個のボールが運動して互いに衝突するシミュレーションを例にとったが、各ボールに対して、半径、質量、位置、速度をデータ(2次元の場合、6個)として持つため、全体で6000個のデータを扱うことになる。運動方程式を解く場合、ボール同士の衝突の確認等を関数として実装しても、呼び出す際にどの変数を用いて計算すべきか等を注意深く確認する必要がある。この方法でも、正しくコードを記述すればプログラムは動作するが、間違いを起こし易いことと、一度バグが含まれた場合に見つけるのが困難であるという問題がある。 オブジェクト指向開発の基本的な考え方は、このような問題点をできるだけなくすためデータの詳細を隠して、ボールに対して詳細なデータ(位置、速度等)を直接扱うのではなく、ボールはボールとして扱えるようにするものである。まず、ボールに付随するデータ(属性)を全てまとめた複合的なデータ構造を作る必要がある。C++ではクラスと呼ばれる仕組みを使って、これを実現する。(C言語でも構造体と呼ばれる同様の仕組みがあった。) クラスを利用すると、下の疑似コードにあるように、{{{class <クラス名> {}; }}}という形で複数のデータをまとめて新しい型を定義できる。この場合、Ball型という新しい型が作られ、intやfloat型の変数を宣言していたのと同様に、Ball型の変数a, b, cを宣言することが可能である。クラスというのは型宣言であり、クラス型の変数として生成されたデータのことをオブジェクト(またはインスタンス)という呼び方をする。オブジェクトのインスタンスには、コード例にあるように{{{<変数名>.<メンバーデータ>}}}という形でアクセスすることができる。 {{{ class Ball { public: float R; float M; float X; float Y; float Vx; float Vy; }; Ball a, b, c; float mass_of_ballA = a.M; float radius_of_ballB = a.R; }}} ここまでは、データ構造に関する話題でC言語にも構造体として実現されていた。オブジェクト指向というのは、もう一歩進んでこのBall型に対して行われるべき操作も同時に定義しようというものである。上の例では、個々のメンバーデータにアクセスしているが、通常個々のデータにはアクセスできないようにして、Ball型を使う際には予め用意されたメンバー関数のみを使って操作しようというものである。下の例では、別のボールと衝突するかの判定及び衝突後の速度の計算を行う関数をメンバー関数として宣言している。また、全てのメンバーデータは{{{ private: }}}以下に宣言しているため、クラスの外部からは見えないようになっている。(この場合、{{{ Ball a; a.X; }}} のようなアクセスができない。) {{{ class Ball { public: Ball(float r, float m); ~Ball(); float momentum() const { return M*sqrt(Vx*Vx + Vy+Vy); } bool checkCollision(const Ball& ball2) const { 引数で与えられた別のボールと衝突するかどうかをチェック。} void processCollision(Ball& ball2) { 引数で与えられた別のボールと衝突した場合に速度を衝突後の速度に変換する。 } private: float R; float M; float X; float Y; float Vx; float Vy; }; }}} このようにすると、Ballクラスを使う際には、個々のデータを直接扱うことが不可能で、常にメンバー関数を使う必要がある。このような制限を設けると一見不自由に見えるが、 {{{ Ball a, b; ... a.checkCollision(b); }}} というコードがあった場合、Ball aとbが衝突するかどうかを判定しているのだと一目で分かり、衝突の判定に必要なボールの位置や速度のデータにアクセスしていないため、コードを記述する際に引数の順序を間違えたりといったミスが起こりようがない。もちろん、{{{checkCollision(...)}}}という関数が正しく動作することが前提である。 ボールのように物理的実態を持つものだけでなく、1000個のボールの運動、衝突を追跡するために必要な一連の動作をまとめてBallSimulatorクラスのような型を作ることも可能である。クラスを利用することによって、プログラムのコードがどれくらい読みやすくなるか比べてみて欲しい。 === CppTutorial4にあるプログラム === ||'''プログラム名''' ||'''実行例''' ||'''内容''' || ||ballDynamics1.exe ||{{{ ./ballDynamics1.exe > balls1.dat }}} ||1000個のボールの運動を追跡 || ||ballDynamics2.exe ||{{{ ./ballDynamics2.exe > balls2.dat }}} ||上と同じ。Ball, BallSimulatorクラスを用いて書き直したもの || == 第6回 == ROOTチュートリアルのページを参照RootTutorial。 = 課題 = == 1. 第2回で作成したプログラムを少し変更してみる == 1から100までの和を計算するのではなく、例えば以下のような計算をしてみる * 1~100までの数、それぞれの2乗の和を計算する * 1^2^+2^2^+3^2^+...+n^2^と計算していき、和が最初に10000を超えるnを求める * 1+2+3+...と順にやっていき、和が最初に1000を超える数を求める == 周長を一定のまま、楕円の面積を最大化する条件を求める == 楕円の長軸と短軸の長さをそれぞれa, bとした時、周長はπ(a+b)となる。a+b=10と周長を一定に保ちながら、a, bの値を様々に変化させて楕円の面積が最大となるa, bの組み合わせを数値的に求める