ホーム » 2018 » 5月 (ページ 2)

月別アーカイブ: 5月 2018

2018年5月
 12345
6789101112
13141516171819
20212223242526
2728293031  

検索・リンク

mallocとfree

前回の講義での、「長いかもしれない名前」を覚える処理は、最悪の場合をどう扱うかでメモリのムダが発生する。
ここで、前回講義で説明した、大きな配列を少しづつ分けて使う処理を考える。

大きな配列を少しづつ貸し出す処理

char str[ 10000 ] ;
char* sp = str ;
char entry( char* s ) {
   char* ret = sp ;
   strcpy( sp , s ) ;
   sp += strlen( s ) + 1 ;
   return ret ;
}
int main() {
   char* names[ 10 ] ;
   names[ 0 ] = entry( "saitoh" ) ;
   names[ 1 ] = entry( "tomoko" ) ;
   return 0 ;
}
// str[] s a i t o h ¥0 t o m o k o ¥0
//       ↑             ↑
//     names[0]        names[1]

このプログラムでは、貸し出す度に、sp のポインタを後ろに移動していく。

スタック

この貸し出す度に、末尾の場所をずらす方式にスタックがある。

int stack[ 100 ] ;
int* sp = stack ;
void push( int x ) {
   *sp = x ;    // 1行で書くなら
   sp++ ;       // *sp++ = x ;
}
int pop() {
   sp-- ;
   return *sp ; // return *(--sp) ;
}
int main() {
   push( 1 ) ;
   push( 2 ) ;
   push( 3 ) ;
   printf( "%d¥n" , pop() ) ;
   printf( "%d¥n" , pop() ) ;
   printf( "%d¥n" , pop() ) ;
   return 0 ;
}


スタックは、最後に保存したデータを最初に取り出せる(Last In First Out)から、LIFO とも呼ばれる。
このデータ管理方法は、最後に呼び出した関数が最初に終了することから、関数の戻り番地の保存や、最後に確保した局所変数が最初に不要となることから、局所変数の管理に利用されている。

スタック上の動的メモリ確保 alloca

最初のプログラム例のような方法で、スタック上にメモリを確保する関数として、alloca() がある。

// C言語では、配列サイズに変数を使えない。
int size = ... ;
int array[ size ] ;

// これを alloca で書くと...
int size = ... ;
int* array ;
array = (int*)alloca( sizeof( int ) * size ) ;
if ( array != NULL ) { // スタック溢れはNULLで検知できないか...
   :
   // array[]を使った処理
   :
}

ただし、alloca はスタック領域を使用するため、数MBといった巨大なデータを確保するのには使えない。
この領域は、スタックのように末尾だけを覚えておけばいいので、管理が簡単である。一方で、関数の局所変数として確保して、「この場所を使ってこの計算してね」的な使い方をしなければならない。「この場所を返すから後は自由に使って」的な使い方はできない。

malloc()とfree()

alloca を使うような処理は、スタックのように「最後に確保したものが最初に不要となる」という状況でしか使えない。
確保した領域が不要となる順序が判らない場合には、malloc() を使う必要がある。

ポインタ = malloc( 確保するbyte数 ) ;
   メモリ不足で malloc に失敗したら NULL を返す。
free( ポインタ ) ;
   確保したメモリ領域を解放する。
   解放されたメモリは、mallocで再利用してくれる。

最初に説明した、入力された文字を次々と保存する処理を malloc で記述すると以下のようになる。

char* names[ 100 ] ;
char buff[ 1000 ] ;
int size ;

// データ入力
for( size = 0 ; size < 100 ; size++ ) {
   fgets( buff , sizeof( buff ) , stdin ) ;
   names[ size ] = (char*)malloc( strlen( buff ) + 1 ) ;
   if ( names[ size ] == NULL )
      break ;
   strcpy( names[ size ] , buff ) ;
}
// データを使う処理
for( int i = 0 ; i < size ; i++ ) {
   // names[] を使う処理...
   printf( "%s" , names[ i ] ) ;
}
// データ領域をfreeで解放
for( int i = 0 ; i < size ; i++ )
   free( names[ i ] ) ;

malloc() で確保したメモリ領域は、free() で解放しない場合、メモリ領域は使われないムダな領域が蓄積して、最終的にはメモリ不足で止まるかもしれない。また、大量のムダなメモリ領域ができると、仮想メモリが使われ処理速度の低下が発生するかもしれない。
このような、解放されないメモリ領域が発生することは、メモリーリークと呼ばれる。

