ホーム » 2015 (ページ 12)

年別アーカイブ: 2015

2025年5月
 123
45678910
11121314151617
18192021222324
25262728293031

検索・リンク

変数の寿命とスコープ

先週の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分探索法の処理時間分析

最初に、先週説明の単純サーチ と、2重ループの最大選択法 との比較をしながら、 以前のBLOG資料を使って、 2分探索法の処理時間が であることを説明する。

オーダ記法

次に、定番の説明であるけれど、 「単純サーチで、100件で1msecかかった。 データ10000件なら何秒かかる?」 同様に、「最大選択法のプログラムで、100件で10msecかかったら、10000件なら何秒?」 という質問から、オーダ記法の導入を説明する。

最後に、 , , といった、Nが大きな値になった時に、式で大きな値となる主要な部分を抜き出したもの がオーダといった説明を行う。

次回の授業での予習ネタとして、以下の式のオーダについて考えておくように…

2015年4月12日(第418回)

  • まるよし Train Pops ~ 国語と遊ぼう! 第98便 「最後の授業」 前編
  • 入学式について
  • 板書に特徴のある先生

担当:稲葉(2EI・MC)、川﨑(2EI)、田中(2B)、中村(教員)

斉藤卒研室の課題

簡単なunix環境での卒研の課題として、サーバに /home/lab15/Sotsu/access.log を 置いておいた。このファイルは、Webサーバでの数日前のアクセス履歴である。 このファイルの中から、2014年卒研のページ "/sotsu/lab14/" を見た人の 人数を数えよ。

(( /home/lab15/Sotsu/access.log ))
192.156.146.101 - - [29/Mar/2015:07:35:46 +0900]
"GET /~t-saitoh/exp/h8/sample/h8car/h8-3664.x  ....

SQLite3でデータベース

卒研でデータベースを使いたい人もいるようだけど、 MySQLとかまで完璧なのが必要なければ、SQLite を使ったほうが楽。 ただし、データ型は実質すべてtext型になるけど、簡単なアプリベースなら 支障はないはず。

<?php
   // データ保存用の sqlite-data はあらかじめ作っておく。
   //  sqlite-data の書き込み許可
   //    (手抜) $ chmod 777 sqlite-data
   //    (厳密) # chgrp sqlite-data
   //           # chmod 774 sqlite-data
   //  sqlite-data/.htaccess には"Require all denied"を書いて
   //  ディレクトリ内をWeb的にアクセス禁止にする。
   // サーバのPHPを使うと、エラーが見つからず苦労するかも
   // その時は、.htaccess ファイルに、以下の設定を記載しておく
   // "php_flag  display_errors On"
   // 説明しやすいように実行だけ関数をつくる
   function exec_command( $db , $cmd ) {
      if ( ($db->exec( $cmd )) === FALSE ) {
         print $db->lastErrorMsg() ;
      }
   }
   // データベースを作って初期データを登録
   function table_initialize( $db ) {
      exec_command( $db ,
         "create table Person(name text,phone text) ;" ) ;
      exec_command( $db ,
         "insert into Person (name,phone) values('t-saitoh','272925');" ) ;
      exec_command( $db ,
         "insert into Person (name,phone) values('tomoko'  ,'123456');" ) ;
      exec_command( $db ,
         "insert into Person (name,phone) values('mitsuki' ,'234567');" ) ;
   }
   // データベースを作る
   if ( !file_exists( "./sqlite-data/sample.db" ) ) {
      // なにも無い状態
      $db = new SQLite3( "./sqlite-data/sample.db" ) ;
      table_initialize( $db ) ;
   } else {
      // すでに作られている場合
      $db = new SQLite3( "./sqlite-data/sample.db" ) ;
   }
?>
<html>
<head>
</head>
<body>
<pre>
<?php
// 登録されているデータを全部表示
   if ( ($query = $db->query( "select * from Person" )) !== FALSE ) {
      while( $res = $query->fetchArray( SQLITE3_NUM ) ) {
         printf( "| %-10s | %-10s |\n" , $res[0] , $res[1] ) ;
      }
   }
?>
</pre>
</body>
</html>

情報構造論ガイダンス

前年度のプログラミング応用の次のステップとしての情報構造論として、 どういった内容を行うのかガイダンスを行う。

最初に、使用する教科書のタイトルにもある、アルゴリズムという言葉の説明として、 アルゴリズムとは、計算の方法の考え方、 そのアルゴリズムをどう表現するのかがパラダイム、 そのパラダイムに沿って実際に書いたものがプログラム

いいプログラムとは?

