ホーム » スタッフ » 斉藤徹 » 講義録 » プログラム応用 (ページ 2)

プログラム応用」カテゴリーアーカイブ

2024年4月
 123456
78910111213
14151617181920
21222324252627
282930  

検索・リンク

絶対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() ;
}

こういったトラブルを避けるためには、局所変数を使えば良い。

局所変数を使って、目的の処理…。改良版はココをクリックで表示。

void foo() { // Aを2回表示
   int i ; // 局所変数 foo::i
   for( i = 0 ; i < 2 ; i++ )
      printf( "A" ) ;
}
void main() {
   int i ; // 局所変数 main::i
   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:

システム

最新の投稿(電子情報)

アーカイブ

カテゴリー