確保したメモリは、プロセス毎に管理されているので、長時間動きっぱなしのプログラムでメモリリークが発生すると問題となる。
ただし、プロセス終了と共に確保されているメモリはOSに回収されるので、処理が終わってすぐにプロセス自体も終わるのであれば、free() を書き忘れても問題は発生しない。

高専ライブ:2018年5月13日(第574回)

  • キャンパスウォークの話
  • 母の日の話
  • サイエンス共和国 第29回「iPhoneで儲ける技術者の話 その2 iPhoneサプライヤーの話」
  • ペットの話

担当:越後(2E,MC)、坂田(2C,MIX)、西島(4EI)、中村(教員)

php5からphp7に移行

自宅サーバにて、毎朝 apache2 が止まる症状が続いた。調べてみると、php5 との相性が悪いみたい。

んで、当然ながら、他の管理しているサーバも動かないものが出てきた。ということで、職場の関連サーバも php7 に移行作業を行った。
Ubuntu(trusty) は、PHP5 で問題なし。

moodleサーバは、切り替えたら色々と表示されたな。

切り替えたら、アップロードできるファイルサイズ上限が2MBに減ってしまっていた。

(( /etc/php/7.0/apache2/php.ini ))
upload_max_filesize = 400M
post_max_size = 450M
memory_limit = 500M
# upload_max_filesize < post_max_size < memory_limit で要設定

(( apache 再起動 ))
$ sudo /etc/init.d/apache2 restart

キャンパスウォークのウエルカムボード

キャンパスウォーク2018

今日は、中学生や近隣の方を対象としたキャンパスウォークを開催中です。
電子情報の展示風景。



入出力リダイレクト

プログラムの動作を確認する場合、指定された値を使って計算したり、その結果を他のプログラムで使いたい場合が多い。そこで、入出リダイレクトについて説明する。

入力リダイレクト

以下に上げるような Excel のデータで平均を計算してみよう。

A6 に =AVERAGE(A1:A5) を入力…
というのは、ナシ。Visual Basic で組むのもナシ。(^_^;

Excelで、「ファイル-名前をつけて保存」、「ファイル形式」に「スペース区切りテキスト(.prn)」で保存する。

// 入力値の平均を求める
//   ファイルは、Z:¥foo¥bar¥avg.c にあるとする。
#include <stdio.h>

int main() {
    int count = 0 , sum = 0 ;
    int x ;
    while( scanf( "%d" , &x ) == 1 ) {
        count++ ;
        sum += x ;
    }
    printf( "%lf¥n" , sum / count ) ; // この行は間違い。修正せよ。
    return 0 ;
}

このプログラムを普通に実行したら、キーボードから 83,95,92,95,77 と毎回入力しなければならない。めんどくさくない?

Excel の空白区切りのデータを読み込む

先程の Excel で保存したファイルを同じディレクトリにコピーする。(Z:¥foo¥bar¥avg.prnとする)

コンパイラで、avg.c をコンパイルし、avg.exe ができていることを確認し、
cmd.exe を起動

Z:¥> cd Z:¥foo¥bar
Z:¥foo¥bar> avg.exe < avg.prn

このように、プログラムを起動する時に、通常はキーボードから入力するプログラムに対し、起動時に “< ファイル名” を付けて起動し、ファイル入力に切り替えることを、入力リダイレクトと言う。

出力リダイレクトとグラフ化

前回の授業での sin(x) のプログラムの実行結果を、Excel で確認してみよう。

// sin の値を出力
// ファイルは、Z:¥foo¥bar¥sin.c にあるとする。
#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 ;
}

プログラムを実行する時に、

コンパイラで sin.c をコンパイルし、sin.exe ができていることを確認し
cmd.exe を起動

Z:¥> cd Z:¥foo¥bar             プログラムのディレクトリに移動
Z:¥foo¥bar> sin.exe > sin.csv  プログラムを起動し出力を sin.csv に保存

このように、プログラムを起動する時に、通常は結果を画面に出力するプログラムに対し、起動時に “> ファイル名” を付けて起動し、結果をファイル出力に切り替えることを、出力リダイレクトと言う。

Excelにインポートしてグラフ化

Excel を起動し、「ファイル-インポート」より、「テキストファイル」を選び、「区切り文字」-「スペース区切り」でデータを取り込む。

あとは、取り込まれたデータ範囲を選択し、「挿入」-「グラフ」で好きなグラフ形式を選ぶ。

実数の扱い・レポート-No.1

プログラムの制御構造と実数の取扱いの確認として、以下のレポートを次回講義までに提出せよ。

No.1-1 制御構造