次に、ある程度プログラム記述を学んできたところだし「あなたはどうプログラムを書くといいと思うか?」 を次々と聞いてみた。 学生さんから帰ってくる言葉は、 「わかりやすい、行数が短い、関数が使われている、変数名が解りやすい、バグが少ない…」 といった物がほとんど。 中には、「プログラムが再利用しやすい」といったオツな答えもあったけど、 どれも大きくまとめれば「わかりやすい」という観点。

途中で、「処理速度が速い」という答えもあったけど、「メモリの使用量が少ない」という 観点がなかなか出てこない。

処理速度という観点では、3秒ルールなどの雑談を交えながら計算時間について説明。 また、メモリの使用量という観点では、限られた主記憶を有効に使わないと、 プログラムが動かなかったり、仮想記憶などを使うことになり、頻繁なスワップ処理で 処理速度の低下につながることを説明する。

これらの「プログラムの分かりやすさ」、「処理速度」、「メモリの使用量」は、一般的に、 どれかを優先すれば、ほかの観点が悪化してしまうトレードオフの関係にあることを説明。

処理時間を式で表現

まず、プログラムの処理時間を分析するために、簡単なプログラムがどんな式で表現できるかを示す。

// 配列(順序はデタラメ)の中の特定データを探す処理。
int data[ N ] ;
   for( i = 0 ; i < N ; i++ ) {
      if ( data[ i ] == key )
         break ;
}

このようなループ処理であれば、ループ回数の平均を考えて式にすると、

で表現できることを説明。

// 最大選択法の並び替え
int data[ N ] ;

for( i = 0 ; i < N-1 ; i++ ) {
   int m = i ;
   for( j = i+1 ; j < N ; j++ ) {
      if ( data[m] > data[j] )
         m = j ;
   }
   tmp = data[ m ] ;
   data[ m ] = data[ i ] ;
   data[ i ] = tmp ;
}

このような処理であれば、ループ回数をΣなどを用いて表しながら、以下のような式で示されることを説明。

プログラミング応用ガイダンス

プログラミング応用では、どういった内容を扱うのかを最初に説明した後、 最初は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:

オブジェクト指向ガイダンス

専攻科2年のオブジェクト指向の最初の講義で、前半ガイダンスで後半、C言語の構造体などの復習。

オブジェクト指向に関連する歴史

簡単にオブジェクト指向プログラミング(Object Oriented Programming – 略してOOP)が出てくるまでの歴史的流れを最初に説明。

