UMLと構造図
UMLの構造図の書き方の説明。 詳しくは、参考ページのUML入門などが、分かりやすい。
雑談
UMLは、プログラムを図によってイメージを説明するために作られたが、プログラムに対する説明はコメントで書くことの方が多いだろう。このプログラムの説明の究極の姿として、とWEBがある。
は、数式などを意味的に記述したものを数学的な書式で表示するためのツールであり、これを開発したクヌースは、そのドキュメントをEWBによって記載している。WEBは、プログラムの説明を記載したドキュメントであり、この中に説明を交えたプログラムを記載する。このドキュメントをツールにかけることで、綺麗にレイアウトしたドキュメントや、プログラムのソースコードを取り出すことができる。
クラス図
クラス図は、構造図の中の基本的な図で、 枠の中に、上段:クラス名、中段:属性(要素)、下段:メソッド(関数)を記載する。 属性やメソッドの可視性を示す場合は、”-“:private、”+”:public、”#”:protected 可視性に応じて、”+-#”などを記載する。
関連
クラスが他のクラスと関係がある場合には、その関係の意味に応じて、直線や矢印で結ぶ。
(a)関連:単純に関係がある場合、
(b)集約:部品として持つが、弱い結びつき。関係先が消滅しても別に存在可能。
(c)コンポジション:部品として持つが強い結びつき。関係先と一緒に消滅。
(d)依存:依存関係にあるだけ
(e)派生:派生・継承した関係
(f)実現: Javaでのinterfaceによる多重継承
上図の例では、乗り物クラスVehicleから自動車がCarが派生し、 自動車は、エンジン(Engine)を部品として持つ。エンジンは車体と一緒に廃棄なら、コンポジションで実装する。
自動車は、同じく車輪(Wheel)を4つ持つが、自動車を廃棄してもタイヤは別に使うかもしれないので、集約で実装する。 集約で実装する場合は、C++などであれば、ポインタで部品を持ち、部品の廃棄(delete)は、別に行うことになる。
is-a 、has-a の関係
前の課題でのFigureクラスで、Color 情報をどう扱うべきかで、悩んだ場合と同じように、 クラスの設計を行う場合には、部品として持つのか、継承として機能を持つのか悩む場合がある。 この場合には、“is-a”の関係、“has-a”の関係で考えると、部品なのか継承なのか判断しやすい。
たとえば、上の乗り物(Vehicle)クラスと、車(Car)のクラスは、”Car is-a Vehicle” といえるので、is-a の関係。 “Car is-a Engine”と表現すると、おかしいことが判る。 車(Car)とエンジン(Engine)のクラスは、”Car has-a Engine”といえるので、has-a の関係となる。 このことから、CarはVehicleからの派生であり、Carの属性としてEngineを部品として持つ設計となる。
オブジェクト図
クラス図だけで表現すると、複雑なクラス関係では、イメージが分かりづらい場合がでてくる。 この場合、具体的な値を図に書き込んだオブジェクトで表現すると、説明がしやすい場合がある。 このように具体的な値で記述するクラス図は、オブジェクト図と言う。 書き方としては、クラス名の下に下線を引き、中段の属性の所には具体的な値を書き込んで示す。
その他の構成図
その他の構成図としては、コンポーネント図(物理的な構成要素から、システムの構造を表現する図)、 配置図(ハードウェアとアプリケーションの関係を図示したもの)、パッケージ図(パッケージ同士の関係をグループ化した図) なども用いる。
リスト構造について
データ処理において、配列は基本的データ構造だが、動的メモリ確保の説明で述べたように、基本の配列では大きさを変更することができない。これ以外にも、配列は途中にデータを挿入・削除を行う場合、の処理時間を伴う。以下にその問題点を整理し、その解決策であるリスト構造について説明する。
配列の利点と欠点
今までデータの保存には、配列を使ってきたが、配列は添字で場所を指定すれば、その場所のデータを簡単に取り出すことができる。配列には苦手な処理がある。例えば、配列の中から目的のデータを高速に探す方式として、2分探索法を用いる。
int find( int array[] , int left , int right , int key ) { // データは left から right-1までに入っているとする。 while( left < right ) { int mid = (left + right) / 2 ; // 中央の場所 if ( array[ mid ] == key ) return mid ; // 見つかった else if ( array[ mid ] > key ) right = mid ; // 左半分にある else left = mid + 1 ; // 右半分にある } return -1 ; // 見つからない }
しかし、配列の中に新たに要素を追加しようとするならば、データは昇順に並んでいる必要があることから、以下のようになるだろう。
void entry( int array[] , int* psize , int key ) { // データを入れるべき場所を探す処理 for( int i = 0 ; i < *psize ; i++ ) // O(N) の処理だけど、 if ( array[ i ] > key ) // O(log N) でも書けるけど break ; // 単純に記載する。 if ( i < *psize ) { // 要素を1つ後ろにずらす処理 for( int j = *psize ; j > i ; j-- ) // O(N)の処理 array[ j ] = array[ j - 1 ] ; array[ i ] = key ; } else { array[ *psize ] = key ; } (*psize)++ ; }
これで判るように、データを配列に追加する場合、途中にデータを入れる際にデータを後ろにずらす処理が発生する。
この例は、データを追加する場合であったが、不要となったデータを取り除く場合にも、データの場所の移動が必要である。
順序が重要なデータ列で途中へのデータ挿入削除
例えば、アパート入居者に回覧板を回すことを考える。この中で、入居者が増えたり・減ったりした場合、どうすれば良いか考える。
通常は、自分の所に回覧板が回ってきたら、次の入居者の部屋番号さえわかっていれば、回覧板を回すことができる。
101 102 103 104 105 106 [ 105 | 106 | -1 | 102 | 104 | 103 ]
このように次のデータの場所という概念を使うと、データの順序を持って扱うことができる。
struct LIST { int data ; int next ; } ; struct LIST array[] = { /*0*/ { 11 , 2 } , /*1*/ { 67 , 3 } , // 末尾にデータ34を加える /*2*/ { 23 , 4 } , // { 23 , 5 } , /*3*/ { 89 , -1 } , // 末尾データの目印 /*4*/ { 45 , 1 } , /*5*/ { 0 , 0 } , // { 34 , 4 } , } ; for( int idx = 0 ; idx >= 0 ; idx = array[ idx ].next ) { printf( "%d¥n" , array[ idx ].data ) ; }
この方法を取れば、途中にデータ入れたり、抜いたりする場合に、データの移動を伴わない。
しかし、配列をベースにしているため、配列の上限サイズを超えて格納することはできない。
移動平均の処理
前回の授業で説明したようなA/D変換した数値データを読み取った場合、どのようなことが発生するか考える。
例えば、以下に示すような測定値があったとする。
- 2018-06-05-wave.csv
6/21のデータはファイルの行末文字の影響で Microsoft Visual Studio の scanf_s() を使うとデータが1件しか読み込めない問題がありました。現時点の上記ファイルは修正済みです。
このデータの一部をグラフ化してみると、次のような波形であった。
この波形をみると、大きく見ればsinカーブだが、細かい点を見るとデータにブレがある。
誤差の原因
このような測定結果が得られた場合、本来コンピュータで処理したいデータは何であろうか?
原因は様々なものが考えられるが、
- 回路のノイズ対策が不十分で、外部の電気的な影響が混入。
オシロスコープで周期を図ると、60Hz なら、交流電源だったり… - D/A 変換を行う場合には、量子化誤差かもしれない。
例えば、最初の波形が、加速度センサーの値であったとして、船の上で揺れているために、大きな周期で加速度が変化しているかもしれない。一方で、船自体がエンジンによる揺れで加速度が変化しているかもしれない。
船の中で波の揺れと、エンジンの揺れが観測されている加速度センサーの情報で、船の揺れの大きさ・揺れの周期を知りたい場合、どうすればいいだろうか?
移動平均
このデータを見ると、10個のデータまでの間で、波形が上下に変動している。船の揺れとエンジンの揺れが原因であれば、10個ぐらいのデータのゆらぎが、エンジンによる揺れと考えられる。では、この10個ぐらいの範囲で値が上下の影響を減らしたければ、どうすればいいか?一番簡単な方法は、前後10個のデータで平均を取ればいいだろう。増減する値を加えれば、プラスの部分とマイナスの部分の値が相殺されて0に近くはず。そこでは、Excel で前後データの平均をとってみよう。
Excelで前後11点の平均を求める式をセルに入れる
青線:元波形データ(B列)、赤線:前後11点の平均(C列)
このように、データの前後の決められた範囲の平均を平均する処理は、移動平均(単純移動平均)と呼ぶ。
時間tにおけるデータをとした場合、前後5点の移動平均は、以下のような式で表せるだろう。
移動平均のプログラム
Excel で計算と同じ処理をプログラムで行うと以下のようになるだろう。
// moving-average.c #include <stdio.h> #define WIDTH 5 double data[ 1000 ] ; // 元データ double ans[ 1000 ] ; // 平均後のデータ int main() { int t , i , size ; // 最初に全部のデータを読み込む for( size = 0 ; size < 1000 ; size++ ) { int num ; // コンマ区切りのデータを読む // 2つのデータが読み込めない時は入力を終了 if ( scanf( "%d,%lf" , &num , &data[size] ) != 2 ) break ; } // 移動平均を求める for( t = WIDTH ; t < size - WIDTH ; t++ ) { // t番目のデータの前後WIDTH個の合計 double sum = 0 ; for( i = -WIDTH ; i <= WIDTH ; i++ ) sum += data[ t + i ] ; ans[ t ] = sum / (2*WIDTH + 1) ; } // 計算後のデータをコンマ区切りで出力 for( t = 0 ; t < size ; t++ ) { printf( "%d, %10.6lf, %10.6lf\n" , t , data[ t ] , ans[ t ] ) ; } return 0 ; }
このプログラムを動かすと、データ番号とデータ値をコンマ区切りで与えること。
入力リダイレクトと出力リダイレクト
上記のプログラムでは、キーボードからデータを入力しなくてはいけない。これでは入力が大変なので、保存したファイルを使ってプログラムにデータを与える。
上記のプログラムを、パソコンの Z:¥課題¥moving-average.c に保存したとする。このプログラムを「コンパイル&実行」すれば、Z:¥課題¥moving-average.exe という実行プログラムが作られ、プログラムが起動する。このままでは、キーボードからデータを入力する必要がある。
(1) ファイルから入力した値を使って処理を行うのであれば、コマンドを起動。
タスクバー左側の検索バーに、cmd.exe と入力すれば、命令入力画面が表示される。
(2) コマンド画面で、以下のように入力し、moving-average.exe があるか確認する。
C:¥WINDOWS¥System32> Z: 青は表示される部分、赤が入力 Z:¥> cd Z:¥課題 Z:¥課題¥> dir *.exe 06/21/2019 12:30PM 12345 moving-average.exe
(3) 最初のデータの記録されたCSVファイルを Z:¥課題 に保存する。
(4) コマンド画面で、以下のようにプログラム名の後ろに “< ファイル名” をつけて起動すると、キーボード入力の代わりに、ファイルから読み込んでプログラムが動く。このような起動は、入力リダイレクトと呼ぶ。
Z:¥課題¥> moving-average.exe < 2018-06-05-wave.csv xx, xxx.xxxx, xxx.xxxx ←結果が画面に表示される
(5) これでは、結果がよく分からないので、ファイルに保存し Excel でグラフ化する。コマンド画面で、以下のようにプログラム名の後ろに“> ファイル名” をつけて起動すると、結果を画面に出力する代わりに、ファイルに結果を保存してくれる。このような起動は、出力リダイレクトと呼ぶ。
Z:¥課題¥> moving-average.exe < 2018-06-05-wave.csv > out.csv
出力された out.csv は、データがコンマ区切りなので、Excel でひらけば、結果を表として簡単に読み込める。後は、グラフ化したい範囲を、マウスでドラッグ(もしくはシフトキーを押しながらカーソル移動)し、[挿入]-[グラフ]-[散布図]-[折れ線グラフ] でグラフ化すればいい。
自宅学習の課題
表計算ソフトで、移動平均を計算させてみよう。 ※
- 元波形
- 前後5点で移動平均
- 前後11点で移動平均
- 前後51点で移動平均
をとるような表計算の式を書き込んで、その結果の波形がどんなグラフになるのか確認しておくこと。
UMLの歴史と意味
プログラミングでの演習もほぼ終わり、オブジェクト指向での設計の話へ。 オブジェクト指向でUMLの書き方は、統一した図法という意味で重要であることを 示しながら、全体の説明を行う。
最初に、UML以前の説明として、フローチャート図やPADの説明を行う。 処理の流れを記載するものとして、使われてきているがデータ構造の設計も重要。
UMLは、ランボーによるOMT(Object Modeling Technique どちらかというとOOA中心?)と、 ヤコブソンによるオブジェクト指向ソフトウェア工学(OOSE)を元に1990年頃に 発生し、ブーチのBooch法(どちらかというとOOD中心?)の考えをまとめて、 UML(Unified Modeling Language)としてでてきた。
OMTでは、OOA(Object Oriented Analyze:分析中心)として(1)問題記述、(2)オブジェクトモデルの記述、(3)状態遷移図の作成、(4)データフロー図の作成といったプロセスが行われる。 これに、OOD(Object Oriented Design:実装目的)でOOA段階の図法に加え、 ユースケース図、シーケンス図などを加えながら設計を行う。 この2つをOOADとまとめる場合も多い。
UMLでよく使われる図を列記すると、以下の物が挙げられる。
- 構造図
- クラス図
- コンポーネント図
- 配置図
- オブジェクト図
- パッケージ図
- 振る舞い図
- アクティビティ図
- ユースケース図
- ステートチャート図(状態遷移図)
- 相互作用図
- シーケンス図
- コミュニケーション図(コラボレーション図)
複雑な継承
課題で取り組んでもらっている、動物の進化を表すクラスの概要を示す。
動物・鳥類・哺乳類クラス
// 動物クラス class Animal { private: char name[ 10 ] ; public: Animal( const char s[] ) { strcpy( name , s ) ; } const char* get_name() const { return name ; } virtual void move() = 0 ; virtual void birth() = 0 ; } ; // 鳥類クラス class Bird : public Animal { public: Bird( const char s[] ) : Animal( s ) {} virtual void move() { printf( "%s fry.\n" , get_name() ) ; } virtual void birth() { printf( "%s lay egg.\n" , get_name() ) ; } } ; // 哺乳類クラス class Mammal : public Animal { public: Mammal( const char s[] ) : Animal( s ) {} virtual void move() { printf( "%s walk.\n" , get_name() ) ; } virtual void birth() { printf( "%s lay baby.\n" , get_name() ) ; } } ; int main() { Bird chiken( "piyo" ) ; chiken.move() ; chiken.birth() ; Mammal cat( "tama" ) ; cat.move() ; cat.birth() ; return 0 ; }
ここで、カモノハシを作るのであれば、どうすれば良いだろうか?
鳥類・哺乳類とは別にカモノハシを作る
class SeaBream : public Animal { public: Mammal( const char s[] ) : Animal( s ) {} virtual void move() { printf( "%s walk.\n" , get_name() ) ; } virtual void birth() { printf( "%s lay egg.\n" , get_name() ) ; } } ;
この例では、簡単な処理だが、move() の中身が複雑であれば、改めて move() を宣言するのではなく、継承するだけの書き方ができないだろうか?
多重継承
C++ には、複数のクラスから、派生する多重継承という機能がある。であれば、鳥類と哺乳類から進化したのだから、以下のように書きたい。
class SeaBream : public Bird , Mammal { } ;
しかし、カモノハシに move() を呼び出すと、鳥類の move() と哺乳類の move() のどちらを動かすか曖昧になる。また、派生クラスは親クラスのデータ領域と、派生クラスのデータ領域を持つため、鳥類の name[] と、哺乳類の name[] を二つ持つことになる。
足と羽のクラス
class Animal { private: char name[ 10 ] ; public: Animal( const char s[] ) { strcpy( name , s ) ; } const char* get_name() const { return name ; } virtual void move() = 0 ; } ; // 羽 class Wing { public: const char* move_method() { return "fly" ; } } ; // class Leg { public: const char* move_method() { return "walk" ; } } ; class Bird : public Animal , Wind { public: Bird( const char s[] ) : Animal( s ) {} virtual void move() { printf( "%s %s.\n" , get_name() , move_method() ) ; } } ; class Mammal : public Animal , Leg { public: Mammal( const char s[] ) : Animal( s ) {} virtual void move() { printf( "%s %s.\n" , get_name() , move_method() ) ; } } ;
# うーむ、継承する処理が1行程度でかける処理だと、どのやり方も「継承が便利」というように見えないな…(x_x;
class Animal { private: char name[ 10 ] ; public: Animal( const char s[] ) { strcpy( name , s ) ; } const char* get_name() const { return name ; } virtual void move() = 0 ; virtual void birth() = 0 ; } ; // 鳥類クラス class Bird : public virtual Animal { public: Bird( const char s[] ) : Animal( s ) {} virtual void move() { printf( "%s fry.\n" , get_name() ) ; } virtual void birth() { printf( "%s lay egg.\n" , get_name() ) ; } } ; // 哺乳類クラス class Mammal : public virtual Animal { public: Mammal( const char s[] ) : Animal( s ) {} virtual void move() { printf( "%s walk.\n" , get_name() ) ; } virtual void birth() { printf( "%s lay baby.\n" , get_name() ) ; } } ; class SeaBream : public virtual Bird , virtual Mammal { public: SeaBream( const char s[] ) : Animal( s ) {} void move() { Mammal::move() ; } void birth() { Bird::birth() ; } } ;
ただし、多重継承は親クラスの情報と、メソッドを継承する。この場合、通常だと name[] を二つ持つことになるので、問題が発生する。そこで、親クラスの継承に virtual を指定することで、ダイヤモンド型継承の 2つの属性をうまく処理してくれるようになる。
しかし、多重継承は処理の曖昧さや効率の悪さもあることから、採用されていないオブジェクト指向言語も多い。特に Java は、多重継承を使えない。その代わりに interface という機能が使えるようになっている。
簡単テストの解説
前に実施した簡単テストの答え。
キーワードの理解
型の理解
上記の問題だけでは、説明しきれないので、下図左のプログラムと、その printf() で表示するデータの型を示す。
型の意味を考えたうえで、何が表示されるか考えよう。
関数の理解
簡単テスト
情報構造論のテストにて結果は両極端な成績。苦手な人は基本理解が怪しいみたい。ということでC言語の理解の確認。
キーワードの理解
以下のプログラムの下線部 A-I の各単語を説明するのにふさわしいものを、(a)~(f)で選べ。キーワードは予約語と呼ばれることも多い。
(a) 型を表すキーワード、(b) キーワード、(c) 変数名、
(d) 関数名、(e) ファイル名、(f) それ以外
解答欄
A_____, B_____, C_____, D_____, E_____,
F_____, G_____, H_____, I_____
型の理解
以下のプログラムの下線部 A-D の型を答えよ。
(a) int , (b) int型へのポインタ, (c) char, (d) char型へのポインタ, (e) void
解答欄
A_____, B_____, C_____, D_____
関数の理解
以下のように、文字列を src から dest にコピーする(ただし最大文字数 countまで) strncpy を作った。
この関数の下線に示す仮引数部分を完成せよ。
解答欄________________________________
型の無い言語からC言語を学ぶと
情報構造論の中間試験の採点をしていて気づいたこと。
プログラムが解っていない学生の回答で、関数呼び出しで実引数に int とか実引数の型を書く学生がいる。
int foo( int x ) { return x * x ; } void main() { for( int i = 0 ; i < 10 ; i++ ) printf( "%d" , foo( int i ) ) ; } ~~~ なぜ int を書くの?
でも、最近の学生はプログラミングで最初に習うプログラム言語が、JavaScript だったり、Python だったりと、型の無い言語であることが多い。
こういう学生さんは、関数宣言と関数呼び出しでは、以下のような感じで習うことだろう。
// JavaScript function foo( x ) { return x * x ; } for( var i = 0 ; i < 10 ; i++ ) print( foo( i ) ) ; ## Python def foo( x ) return x * x for i in range( 0 , 10 ) print( foo( i ) )
んで、こういう学生さんは、関数宣言の仮引数部分を見ると、変数名しか書いてない。この後にC言語を習うと、仮引数の宣言で foo( int x ) みたいに書いてあると、そこに int と書かないとダメみたいに思う、もしくは int x で一つの引数みたいに思うんだろうな。
だから、関数呼び出しの実引数にで、foo( int i ) みたいに型名を書いてしまうと思われる。int というのは、整数型にするためのもの…みたいなイメージなんだろうか?。
C言語での関数宣言と関数呼び出しの説明
// fooという関数の宣言 int foo( int x ) // foo()の関数の答えの型は、int型。 ~~~~~~仮引数 { // 仮引数 x の型は、int 型。 return x * x ; } void main() { for( int i = 0 ; i < 10 ; i++ ) printf( "%d¥n" , foo( i ) ) ; ~~~実引数 // foo() の第一引数は、整数型を書かないといけない。 // 1番目の実引数 i の型は、int i で宣言されているから、int 型。 // foo( 1.2 ) と書くと、第一引数は double だから、 // 引数の型が合わないのでエラー。 }
関数宣言の引数は、仮引数と呼ぶ。関数を呼び出すときの引数は、実引数と呼ぶ。
C言語では、関数宣言の仮引数には「型」を明示する必要がある。(というか変数宣言では型を明示しないとダメ)
さらにC言語では関数を呼び出すときに、仮引数と実引数で「型」が一致していないとエラーとなる。
JavaScript や Python では、変数に「型」という概念が不要なので、仮引数宣言には「型」を明示する必要がない。
C言語 や Java は、静的な型付けの言語。最初から型がきまっていないとダメな言語。
これに対し、JavaScript や Python という言語は、動的な型付けの言語。プログラム言語が実行中に型を意識しながら動く言語。
福井高専の対外接続の速度
福井高専ではSINETによって接続されているけど、次の更新の際に十分な性能があるかのために、ネットワーク利用状況の確認。
瞬間最大風速で、150Mbps 。ムーアの法則に従うとすれば、2年で2倍と考えると、2022年で約500Mbps, 2025年で1.2Gbps 程度となる。特に今後 学内でBYODが拡大すると、さらなる通信速度が必要と考えられる。