以下の3つ(No.1-1-1,No.1-1-2,No1-1-3)の問題から、No.1-1-「(自分の出席番号 % 3)+1」について、プログラムのフローチャートを描き、その実行順序を20ステップめまで答えよ。

レポートには、

  • 元プログラム
  • フローチャート
  • 実行順序
  • 変数の変化がわかる内容

を明記すること。

No.1-1-1

No.1-1-2

switch-case 文は説明していませんが、挙動をよく調べて回答してください。

No.1-1-3

No.1-2 実数の扱い

自分の携帯電話番号(もしくは家の電話番号)の末尾4桁のうち、2桁を整数部、末尾2桁を小数部として、その値を2進数に変換した結果を、計算方法が判るように答えよ。ただし、結果の2進数の小数部は8桁まで答えよ。

例えば、0778628278 ならば、82.78 )10を、xxxxxxx.xxxxxxxx )2 のように答えること。

実数の注意点・回答編

回答がすぐに見えないように、別記事に分ける

数値の精度に注意

// 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 ;
}

このプログラムの問題点は、終了条件が th != PI で書かれている点。

コンピュータの中では、データはすべて2進数で扱い、データを保存する桁数も有限である。

このため、0.0314159265 を 100 回 加えても、桁末尾の誤差のため、3.14159265 != 3.1415926535となってしまう。(ただしこの説明は10進数で説明しているけど、実際は2進数で同じような現象が発生している。)

int型とdouble型での暗黙の型変換

// 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 ;
}

このプログラムの問題点は、th / 180 。これがゼロになる原因。

コンピュータの中では、整数型 int と、実数 double 型では、計算の仕方が異なる。特に、実数型は、小数点の位置や指数を考えながら計算を行う必要があるため、処理に時間がかかる。このため、大量のデータを扱う場合にはコンピュータにとって簡単な計算となるように書く必要がある。

整数型は、小数点以下の値を持たない。このためコンピュータの中では、th = 5 の時、th / 180 を計算すると、5/180 = 0.0277 ではなく、5/180 = 0 で計算を行う(小数点以下は切り捨て)。

C言語の原則: 暗黙の型変換
    同じ精度で計算できるのなら同じ精度の型で計算する。
    精度が異なる場合は、精度の高い型に変換してから計算する。
        int 演算子 int    = int
        int 演算子 double = double

このようなことが発生するので、y=sin(th…)の行は、以下のように書くべきである。

y = sin( th / 180.0 * 3.1415926535 ) ;  // 180.0 は double 型
y = sin( (double)th / 180 * 3.1415926535 ) ; // 型キャストで double 型
y = sin( double( th ) / 180 * 3.1415926535 ) ; // C++の型変換関数
y = sin( (double)th / 180.0 * 3.1415926535 ) ; // 暗黙の型変換を排除

数値の範囲に注意

// 16bit コンピュータの時代に、
//   画面上のマウスとターゲットの距離を計算したかった
int distance( int x0 , int y0 , int x1 , int y1 ) {
    int dx = x1 - x0 ;
    int dy = y1 - y0 ;
    return sqrt( dx * dx + dy * dy ) ;
}

例えば、このプログラムで (x0,y0)=(0,0) , (x1,y1)=(200,200) 出会った場合、sqrt( 40000 + 40000 ) という計算が出て来る。

ところで、16bit コンピュータにおける int 型は、どうやって覚えているのか?

符号あり整数型

コンピュータの中では、負の数を扱う必要から、2の補数表現が用いられる。

         x = 0000,0000,0000,1010(2)    = 10(10)
        ~x = 1111,1111,1111,0101      1の補数
    ~x + 1 = 1111,1111,1111,0110      1の補数に+1 ⇒ -10(10)
x + ~x     =   1111,1111,1111,1111
x + ~x + 1 = 1,0000,0000,0000,0000 ≒ 0 // 16bit目ははみ出るからzero

このため、16bit コンピュータの int 型で扱える数は、

   max = 0111,1111,1111,1111(2) = 32767(10)
   min = 1000,0000,0000,0000(2) = -32768(10)

40000 は、16bit コンピュータでは、扱える範囲を越えている。

ということで、前述のプログラムは、

// 16bit コンピュータなら、long int 型は 32bit 
int distance( int x0 , int y0 , int x1 , int y1 ) {
    long int dx = x1 - x0 ;
    long int dy = y1 - y0 ;
    return (int)sqrt( dx * dx + dy * dy ) ;
}
// スピードを気にしないのなら(sqrtがdouble型だし...)
double distance( double x0 , double y0 , double x1 , double y1 ) {
    double dx = x1 - x0 ;
    double dy = y1 - y0 ;
    return sqrt( dx * dx + dy * dy ) ;
}

