リダイレクトとパイプ
Linuxを使う上で、キーボードでコマンドを入力しているが、こういうコマンドの管理を行うプログラムを 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 |
C言語のコンパイルまとめ
C言語のコンパイル(実行ファイルはa.out) | gcc ソースファイル |
実行ファイル名を指定してコンパイル | gcc -o 実行ファイル ソースファイル |
UMLの概要
巨大なプロジェクトでプログラムを作成する場合、設計の考え方を図で示すことは、直感的な理解となるため重要であり、このために UML がある。以下にその考え方と記述方法を説明していく。
プログラムの考え方の説明
今まで、プログラムを人に説明する場合には、初心者向けの方式としてフローチャートを使うのが一般的であろう。しかし、フローチャートは四角の枠の中に説明を書ききれないことがあり、使い勝手が悪い。他には、PAD と呼ばれる記述法もある。この方法は、一連の処理を表す縦棒の横に、処理を表す旗を並べるようなイメージで記載する。
しかし、これらの記法は、手順を記載するためのものであり、オブジェクト指向のようなデータ構造を説明するための図が必要となってきた。
個人的な経験では、企業にてプログラムを作っていた頃(1990年頃)、UML などの考え方は普及していなかった。処理を説明するためのフローチャートでも、通信関係のプログラムでは、送信側と受信側の相互関係を説明する場合、フローチャートでは相互のタイミングなどの説明は困難であった。また、通信では、リトライ・タイムアウトといった状態も発生するが、その場合だと状態遷移図なども併記する必要があり、フローチャートの限界を感じていた。
また、データ構造については、オブジェクト指向も普及前であればデータ要素の一覧表が中心であった。プログラム書式(コーディングスタイル)などの統一もされていないので、同じチーム内で誤解などを解消するための意思統一が重要であった。
プログラムのドキュメント
学生のみなさんは、プログラムの説明の文書はどのように残しているだろうか?
私が仕事をしていた頃は、プログラムと別にドキュメントをワープロで残そうとすると、プログラム変更に合わせて編集することが難しく、プログラムとドキュメントの乖離が発生する。このため、プログラムの中にコメントの形で残すことが重要であった。特にデータ構造の説明は、ヘッダファイルの中に大量のコメントで残すことが多かった。
TeXを改発した Knuth は、文芸的プログラミングとして、プログラム中にドキュメントを併記するための WEB を同時に開発している。このシステムでは、プログラムとドキュメントを併記したソースプログラムから、ドキュメントを取り出すプログラムと、ソースコードを取り出すプログラムがあり、情報の一体性を高めている。
最近では、プログラムのエディタで Markdown という、マークアップ言語でドキュメントを残す場合も多いだろう。これであれば、プレーンテキストで書いたドキュメントを、HTMLやLaTeXといった読みやすいドキュメントに変換も容易である。
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中心)と、 ヤコブソンによるオブジェクト指向ソフトウェア工学(OOSE)を元に1990年頃に 発生し、ブーチのBooch法(どちらかというとOOD中心)の考えをまとめ、 UML(Unified Modeling Language)としてでてきた。
UMLでよく使われる図を列記すると、以下の物が挙げられる。
- 構造図
- クラス図
- コンポーネント図
- 配置図
- オブジェクト図
- パッケージ図
- 振る舞い図
- アクティビティ図
- ユースケース図
- ステートチャート図(状態遷移図)
- 相互作用図
- シーケンス図
- コミュニケーション図(コラボレーション図)
その他の関連雑談のためのリンク
- CASE(Computer Aided Software Engineering) – ソフトウェア設計の GUI
- astar* – ソフトウェア設計ツール(永和システムマネジメント)
- 中国・インド・フィリピンにソフトウェア開発をアウトソーシングして分かったこと
- Git – 分散型バージョン管理システム
派生や集約と多重継承
派生や継承について、一通りの説明が終わったので、データ構造(クラスの構造)の定義の方法にも様々な考え方があり、どのように実装すべきかの問題点を考えるための説明を行う。その中で特殊な継承の問題についても解説する。
動物・鳥類・哺乳類クラス
派生や継承を使うと、親子関係のあるデータ構造をうまく表現できることを、ここまでの授業で示してきた。
しかしながら、以下に述べるような例では、問題が発生する。
// 動物クラス 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 では、適用するメソッドやインスタンス変数の曖昧さについては親クラスへの優先度を明確にできる機能がある。曖昧さの問題を避けるのであればクラス限定子”::”を使うべきである。
fgetsではみ出たら
C言語で長い一行を読み込むのであれば、通常は”それなりに”大きい配列に読み込んでから、strdup() などでデータに応じた大きさで保存する。しかし、それ以上に長い1行を扱いたいのならどうするか…
どうしても長い一行を扱いたいのなら、realloc() などで拡張しながらデータを読み込む。
#include <stdio.h> #include <stdlib.h> #include <string.h> int main() { char buff[ 10 ] ; char*str ; if ( fgets( buff , sizeof( buff ) , stdin ) != NULL ) { // '\0'を覚える必要があるので最大sizeof(buff)-1文字まで読み込まれる int len = strlen( buff ) ; if ( (str = (char*)malloc( len + 1 )) != NULL ) { strcpy( str , buff ) ; // printf( "|%s|\n" , str ) ; // 通常はここまで書けばひとまず十分。 // fgetsは行末文字'\n'まで読み込むのが基本 // 最終文字が'\n'でなかったら、読み残しがある。 while( buff[ len ] != '\n' ) { char*rp ; // '読み残し'を読み込む if ( fgets( buff , sizeof( buff ) , stdin ) == NULL ) break ; len = strlen( buff ) ; // str を realloc() で領域を拡張する // realloc()は、拡張するときは新しくメモリを確保し、 // 格納されているデータをコピーし、元領域を解放してくれる if ( (rp = (char*)realloc( str , strlen( str ) + len + 1 )) == NULL ) break ; else str = rp ; // reallocでは、広げられた領域に元データがコピーされているので、 // 後ろに'読み残し'分を追加する。 strcpy( str + strlen( str ) , buff ) ; // printf( "%s\n" , str ) ; } // 読み込んだ一行を表示 printf( "|%s|\n" , str ) ; free( str ) ; } } }
2022年度授業アンケート
2022年度の授業アンケートの結果。今年は、私が作ったアンケートシステムが、moodle サーバの移行トラブルと共に使えなくなってしまったので、Forms により集計が行われた。
情報ネットワーク基礎(3EI/後期/必修)
総合ポイント 83.8 でまあまあの評価。課題の評価が高くなっているが、課題を最終成績の中にはあまり組み込んでいないが、Forms により小テスト形式で実施したことが評価されたと思われる。
情報制御基礎(3年学際(EIのみの回答)/前期/選択)
総合ポイント83.0 でまあまあの評価。指導書の評価が80.4 と低い評価となっている。Web資料で講義資料を提示しているが、口頭での説明で資料との連携が弱かったのが原因かもしれない。
情報メディア工学(前期)(4EI前期/必修)
総合ポイント 85.4 で評価は高いと思われる。講義内容の評価が82.9と若干低いが、それ以外は、86.7ポイントなどで評価が高かった。
情報構造論(4EI通年/必修)
総合ポイント89.0で、他の科目とくらべ良い評価が得られている。プログラムなどを苦手とする学生からは、敬遠されがちな中、高く評価されていると思う。
データベース(5EI後期/選択)
総合ポイント 86.3 で高い評価が得られた。シラバスと課題について他よりも高い評価であった。前年までは、自分のサーバで運用していた SQL サーバの実験環境を使っていたが、今年度は Paiza.io を用いたことで、自宅でも課題に取り組みやすかったことが評価されたと思われる。
実数の取り扱いと誤差
実数型(float / double)
実数型は、単精度実数(float型)と、倍精度実数(double型)があり、それぞれ32bit,64bitでデータを扱う。
指数表現は、大きい値や小さい値を表現する場合に使われ、物理などで1.2345×10-4といった、仮数×基数指数で表現する方法。数学や物理では基数に10を用いるが、コンピュータの世界では基数を2とすることが多い。
単精度型(float)では、符号1bit,指数部8bit,仮数部23bitで値を覚え、数値としては、以下の値を意味する。
符号✕ 1.仮数部 ✕ 2(指数数部-127)
符号部は、正の値なら0, 負の値なら1 を用いる。
仮数部が23bitなので、有効桁(正しい桁の幅)は約7桁となる。
例えば、float型で扱える最大数は、以下のようになる。
0,1111,1111,111,1111,1111,1111,1111,1111 = 1.1111…×2128 ≒ 2129 ≒ 1038
float 型は、計算精度が低いので 通常の数値計算のプログラミングではあまり使われることはない。一方で、ゲームなどの3次元座標計算などでは、精度は必要もないことから、GPU(グラフィックス専用のプロセッサ)では float 型を使うことも多い。また、最近の機械学習のプログラミングでは、神経の動きをまねた計算(ニューラルネットワークプログラミング)が行われるが、これも精度はあまり高くなくてもいいので float 型を使うことも多く、グラフィックス用の GPU で float 型で機械学習の計算を行うことも多い。
倍精度型(double)では、符号1bit,指数部11bit,仮数部52bitで値を覚え、数値としては、以下の意味を持つ。
符号✕ 1.仮数部 ✕ 2(指数部-1023)
これらの実数で計算を行うときには、0.00000001011×210といった値の時に、仮数部に0が並んだ状態を覚えると、計算の精度が低くなるので、1.01100000000×22のように指数部の値を調整して小数点の位置を補正しながら行われる。
double型の場合、52bit=10進数16桁相当の有効桁、最大数で、1.1111…×21024≒10308
倍精度型を使えば、正しく計算できるようになるかもしれないが、実数型はただの加算でも仮数部の小数点の位置を合わせたりする処理が必要で、浮動小数点専用の計算機能を持っていないような、ワンチップコンピュータでは整数型にくらべると10倍以上遅い場合もある。
実数の注意点
C言語でプログラムを作成していて、簡単な数値計算のプログラムでも動かないと悩んだことはないだろうか?解らなくて友達のプログラムを真似したら動いたけど、なぜ自分のプログラムは動かなかったのか深く考えたことはあるだろうか?
単純な合計と平均
整数を入力し、最後に合計と平均を出力するプログラムを以下に示す。
しかし、C言語でこのプログラムを動かすと、10,10,20,-1 と入力すると、合計(sum)40,件数(cnt)3で、平均は13と表示され、13.33333 とはならない。
小数点以下も正しく表示するには、どうすればいいだろうか?
ただし、変数の型宣言を “double data,sum,cnt ;” に変更しないものとする。
// 入力値の合計と平均を求める。 #include <stdio.h> int main() { int data ; int sum = 0 ; int cnt = 0 ; for(;;) { printf( "数字を入力せよ。-1で終了¥n" ) ; scanf( "%d" , &data ) ; if ( data < 0 ) break ; cnt = cnt + 1 ; sum = sum + data ; } printf( "合計 %d¥n" , sum ) ; printf( "平均 %d¥n" , sum / cnt ) ; }
C言語では、int型のsum / int型のcnt の計算は、int 型で計算を行う(小数点以下は切り捨てられる)。このため、割り算だけ実数で行いたい場合は、以下のように書かないといけない。
printf( "平均 %lf¥n" , (double)sum / (double)cnt ) ;
// (double)式 は、sum を一時的に実数型にするための型キャスト
まずは動く例
以下のプログラムは、見れば判るけど、th を 0度〜360度まで5度刻みで変化させながら、y = sin(th) の値を表示するプログラム。
// sin の値を出力 #include <stdio.h> #include <math.h> int main() { double th , y ; for( th = 0.0 ; th <= 360.0 ; th += 5.0 ) { y = sin( th / 180.0 * 3.1415926535 ) ; printf( "%lf %lf¥n" , th , y ) ; } return 0 ; }
動かないプログラム
では、以下のプログラムはどうだろうか?
// case-1 ---- プログラムが止まらない #define PI 3.1415926535 int main() { double th , y ; // 0〜πまで100分割でsinを求める for( th = 0.0 ; th != PI ; th += PI / 100.0 ) { y = sin( th ) ; printf( "%lf %lf¥n" , th , y ) ; } return 0 ; }
// case-2 ---- y の値が全てゼロ int main() { int th ; double y ; for( th = 0 ; th <= 360 ; th += 5 ) { y = sin( th / 180 * 3.1415926535 ) ; printf( "%d %lf¥n" , th , y ) ; } return 0 ; }
どちらも、何気なく読んでいると、動かない理由が判らないと思う。そして、元のプログラムと見比べながら、case-1 では、「!=」を「<=」に書き換えたり、case-2 では、「int th ;」を「double th ;」に書き換えたら動き出す。
では何が悪かったのか…
回答編
数値と誤差
コンピュータで計算すると、計算結果はすべて正しいと勘違いをしている人も多い。ここで、改めて誤差について考える。特に、計器で測定した値であれば、測定値自体に誤差が含まれている。
こういった誤差が含まれる数字を扱う場合注意が必要である。例えば実験値を手書きで記録する場合、12.3 と 12.300 では意味が異なる。測定値であやふやな桁を丸めたのであれば、前者は 12.2500〜12.3499… の間の値であり有効数字3桁である。後者は、12.2995〜12.300499… の間の値であり、有効数字5桁である。このため、誤差が含まれる数字の加算・減算・乗算・除算では注意が必要である。
加減乗除算の場合
加減算であれば小数点の位置を揃え、誤差が含まれる桁は有効桁に含めてはいけない。
上記の計算では、0.4567の0.0567の部分は意味がないデータとなる。(情報落ち)
乗除算であれば、有効桁の少ない値と有効桁の多い値の計算では、有効桁の少ない方の誤差の影響が計算結果に出てくるため、通常は、有効桁5桁と2桁の計算であれば、乗除算結果は少ない2桁で書くべきである。
桁落ち
有効桁が大きい結果でも、減算が含まれる場合は注意が必要である。
例えば、以下のような計算では、有効桁7桁どうしでも、計算結果の有効桁は3桁となる。
このような現象は、桁落ちと呼ばれる。
演習問題(4回目)
こちらのフォルダに示す、Excel の表で、有効桁を考えてもらうための演習問題(ランダムに値が作られます)を有効数字を考えながら計算し、答えをレポートにまとめてください。例を以下に示す。
レポートは、こちらのひな型をベースに作成し(手書きノートをキャプチャした資料でもOKです)、同じフォルダに提出してください。
抽象クラス(純粋仮想基底クラス)
前回説明した仮想関数では、基底クラスから派生させたクラスを作り、そのデータが混在してもクラスに応じた関数(仮想関数)を呼び出すことができる。
この仮想関数の機能を逆手にとったプログラムの記述方法として、抽象クラス(純粋仮想基底クラス)がある。その使い方を説明する。
JavaのGUIにおける派生の使い方
先週の講義では、派生を使ったプログラミングは、GUI で使われていることを紹介したが、例として Java のプログラミングスタイルを少し紹介する。
例えば、Java で アプレット(ブラウザの中で動かすJavaプログラム)を作る場合の、最小のサンプルプログラムは、以下のようになる。
import java.applet.Applet; // C言語でいうところの、Applet 関連の処理を include import java.awt.Graphics; public class App1 extends Applet { // Applet クラスから、App1 クラスを派生 public void paint(Graphics g) { // 画面にApp1を表示する必要がある時に呼び出される。 g.drawString("Hello World." , 100 , 100); } }
この例では、ブラウザのGUIを管理する Applet クラスから、App1 というクラスを派生(extendsキーワード)し、App1 固有の処理として、paint() メソッドを定義している。GUI のプログラミングでは、本来ならマウスが押された場合の処理などを記述する必要があるが、このプログラムでは paint() 以外何も書かれていない。これはマウス処理などは、基底クラスのAppletのマウス処理が継承されるので、省略してもうまく動くようになっている。
このように、派生クラスの継承機能を使うことで、雑多な処理を基底クラスですべて書くようにすれば、同じようなデータ型が出てくる場合プログラムを書く手間を減らすことができる。
抽象クラス(純粋仮想基底クラス)
抽象クラス(純粋仮想基底クラス)とは、見かけ上はデータを何も持たないクラスであり、本来なら意味がないデータ構造となってしまう。しかし、派生クラスで要素となるデータと仮想関数で機能を与えることで、基底クラスという共通部分から便利な活用ができる。(実際には、型を区別するための型情報を持っている)
例えば、C言語であれば一つの配列に、整数、文字列、実数といった異なる型のデータを記憶させることは本来ならできない。しかし、以下のような処理を記載すれば、可能となる。
C言語では、1つの記憶域を共有するために共用体(union)を使うが、C++では仮想関数が使えるようになり、型の管理をプログラマーが行う必要のある「面倒で危険な」共用体を使う必要はなくなった。
// 純粋仮想基底クラス class Object { public: virtual void print() const = 0 ; // 中身の無い純粋基底クラスで、 // 仮想関数を記述しない時の書き方。 } ; // 整数データの派生クラス class IntObject : public Object { private: int data ; public: IntObject( int x ) { data = x ; } virtual void print() const { printf( "%d\n" , data ) ; } } ; // 文字列の派生クラス class StringObject : public Object { private: char data[ 100 ] ; public: StringObject( const char* s ) { strcpy( data , s ) ; } virtual void print() const { printf( "%s\n" , data ) ; } } ; // 実数の派生クラス class DoubleObject : public Object { private: double data ; public: DoubleObject( double x ) { data = x ; } virtual void print() const { printf( "%lf\n" , data ) ; } } ; // 動作確認 int main() { Object* data[3] = { new IntObject( 123 ) , new StringObject( "abc" ) , new DoubleObject( 1.23 ) , } ; for( int i = 0 ; i < 3 ; i++ ) { // 123 data[i]->print() ; // abc } // 1.23 と表示 return 0 ; } ;
このプログラムでは、純粋仮想基底クラスObjectから、整数IntObject, 文字列StringObject, 実数DoubleObject を派生させ、そのデータを new により生成し、Objectの配列に保存している。
仮想関数を使うと、Object型の中に自動的に型情報が保存されるようになる。一般的な実装では、各派生クラス用の仮想関数のポインタテーブル(vtable)へのポインタが使われる。
Javaなどのオブジェクト指向言語では、全てのクラス階層のスーパークラスとして、Object を持つように作られている。
様々な型に適用できるプログラム
次に、抽象クラス(純粋仮想基底クラス)の特徴を応用したプログラムの作り方を説明する。
例えば、以下のような最大選択法で配列を並び替えるプログラムがあったとする。
int a[5] = { 11, 55, 22, 44, 33 } ; void my_sort( int array[] , int size ) { for( int i = 0 ; i < size - 1 ; i++ ) { int max = i ; for( int j = i + 1 ; j < size ; j++ ) { if ( array[j] > array[max] ) max = j ; } int tmp = array[i] ; array[i] = array[max] ; array[max] = tmp ; } } int main() { my_sort( a , 5 ) ; }
しかし、この整数を並び替えるプログラムがあっても、文字列の並び替えや、実数の並び替えがしたい場合には、改めて文字列用並び替えの関数を作らなければいけない。しかも、ほとんどが同じような処理で、改めて指定された型のためのプログラムを作るのは面倒である。
C言語のデータの並び替えを行う、qsort() では、関数ポインタを用いることで、様々なデータの並び替えができる。しかし、1件あたりのデータサイズや、データ実体へのポインタを正しく理解する必要がある。
#include <stdio.h> #include <stdlib.h> int a[ 4 ] = { 11, 33, 22, 44 } ; double b[ 3 ] = { 1.23 , 5.55 , 0.11 } ; // 並び替えを行いたいデータ専用の比較関数を作る。 // a>bなら+1, a=bなら0, a<bなら-1を返す関数 int cmp_int( int* pa , int* pb ) { // int型用コールバック関数 return *pa - *pb ; } int cmp_double( double* pa , double* pb ) { // double型用コールバック関数 if ( *pa == *pb ) return 0 ; else if ( *pa > *pb ) return 1 ; else return -1 ; } int main() { // C言語の怖さ qsort( a , 4 , sizeof( int ) , // このあたりの引数を書き間違えると (int(*)(void*,void*)) cmp_int ) ; // とんでもない目にあう。 qsort( b , 3 , sizeof( double ) , (int(*)(void*,void*)) cmp_double ) ; }このように、自分が作っておいた関数のポインタを、関数に渡して呼び出してもらう方法は、コールバックと呼ぶ。
JavaScript などの言語では、こういったコールバックを使ったコーディングがよく使われる。// コールバック関数 f を呼び出す関数 function exec_callback( var f ) { f() ; } // コールバックされる関数 foo() function foo() { console.log( "foo()" ) ; } // foo() を実行する。 exec_callback( foo ) ; // 無名関数を実行する。 exec_callback( function() { console.log( "anonymous" ) ; } ) ;
任意のデータを並び替え
class Object { public: virtual void print() const = 0 ; virtual int cmp( Object* ) = 0 ; } ; // 整数データの派生クラス class IntObject : public Object { private: int data ; public: IntObject( int x ) { data = x ; } virtual void print() const { printf( "%d\n" , data ) ; } virtual int cmp( Object* p ) { int pdata = ((IntObject*)p)->data ; // 本当はこのキャストが危険 return data - pdata ; // ↓安全な実装したいなら↓ } // IntObject* pi = dynamic_cast<IntObject*>(p) ; } ; // return pi != NULL ? data - pi->data : 0 ; // 文字列の派生クラス class StringObject : public Object { private: char data[ 100 ] ; public: StringObject( const char* s ) { strcpy( data , s ) ; } virtual void print() const { printf( "%s\n" , data ) ; } virtual int cmp( Object* p ) { char* pdata = ((StringObject*)p)->data ; return strcmp( data , pdata ) ; // 文字列比較関数 } } ; // 実数の派生クラス class DoubleObject : public Object { private: double data ; public: DoubleObject( double x ) { data = x ; } virtual void print() const { printf( "%lf\n" , data ) ; } virtual int cmp( Object* p ) { double pdata = ((DoubleObject*)p)->data ; if ( data == pdata ) return 0 ; else if ( data > pdata ) return 1 ; else return -1 ; } } ; // Objectからの派生クラスでcmp()メソッドを // 持ってさえいれば、どんな型でもソートができる。 void my_sort( Object* array[] , int size ) { for( int i = 0 ; i < size - 1 ; i++ ) { int max = i ; for( int j = i + 1 ; j < size ; j++ ) { if ( array[j]->cmp( array[max] ) > 0 ) max = j ; } Object* tmp = array[i] ; array[i] = array[max] ; array[max] = tmp ; } } // 動作確認 int main() { Object* idata[3] = { new IntObject( 11 ) , new IntObject( 33 ) , new IntObject( 22 ) , } ; Object* sdata[3] = { new StringObject( "abc" ) , new StringObject( "defghi" ) , new StringObject( "c" ) , } ; my_sort( idata , 3 ) ; // 整数のソート for( int i = 0 ; i < 3 ; i++ ) idata[i]->print() ; my_sort( sdata , 3 ) ; // 文字列のソート for( int i = 0 ; i < 3 ; i++ ) sdata[i]->print() ; return 0 ; } ;
このような方式でプログラムを作っておけば、新しいデータ構造がでてきてもソートのプログラムを作らなくても、比較専用の関数 cmp() を書くだけで良い。
ただし、この並び替えの例では、Object* を IntObject* に強制的に型変換している。
また、このプログラムでは、データを保管するために new でポインタを保管し、データの比較をするために仮想関数の呼び出しを行うことから、メモリの使用効率も処理効率でもあまりよくない。
こういう場合、最近の C++ ではテンプレート機能が使われる。
template <typename T> void my_sort( T a[] , int size ) { for( int i = 0 ; i < size - 1 ; i++ ) { int max = i ; for( int j = i + 1 ; j < size ; j++ ) { if ( a[j] > a[max] ) max = j ; } T tmp = a[i] ; a[i] = a[max] ; a[max] = tmp ; } } int main() { int idata[ 5 ] = { 3, 4, 5 , 1 , 2 } ; double fdata[ 4 ] = { 1.23 , 0.1 , 3.4 , 5.6 } ; // typename T = int で int::mysort() が作られる my_sort<int>( idata , 5 ) ; for( int i = 0 ; i < 5 ; i++ ) printf( "%d " , idata[i] ) ; printf( "\n" ) ; // typename T = double で double::mysort() が作られる my_sort<double>( fdata , 4 ) ; for( int i = 0 ; i < 4 ; i++ ) printf( "%lf " , fdata[i] ) ; printf( "\n" ) ; return 0 ; }C++のテンプレート機能は、my_sort( int[] , int ) で呼び出されると、typename T = int で、整数型用の my_sort() の処理が自動的に作られる。同じく、my_sort( double[] , int ) で呼び出されると、typename = double で 実数型用の my_sort() が作られる。
テンプレート機能では、各型用のコードが自動的に複数生成されるという意味では、出来上がったコードがコンパクトという訳ではない。
仮想関数レポート課題
ここで示したプログラムを参考に、独自のデータ(例えば、複素数のデータや名前と誕生日といったデータ)について、my_sort() などで並び替えるプログラムを作成せよ。並び替える時の順序も、各自て定義すればいい。(複素数なら絶対値順とか、名前と誕生日なら、誕生日順とか)
ネットワークとセキュリティ
前回の授業では、バックエンドのプログラムの問題によるセキュリティ問題を説明した。今回は、ネットワークの物理的な接続方法などの話を中心にセキュリティについて説明を行う。
ネットワークからの攻撃とFireWall
脆弱性とバッファオーバーフロー
プログラムには、何らかのバグが潜んでいる可能性があり、悪用すると悪意のプログラムの実行や、情報の漏えい、システム異常を発生させサービスができなくするなどの脆弱性があって、悪意のある利用者から攻撃をうける可能性がある。
例えば、下記のようなC言語のプログラムは、配列をはみ出るようなデータを与えることで、関数の戻り番地を破壊させ、はみ出た部分に送り込んだ悪意のプログラムを実行させることが可能となる。このような入力用のデータ領域(バッファ)をはみ出させる攻撃はバッファオーバーフローと呼ばれる。
なお、最近のC言語のライブラリでは、上記のようなバッファオーバーフロー攻撃が一般的であることから、ASLR(Address Space Layout Randomization)により、スタック領域などの位置をランダム化することで、バッファオーバーフローによる攻撃が失敗するようになっている。
ルータとFireWall
外部にサービスを提供するようなシステムで、何らかの脆弱性のあるプログラムが使われていると、外部からのネットワーク接続で悪意のあるプログラム(マルウェア)を実行させられてしまうかもしれない。
このため、コンピュータでは不必要なプログラム(ネットワークサービス)は、起動されないようにする必要がある。もしくは、そのサービスは外部から利用できないように、途中のルータで FireWall(防火壁) を設置する。
FireWall では、(1)攻撃の可能性のあるIPアドレスからの接続を拒否、(2)外部に公開していないネットワークサービスのポート番号の接続を拒否といった方法をとる(拒否リスト方式)。もっと厳しい対策であれば、(3)特定のIPアドレスの機器からのみ接続を許可、(4)許可されているネットワークサービスのポート番号だけからだけ許可する方式をとる(許可リスト方式)
外部に公開する必要のないサービスがFireWallなどで正しく保護されていないと、攻撃をうける可能性がある。
ネットワーク接続のための装置
ルータやFireWallなどの仕組みをもう少し理解するために、組織内でネットワークを接続するための機器とその機能について改めて確認する。
ルータとは
元々の有線LANでは、1本のケーブルを時分割多重で共有しながら通信を行う。このため、瞬間的にはとある機器がネットワークを使用している時は、他の機器はデータ通信ができなくなる。この1本の線を大量の機器で使おうとすると、機器が使えるタイミングは減ってしまう。そこで、1本の線に直接接続する機器を分割したサブネットに分けて、必要な時だけ隣接するサブネットにパケットを中継するルータ or ブリッジが重要となる。
ルータは、隣接するサブネットのネットワーク番号(IPアドレスとサブネットマスク)を確認し、パケットを流す先を決定する。このネットワーク番号(IPアドレスとサブネットマスクの論理積)と中継を依頼するゲートウェイ(転送先)の一覧をルーティングテーブルと呼ぶ。
組織内のルータであれば、ネットワークの構造に合わせてあらかじめルーティングテーブルを定義しておく(静的ルーティング)。組織と組織を接続するようなルータは、自分に送ってほしいネットワーク番号の情報を相互に交換している(動的ルーティング)。
ブリッジとHUB
スイッチングHUB
機器を接続するための古いHUB(ダムHUB)では、通信中は他の機器の通信ができず効率が悪い。最近のHUBでは、通信する相手に応じて、内部のネットワークケーブルをスイッチのように接続・分離することができるスイッチングHUBを用いる。通信相手の識別には、一般的にMACアドレスが用いられる。(レイヤ2でのスイッチングHUB)
家庭用のスイッチングHUBは、特に細かい設定などは不要で管理機能が無いものは、アン マネージド スイッチングHUBと呼ばれる。
L2スイッチとL3スイッチ
サブネットに分割し、それぞれに異なるネットワーク番号を割り振り、中継するルータで FireWall を機能させることで、セキュリティを高めることが行われる。しかし、性能の高いスイッチングHUBは高価でもあり、1つのHUBに異なるネットワークを接続する必要がでてくることもある。この場合、IPアドレスを異なるネットワークの番号を偽装されると、データが盗み見られるかもしれない。
こういった相互に分離すべきネットワークであっても、柔軟なネットワーク構成とするためには、VLAN機能を持った L2スイッチ(レイヤ2スイッチングHUB) が使われる。タグVLAN機能付きのL2スイッチでは、特定のポートにVLANのタグ番号を割り当て、ポートに入る時にパケットに VLAN のタグ情報を付加し、そのパケットは同じ VLAN のタグをもつポートからしかデータを取り出せない。
L2スイッチ(レイヤ2スイッチ)は、機器のMACアドレスやパケットに付けられたVLANのタグ番号の情報(レイヤ2=データリンク層)でパケットの流れを制御している(下記OSI参照モデルの表を参照)。最近では、許可されていない機器がネットワークに侵入する不正侵入を防ぐために、登録されていないMACアドレスのパケットを通さないといった機能がある。
OSI参照モデルとレイヤ 第7層 アプリケーション層 アプリケーションの種類の規定 第6層 プレゼンテーション層 データフォーマットの交換 第5層 セッション層 コネクションの確立や切断などの管理 第4層 トランスポート層 パケットの分割合成や再送といった管理(TCP) 第3層 ネットワーク層 隣接するネットワーク間の通信(IPアドレス) 第2層 データリンク層 直接接続された機器間の通信(MACアドレス) 第1層 物理層 物理的な接続方法(コネクタや電圧など)
スイッチングHUBの中には、レイヤ3(IPアドレス)の情報でパケットの流れを制御するものもある。こういったスイッチは、L3スイッチ(レイヤ3スイッチ)と呼ばれるが、機能的にはルータと同じである。
一般的には、LANとWANを接続するための機器はルータ、LAN内部のネットワークを分離するためのものはL3スイッチと呼ぶ。
インターネットと接続するルータの機能
ネットワーク通信のIPアドレスとポート番号
クライアントの機器と通信相手との通信では、通信相手のIPアドレスとポート番号を指定してパケットを送出するが、処理結果を送信元に送り返すために、送信元のIPアドレスとポート番号が付加されている。送信元ではポート番号は、通信でよく使われる0~1023までのポート番号(ウェルノウンポート)以外で、1024~65535のポート番号(エフェメラルポート)の中から使われていないものをランダムに選んで使う。
送信相手に届いたパケットの返信データには、送信元と送信相手のIPアドレスとポート番号を入れ替えたものを割り当てることで、送信元にパケットが戻ってくる。
- DIP = 送信先IPアドレス、DP = 送信先ポート番号
- SIP = 送信元IPアドレス、SP = 送信元ポート番号
NAT(Network Address Translation)
現在広く使われているIPv4アドレス(32bit)では、40億台の機器間の通信しかできない。このため、組織内だけで使われるIPアドレス(プライベートIPアドレス)を使い、インターネットではグローバルIPアドレスを使う。
プライベートIPアドレス クラスA/8 10.0.0.0~10.255.255.255 大規模組織向け クラスB/12 172.16.0.0~172.31.255.255 中規模組織向け クラスC/16 192.168.0.0~192.168.255.255 家庭用ルータ向け
組織内のLANからインターネット(WAN)に接続する際には、プライベートアドレスをグローバルアドレスに変換するNAT(Network Address Translation)の機能が使われる。
NATの問題点
しかし、インターネットの内側で異なる機器で同じポート番号が割り振られると、戻ってきたパケットをどちらの機器に送ればいいのか区別できなくなる。
NAPT(Netowrk Address and Port Translation)
そこで、最近のNATでは、IPアドレスの変換だけでなくポート番号の付け替えも行われる。この機能は正式には NAPT(Network Address and Port Translation) と呼ぶが、単に NAT と呼ぶことも多い。Linuxでは、NAPTの実装をIPマスカレードと呼ぶ。
FireWall と DMZ
組織内で外部に公開しているサーバがある場合は、以下の図のような構成にするかもしれない。しかし、このようなネットワーク構成では、FireWallの内側の公開サーバが攻撃されて、踏み台となった場合、組織内のPCが簡単に攻撃をうけてしまう。
そこで、外部からの接続を行う DMZ(De-Militarized Zone 非武装地帯) を設け、外部公開用の公開サーバは DMZ 内に設置する。外部とつながる FireWall では、外部からのパケットは DMZ に流れるように構成する。DMZ 内のサーバが踏み台になった場合を想定し、組織内のルータでは DMZ のサーバと組織内PCは通信できないように FireWall を2重に設置する。
様々なデータの覚え方のレポート課題
前回の malloc() + free() の資料で、様々なデータ構造の覚え方の例やメモリイメージを説明し、前期中間のレポート課題を示す。
malloc+freeの振り返り
// 文字列(可変長)の保存 char str[] = "ABCDE" ; char* pc ; pc = (char*)malloc( strlen( str ) + 1 ) ; if ( pc != NULL ) { // ↑正確に書くと sizeof( char ) * (strlen(str)+1) strcpy( pc , str ) ; //////////////////// // pcを使った処理 //////////////////// free( pc ) ; } // // 可変長の配列の保存 int data[] = { 11 , 22 , 33 } ; int* pi ; pi = (int*)malloc( sizeof( int ) * 3 ) ; if ( pi != NULL ) { for( int i = 0 ; i < 3 ; i++ ) pi[ i ] = data[ i ] ; //////////////////// // piを使った処理 //////////////////// free( pi ) ; } // // 1件の構造体の保存 struct Person { char name[ 10 ] ; int age ; } ; struct Person* pPsn ; pPsn = (struct Person*)malloc( sizeof( struct Person ) ) ; if ( pPsn != NULL ) { strcpy( pPsn->name , "t-saitoh" ) ; pPsn->age = 55 ; //////////////////// // pPsnを使った処理 //////////////////// free( pPsn ) ; }
安全な1行1件のデータ入力
C言語では、scanf などの関数は、バッファオーバーフローなどの危険性があるため、以下のような処理を使うことが多い。fgets は、指定されたファイルから1行分のデータを読み込む。sscanf は、文字列のなかから、scanf() と同じようなフォーマット指定でデータを読み込む。
fgets は、これ以上の入力データが無い場合には、NULL を返す。
(Windowsであれば、キー入力でCtrl+Z を入力、macOSやLinuxであれば、Ctrl+Dを入力で終了)
sscanf() は、読み込めたデータ件数を返す。
int main() { char buff[ 1024 ] ; for( int i = 0 ; i < 3 ; i++ ) { if ( fgets( buff , sizeof( buff ) , stdin ) != NULL ) { char name[ 1024 ] ; int age ; if ( sscanf( buff , "%s%d" , name , &age ) == 2 ) { // 名前と年齢の2つのデータが正しく読み込めたとき ... } } } return 0 ; }
様々なデータの覚え方
配列サイズ固定・名前が固定長
例えば、このデータ構造であれば、table1[] の場合、長い名前にある程度対応できるように nameの配列を100byteにしたりすると、データ件数が少ない場合には、メモリの無駄も多い。
そこで、実際に入力された存在するデータだけをポインタで覚える方法 table2[] という保存方法も考えられる。
// 固定長データのプログラム #define SIZE 50 // 名前(固定長)と年齢の構造体 struct NameAge { char name[ 32 ] ; int age ; } ; struct NameAge table1[ SIZE ] ; int size1 = 0 ; void entry1( char s[] , int a ) { strcpy( table1[ size1 ].name , s ) ; table1[ size1 ].age = a ; size1++ ; } // ポインタで覚える場合 struct NameAge* table2[ SIZE ] ; int size2 = 0 ; void entry2( char s[] , int a ) { table2[size2] = (struct NameAge*)malloc( sizeof( struct NameAge ) ) ; if ( table2[size2] != NULL ) { // なぜ != NULL のチェックを行うのか、説明せよ strcpy( table2[size2]->name , s ) ; table2[size2]->age = a ; size2++ ; } } // データ出力 void print_NA( struct NameAge* p ) { printf( "%s %d¥n" , p->name , p->age ) ; } int main() { // table1に保存 entry1( "t-saitoh" , 55 ) ; entry1( "tomoko" , 44 ) ; print_NA( &table1[0] ) ; print_NA( &table1[1] ) ; // table2に保存 entry2( "t-saitoh" , 55 ) ; entry2( "tomoko" , 44 ) ; print_NA( _________________ ) ; // table2の中身を表示せよ print_NA( _________________ ) ; return 0 ; }
配列サイズ固定・名前が可変長
しかしながら、前回の授業で説明したように、際限なく長い名前があるのであれば、以下の様に名前は、ポインタで保存し、データを保存する時に strdup(…) を使って保存する方法もあるだろう。
// 名前が可変長のプログラム // 名前(可変長)と年齢の構造体 struct NamePAge { char* name ; // ポインタで保存 int age ; } ; struct NamePAge table3[ SIZE ] ; int size3 = 0 ; void entry3( char s[] , int a ) { table3[ size3 ].name = strdup( s ) ; // ★★★★ table3[ size3 ].age = a ; size3++ ; } // ポインタで覚える場合 struct NamePAge* table4[ SIZE ] ; int size4 = 0 ; void entry4( char s[] , int a ) { table4[size4] = (struct NamePAge*)malloc( ____________________ ) ; if ( table4[size4] != NULL ) { // ↑適切に穴埋めせよ table4[size4]->name = strdup( s ) ; // ★★★★ _________________________________ ; // ←適切に穴埋めせよ size4++ ; } } // データ出力 void print_NPA( struct NamePAge* p ) { printf( "%s %d¥n" , ____________ , ____________ ) ; } // ↑適切に穴埋めせよ int main() { // table3に保存 entry3( "t-saitoh" , 55 ) ; entry3( "jyugemu jyugemu ..." , 44 ) ; print_NPA( _________________ ) ; // table3[] の中身を表示せよ。 print_NPA( _________________ ) ; // table4に保存 entry4( "t-saitoh" , 55 ) ; entry4( "jyugemu jyugemu ..." , 44 ) ; print_NPA( table4[0] ) ; print_NPA( table4[1] ) ; return 0 ; }
データ件数が可変長ならば
前述のプログラムでは、データ件数全体は、SIZE という固定サイズを想定していた。しかしながら、データ件数自体も数十件かもしれないし、数万件かもしれないのなら、配列のサイズを可変長にする必要がある。
struct NamePAge* table5 ; int size5 = 0 ; void entry5( char s[] , int a ) { strcpy( table5[ size5 ].name , s ) ; table5[ size5 ].age = a ; size5++ ; } int main() { // table5に保存 table5 = (struct NameAge*)malloc( sizeof( struct NameAge ) * 2 ) ; if ( table5 != NULL ) { entry5( "t-saitoh" , 55 ) ; entry5( "tomoko" , 44 ) ; } return 0 ; }
メモリの管理に十分気を付ける必要があるが、名前の長さも配列全体のサイズも可変長であれば、以下のようなイメージ図のデータを作る必要があるだろう。(JavaScriptやJavaといった言語ではデータのほとんどがこういったポインタで管理されている)
レポート課題
授業での malloc , free を使ったプログラミングを踏まえ、以下のレポートを作成せよ。
以下のデータのどれか1つについて、データを入力し、何らかの処理を行うこと。
課題は、原則として、(自分の出席番号%3)+1 についてチャレンジすること。
- 名前と電話番号
- 名前と身長・体重
- 名前と生年月日
このプログラムを作成するにあたり、以下のことを考慮しmallocを適切に使うこと。
名前は、長い名前の人が混ざっているかもしれない。
保存するデータ件数は、10件かもしれない1000件かもしれない。(データ件数は、処理の最初に入力すること。)
ただし、mallocの理解に自信がない場合は、名前もしくはデータ件数のどちらか一方は固定値でも良い。
レポートには、(a)プログラムリスト, (b)プログラムの説明, (c)正しく動いたことがわかる実行例, (d)考察 を記載すること。
考察には、自分のプログラムが正しく動かない事例はどういう状況でなぜ動かないのか…などを検討したり、プログラムで良くなった点はどういう所かを説明すること。
プログラム言語(C言語)の基礎
学際科目の情報制御基礎において、学科間でプログラミングの初歩の理解差があるので、簡単なC言語プログラミングの基礎の説明。
Hello World
“Hello World”と表示するだけのC言語プログラムは以下のようになる。
// コメントの書き方1 // "//"で始まる行は、プログラムの説明(コメント) /* コメントの書き方2 */ // "/*"から"*/"で囲まれる範囲もコメント #include <stdio.h> // #で始まる行はプリプロセッサ行 // stdio.h には、入出力関数の説明が書いてある int main() { // 一連の処理の塊を関数と呼ぶ。 // C言語では main() 関数を最初に実行する。 printf( "Hello World\n" ) ; // printf() は、以下の物を表示する関数。 // "\n"は、文字を出力して改行するための特殊文字 return 0 ; // main() 関数が、正常終了したことを意味する } // 0 を返り値として返す。
“#include <…>“のプリプロセッサ行は、最初のうちは解りにくいので、「これを書かないとダメ…」と思っていればいい。
#include <stdio.h> は、別ファイル(ヘッダファイル) stdio.h に記載されているプログラムリストを読み込む機能。
stdio.h には、printf() や scanf() などの基本的な関数や定数などの情報が記載されている。
C言語の基本的な命令(文)は、”;”で終わる。(単文)
複数の処理をまとめる場合には、”{“から”}”の中に、複数の文を書き並べる。(複文)
関数とは、複数の処理をひとまとめにした、処理の「かたまり」と思えばいい。
関数の型 関数名( 仮引数 ... ) { 処理1 ... ; 処理2 ... ; }printf() の 文字列中の”\n”(あるいは”¥n”)は、改行を意味する。
「\:バックスラッシュ」は、日本語環境では「¥:円記号」で入力・表示することが多い。
Paiza.io で動かしてみよう
C言語を本格的に使いたいなら、Microsoft Visual Studio などをインストールして使う方が便利だが、情報制御基礎で説明する程度のプログラムなら、Paiza.io が便利。ブラウザの画面で簡単にプログラムの動作を確認することができる。https://paiza.io/jaにアクセスして、上述の Hello World を動かしてみよう。
変数と代入
#include <stdio.h> #include <math.h> // 数学関数を使う 平方根 sqrt() を使っている int main() { // 変数の宣言 int i ; // 符号付き32bit変数 i の宣言 int a = 123 , j ; // a を 123 で初期化 , j も整数型 float x ; // 単精度実数の x を宣言 double y = 1.234 , z ; // 倍精度実数の y を宣言し 1.234 で初期化, // z も倍精度実数 // 変数への代入 i = 1 ; // i に 1 を代入 i = 12 + 2 * a ; // 12+2*a を代入 a は123なので、 // iには、258 が入る。 x = sqrt( 2.0 ) ; // x に 2.0 の平方根(1.4142)を代入 z = y * 2.0 + x * 3.0 ; // y*2+x*3をzに代入 // 変数の内容の表示 printf( "%d\n" , i ) ; // 整数型(%d)で、 i の値を表示 printf( "%f\n" , x ) ; // 単精度実数(%f) で、x の値を表示 printf( "%lf\n" , z ) ; // 倍精度実数(%lf)で、z の値を表示 printf( "iの値は%d,xの値は%lfです。\n" , i , x ) ; return 0 ; // 正常終了 0 を返す }
変数(計算結果を格納する入れ物)を使う場合は、変数を宣言する。
変数名には、何が入っているのか理解しやすいように、名前をつければいい。(英字で始まり、英数字が続くもの,_が入ってもいい)
変数に値を記憶する時は、”変数名=式 ;”の様に書くと、代入演算子”=” の右辺を計算し、その計算結果が左辺の変数に保存される。
変数の内容を表示する時には、printf() の文字列の中に、%d,%f,%lf などの表示したい式の型に応じたものを書いておく。%d=int型 , %f=float型 , %lf=double型
式の値が、その %.. の部分に書き込まれて、出力される。
繰り返しの制御命令
最も基礎的な繰り返し命令として、for() 文を説明。
#include <stdio.h> int main() { int i ; for( i = 1 ; i <= 10 ; i++ ) { // iを1から10まで変化させる。 printf( "%d %d\n" , i , i*i ) ; // i と iの二乗を表示 } return 0 ; }
for文の意味を説明するために、対応するフローチャートを示す。
先のプログラムをフローチャートで示し、その命令の実行順序と、その変数の変化を下図に示す。
練習問題1
簡単なプログラミングの練習として、前回講義の練習問題をC言語で書いてみよう。
- 電気電子工学科,電子情報工学科の学生は、出席番号が奇数は処理C,偶数は処理Dについて回答せよ。
- それ以外の学科の学生は、出席番号が奇数は処理A,偶数は処理Bの結果について回答せよ。
- 自分が考えたプログラムは、前述の Paiza.io や、自分のパソコンのC言語環境で入力し、動作結果も確認せよ。
制御構文とフローチャート
構文の入れ子
文と複文
C言語の文法で、{,} は複数の処理を連続して実行し、複文とよばれる。複数ので文を構成する。
これに対して、a = 123 ; といったセミコロンで終わる「処理 ;」は単文といい、1つの式で文となる。
制御構文のif文は、「if ( 条件 ) 文真」で文となる。このため条件が満たされたときに実行する文真が単文であれば、{,} は不要である。条件が満たされない場合の処理も記述するときには、「if ( 条件 ) 文真 else 文偽」を使う。
// if文 if ( 条件 ) { a = 123 ; } if ( 条件 ) a = 123 ; // 単文なら中括弧は不要 // if-then-else if ( x >= 60 ) { printf( "合格点\n" ) ; } else { printf( "不合格点\n" ) ; }
同じように、「while(条件) 文」、「for(A,B,C) 文」、「do 文 while(条件) ;」も、それぞれ文を構成する。
{,} の複文は、{ 文 文 文… } のように、一連の文を実行し、それを1つの文として扱うための機能である。
// while 文 i = 0 ; while( i < 10 ) { printf( "%d\n" , i ) ; i++ ; } // for 文 for( i = 0 ; i < 10 ; i++ ) { printf( "%d\n" , i ) ; } // do-while 文 i = 0 ; do { printf( "%d\n" , i ) ; i++ ; } while( i < 10 ) ;
練習問題2
プログラムの制御構造の確認として、以下の3つ(No.1,No.2,No.3)の問題から、
M科,C科,B科の学生は((自分の出席番号+1) % 2)+1 の問題、E科,EI科の学生は、((自分の出席番号+1) % 3)+1について、プログラムのフローチャートを描き、その処理がどのように進むのか答えよ。
レポートには、以下の点を記載すること。
- フローチャート
- 実行順序と変数の変化がわかる内容
- (できれば、実際にプログラムを動かし、正しいことを検証すること)
// No.1 --------------------------------------------------------- #include <stdio.h> int main() { int i , j ; for( i = 1 ; i <= 4 ; i++ ) { if ( i % 2 == 0 ) { // i%2 は2で割った余り,i%2==0ならば偶数のとき for( j = 1 ; j <= 2 ; j++ ) printf( "%d %d\n" , i , j ) ; } } return 0 ; } // No.2 --------------------------------------------------------- #include <stdio.h> int main() { int x = 10 , y = 7 , s = 0 ; while( x > 0 ) { if ( x % 2 != 0 ) s = s + y ; y = y * 2 ; x = x / 2 ; // 注意: xは整数型 } printf( "%d\n" , s ) ; return 0 ; } // No.3 --------------------------------------------------------- #include <stdio.h> int a[ 6 ] = { 2 , 3 , 5 , 8 , 13 , 21 } ; int main() { int left = 0 , right = 6 , mid ; int key = 13 ; while( right - left > 0 ) { mid = (left + right) / 2 ; // 整数型で計算 printf( "%d\n" , a[ mid ] ) ; if ( a[ mid ] == key ) break ; else if ( a[ mid ] > key ) right = mid ; else left = mid + 1 ; } return 0 ; }