パイプとフィルタ
フィルタプログラム
パイプを使うと、標準入力からデータをもらい・標準出力に結果を出力するような簡単なプログラムを組み合わせて、様々な処理が簡単にできる。こういったプログラムは、フィルタと呼ぶ。
簡単な例として、入力をすべて大文字に変換するプログラム(toupper)、入力文字をすべて小文字に変換するプログラム(tolower)が、下記の例のように保存してあるので動作を確かめよ。
guest00@nitfcei:~$ cp /home0/Challenge/2.1-RedirectPipe.d/toupper.c . guest00@nitfcei:~$ gcc -o toupper toupper.c . guest00@nitfcei:~$ cat toupper.c | ./toupper #INCLUDE <STDIO.H> #INCLUDE <CTYPE.H> INT MAIN() { INT C ; WHILE( (C = GETCHAR()) != EOF ) PUTCHAR( TOUPPER( C ) ) ; RETURN 0 ; } guest00@nitfcei:~$ cp /home0/Challenge/2.1-RedirectPipe.d/tolower.c . guest00@nitfcei:~$ gcc -o tolower tolower.c guest00@nitfcei:~$ cat tolower.c | ./tolower (((何が出力されるか答えよ)))
よく使われるフィルタのまとめ
文字パターンを含む行だけ出力 | grep 文字パターン |
文字パターンを含まない行を出力 文字パターンを正規表現でマッチングし該当行を出力 大文字小文字を区別しない |
grep -v 文字パターン grep -e 正規表現 grep -i 文字パターン |
入力文字数・単語数・行数をカウント(word counter) | wc |
入力行数をカウント | wc -l |
データを昇順に並べる | sort |
データを降順に並べる 先頭を数字と見なしてソート(-gなしの場合、文字とみなしてソート) |
sort -r sort -g |
同じ行データが連続したら1つにまとめる | uniq |
同じ行が連続したら1つにまとめ、連続した数を出力 | uniq -c |
空白区切りで指定した場所(1番目)を抽出 | awk ‘{print$1;}’ |
入力の先頭複数行を表示(10行) | head |
入力の末尾複数行を表示(10行) | tail |
指定した行数だけ、先頭/末尾を表示 | head -行数 tail -行数 |
入力したデータを1画面分表示した所で一時停止する(ページャ) more は、最も単純なページャで、SPACE で1画面送り、ENTER で1行送り、”q”で終了。 lv は前後に移動できるページャで、カーソルキー↑(b)↓(f) で行を前後に移動できる。 |
more lv |
LOG解析
Linux は利用者に様々なサービスを提供するサーバで広く利用されている。しかし、幅広いサービス提供となると、中にはウィルス拡散や個人情報収集のための悪意のあるアクセスも増えてくる。
このためサーバでは、アクセスを受けた時の状況を記録し保存する。このような情報はアクセス履歴、ログと呼ぶ。
ログの中には、以下のような情報が混在することになるが、大量の 1. や 2. 目的のアクセスの中に、3. や 4. といったアクセスが混ざることになるが、これを見逃すとシステムに不正侵入を受ける可能性もある。
- 本来の利用者からのアクセス
- 検索システムの情報収集(クローラーからのアクセス)
- 不正な情報収集のためのアクセス
- システムの不備を探して不正侵入などを試みるアクセス
今回の演習では、電子情報の web サーバのとある1日のアクセス履歴ファイルを用い、パイプ機能を使い様々なフィルタを使い LOG解析の練習を行う。
アクセス履歴の解析
Webサーバのアクセス履歴が、/home0/Challenge/2.2-LOG.d/access.log に置いてある。このファイルで簡単な確認をしてみよう。
(( ファイルの場所に移動 )) $ cd /home0/Challenge/2.2-LOG.d/ (( .asp という文字を含む行を表示 )) $ grep .asp access.log
電子情報のWebサーバには、.asp (WindowsのWebサーバで動かすプログラムの拡張子) など存在しない。明らかに設定不備を探すための攻撃である。
これを見ると、grep で .asp を含む行が抜粋され、.asp の部分が強調されていることで、攻撃を簡単に確認できる。しかしこれは画面行数で10件程度が確認できるが、本当は何回攻撃を受けたのだろうか?この場合は、行数をカウントする”wc -l” を使えばいい。
(( アクセス回数を数える )) $ grep .asp access.log | wc -l 37
access.log の各項目の意味
電子情報のWebサーバの access.log に記録されている各項目の意味は以下の通り。
項目 | log項目 | 内容 |
---|---|---|
1 | %h | リモートホスト。WebサーバにアクセスしてきたクライアントのIPアドレス |
2 | %l | リモートログ名。説明略。通常は “-“ |
3 | %u | ログインして操作するページでのユーザ名。通常は “-“ |
4 | %t | アクセスを受けた時刻 |
5 | %r | 読み込むページ。アクセス方法(GET/POSTなど)と、アクセスした場所。 |
6 | %>s | ステータスコード。(200成功,403閲覧禁止,404Not Found) |
7 | %b | 通信したデータのバイト数。 |
8 | %{Referer}i | Referer どのページからアクセスが発生したのか |
9 | %{User-Agent}i | User-Agent ブラウザ種別(どういったブラウザからアクセスされたのか) |
以下に、フィルタプログラムを活用して、色々な情報を探す例を示す。実際にコマンドを打って何が表示されるか確認しながら、フィルタプログラムの意味を調べながら、何をしているか考えよう。
.asp を使った攻撃を探す
(( .asp を試す最初の履歴を探す )) $ grep "\.asp" access.log | head -1 49.89.249.9 - - [20/Dec/2019:09:19:06 +0900] "POST /Include/md5.asp HTTP/1.1" 404 64344 "https://www.ei.fukui-nct.ac.jp/Include/md5.asp" "Mozilla/4.0 (compatible; MSIE 9.0; Windows NT 6.1)" (( 49.89.249.9 がどんなアクセスを試みているのか探す )) $ grep ^49.89.249.9 access.log | head 49.89.249.9 - - [20/Dec/2019:09:19:06 +0900] "POST /Include/md5.asp HTTP/1.1" 404 64344 "https://www.ei.fukui-nct.ac.jp/Include/md5.asp" "Mozilla/4.0 (compatible; MSIE 9.0; Windows NT 6.1)" 49.89.249.9 - - [20/Dec/2019:09:19:06 +0900] "POST /inc/md5.asp HTTP/1.1" 404 61056 "https://www.ei.fukui-nct.ac.jp/inc/md5.asp" "Mozilla/4.0 (compatible; MSIE 9.0; Windows NT 6.1)"せ
- 正規表現とは
- \.asp と先頭に \ がついているのは、ピリオドだけだと「ワイルドカード文字の任意の1文字の意味」になるため、ワイルドカードをエスケープしている。
- ^49.89…. の先頭に ^ がついているのは、行頭にマッチングさせるため。
- ステータスコードが404は”Not Found”なので、読み出しに失敗している。
- IPアドレス検索で、49.89.249.9 がどこのコンピュータか調べよう。
攻撃の時間を確認
(( 49.89.249.9 がどんな時間にアクセスを試みているのか探す )) $ grep ^49.89.249.9 access.log | awk '{print $4;}' [20/Dec/2019:09:19:06 [20/Dec/2019:09:19:06 [20/Dec/2019:09:19:07 :
- 不正アクセスを試みている時間を調べると、そのアクセス元の09:00~17:00に攻撃していることがわかる場合がある。どういうこと?
ページの閲覧頻度を確認
(( /~t-saitoh/ 見たIPアドレスと頻度 )) $ grep "/~t-saitoh/" access.log | awk '{print $1;}' | sort | uniq -c | sort -g -r | head 38 151.80.39.78 35 151.80.39.209 32 203.104.143.206 31 5.196.87.138 :
- grep – “/~t-saitoh/”のページをアクセスしているデータを抽出
- awk – 項目の先頭(IPアドレス)だけ抽出
- sort – IPアドレス順に並べる(同じIPアドレスならその数だけ重複した行になる)
- uniq – 重複している行数を数える
- sort -g -r – 先頭の重複数で大きい順にソート
- head – 先頭10行だけ抽出
(( /~t-saitoh/ 見たIPアドレスと頻度 )) (( t-saitoh のテスト問題のページを誰が見ているのか? )) $ grep "/~t-saitoh/exam/" access.log 5.196.87.156 - - [20/Dec/2019:06:36:02 +0900] "GET /~t-saitoh/exam/db2009/ex2009-5-1.pdf HTTP/1.1" 200 20152 "-" "Mozilla/5.0 (compatible; AhrefsBot/6.1; +http://ahrefs.com/robot/)" : (( クローラーのアクセスが多くてよくわからないので bot を含まない行を抽出 )) $ grep "/~t-saitoh/exam/" access.log | grep -v -i bot | lv 213.242.6.61 - - [20/Dec/2019:06:33:12 +0900] "GET /%7Et-saitoh/exam/ HTTP/1.0" 200 19117 "http://www.ei.fukui-nct.ac.jp/%7Et-saitoh/exam/" "Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/67.0.3396.87 Safari/537.36 OPR/54.0.2952.64 (Edition Yx)" 188.163.109.153 - - [20/Dec/2019:06:43:04 +0900] "GET /%7Et-saitoh/exam/ HTTP/1.0" 200 19117 "http://www.ei.fukui-nct.ac.jp/%7Et-saitoh/exam/" "Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/66.0.3359.170 Safari/537.36 OPR/53.0.2907.99" 188.163.109.153 - - [20/Dec/2019:06:43:05 +0900] "POST /cgi-bin/movabletype/mt-comments.cgi HTTP/1.0" 404 432 "http://www.ei.fukui-nct.ac.jp/%7Et-saitoh/exam/" "Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/66.0.3359.170 Safari/537.36 OPR/53.0.2907.99" 45.32.193.50 - - [20/Dec/2019:07:06:15 +0900] "GET /~t-saitoh/exam/apply-prog.html HTTP/1.0" 200 5317 "http://www.ei.fukui-nct.ac.jp/" "Mozilla/5.0 (Windows NT 5.2) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/63.0.3239.132 Safari/537.36"
- この結果を見ると mt-comments.cgi というアクセスが見つかる。どうも コメントスパム(ブログのコメント欄に広告を勝手に書き込む迷惑行為)をしようとしている。
ネットワーク攻撃への対処
今回の access.log のアクセス履歴解析は、Webサーバへのアクセスへの基本的な対処となる。しかし、もっと違うネットワーク接続ではどのような対処を行うべきであろうか?
一般的には、
- サーバにネットワークアクセスの記録ソフトを使う(例ネットワークプロトコルアナライザーWireShark)
- ファイアウォールのアクセス履歴を解析
授業内レポート
- ここまでのLOG解析の例の1つについて、どういう考え方でフィルタを使っているのか、自分の言葉で説明せよ。
- LOG 解析のためのコマンドを考え、その実行結果を示し、それから何が判るか説明せよ。
(例) 自分で考えたコマンドの実行結果をつけたうえで、「コメントスパムを何度も試す危ないアクセス元は〇〇である。」 - レポート提出先フォルダ ファイル名は、出席番号2桁-名前-レポート名.docx (ファイル形式は pdf などでも良い) とする。
UMLと構造図
UMLの構造図の書き方の説明。 詳しくは、参考ページのUML入門などが、分かりやすい。
クラス図
クラス図は、構造図の中の基本的な図で、 枠の中に、上段:クラス名、中段:属性(要素)、下段:メソッド(関数)を記載する。 属性やメソッドの可視性を示す場合は、”-“:private、”+”:public、”#”:protected 可視性に応じて、”+-#”などを記載する。
関連
クラスが他のクラスと関係がある場合には、その関係の意味に応じて、直線や矢印で結ぶ。
(a)関連(association):単純に関係がある場合、
(b)集約(aggregation):部品として持つが、弱い結びつき。関係先が消滅しても別に存在可能。(has-a)
(c)コンポジション(composition):部品として持つが強い結びつき。関係先と一緒に消滅。(has-a)
(d)依存(dependency):依存関係にあるだけ
(e)派生(generalization):派生・継承した関係(is-a)
(f)実現(realization): Javaでのinterfaceによる多重継承
上図の例では、乗り物クラスVehicleから自動車Carが派生し(CarからVehicleへの三角矢印―▷)、 自動車は、エンジン(Engine)を部品として持つ(EngineからCarへのひし形矢印―◆)。エンジンは車体と一緒に廃棄なら、コンポジション(C++であれば部品の実体を持つ)で実装する。
自動車は、同じく車輪(Wheel)を4つ持つが、自動車を廃棄してもタイヤは別に使うかもしれないので、集約(部品への参照を持つ)で実装する(WheelからCarへのひし形矢印―◇)。 集約で実装する場合は、C++などであれば、ポインタで部品を持ち、部品の廃棄(delete)は、別に行うことになる。
Javaなどのプログラム言語では、オブジェクトはデータの実体へのポインタで扱われるため、コンポジションと集約を区別して表現することは少ない。
is-a 、has-a の関係
前の課題でのカモノハシクラスで、羽や足の情報をどう扱うべきかで、悩んだ場合と同じように、 クラスの設計を行う場合には、部品として持つのか、継承として機能を持つのか悩む場合がある。 この場合には、“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を部品として持つ設計となる。
ER図
UMLではないが、オブジェクト図に近いものとしてER図がある。これはリレーショナルデータベースの設計が正しいか確認しながら設計するための図で、Entity(実体)とRelation(関連)を相互に線で結んだもので、最近のER図の書き方は、かなりクラス図の書き方に似ている。
オブジェクト図
クラス図だけで表現すると、複雑なクラス関係では、イメージが分かりづらい場合がでてくる。 この場合、具体的な値を図に書き込んだオブジェクトで表現すると、説明がしやすい場合がある。 このように具体的な値で記述するクラス図は、オブジェクト図と言う。 書き方としては、クラス名の下に下線を引き、中段の属性の所には具体的な値を書き込んで示す。
その他の構造図
パッケージ図
パッケージ図は、クラス図をパッケージ毎に分類して記載する図。 パッケージのグループを、フォルダのような図で記載する。

IT専科から引用
コンポーネント図とコンポジット構造図
コンポジット構造図は、クラスやコンポーネントの内部構造を示すもので、コンポーネント図は、複数のクラスで構成される処理に、 インタフェースを用意し、あたかも1つのクラスのように扱ったもの。 接続するインタフェースを飴玉と飴玉を受けるクチのイメージで、提供側を◯───で表し、要求側を⊃──で表す。

IT専科から引用
配置図
配置図は、システムのハードウェア構成や通信経路などを表現するための図。 ハードウェアは直方体の絵で表現し、 デバイスの説明は、”≪device≫”などを示し、実行環境には、”≪executionEnvironment≫” などの目印で表現する。

IT専科から引用
スタックと待ち行列
前回の授業では、リストの先頭にデータを挿入する処理と、末尾に追加する処理について説明したが、この応用について説明する。
計算処理中に一時的なデータの保存として、スタック(stack)と待ち行列・キュー(queue)がよく利用される。それを配列を使って記述したり、任意の大きさにできるリストを用いて記述することを示す。
スタック
配列を用いたスタック
一時的な値の記憶によく利用されるスタック(stack)は、データの覚え方の特徴からLIFO( Last In First out )とも呼ばれる。配列を使って記述すると以下のようになるであろう。
import java.util.*; public class Main { static final int STACK_SIZE = 10 ; static int[] stack = new int[ STACK_SIZE ] ; static int sp = 0 ; static void push( int x ) { stack[ sp++ ] = x ; } static int pop() { return stack[ --sp ] ; } public static void main(String[] args) throws Exception { push( 11 ) ; push( 22 ) ; push( 33 ) ; System.out.println( pop() ) ; System.out.println( pop() ) ; System.out.println( pop() ) ; } }
配列を使った Stack をオブジェクト指向で記述するなら、以下のように書ける。
import java.util.*; class Stack { static final int STACK_SIZE = 10 ; int[] array ; int sp ; Stack() { this.array = new int[ STACK_SIZE ] ; this.sp = 0 ; } void push( int x ) { array[ sp++ ] = x ; } int pop() { return array[ --sp ] ; } } ; public class Main { public static void main(String[] args) throws Exception { Stack stack = new Stack() ; stack.push( 11 ) ; stack.push( 22 ) ; stack.push( 33 ) ; System.out.println( stack.pop() ) ; System.out.println( stack.pop() ) ; System.out.println( stack.pop() ) ; } }
C言語で書いた場合
#define STACK_SIZE 32 int stack[ STACK_SIZE ] ; int sp = 0 ; void push( int x ) { // データをスタックの一番上に積む stack[ sp++ ] = x ; } int pop() { // スタックの一番うえのデータを取り出す return stack[ --sp ] ; } void main() { push( 1 ) ; push( 2 ) ; push( 3 ) ; printf( "%d\n" , pop() ) ; // 3 printf( "%d\n" , pop() ) ; // 2 printf( "%d\n" , pop() ) ; // 1 }


++,–の前置型と後置型の違い
// 後置インクリメント演算子 int i = 100 ; printf( "%d" , i++ ) ; // これは、 printf( "%d" , i ) ; i++ ; // と同じ。100が表示された後、101になる。 // 前置インクリメント演算子 int i = 100 ; printf( "%d" , ++i ) ; // これは、 i++ ; printf( "%d" , i ) ; // と同じ。101になった後、101を表示。
リスト構造を用いたスタック
しかし、この中にSTACK_SIZE以上のデータは貯えられない。同じ処理をリストを使って記述すれば、配列サイズの上限を気にすることなく使うことができるだろう。では、リスト構造を使ってスタックの処理を記述してみる。
import java.util.*; class ListNode { int data ; ListNode next ; ListNode( int d , ListNode n ) { this.data = d ; this.next = n ; } } public class Main { static ListNode stack = null ; static void push( int x ) { stack = new ListNode( x , stack ) ; } static int pop() { int ans = stack.data ; stack = stack.next ; return ans ; } public static void main(String[] args) throws Exception { push( 1 ) ; push( 2 ) ; push( 3 ) ; System.out.println( pop() ) ; System.out.println( pop() ) ; System.out.println( pop() ) ; } }
struct List* stack = NULL ; void push( int x ) { // リスト先頭に挿入 stack = cons( x , stack ) ; } int pop() { // リスト先頭を取り出す int ans = stack->data ; struct List* d = stack ; stack = stack->next ; // データ 0 件で pop() した場合のエラー対策は省略 free( d ) ; return ans ; }
オブジェクト指向っぽく書くならば、下記のようになるだろう。初期状態で stack = null にしておくと、stack.push() ができないので、stack の先頭には、ダミーデータを入れるようにプログラムを書くと以下のようになるだろう。
import java.util.*; class ListNode { int data ; ListNode next ; ListNode( int d , ListNode n ) { this.data = d ; this.next = n ; } ListNode() { // stack初期化用のコンストラクタ this.data = -1 ; this.next = null ; } void push( int x ) { this.next = new ListNode( x , this.next ) ; } int pop() { int ans = this.next.data ; this.next = this.next.next ; return ans ; } } ; public class Main { public static void main(String[] args) throws Exception { ListNode stack = new ListNode() ; // stack初期化用のコンストラクタを使う stack.push( 1 ) ; stack.push( 2 ) ; System.out.println( stack.pop() ) ; System.out.println( stack.pop() ) ; } }
キュー(QUEUE)
2つの処理の間でデータを受け渡す際に、その間に入って一時的にデータを蓄えるためには、待ち行列(キュー:queue)がよく利用される。 データの覚え方の特徴からFIFO(First In First Out)とも呼ばれる。
配列を用いたQUEUE / リングバッファ
配列にデータを入れる場所(wp)と取り出す場所のポインタ(rp)を使って蓄えれば良いが、配列サイズを超えることができないので、データを取り出したあとの場所を循環して用いるリングバッファは以下のようなコードで示される。
import java.util.*; public class Main { static final int QUEUE_SIZE = 32 ; static int[] queue = new int[ QUEUE_SIZE ] ; static int wp = 0 ; static int rp = 0 ; static void put( int x ) { queue[ wp++ ] = x ; if ( wp >= QUEUE_SIZE ) // wp = wp % QUEUE_SIZE ; or wp = wp & (QUEUE_SIZE - 1) ; wp = 0 ; } static int get() { int ans = queue[ rp++ ] ; if ( rp >= QUEUE_SIZE ) // rp = rp % QUEUE_SIZE ; or rp = rp & (QUEUE_SIZE - 1) ; rp = 0 ; return ans ; } public static void main(String[] args) throws Exception { // Your code here! put( 1 ) ; put( 2 ) ; put( 3 ) ; System.out.println( get() ) ; System.out.println( get() ) ; System.out.println( get() ) ; } }
#define QUEUE_SIZE 32 int queue[ QUEUE_SIZE ] ; int wp = 0 ; // write pointer(書き込み用) int rp = 0 ; // read pointer(読み出し用) void put( int x ) { // 書き込んで後ろ(次)に移動 queue[ wp++ ] = x ; if ( wp >= QUEUE_SIZE ) // 末尾なら先頭に戻る wp = 0 ; } int get() { // 読み出して後ろ(次)に移動 int ans = queue[ rp++ ] ; if ( rp >= QUEUE_SIZE ) // 末尾なら先頭に戻る rp = 0 ; return ans ; } void main() { put( 1 ) ; put( 2 ) ; put( 3 ) ; printf( "%d\n" , get() ) ; // 1 printf( "%d\n" , get() ) ; // 2 printf( "%d\n" , get() ) ; // 3 }


このようなデータ構造も、get() の実行が滞るようであれば、wp が rp に循環して追いついてしまう。このため、上記コードはまだエラー対策としては不十分である。どのようにすべきか?
リスト構造を用いたQUEUE
前述のリングバッファもget()しないまま、配列上限を越えてput()を続けることはできない。
この配列サイズの上限問題を解決したいのであれば、リスト構造を使って解決することもできる。この場合のプログラムは、以下のようになるだろう。
import java.util.*; class ListNode { int data ; ListNode next ; ListNode( int d , ListNode n ) { this.data = d ; this.next = n ; } } ; public class Main { static ListNode top = new ListNode( -1 , null ) ; static ListNode tail = top ; static void put( int x ) { tail.next = new ListNode( x , null ) ; tail = tail.next ; } static int get() { int ans = top.next.data ; top.next = top.next.next ; return ans ; } public static void main(String[] args) throws Exception { put( 1 ) ; put( 2 ) ; put( 3 ) ; System.out.println( get() ) ; System.out.println( get() ) ; System.out.println( get() ) ; } }
Javaで書かれた ListNode を用いた待ち行列のイメージ図は下記のように示される。
struct List* queue = NULL ; struct List** tail = &queue ; void put( int x ) { // リスト末尾に追加 *tail = cons( x , NULL ) ; tail = &( (*tail)->next ) ; } int get() { // リスト先頭から取り出す int ans = queue->data ; struct List* d = queue ; queue = queue->next ; free( d ) ; return ans ; }
ただし、上記のプログラムは、データ格納後にget()で全データを取り出してしまうと、tail ポインタが正しい位置になっていないため、おかしな状態になってしまう。
また、このプログラムでは、rp,wp の2つのポインタで管理することになるが、 2重管理を防ぐために、リストの先頭と末尾を1つのセルで管理する循環リストが使われることが多い。
理解確認
- 配列を用いたスタック・待ち行列は、どのような処理か?図などを用いて説明せよ。
- リスト構造を用いたスタック・待ち行列について、図などを用いて説明せよ。
- スタックや待ち行列を、配列でなくリスト構造を用いることで、どういう利点があるか?欠点があるか説明せよ。
- 配列を用いたリングバッファが用いられている身近な例にはどのようなものがあるか?
- 配列を用いたリングバッファを実装する場合配列サイズには 2n 個を用いることが多いのはなぜだろうか?
リスト処理のレポート課題(前期期末)
プログラムは書いて・動かして・間違って・直す が重要ということで、以下に前期期末試験前までに取り組むレポート課題をしめす。
レポート課題(プログラム例)
Java を用いて、後に示すデータ処理をするためのリスト構造を定義し、与えられたデータを追加していく処理を作成せよ。
課題の説明用に、複素数のリスト構造を定義し、指定した絶対値以下の複素数を抜き出す関数をつくった例を示す。
import java.util.* ; class ComplexListNode { double re ; double im ; ComplexListNode next ; ComplexListNode( double r , double i , ComplexListNode n ) { this.re = r ; this.im = i ; this.next = n ; } } ; public class Main { // 先頭からデータを挿入する方式 static ComplexListNode top = null ; // 全リストを表示する処理 static void print( ComplexListNode p ) { for( ; p != null ; p = p.next ) { System.out.println( "(" + p.re + ")+j(" + p.im + ")" ) ; } } // top に要素を追加する処理(先頭に入れる) static void add( double r , double i ) { top = new ComplexListNode( r , i , top ) ; } // 特定のデータを対象にした処理の例 static ComplexListNode filter_lessthan( ComplexListNode p , double v_abs ) { ComplexListNode ans = null ; for( ; p != null ; p = p.next ) { if ( Math.sqrt( p.re * p.re + p.im * p.im ) <= v_abs ) ans = new ComplexListNode( p.re , p.im , ans ) ; } return ans ; } public static void main(String[] args) throws Exception { add( 1.0 , 2.0 ) ; add( -1.0 , -1.0 ) ; add( 2.0 , -1.0 ) ; add( 1.0 , 0 ) ; print( top ) ; ComplexListNode less_than_2 = filter_lessthan( top , 2 ) ; System.out.println( "less than 2" ) ; print( less_than_2 ) ; } } ((( 実行結果の例 ))) (1.0)+j(0.0) (2.0)+j(-1.0) (-1.0)+j(-1.0) (1.0)+j(2.0) less than 2 (-1.0)+j(-1.0) (1.0)+j(0.0)
レポート内容
上記のプログラムをまねて、以下のレポート課題を作成すること。テーマは ((出席番号-1)%3+1) を選択すること。
- 年号のデータが、年号の名称と年号の始まりの年月日がYYYYMMDD形式で
- “Meiji”,18681023
- “Taisho”,19120730
- “Showa”,19261225
- “Heisei”,19890108
- “Reiwa”,20190501
の様に与えられる。このデータ構造を覚えるリスト構造を作成せよ。また ListNode のデータで、西暦の日付のリストが seireki_list = new ListNode( 19650207, new ListNode( 20030903 , null ) ) ; のように与えられたら、そのデータを和暦で表示するプログラムを作成せよ。 (参考2023年前期期末)
- 市町村名,月,日,最高気温,最低気温のデータが、
- “fukui”,8月,4日,27.6℃,22.3℃
- “fukui”,8月,5日,31.5℃,23.3℃
- “fukui”,8月,7日,34.7℃,25.9℃
- “obama”,8月,6日,34.2℃,23.9℃
の様に与えられる。このデータ構造で覚えるリスト構造を作成せよ。また、この中から真夏日(最高気温が30℃以上)でかつ熱帯夜(最低気温が25℃以上)の日のリストを抽出し表示するプログラムを作成せよ。(参考2022年前期期末)
- ホスト名と、IPアドレス(0~255までの8bitの値✕4個で与えるものとする)のデータ構造で、
- “www.fukui-nct.ac.jp”,104,215,54,205
- “perrine.tsaitoh.net”,192,168,11,2
- “dns.fukui-nct.ac.jp”,10,10,21,51
- “dns.google.com”,8,8,8,8
の様に与えられる。このデータ構造をリスト構造で覚えるプログラムを作成せよ。また、この中からプライベートアドレスのリストを抽出し表示するプログラムを作成せよ。プライベートアドレスは 10.x.x.x, 172.16~31.x.x,192.168.x.x とする。(参考2019年前期期末)
プログラムを作るにあたり、リスト構造には add( 与えられたデータ… ) のように呼び出してリストに追加すること。この時、生成されるリストが、登録の逆順になるか(先頭に挿入)、登録順(末尾に追加)になるかは、自分の理解度に応じて選択すること。抽出する処理を書く場合も登録順序どおりにするかは自分の理解度に応じて選べばよい。
また、理解度に自信がある人は、add() などの処理を「オブジェクト指向」のように記述する方法を検討すること。
あくまで、リスト構造の理解を目的とするため、ArrayList<型> , LinkedList<型> のようなクラスは使わないこと。(ただし考察にて記述性の対比の対象として使うのはOK。複素数の場合の LinkedList を使った例を以下に示す。)
import java.util.* ; import java.util.stream.Collectors ; class Complex { double re ; double im ; Complex( double r , double i ) { this.re = r ; this.im = i ; } public double abs() { return Math.sqrt( re * re + im * im ) ; } @Override public String toString() { return "(" + re + ")+j(" + im + ")" ; } } ; public class Main { public static LinkedList<Complex> top = new LinkedList<Complex>() ; // 絶対値が指定した値以下の要素だけを集める関数 public static LinkedList<Complex> filter_lessthan( LinkedList<Complex> list , double v_abs ) { var ans = new LinkedList() ; for( var c : list ) { if ( Math.sqrt( c.re * c.re + c.im * c.im ) <= v_abs ) ans.add( c ) ; } return ans ; } public static void main( String[] args ) throws Exception { // リストに複素数を追加 top.add( new Complex( 1.0 , 2.0 ) ) ; top.add( new Complex( -1.0 , -1.0 ) ) ; top.add( new Complex( 2.0 , -1.0 ) ) ; top.add( new Complex( 1.0 , 0 ) ) ; for( var c : top ) { System.out.println( c ) ; } System.out.println( "---" ) ; // static な関数でフィルタリング for( var c : filter_lessthan( top , 2.0 ) ) { System.out.println( c ) ; } /* Stream API を使ってフィルタリング for( var c : top.stream() // LinkedList を Stream に変換 .filter( c->c.abs() < 2.0 ) // filter条件をラムダ式で渡す .collect( Collectors.toList() ) // Collectorsで結果を集める ) { System.out.println( c ) ; } */ } }
- LinkedListを使ったプログラム例(Paiza.io)
syslog とは
syslog とは
- OSからトラブルなどの履歴を残すためにLOGファイルに残す機能。
- Linuxのsyslogでは、rsyslog などにより、1つのサーバに履歴をネットワーク経由でまとめることができる。
- トラブルなどの原因などの種別ごとにファシリティが定義されている。
- kernel, user, daemon, mail, ….
- ファシリティ毎に出力ファイルを指定することができる。
- /etc/(r)syslog.conf , /etc/(r)syslog.d
- /var/log/syslog, dmesg, user.log , auth.log, mail.log
- syslog に出力するには
- logger コマンド
- syslog(3) – ライブラリ関数
- printk – カーネル内でメッセージ出力
システムコールとは
- ユーザーモードからカーネルモードを呼び出す
- カーネルモード
- すべてのハードウェアに直接アクセスでき、メモリ管理、プロセス管理、ファイルシステム管理など、システムの基本的な機能を制御する特権を持っています。
- カーネルモードを呼び出す関数はシステムコールと呼ぶ
システム監視ツール
- モニタリング
- Nagios – システムやネットワークの異常を検知し、管理者にアラートを送信
- Zabbix , Munin – 詳細なデータ収集と可視化機能を提供。
- 不正侵入検知システム(IDS)
- ホスト型 IDS
- rkhunter – ルートキット、バックドア、およびローカルエクスプロイトの可能性をスキャンして検出する
- ホスト型 IDS
FIreWall 管理ソフト
- iptables – 低レベルだけど柔軟な設定が可能。設定が複雑。
- firewalld – ゾーンベースの管理や動的な設定変更を可能にした高機能なツール。GUI ツールが充実。
- ufw – 初心者向け。iptablesのラッパーで、より簡単にファイアウォールを管理。
リダイレクトとパイプ
Linux/unixを使う上で、キーボードでコマンドを入力しているが、こういうコマンドの管理を行うプログラムを shell と呼ぶ。shell には、色々なものがある(sh, csh, bash, zsh)が、広く使われている bash( born-again shell )について説明する。最初に、コマンドの入出力を組み合わせるために重要となるリダイレクトとパイプについて説明する。
標準入出力とリダイレクト
出力リダイレクト
C言語のプログラミングで、プログラムの実行結果をレポートに張り付ける時はどのように行っているだろうか?多くの人は、実行画面を PrintScreen でキャプチャした画像を張り付けているかもしれない。しかし、数十行にわたる結果であれば何度もキャプチャが必要となる。
そこで、今日の最初はリダイレクト機能について説明する。
“gcc ファイル.c” は、C言語のプログラムをコンパイルし、a.out という実行ファイルを生成する。”./a.out” にてプログラムを実行する。実行する命令に、“> ファイル名” と書くと、通常の出力画面(標準出力) をファイル名に記録してくれる。これを出力リダイレクトと呼ぶ。また、“>> ファイル名” と書くと、既存ファイルの後ろに追記してくれる。
guest00@nitfcei:~$ cat helloworld.c #include <stdio.h> int main() { printf( "Hello World\n" ) ; return 0 ; } guest00@nitfcei:~$ gcc helloworld.c guest00@nitfcei:~$ ./a.out Hello World guest00@nitfcei:~$ ./a.out > helloworld.txt guest00@nitfcei:~$ cat helloworld.txt Hello World guest00@nitfcei:~$ ./a.out >> helloworld.txt guest00@nitfcei:~$ cat helloworld.txt Hello World Hello World
入力リダイレクト
次に、1行に名前と3教科の点数が書いてある複数行に渡るデータの各人の平均点を求めるプログラムを考える。
guest00@nitfcei:~$ cp /home0/Challenge/2.1-RedirectPipe.d/avg-each-low.c . guest00@nitfcei:~$ cat avg-each-low.c #include <stdio.h> // ((input)) ((output)) // saitoh 43 54 82 saitoh 59.67 // tomoko 89 100 32 tomoko 73.67 // mitsuki 79 68 93 mitsuki 80.00 int main() { char name[ 100 ] ; int point[ 3 ] ; while( scanf( "%s%d%d%d" , name , &point[0] , &point[1] , &point[2] ) == 4 ) { double sum = 0.0 ; for( int i = 0 ; i < 3 ; i++ ) sum += point[i] ; printf( "%s %6.2f\n" , name , sum / 3.0 ) ; } return 0 ; } guest00@nitfcei:~$ gcc avg-each-low.c guest00@nitfcei:~$ ./a.out saitoh 43 54 82 入力 saitoh 59.67 出力 tomoko 89 100 32 入力 tomoko 73.67 出力 ^D ← Ctrl-D を押すとファイル入力を終了
しかし、プログラムの書き方を間違えてプログラムを修正していると、動作確認のたびに何度も同じデータを入力するかもしれないが、面倒ではないだろうか?
プログラムを実行する時に、“< ファイル名” をつけると、通常はキーボードから入力する所を、ファイルからの入力に切り替えて実行することができる。このようなscanf()を使う時のようなプログラムの入力を標準入力といい、それをファイルに切り替えることを入力リダイレクトと呼ぶ。
guest00@nitfcei:~$ cp /home0/Challenge/2.1-RedirectPipe.d/name-point3.txt . guest00@nitfcei:~$ cat name-point3.txt saitoh 43 54 82 tomoko 89 100 32 mitsuki 79 68 93 guest00@nitfcei:~$ ./a.out < name-point3.txt saitoh 59.67 tomoko 73.67 mitsuki 80.00
この入力リダイレクトと出力リダイレクトを合わせて使うこともできる。
guest00@nitfcei:~$ ./a.out < name-point3.txt > name-avg.txt guest00@nitfcei:~$ cat name-avg.txt saitoh 59.67 tomoko 73.67 mitsuki 80.00
パイプ
先の名前と3教科のプログラムの結果から、全員の平均点をも計算したい場合、どのようなプログラムを作るだろうか?C言語だけの知識なら、各人の行のデータを計算するループの中に、全員の合計と人数を求めるプログラムを書いて、最後に平均点を出力するだろう。
一方で、複数人の名前と平均点のデータから平均点を求めるプログラムを書いて、前述のプログラムの実行結果を使う人もいるだろう。
以下の例では、“gcc -o avg-each-row avg-each-row.c” で、avg-each-row という実行ファイル、“gcc -o avg-all avg-all.c” で、avg-all という実行ファイルを生成し、avg-each-row で入力リダイレクト・出力リダイレクトを使って、name-avg.txt を作り、avg-all を入力リダイレクトで、最終結果を表示している。
guest00@nitfcei:~$ cp /home0/Challenge/2.1-RedirectPipe.d/avg-all.c . guest00@nitfcei:~$ cat avg-all.c #include <stdio.h> // ((input)) ((output)) // saitoh 59.67 73.11 // tomoko 73.67 // mitsuki 80.00 int main() { char name[ 100 ] ; double point ; double sum = 0 ; int count = 0 ; while( scanf( "%s%lf" , name , &point ) == 2 ) { sum += point ; count++ ; } printf( "%6.2f\n" , sum / (double)count ) ; return 0 ; } guest00@nitfcei:~$ gcc -o avg-each-low avg-each-low.c guest00@nitfcei:~$ gcc -o avg-all avg-all.c guest00@nitfcei:~$ ./avg-each-low < name-point3.txt > name-avg.txt guest00@nitfcei:~$ ./avg-all < name-avg.txt 71.11
しかし、いちいち入出力の結果を name-avg.txt を作るのは面倒である。であれば、以下の様なイメージで処理をすれば答えが求まる。
name-point3.txt→(avg-each-row)→name-avg.txt→(avg-all)→結果
これは、パイプ機能を使って以下の様に動かすことができる。
guest00@nitfcei:~$ ./avg-each-low < name-point3.txt | ./avg-all 71.11 guest00@nitfcei:~$ cat name-point3.txt | ./avg-each-low | ./avg-all 71.11
プログラムを実行する時に、“A | B” ように書くと、プログラムA の標準出力結果を、プログラムB の標準入力に接続させて、2つのプログラムを実行できる。このような機能を、パイプと呼ぶ。上記例の2つめ “cat… | ./avg-each-low | ./avg-all” では3つのプログラムをパイプでつないでいる。
リダイレクトのまとめ
入力リダイレクト(標準入力) | 実行コマンド < 入力ファイル |
出力リダイレクト(標準出力) | 実行コマンド > 出力ファイル |
出力リダイレクト(標準出力の追記) | 実行コマンド >> 出力ファイル |
標準エラー出力のリダイレクト | 実行コマンド 2> 出力ファイル |
パイプ コマンドAの標準出力をコマンドBの標準入力に接続 |
コマンドA | コマンドB |
標準エラー出力,/dev/null, /dev/zero デバイス
パイプやリダイレクトを使っていると、出力をファイルに保存する場合、途中で異常を伝える目的で出力メッセージを出力するものが見逃すかもしれない。こういった際に、計算結果などを出力する標準出力(stdout)とは別に標準エラー出力(stderr)がある。ファイルを使う際には、デバイスを区別するためのデバイス番号(ファイルディスクリプタ)を使う。この際に、標準入力(stdin) = 0, 標準出力(stdout) = 1, 標準エラー出力(stderr) = 2 という番号を使うことになっている。
#include <stdio.h> int main() { int x , y ; scanf( "%d%d" , &x , &y ) ; if ( y == 0 ) fprintf( stderr , "zero divide\n" ) ; else printf( "%f\n" , (double)x / (double)y ) ; }
Unix のシステムでは、特殊なデバイスとして、/dev/null, /dev/zero というのがある。
/dev/null は、入力リダイレクトに使うと ファイルサイズ 0 byte のデータが得られるし、出力リダイレクトに使うと 書き込んだデータは捨てられる。 /dev/zero は、入力リダイレクトに使うと、’\0′ のデータを延々と取り出すことができ、ファイルをゼロで埋めるなどの用途で使う。
/dev/null の使い方の例としては、例えば標準エラー出力が不要なときは、コマンドラインで以下のようにすれば、標準エラー出力(デバイス番号2番)の内容を捨てることができる。
$ command 2> /dev/null
C言語のコンパイルまとめ
C言語のコンパイル(実行ファイルはa.out) | gcc ソースファイル |
実行ファイル名を指定してコンパイル | gcc -o 実行ファイル ソースファイル |
理解度確認
Windows における PRN デバイス, AUX デバイス
/dev/null といった特殊デバイスの一例として、昔の Windows では PRN デバイス、AUX デバイスというのがあった。
C:< command > PRN昔の文字印刷しかできないプリンタが普通だったころは、PRN という特殊デバイスがあって、このデバイスにリダイレクトをすれば、出力をプリンタに出力することができた。同じように AUX というデバイスは、通信ポートにつながっていて ” command > AUX ” と入力すれば、通信先にデータを送ることができた。最近のプリンタや通信デバイスはもっと複雑になっているためにこういった機能は使えなくなっているが、このデバイス名の名残りで、Windows では PRN とか AUX という名前のファイルを作ることはできない。
UMLの概要
巨大なプロジェクトでプログラムを作成する場合、設計の考え方を図で示すことは、直感的な理解となるため重要であり、このために UML がある。以下にその考え方と記述方法を説明していく。
プログラムの考え方の説明
今まで、プログラムを人に説明する場合には、初心者向けの方式としてフローチャートを使うのが一般的であろう。しかし、フローチャートは四角の枠の中に説明を書ききれないことがあり、使い勝手が悪い。他には、PAD と呼ばれる記述法もある。この方法は、一連の処理を表す縦棒の横に、処理を表す旗を並べるようなイメージで記載する。
しかし、これらの記法は、手順を記載するためのものであり、オブジェクト指向ではデータ構造の理解も重要でありデータ構造を説明するための図が必要となってきた。
個人的な経験では、企業にてプログラムを作っていた頃(1990年頃)、UML などの考え方は普及していなかった。処理を説明するためのフローチャートでも、通信関係のプログラムでは、送信側と受信側の相互関係を説明する場合、フローチャートでは相互のタイミングなどの説明は困難であった。また、通信では、リトライ・タイムアウトといった状態も発生するが、その場合だと状態遷移図なども併記する必要があり、フローチャートの限界を感じていた。
また、データ構造については、オブジェクト指向も普及前であればデータ要素の一覧表が中心であった。プログラム書式(コーディングスタイル)などの統一もされていないので、同じチーム内で誤解などを解消するための意思統一が重要であった。
ドキュメントを残す技術
学生のみなさんは、プログラムの説明の文書はどのように残しているだろうか?
私が仕事をしていた頃は、プログラムと別にドキュメントをワープロで残そうとすると、プログラム変更に合わせて編集することが難しく、プログラムとドキュメントの乖離が発生する。このため、プログラムの中にコメントの形で残すことが重要であった。特にデータ構造の説明は、ヘッダファイルの中に大量のコメントで残すことが多かった。
企業であれば、関数宣言の前には、コーディングスタイルとして決められた書式のコメントで、引数や返り値などを明記することが求められるのが一般的。
文芸的プログラミング TeX
TeX(LaTeX)を改発した Knuth は、文芸的プログラミングとして、プログラム中にドキュメントを併記するための WEB(注記:Internetの意味のWebではない) を同時に開発している。このシステムでは、プログラムとドキュメントを併記したソースプログラムから、ドキュメントを取り出すプログラムと、ソースコードを取り出すプログラムがあり、情報の一体性を高めている。
手っ取り早くドキュメント Markdown
最近では、プログラムのエディタで Markdown という、マークアップ言語でドキュメントを残す場合も多いだろう。これであれば、プレーンテキストで書いたドキュメントを、HTMLやLaTeXといったWeb形式・論文形式といったドキュメントに変換も容易である。
このような方法で、ドキュメントとプログラムの乖離を防ぐことが重要となる。
github では、ドキュメントとして README.md といったファイル名で Markdown によるドキュメントを残すのが一般的。
UML記法が生まれるまで
巨大なプロジェクトでプログラムを作る場合、対象となるシステムを表現する場合、オブジェクト指向分析(Object Oriented Analysis)やオブジェクト指向設計(Object Oriented Design)とよばれるソフトウェア開発方法が重要となる。(総称して OOAD – Object Oriented Analysis and Design)
これらの開発方法をとる場合、(1)自分自身で考えを整理したり、(2)グループで設計を検討したり、(3)ユーザに仕様を説明したりといった作業が行われる。この時に、自分自身あるいはチームメンバーあるいはクライアントに直感的に図を用いて説明する。この時の図の書き方を標準化したものが UML であり、(a)処理の流れを説明するための振る舞い図(フローチャートやPAD)と、(b)データ構造を説明するための構造図を用いる。
UMLは、ランボーによるOMT(Object Modeling Technique どちらかというとOOA(Object Oriented Anarisys)中心)と、 ヤコブソンによるオブジェクト指向ソフトウェア工学(OOSE/Object Oriented Software Engineering)を元に1990年頃に 発生し、ブーチのBooch法(どちらかというとOOD(Object Oriented Design)中心)の考えをまとめ、 UML(Unified Modeling Language)としてでてきた。
UMLでよく使われる図を列記すると、以下の物が挙げられる。
- 構造図
- クラス図
- コンポーネント図
- 配置図
- オブジェクト図
- パッケージ図
- 振る舞い図
- アクティビティ図
- ユースケース図
- ステートチャート図(状態遷移図)
- 相互作用図
- シーケンス図
- コミュニケーション図(コラボレーション図)
その他の関連雑談のためのリンク
- Computer Aided …
- CAD – Design : 製品の設計図を描くための製図ツール、コンピューター上で図面を作成
- CAM – Manufacturing: 製造・加工を行う際 CADで作成した図面を基に、工作機械での加工に必要なNCプログラムなどを作成する
- CAE – Engineering:コンピュータ上で仮想試作・試験といったシミュレーションや解析を行う
- CAT – Testing: 製品の寸法・形状,特性,性能などをコンピュータを利用して自動検査する
- システム開発工程
- CAI(Computer Assisted Instruction) – コンピュータ支援による教育(最近は使われなくなってきた用語)
- CASE(Computer Aided Software Engineering) – ソフトウェア設計を GUI で行う
- UMLエディタ(上流CASEツール)、ソースコード生成(下流CASEツール)
- astar* – ソフトウェア設計ツール(永和システムマネジメント)
- CASE で生成された COBOL プログラムがシステム移行の妨げに
- 中国・インド・フィリピンにソフトウェア開発をアウトソーシングして分かったこと
- Git – 分散型バージョン管理システム
Linux環境の使い方
unixにおけるセキュリティ
- /etc/passwd , /etc/group , /etc/shadow
- デバイスファイルとグループアクセス権
- ls -al /dev/ttyUSB0
- crw-rw—- 1 root dialout 188, 0 6月 13 04:00 ttyUSB0
- sudo adduser user dialout
- デバイスファイルとグループアクセス権
- sudo
- 元々は su コマンドを使うことが多かった
- システム管理者になって作業
- sudo bash で作業は危険
- /etc/sudoers , sudoers.d
- sudo adduser user sudo # sudoグループに user を追加
- sudo vi /etc/sudoers
%sudo ALL=(ALL:ALL) NOPASSWD: ALL- sudoers ファイルを書き間違えると、sudo 自体が使えなくなるので要注意
- suid
- ファイル所有者権限でコマンドを実行
- sudo chown root /usr/local/bin/foobar
- sudo chmod u+s /usr/local/bin/foobar
リモートシステムの使い方
- ssh
- ssh-keygen 秘密鍵・公開鍵を作る
- ssh-copy-id user@remoteserver 公開鍵をリモートサーバにコピー
- パスワードなしでログインできる。
- $HOME/.ssh ディレクトリは rwx,—,— になっていること
- ssh remoteserver ls # リモートサーバで ls を実行
- ssh X11 foward
- ssh 経由で GUI を利用することができる
- Windows であれば WSL2 , macOS であれば XQuartz をインストールしておくこと
- WSL2 Ubuntu 24 LTS が無難
- local$ slogin -X remoteserver # X11forward つきでログイン
- remote$ echo $DISPLAY
- remote$ xeyes # X11経由で local の X11 に
管理コマンド
- sudo apt update
- sudo apt upgrade
- apt search パッケージを探す
- sudo apt install パッケージ名
- sudo apt install nodejs
- npm install JavaScriptパッケージ
- sudo apt install python3
- pip install Pythonパッケージ
VSCode
- Japanese Language Pack for Visual Studio Code
- Remote – SSH
Webプログラミングとセキュリティ
ここまでの授業では、Webを使った情報公開で使われる、HTML , JavaScirpt , PHP , SQL などの解説を行ってきたが、これらを組み合わせたシステムを構築する場合には、セキュリティについても配慮が必要である。
今回は、初心者向けの情報セキュリティの講習で使われるCTFという競技の練習問題をつかって、ここまで説明してきた Web の仕組みを使ったセキュリティの問題について解説を行う。
派生や集約と多重継承
派生や継承について、一通りの説明が終わったので、データ構造(クラスの構造)の定義の方法にも様々な考え方があり、どのように実装すべきかの問題点を考えるための説明を行う。その中で特殊な継承の問題についても解説する。
動物・鳥類・哺乳類クラス
派生や継承を使うと、親子関係のあるデータ構造をうまく表現できることを、ここまでの授業で示してきた。
しかしながら、以下に述べるような例では、問題が発生する。
// 動物クラス 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++ には、複数のクラスから、派生する多重継承という機能がある。であれば、鳥類と哺乳類から進化したのだから、以下のように書きたい。
// 多重継承 鳥(Bird)と哺乳類(Mammal) から SeaBeam を作る class SeaBream : public Bird , public 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 , public Wind { public: Bird( const char s[] ) : Animal( s ) {} virtual void move() { printf( "%s %s.\n" , get_name() , move_method() ) ; } } ; class Mammal : public Animal , public Leg { public: Mammal( const char s[] ) : Animal( s ) {} virtual void move() { printf( "%s %s.\n" , get_name() , move_method() ) ; } } ;
継承を使うべきか、部品として持つべきか
ただし、ここで述べた方式は、UML による設計の際に改めて説明を行うが、is-a , has-a の関係でいうなら、
- Bird is a Animal. – 鳥は動物である。
- “Bird has a Animal” はおかしい。
- 鳥は、動物から派生させるのが正しい。
- Bird has a Wing. – 鳥は羽をもつ。
- “Bird is a Wing” はおかしい。
- 鳥は、羽を部品として持つべき。
であることから、Wing は 継承で実装するのではなく、集約もしくはコンポジションのような部品として実装すべきである。
このカモノハシ問題をどうしても多重継承で実装したいのなら、C++では、以下のような方法で、ダイヤモンド型の継承問題を解決できる。
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 という機能が使えるようになっている。
多重継承を使える CLOS や Python では、適用するメソッドやインスタンス変数の曖昧さについては親クラスへの優先度を明確にできる機能がある。曖昧さの問題を避けるのであればクラス限定子”::”を使うべきである。