メモリを効率よく使うには

メモリ利用の効率

次にメモリの利用効率の話について解説する。
例えば、1クラスの名前のデータを覚えるなら、以下のような宣言が一般的であろう。

#define MEMBER_SIZE 50
#define NAME_LENGTH 20
char name[ MEMBER_SIZE ][ NAME_LENGTH ] ;

しかしながら、クラスに寿限無とか銀魂の「ビチグソ丸」のような名前の人がいたら、20文字では足りない。 また、クラスの人数も、巨大大学の学生全員を覚えたいとい話であれば、 10000人分を用意する必要がある。 ただし、10000人の寿限無ありを考慮して、5Mbyte の配列を準備したのに、与えられたデータ量が100件で終わってしまうなら、その際のメモリの利用効率は極めて低い。

このため、最も簡単な方法は、以下のように巨大な文字配列に先頭から名前を入れていき、 文字ポインタ配列に、各名前の先頭の場所を入れる方式であれば、 途中に寿限無がいたとしても、問題はない。

char array[2000] = "ayuka¥0mitsuki¥0t-saitoh¥0tomoko¥0....." ;
char *name[ 50 ] = {
array+0 , array+6 , array+14 , array+23 , ...
} ;

この方式であれば、2000byte + 4byte(32bitポインタ)×50 のメモリがあれば、 無駄なメモリ空間も必要最低限とすることができる。

参考:
寿限無(文字数:全角103文字)

ビチクソ丸(文字数:全角210文字)

引用Wikipedia

複素数とクラス・隠蔽化の演習

コンストラクタやメソッドの書き方

前回のプログラムを、もう少しC++のオブジェクト指向っぽい書き方を導入してみる。この際に、分かりやすく記述するため、行数が長くなってきた時のための処理を考慮して、記述してみる。

#include <stdio.h>
#include <math.h>
class Complex {
private:
   double re ;
   double im ;
public:
   // 絶対座標系でも極座標系でも、実部・虚部・絶対値・偏角を扱うため
   inline double get_re() const { return re ; }
   inline double get_im() const { return im ; }
   inline double get_r()  const { return sqrt( re*re + im*im ) ; }
   inline double get_th() const { return atan2( im , re ) ; }

   // 極座標系なら、以下のような getterメソッド を書いておけば、
   //   座標系を気にせず処理がかける。
   // inline double get_re() const { return r * cos( th ) ; }
   // inline double get_r()  const { return r ; }

   // inline は、開いたサブルーチンで実装してくれるので処理速度が遅くならない
   // foo() const { ... } は、オブジェクトに副作用が無いことを示す。

   // コンストラクタのメンバー変数の初期化の書き方
   // Complex( ... )
   // : 要素1( 値1 ) , 要素2( 値2 ) ...
   // {  その他の初期化
   // }
   Complex()                       // コンストラクタ
   : re( 0.0 ) , im( 0.0 ) {}
   Complex( double r , double i )  // コンストラクタ
   : re( r ) , im( i ) {}

   // メソッドのプロトタイプ宣言
   //  メソッドの中身が長くなる場合は、
   //  名前と引数だけを宣言しておく
   void print() ;
   void add( Complex ) ;
   void mul( Complex ) ;
} ; // ←しつこいけどセミコロン忘れないでね。

// クラス宣言の外にメソッドの定義を書く
//   メソッドの中身の記述 「::」をクラス限定子と呼ぶ
void Complex::print() {
   printf( "%lf + j%lf¥n" , get_re() , get_im() ) ;
}
void Complex::add( Complex z ) {
   re = re + z.re ;
   im = im + z.im ;
}
void Complex::mul( Complex z ) {
   double r = re * z.re - im * z.im ;
   double i = re * z.im + im * z.re ;
   re = r ;
   im = i ;
}

int main() {
   Complex a( 1.0 , 2.0 ) ;
   Complex b( 2.0 , 4.0 ) ;
   a.add( b ) ;
   a.print() ;
   return 0 ;
}

参考資料

レポート1(複素数の加減乗除)

授業中に示した上記のプログラムをベースに、 記載されていない減算・除算のプログラムを作成し、レポートを作成する。 レポートには、下記のものを記載すること。

  • プログラムリスト
  • プログラムへの説明
  • 動作確認の結果
  • プログラムより理解できること。
    実際にプログラムを書いてみて分かった問題点など…

システム

最新の投稿(電子情報)

アーカイブ

カテゴリー