最初のプログラム言語のFortran(科学技術計算向け)の頃は、処理を記述するだけだったけど、 COBOL(商用計算向け)ができた頃には、データをひとまとめで扱う「構造体」(C言語ならstruct …}の考えができた。 その後のALGOLの頃には、処理をブロック化して扱うスタイル(C言語なら{ 文 … }の複文で 記述する方法ができて、処理の構造化・データの構造化ができる。これが「構造化プログラミング(structured programming)」 の始まりとなる。

Wikipediaの記事だと、構造化プログラミングは手続きの構造化のことしか書いてないなぁ… 個人的にはOOPの話の一環として話すため、データ構造化も構造化プログラミングの一部としています。

この後、様々なプログラム言語が開発され、C言語などもできてきた。 一方で、シミュレーションのプログラム開発(例simula)では、 シミュレーション対象(object)に対して、命令するスタイルの書き方が生まれ、 データに対して命令するという点で、擬人法のようなイメージで直感的にも分かりやすかった。 これがオブジェクト指向の始まりとなる。

この考え方を導入した言語の1つが Smalltalk であり、この環境では、プログラムのエディタも Smalltalk で記述したりして、オブジェクト指向がGUIのプログラムと親和性が良いことから普及が拡大する。

C言語にこのオブジェクト指向を取り入れて、C++が開発される。さらに、この文法をベースとした、 Javaなどが開発される。最近の新しい言語では、どれもオブジェクト指向の考えが使われている。

構造体の導入

C++でのオブジェクト指向は、構造体の表記がベースになっているので、まずは構造体の説明。

// 構造体の宣言
struct Person {
   char name[ 20 ] ; // 要素1
   int  phone ;    // 要素2
} ;
// 構造体変数の宣言
struct Person saitoh ;
struct Person data[ 10 ] ;
// 実際にデータを参照 構造体変数.要素名
strcpy( saitoh.name , "t-saitoh" ) ;
saitoh.phone = 272925 ;
for( int i = 0 ; i < 10 ; i++ ) {
   scanf( "%d%s" , data[ i ].name , &(data[ i ].phone) ) ;
}

値渡し、ポインタ渡し、参照渡し

構造体の使い方の話では、関数との構造体のデータ渡しでポインタなどが出てくるので、 値渡し・ポインタ渡し・参照渡しの復習。(参照渡しはC++で導入された考え方)

C言語の基本は、値渡し。呼び出し側の実引数は、関数側の仮引数に値がコピーされる。 このため、呼び出し側の変数(下の例ではa)の中身は変化しない。 よって、関数の呼び出しで呼び出し側の変数が勝手に中身が変わらないので、予想外の変数の中身の変化が無く分かりやすい。

// 値渡し(call by value)の例
void foo( int x ) {
   x++ ;
   printf( "%d¥n" , x ) ;
}
void main() {
   int a = 123 ;
   foo( a ) ;  // 124が表示
   foo( a ) ;  // 124が表示
}

しかし、上の例では、foo()の呼び出しで、変数aの中身が変化してくれたほうが都合が良い場合もある。 この場合は、C言語ではポインタを使って記述する。 このように、関数を呼び出して、手元の変数が変化することは、副作用と呼ばれる。 副作用の多いプログラムは、変数の値の管理がわかりにくくなるので、副作用は最小限に記述すべき。

// ポインタ渡し(call by pointer)の例
void foo( int *px ) {
   (*px)++ ;
   printf( "%d¥n" , (*px) ) ;
}
void main() {
   int a = 123 ;
   foo( &a ) ;  // 124が表示
   foo( &a ) ;  // 125が表示
}

しかし、ポインタを多用すると、ポインタを動かしてトラブルも増えることから、ポインタはあまり使わない方が良い。 そこで、C++では参照型というものがでてきた。

// 参照型(call by reference)の場合
void foo( int &x ) {
   x++ ;
   printf( "%d¥n" , x ) ;
}
void main() {
   int a = 123 ;
   foo( a ) ;  // 124が表示
   foo( a ) ;  // 125が表示
}

参照型は、ポインタを使っていないように見えるけれども、機械語レベルでみればポインタを使ったものと同じ。

2015年4月5日(第417回)

入学式のため、収録でお送りしました。

  • まるよし Train Pops ~ 国語と遊ぼう! 第97便 「送り仮名」
  • 放送・メディア研究会の部活紹介動画
  • 新入生に向けてのアドバイス
  • 桜の話

担当:松島(4C・MC)、田嶋(3C)、川﨑(2EI・MIX)、植村(2E)、中村(教員)、西(教員)

創造工学演習・プログラム予備実験(1)

プログラミングコンテストの競技部門では、パズルのような組み合わせ問題が 出題されることが多い。そこで、この予備実験では、きわめて単純なパズル問題(組み合わせ問題) のプログラムについて扱う。

組み合わせ問題の基礎

簡単な問題として、「100未満の整数の値を3つ選び、その値を辺の長さとした場合、 直角三角形となるものをすべて表示する。」について考える。

一番簡単な方法は、以下となるであろう。

#include <stdio.h>
#include <math.h>
#include <time.h>
// 整数比の直角三角形の一覧を求める。
void integer_triangle( int n ) {
for( int a = 1 ; a < n ; a++ ) {
for( int b = 1 ; b < n ; b++ ) {
// 一番ダサい方法
for( int c = 1 ; c < n ; c++ ) {
if ( a*a + b*b == c*c ) {
printf( "%d %d %d\n" , a , b , c ) ;
}
}
}
}
}
int main() {
integer_triangle( 100 ) ;
return 0 ;
}

しかしこのプログラムの欠点としては、100×100×100回のループで無駄な処理が多い。

ループ回数を減らすだけなら、最も内側の処理を、計算で整数値か確認すればいい。

void integer_triangle( int n ) {
for( int a = 1 ; a < n ; a++ ) {
for( int b = 1 ; b < n ; b++ ) { // ココも改良できるよね?
int d = a*a + b*b ;
int c = (int)sqrt( d ) ; // 斜辺Cの整数値を求め、改めて確認する。
if ( c*c == d ) {
printf( "%d %d %d\n" , a , b , c ) ;
}
}
}
}

(1) 計算誤差の問題を考えてみよう。(問題点を十分に考えたらここをクリック)

たとえば、3:4:5の直角三角形で、3*3+4*4 = 25 だが、sqrt(25)は実数で計算するから、 計算誤差で4.99999で求まったらどうなるだろうか?

1~100までの数値で、"int a = sqrt( (double)(i*i) ) ;" を計算してみて、 異なる値が求まることはあるか? 多少の計算誤差があっても正しく処理が行われるにはどうすればいいか、考えてみよう。

(2) 無駄な答えについて考えてみよう。(問題点を十分に考えたらここをクリック)

このプログラムの答えでは、簡単な整数比の答えの「整数倍の答え」も表示されてしまう。 たとえば、(3:4:5)の答えのほかに、(6:8:10)も表示される。 こういった答えを表示しないようにするにはどうすればよいか?

また、この2つのプログラムの処理時間を実際に比べてみる。

int main() {
time_t start , end ;
// time() 関数は、秒数しか求まらないので、
// あえて処理を1000回繰り返し、数秒かかる処理にする。
start = time( NULL ) ;
for( int i = 0 ; i < 1000 ; i++ ) {
// ただし、関数内のprintfをコメントアウトしておくこと
integer_triangle( 100 ) ;
}
end = time( NULL ) ;
printf( "%lf\n" , difftime( end , start ) ) ;
return 0 ;
}

再帰プログラミング

組み合わせ問題では、forループの多重の入れ子で問題を解けない場合が多い。 (書けないことはないけど無駄なループで処理が遅くなるか、入れ子段数が可変にできない。)

こういった場合には、再帰プログラミングがよく利用される。 もっとも簡単な再帰の例として、階乗のプログラムを考える。 通常であれば、以下のような for ループで記述することになるだろう。

// 階乗の計算
int fact( int x )
{ // ループ
int f = 1 ;
for( int i = 2 ; i <= x ; i++ )
f = f * i ;
return f ;
}

再帰呼び出しでは、関数の処理の中に、自分自身の関数呼び出しが含まれる。 また、無限に自分自身を呼び出したら処理が止まらないので、 問題を一つ小さくして、これ以上小さくできないときは処理を止めるように記述する。

int fact( int x )
{ // 再帰呼び出し
if ( x <= 1 )
return 1 ;
else
return x * fact( x - 1 ) ;
}

指定長を指定辺の組み合わせで作る

再帰を使った簡単なパズル問題として、以下のプログラムを作成したい。

配列の中に、複数の辺の長さが入っている。これを組み合わせて指定した長さを作れ。 使用する辺はできるだけ少ない方がよい。
int a[] = { 4 , 5 , 2 , 1 , 3 , 7 } ;
(例) 辺の長さ10を作るには、(5,4,1)とか(7,3)などが考えられる。
これは、ナップサック問題の基本問題で、容量の決まったナップサックに最大量入れる組合せを求めるのと同じである。

このプログラムを解くには…

10 を [4,5,2,1,3,7] で作るには...
 (0) 6=10-4 を [5,2,1,3,7]で作る。
 (1) 5=10-5 を [4,2,1,3,7]で作る。
 (2) 8=10-2 を [5,4,1,3,7]で作る。
 (3) 9=10-1 を [5,2,4,3,7]で作る。
 (4) 7=10-3 を [5,2,1,4,7]で作る。
 (5) 3=10-7 を [5,2,1,3,4]で作る。

そこで、ここまでの考えを、以下のようなプログラムで記述してみる。

// 指定されたデータを入れ替える。
void swap( int*a , int*b )
{ int x = *a ;
*a = *b ;
*b = x ;
}
void check( int array[] , int size , int len , int n )
{
// array[] 配列
// size    配列サイズ
// len     作りたい長さ
// n       使った個数
for( int i = n ; i < size ; i++ ) {
// i番目を先頭に...
swap( &array[ n ] , &array[ i ] ) ;
printf( "check( array , %d , %d , %d )\n" ,
size , len - array[ n ] , n+1 ) ;
// 最初のswapでの変更を元に戻す。
swap( &array[ i ] , &array[ n ] ) ;
}
}
int main() [
int a[] = { 4 , 5 , 2 , 1 , 3 , 7 } ;
check( a , 6 , 10 , 0 ) ;
}

(1) これを再帰呼び出しにしてみよう。どう書けばいい?(考えたらここをクリック)

void check( int array[] , int size , int len , int n )
{
// array[] 配列
// size    配列サイズ
// len     作りたい長さ
// n       使った個数
if ( len < 0 ) {
// 指定した丁度の長さを作れなかった。
;
} else if ( len == 0 ) {
// 指定した長さを作れたので答えを表示。
for( int i = 0 ; i < n ; i++ ) {
printf( "%d " , array[ i ] ) ;
}
printf( "\n" ) ;
} else {
// 問題を一つ小さくして再帰。
for( int i = n ; i < size ; i++ ) {
swap( &array[ n ] , &array[ i ] ) ;
printf( "check( array , %d , %d , %d )\n" ,
size , len - array[ n ] , n+1 ) ;
check( array , size , len - array[ n ] , n + 1 ) ;
swap( &array[ i ] , &array[ n ] ) ;
}
}
}

(2) 少ない組み合わせの方がポイントが高い場合には、プログラムをどう変更する?
(3) 答えが1つだけで良い場合は、プログラムをどう変更する?
(4) このプログラムでは、冗長な答えがあるか?ないか?検討せよ。
(5) 前設問の整数比直角三角形のプログラムで、冗長な答えを削除するプログラムを作成せよ。
# ただし、(2)〜(5)は、演習の実験の時間内でできる範囲とする。

システム

最新の投稿(電子情報)

アーカイブ

カテゴリー