絶対PATH,相対PATHの演習
絶対PATH,相対PATHの理解のため、コマンドラインを用いた演習も行う。
ファイル関連コマンドの基本
(( まずは cmd.exe の起動 )) メニュー検索より、"cmd.exe" を探して起動 (( ファイル操作命令の基礎 )) dir PATH ディレクトリの一覧を表示 例:dir C:¥Windows type PATH ファイルの内容を表示 例:type Z:¥sample¥sample.c mkdir PATH ディレクトリを作成 例:mkdir Z:¥foo rmdir PATH ディレクトリを削除 例:rmdir Z:¥foo echo "abc" > Z:¥sample¥data.txt テキストファイルに出力
演習
(( 絶対PATH )) Z: dir ¥ mkdir ¥fukui echo "fukui" > ¥fukui¥fukui.txt type ¥fukui¥fukui.txt mkdir ¥fukui¥sabae echo "sabae" > ¥fukui¥sabae¥sabae.txt dir ¥fukui¥sabae type ¥fukui¥sabae¥sabae.txt (( 相対PATH )) cd ¥fukui¥sabae dir . dir .. type sabae.txt type ..¥fukui.txt dir ..¥..
ファイル入出力
今日は、ファイル入出力のプログラム演習。
ファイル入出力
// 基礎的なファイル入力 #include <stdio.h> void main() { FILE* fp ; if ( (fp = fopen( "data.txt" , "rt" )) != NULL ) { char name[ 100 ] ; int age ; while( fscanf( fp , "%s%d" , name , &age ) == 2 ) { printf( "%s %d\n" , name , age ) ; } fclose( fp ) ; } }
// 入力したデータから条件を満たすデータだけを出力 #include <stdio.h> void main() { FILE* fp_in ; FILE* fp_out ; if ( (fp_in = fopen( "in.txt" , "rt" )) != NULL ) { if ( (fp_out = fopen( "out.txt" , "wt" )) != NULL ) { char name[ 100 ] ; int age ; while( fscanf( fp_in , "%s%d" , name , &age ) == 2 ) { if ( age >= 18 ) fprintf( fp_out , "%s %d\n" , name , age ) ; } fclose( fp_out ) ; } fclose( fp_in ) ; } }
テキストモードとバイナリモード
前述プログラムでは、fopen のファイルモードで、"rt" , "wt" のように 文字"t"をつけている。これは、OSによる行末文字の取り扱いの トラブルを防ぐためのもの。
OSの成り立ちの違いで、Windows と Unix では、行末文字の取り扱い方法 が違う。
Windows では、次の行の先頭に移動するには、"\r"(キャリッジリターン)と "\n"(ラインフィード)が必要となる。
一方、Unix では、次の行の先頭に移動するには、"\n"(ラインフィード)だけ でいい。
この違いがあるため、Unixで開発されたC言語で書いたプログラムを Windows で動かそうとすると、
// Unix Windows printf( "Hello\n" ) ; // Hello Hello printf( "World\n" ) ; // World World
と異なる表示になってしまう。これでは、WindowsでC言語を書く場合は、 printf( "Hello\r\n" ) といった書き換えが必要になってしまう。
これを防ぐために、fopen で "…t" 書くことで「テキストモード」に 指定すると Windows で printf( "\n" ) を出力すれば、 "\r\n" に変換して出力してくれる。入力時にも同様に、"\r\n" があると、 "\n" に変換してくれる。
絶対PATHと相対PATH
前述のプログラムで、fopen( "data.txt" , "rt" ) と指定すると、 プログラムの保存しているディレクトリと同じ場所から、"data.txt" を 探そうとする。(相対PATH)
しかし、別のディレクトリにあるデータを読む場合には、 場所を明記するために、Z:\directory\data.txt のように、 ルートディレクトリからの場所(Windowsではドライブ名付きで)を 指定する。(絶対PATH)
ただし、Windows でのディレクトリ区切り文字 '\' は、C言語の文字列内では、 特殊文字を指定する時に使われる。このため、文字列内では \\ の様に 書く必要がある。
fp = fopen( "Z:\\directory\\data.txt" , "rt" ) ;
この場合、"\\"と2重で書くことで、Unix と Windows のプログラムの 書き換えが必要となるのを防ぐため、上例は、Unixのディレクトリ区切り文字"/"を使って、以下のように書いても良い。
fp = fopen( "Z:/directory/data.txt" , "rt" ) ;
プログラミング応用・ガイダンス
初回の授業ということで、シラバスを配り、今年の授業の進め方を説明。 基本はC言語で行い、(1)C言語の基本、(2)ファイルと入出力、(3)構造体 の説明を行う。
今年の3年生は、JavaScript でプログラミング基礎を行っているので、 C言語に慣れてもらう必要がある。特に型(Type)の概念ができていないと 思われるので、注意が必要と思われる。
プログラム言語の歴史
まずは、JavaScript を習ってきたのに、C言語をするのかを分かって もらうために、プログラム言語の歴史の説明を交えて、 構造化プログラミングの説明。
機械語 計算方法や番地がそのまま数値... アセンブリ言語 機械語は人間に解らないので書きやすく。 FORTRAN 科学技術計算向け言語 COBOL 商用計算向け言語(データの構造化[構造体]) ALGOL 命令の構造化 + データ構造化 while( ) { C言語の命令の構造化 } BCPL,B言語 C言語の前身 C言語 unixを開発するために作られた SIMULA シミュレーション用言語(オブジェクト指向の前身) C++ C言語にオブジェクト指向を取りれ Java Internetでのプログラム用 C# Microsoftの対Java戦略 D BCPL,B,C,C++,今ココ JavaScript ブラウザで動くように
前述の高級言語を動かす場合、コンパイラ方式とインタプリタ方式がある。 コンパイラ方式は、命令を最初にすべて機械語に直す。 インタプリタ方式は、高級言語の意味に合わせて動く言語。 通常は、コンパイラ方式の方が高速。
C言語の導入
#include <stdio.h> int main() { printf( "Hello World\n" ) ; return 0 ; }
と説明をしようと思ったけど、記号の読み方とか読み間違いを防ぐため
# ナンバーサイン(正しくはシャープではない) : コロン ; セミコロン @ アット ^ ハット * アスタリスク
あと、私の板書を読み間違えられても困るので、紛らわしい文字の 話をしておく。
GrWinのライセンスキーの指定方法
プログラミング応用のグラフィックスを用いた演習では、 簡単な設定で使えるようにということで GrWin を使用している。 しかし、昨年度まで利用していたものから、正式バージョン GrWin 1.0 となった際に、 ライセンスキーが必要となった。
GrWinのライセンスキーの設定
総合情報処理センターでは、複数端末の教育利用ライセンスを取得し、 そのキーの情報が端末に保存されています。 プログラムを走らせる場合は、以下のようなコードにてコンパイルして下さい。
#include <GrWin.h> #include "C:\Program Files\GrWin\gwkey.c" int main(){ GWinit(); GWopen(0); GWindow(-1,-1,1,1); GWline(-1,-1,1,1); return 0; } // 以下の C:¥Program Files¥GrWin¥gwkey.c" は、保存済み // /* gwkey.c * * Copyright (C) 1998 - 2014 TAMARIBUCHI, Tsuguhiro * * WWW: http://spdg1.sci.shizuoka.ac.jp/grwin/ja-jp/ * ********************************************** * GrWin Version 1.1.0 用のキー・ファイルです * * このファイルを編集しないでください * ********************************************** */ char GW_key[] = "ライセンスキーの値";
自宅で GrWinを使って演習を行いたい場合は、必要な GrWinサーバインストーラ(非商用)とGrWinライブラリインストーラ、をダウンロード&インストールし、 個人用ライセンス取得ページにて、ライセンスを発行し送られてきたキーを、上記 "C:¥Program Files¥GrWin¥gwkey.c" の GW_key[] の文字配列に記載してください。
構造体の参照渡しとオブジェクト指向
一緒に、来週のプログラミング応用の資料書いちゃえ。
構造体の参照渡し
構造体のデータを関数の呼び出しで記述する場合には、参照渡しを利用する。
struct Person { char name[ 20 ] ; int age ; } ; void print( struct Person* p ) { printf( "%s %d¥n" , p->name , p->age ) ; } void main() { struct Person saitoh ; strcpy( saitoh.name , "t-saitoh" ) ; saitoh.age = 50 ; print( &saitoh ) ; }
このようなプログラムの書き方をすると、「データ saitoh に、print() せよ…」 といった処理を記述したようになる。 これを発展して、データ saitoh に、print という命令をするイメージにも見える。
この考え方を、そのままプログラムに反映させ、Personというデータは、 名前と年齢、データを表示するprintは…といったように、 データ構造と、そのデータ構造への処理をペアで記述すると分かりやすい。
オブジェクト指向の導入
オブジェクト指向では、データ構造とその命令を合わせたものをクラス(class)と呼ぶ。 また、データ(class)への命令は、メソッド(method)と呼ぶ。
class Person { private: char name[ 20 ] ; int age ; public: Person( char s[] , int a ) { strcpy( name , s ) ; age = a ; } int scan() { return scan( "%s %d" , name , &age ) ; } void print() { printf( "%s %d¥n" , name , age ) ; } } ; void main() { Person saitoh( "t-saitoh" , 50 ) ; saitoh.print() ; Person table[ 50 ] ; for( int i = 0 ; i < 50 ; i++ ) { if ( table[ i ].scan() != 2 ) break ; table[ i ].print() ; } }
構造体とオブジェクト指向
プログラミング応用の後期では、構造体とコンピュータグラフィックスの基礎を扱う予定。 CGの基礎でも、X座標,Y座標…をひと塊の構造体で表現という意味では、構造体の延長として授業を進める予定。
構造体
上記資料を元に説明。 最初に構造体が無かったら、名前・国語・算数・理科の1クラス分のデータをどう表現しますか?
// まずは基本の宣言 char name[ 50 ][ 20 ] ; int kokugo[ 50 ] ; int sansu[ 50 ] ; int rika[ 50 ] ; // もしクラスが最初20人だったら、20→50に変更する際に、 // 文字列長の20も書きなおしちゃうかも。 // 50とか20とかマジックナンバーは使わないほうがいい。 #define SIZE 50 #define LEN 20 char name[ SIZE ][ LEN ] ; int kokugo[ SIZE ] ; : // 2クラス分のデータ(例えばEI科とE科)を保存したかったら? // case-1(配列2倍にしちゃえ) char name[ 100 ][ 20 ] ; // どこからがEI科? int kokugo[ 100 ] ; : // case-2(2次元配列にしちゃえ) char name[ 2 ][ 50 ][ 20 ] ; // 0,1どっちがEI科? int kokugo[ 2 ][ 50 ] ; : // case-3(目的に応じた名前の変数を作っちゃえ) char ei_name[ 50 ][ 20 ] ; // EI科は一目瞭然 int ei_kokugo[ 50 ] ; // だけど変数名が違うから : // 処理を2度書き char ee_name[ 50 ][ 20 ] ; int ee_kokugo[ 50 ] ; :
このような問題に対応するために構造体を用いる。
struct Person { // Personが構造体名(タグ名) char name[ 20 ] ; int kokugo ; int sansu ; int rika ; } ; struct Person saitoh ; struct Person ei[ 50 ] , ee[ 40 ] ; strcpy( saitoh.name , "t-saitoh" ) ; saitoh.kokugo = 100 ; ei[ 0 ].sansu = 80 ; ee[ 1 ].rika = 75 ;
授業では、構造体の初期化、入れ子の話をする。詳細は配布資料参照。
途中で、C言語の歴史として、unix開発時に、BCPL→B言語→C言語(K&R)→ANSI-C→…C++→D言語 といった雑談も説明。
入れ子の話では、 for(…) { for(…) { } } のような、処理の入れ子(処理の構造化)と、 構造体の入れ子(データの構造化)の話から、構造化プログラミング(structured programming)といった話も紹介する。
数値の範囲とトラブル事例
先日の数値の範囲の説明で、浮動小数点型(float,double)などについても説明を行う。
16bitコンピュータの時代…
簡単な桁あふれの事例として、古いコンピュータ16bitの時代の事例。
// int は16bitとする。 // パソコン画面上の2点の距離を計算したい。 int x1 , y1 ; int x2 , y2 ; int r = (int) sqrt( (x1-x2)*(x1-x2) + (y1-y2)*(y1-y2) ) ;
このプログラムは、負の値の平方根を求めた…というエラーメッセージで止まってしまう。
これは、座標が200ドット離れていれば、2乗の和を求めた段階で、40000となるが、 16bit 符号付き整数であれば、最大数が32767 なので桁あふれして、負の数として扱われている。
2038年問題
次に、20XX年問題の説明。 2000年問題とは、古いプログラムで西暦末尾2桁で年を扱っていたプログラムが誤動作を 起こす可能性の問題。
同じように数値の範囲が原因となるものとして、2038年問題を紹介する。 OS unix では、時間を1970年からの経過秒数で表す。 この時、32bit(符号付きint型)であれば、2^31-1が扱える最大数であり、 (2^31-1) / (3600*24*365.22)を求めると、約68年となる。 つまり、32bit int であった場合は、2038年以降は桁あふれで時間を正しく扱えなくなる可能性がある。
しかし、計算の途中の段階で桁あふれして動かない事例はある。 以下の事例は、2004年に実際に発生したトラブルと同様の簡単なコードである。
// intは32bitとする // 2つの時間の中央時間を計算するコード int t1 , t2 ; int tm = (t1 + t2) / 2 ;
32bitの時間情報では、2004年以降は、 t1,t2の上位ビットが01XX……となっているため、 t1+t2 を計算すると最上位ビット(符号ビット)が1となり この後の計算は、負の値として扱われた。
上記のコードは、次のように記述するか、64bit化されたtime_t型で 記述すべきである。
// 32bitでも動く int tm = t1 + (t2 - t1) / 2 ; // 64bitのtime_t型 time_t t1 , t2 ; time_t tm = (t1 + t2) / 2 ; // △ time_t tm = t1 + difftime( t2 , t1 ) / 2 ; // ◎
248日問題
丁度ニュースで、「ボーイング787が248日以上連続稼働すると、電源停止で制御不能となる」というネタが 紹介されている。点検などがあるから、実際はあり得ない状況だとは思えるが、 これもint型の桁あふれが原因らしい。 あり得ないとはいえ、フライバイワイヤ(操縦桿を電気信号で制御)の最新機は、電源が落ちれば即墜落。 これは、10msec単位で、OSの経過時間をint型(32bit)で制御しているのが 原因らしい。10msec*(2^31)=248日で桁あふれが発生して、電源稼働経過時間を誤認するらしい。 これと同様の事象は、Windows Xpなどが出た頃にも発生している。 787ほど深刻な墜落はないにしても、サーバ機なら248日稼働ぐらいはよくある話。
浮動小数点型と演算順位の問題
float型やdouble型は、浮動小数点で実数の小さな値から大きな値まで扱える。
しかしながら、プログラム中の式の順序によっては、注意が必要となる。
int a[ 5 ] = { 10 , 10 , 10 , 10 , 11 } ; int s = 0 , size = 5 ; for( int i = 0 ; i < size ; i++ ) s += a[ i ] ; printf( "%lf¥n" , s / size ) ; // × %lf はdouble型、s/size はint型で異常な値が表示される。 printf( "%lf¥n" , (double)( s / size ) ) ; // × s/sizeはint型で、小数点以下が求まらない。 printf( "%lf¥n" , (double)s / (double)size ) ; // ◎
#define PI 3.141592 double x ; for( x = 0.0 ; x < 2*PI ; x += (1/6) * PI ) { printf( "%lf %lf¥n" , x , sin( x ) ) ; }
このプログラムは、1/6が整数型で行われるため(つまり0とPIを掛ける)、 xの値が変化しない。 これは、"x += (1.0/6.0) * PI" と書かなければならない。
関数の値渡しと、整数型の数値の範囲
丁度、講義の前に別授業の課題に取り組んでいる学生を見ていたら、 次週に説明を行おうと思っていたN進数、小数点を含む2進数であった。 丁度、「計算機構成論」の補数、「数値計算」の小数点を含む2進数の講義で、 いつもになく、内容と説明時期が重複している…(^_^;
関数の値渡し
関数との引数の値渡しについて解説。 C言語では基本の値渡しメカニズムしかない。 引数で副作用(side effect)を返したい場合は、その代用としてポインタ渡しを利用する。 また、配列が引数の場合、値渡しのためのコピーを最小限とするため、 配列先頭アドレスによるポインタ渡しで行われることを説明する。
// 値渡し void foo( int x ) { x++ ; printf( "%d¥n" , x ) ; } void main() { int a = 123 ; foo( a ) ; // 124 foo( a ) ; // 124 } // ポインタ渡し void foo( int* px ) { (*px)++ ; printf( "%d¥n" , *px ) ; } void main() { int a = 123 ; foo( &a ) ; // 124 foo( &a ) ; // 125 } // 参照渡し(C++の新しい文法で紹介のみ) void foo( int &x ) { x++ ; printf( "%d¥n" , x ) ; } void main() { int a = 123 ; foo( a ) ; // 124 foo( a ) ; // 125 } // 配列でのポインタ渡し void foo( int x[] ) { x[0]++ ; printf( "%d¥n" , x[0] ) ; } void main() { int a[1] = { 123 } ; foo( a ) ; // 124 foo( a ) ; // 125 }
整数型の数値範囲
整数型などの数値範囲について説明を行うために、2の補数表現を復習したあと、 数値の範囲について説明する。
type | range | unsigned | signed --------------+-----------+----------+--------------- char | 8bit | 0..255 | -128..127 short int | 16bit | 0..65535 | -32768..32767 int | 32bit | 0..2^32-1| -2^31..2^31-1 long int | ?32bit? | | long long int | gcc 64bit | 0..2^64-1| -2^63..2^63-1
数値範囲の大まかな計算のための2つの方法として、 や、 10進数での桁数の概算のために、
より、
といった計算を行う方法について説明。
次週は、16bitコンピュータで int が簡単に桁あふれする問題や、 2038年問題や2004年問題などを解説する予定。
変数の寿命とスコープ
先週のC言語の制御構文のシメとして、switch-case文の説明をしてから、 変数の寿命とスコープの説明を行った。
switch-case文の説明では、以下の例は期待通りの動きをしない…といった例も交えて説明。 ただし、double誤差問題や文字列のポインタ関連なので、以後の授業で改めて説明が必要だろう。
// double誤差問題 double x ; for( x = 0.0 ; x <= 1.0 ; x += 0.1 ) switch( x ) { case 0.3 : printf( "A" ) ; // 0.299999...と0.3は違う値になるかも break ; } // 文字列の比較の問題 char str[ 10 ] ; scanf( "%s" , str ) ; switch( str ) { // strの先頭番地と"yes","no"の先頭番地の比較 case "yes" : printf( "はい" ) ; break ; case "no" : printf( "いいえ" ) ; break ; }
変数の寿命とスコープ
最初に、以下の様なプログラムが期待通りに動かない説明をして、 大域変数を共用することの問題を話す。
int i ; // 大域変数(global variable) void foo() { // Aを2回表示 for( i = 0 ; i < 2 ; i++ ) printf( "A" ) ; } void main() { // foo(Aを2回表示)を2回呼び出すつもり for( i = 0 ; i < 2 ; i++ ) foo() ; }
こういったトラブルを避けるためには、局所変数を使えば良い。
局所変数を使って、目的の処理…。改良版はココをクリックで表示。
最近の構造型プログラム言語であれば、 変数には寿命(変数が、作られる/消える、タイミング)と、 スコープ(変数が使える範囲)がある。
#include <stdio.h> // 静的大域変数(スコープ全体,寿命:起動から終了) int x = 123; void foo() { // 動的局所変数(スコープ:foo内部,寿命:foo呼出から戻るまで) int y = 234 ; // 静的局所変数(スコープ:foo内部,寿命:起動から終了) static int z = 345 ; x++ ; y++ ; z++ ; printf( "%d %d %d¥n" , x , y , z ) ; } void main() { foo() ; // 124,235,346が表示 foo() ; // 125,235,347が表示 }
関数の引数
関数の引数の受け渡しの説明。
返り値の型 関数名( 仮引数の宣言 ) { 何らかの処理 ; // 関数に入ると仮引数が局所変数で作られ、 return 式 ; // 実引数が代入される。 } void main() { 関数名( 実引数 ) ; // 実引数は仮引数にコピーされる。 }
値渡し、ポインタ渡しなどの説明をしたいけど、時間なので次週に説明。 残り時間では、BCPL→B言語→C言語(K&R-C→ANSI-C)→C++といった C言語の変遷を簡単に紹介。
プログラミング応用ガイダンス
プログラミング応用では、どういった内容を扱うのかを最初に説明した後、 最初は2年の復習となることから、C言語の文法のおさらい。 特に制御構文のfor,ifを中心にフローチャートとの対応をとる説明とした。
最初に、簡単なプログラムでの式の実行順序の理解の確認。
// (1) int i ; for( i = 0 ; i < 4 ; i++ ) { if ( (i % 2) == 0 ) { printf( "%d\n" , i ) ; } } // (2) int i , j ; for( i = 0 ; i < 3 ; i++ ) { for( j = 0 ; j < 2 ; j++ ) { printf( "%d\n" , i * j ) ; } }
次に、break文とcontinue文の説明を行う。
// for文 int i ; for( i = 0 ; i < N ; i++ ) { : if ( .... ) break ; if ( .... ) continue ; : } // break , continueの動き i = 0 ; // for初期化 LOOP: if ( i >= N ) goto BREAK ; // for終了判定 : if ( .... ) goto BREAK ; // break(ループ脱出) if ( .... ) goto CONTINUE ; // continue(次の繰返し) : CONTINUE: i++ ; // for繰返し変数更新 goto LOOP ; BREAK: