ホーム » 2020 (ページ 9)

年別アーカイブ: 2020

2025年5月
 123
45678910
11121314151617
18192021222324
25262728293031

検索・リンク

効率のよいメモリ使用と動的メモリ/スタック/malloc+free

次にメモリの利用効率の話について解説する。

配列宣言でサイズは定数

C言語では、配列宣言を行う時は、本来ならば、配列サイズに変数を使うことはできない。

最近のC(C99)では、実は下記のようなものは、裏で後述のalloca()を使って動いたりするので普通に書けるけど…。(^_^;

void foo( int size ) {
   int array[ size ] ;         // エラー
   for( int i = 0 ; i < size ; i++ )
      array[ i ] = i*i ;
}
int main() {
   foo( 3 ) ;
   foo( 4 ) ;
   return 0 ;
}

メモリ利用の効率

配列サイズには、定数式しか使えないので、1クラスの名前のデータを覚えるなら、以下のような宣言が一般的であろう。

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

しかしながら、クラスに寿限無とか銀魂の「ビチグソ丸」のような名前の人がいたら、20文字では足りない。(“t-saitoh”くんは配列サイズ9byte、”寿限無”くんは配列220byte といった使い方はできない) また、クラスの人数も、巨大大学の学生全員を覚えたいとい話であれば、 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(2020年5月リンク切れてる…)

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

// 巨大な配列
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( "jugemu-jugemu-gokono-surikire..." ) ;
   return 0 ;
}
// str[] s a i t o h ¥0 j u g e m u - .... ¥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() 関数が便利である。alloca の引数には、必要なメモリの byte 数を指定する。100個の整数データを保存するのであれば、int が 32bit の 4byte であれば 400byte を指定する。ただし、int 型は16bitコンピュータなら2byteかもしれないし、64bitコンピュータなら、8byte かもしれないので、sizeof() 演算子を使い、100 * sizeof( int ) と書くべきである。

#include <alloca.h>
void foo( int size ) {
   int* p ;
   // 
   p = (int*)alloca( sizeof( int ) * size ) ;
   for( int i = 0 ; i < size ; i++ )
      p[ i ] = i*i ;
}
int main() {
   foo( 3 ) ;
   foo( 4 ) ;
   return 0 ;
}

alloca() は、指定された byte 数のデータ領域の先頭ポインタを返すが、その領域を 文字を保存するために使うか、int を保存するために使うかは alloca() では解らない。alloca() の返り値は、使う用途に応じて型キャストが必要である。文字を保存するなら、(char*)alloca(…) 、 intを保存するなら (int*)alloca(…) のように使う。

ただし、関数内で alloca で確保したメモリは、その関数が終了すると、その領域は使えなくなる。このため、最後に alloca で確保したメモリが、最初に不要となる…ような使い方でしか使えない。

malloc()とfree()

malloc() は、動的(ヒープ領域)にメモリを確保する命令で、データを保存したい時に malloc() を実行し、不要になった時に free() を実行する。

malloc() では、alloca() と同じように、格納したいデータの byte 数を指定する。また、malloc() は、確保したメモリ領域の先頭を返すが、ヒープメモリが残っていない場合 NULL ポインタを返す。処理が終わってデータ領域をもう使わなくなったら、free() で解放する必要がある。

基本的には、確保したメモリ領域を使い終わった後 free() を実行しないと、再利用できないメモリ領域が残ってしまう。こういう処理を繰り返すと、次第にメモリを食いつぶし、仮想メモリ機能によりハードディスクの読み書きで性能が低下したり、最終的にOSが正しく動けなくなる可能性もある。こういった free() 忘れはメモリーリークと呼ばれ、malloc(),free()に慣れない初心者プログラマーによく見られる。

ヒープメモリは、プロセスの起動と共に確保され、プログラムの終了と同時にOSに返却される。このため、malloc()と処理のあとすぐにプロセスが終了するようなプログラムであれば、free() を忘れても問題はない。授業では、メモリーリークによる重大な問題を理解してもらうため、原則 free() は明記する。

文字列を保存する場合

#include <stdlib.h>
char* names[ 10 ] ;
char  buff[ 1000 ] ;

// 名前を10件読み込む
void inputs() {
   for( int i = 0 ; i < 10 ; i++ ) {
      if ( fgets( buff ,
                  sizeof( buff ) ,
                  stdin ) != NULL ) {
         names[ i ]
            = (char*)malloc( strlen(buff)+1 ) ;
         if ( names[ i ] != NULL )
            strcpy( names[ i ] , buff ) ;
      }
   }
}
// 名前を出力する
void prints() {
   for( int i = 0 ; i < 10 ; i++ )
      printf( "%s" , names[ i ] ) ;
}
int main() {
   // 文字列の入力&出力
   inputs() ;
   prints() ;
   // 使い終わったら、free() で解放
   for( int i = 0 ; i < 10 ; i++ )
      free( names[ i ] ) ;
   return 0 ;
}

文字列を保存する場合には、上記の names[i] への代入のような malloc() と strcpy() を使うことが多い。
このための専用の関数として、strdup() がある。基本的には、以下のような機能である。

char* strdup( char* s ) {
   char* p ;
   if ( (p = (char*)malloc( strlen(s)+1 )) != NULL )
      strcpy( p , s ) ;
   return p ;
}

また、入力した文字列をポインタで保存する場合、以下のようなプログラムを書いてしまいがちであるが、図に示すような状態になることから、別領域にコピーする必要がある。

char  buff[ 1000 ] ;
char* name[10] ;
for( int i = 0 ; i < 10 ; i++ ) {
   if ( fgets( buff , sizeof(buff) , stdin ) != NULL )
      name = buff ;
}

配列に保存する場合

任意サイズの配列を作りたい場合には、malloc() で一括してデータの領域を作成し、その先頭アドレスを用いて配列として扱う。

#include <stdlib.h>
void main() {
   int size ;
   int* array ;
   // 処理するデータ件数を入力
   scanf( "%d" , &size ) ;

   // 整数配列を作る
   if ( (array = (int*)malloc( sizeof(int) * size )) != NULL ) {
      int i ;
      for( i = 0 ; i < size ; i++ )
         array[i] = i*i ; // あんまり意味がないけど
      for( i = 0 ; i < size ; i++ )
         printf( "%d¥n" , array[i] ) ;

      // mallocしたら必ずfree
      free( array ) ;
   }
}

構造体の配列

同じように、任意サイズの構造体の配列を作りたいのであれば、配列サイズに「sizeof( struct Complex ) * データ件数」を指定すればいい。

#include <stdlib.h>
struct Complex {
   double re , im ;
} ;

// 指定した場所にComplexを読み込む。
int input_Complex( struct Complex* p ) {
   return scanf( "%f %f" ,
                 &(p->re) , &(p->re) ) == 2 ;
}

// 指定したComplexを出力
void print_Complex( struct Complex* p ) {
   printf( "%f+j%f¥n" , p->re , p->im ) ;
}
void main() {
   int size ;
   struct Complex* array ;
   // 処理する件数を入力
   scanf( "%d" , &size ) ;
   // 配列を確保して、データの入力&出力
   if ( (array = (struct Complex*)malloc(
                    sizeof(struct Complex) * size )) != NULL ) {
      int i ;
      for( i = 0 ; i < size ; i++ )
         if ( !input_Complex( &array[i] ) )
            break ;
      for( i = 0 ; i < size ; i++ )
         print_Complex( &array[i] ) ;

      // mallocしたら必ずfree
      free( array ) ;
   }
}

派生と継承と仮想関数

前回の派生と継承のイメージを改めて記載する。

// 基底クラス
class Person {
private:
   char name[ 20 ] ;
   int  age ;
public:
   Person( const char s[] , int x )
     : age( x ) {
      strcpy( name , s ) ;
   }
   void print() {
      printf( "%s %d\n" , name , age ) ;
   }
} ;
// 派生クラス(Student は Person から派生)
class Student : public Person {
private:
   char dep[ 20 ] ;
   int  grade ;
public:
   Student( const char s[] , int x ,
            const char d[] , int g )
            : Person( s , x ) // 基底クラスのコンストラクタ
   {  // 追加された処理
      strcpy( dep , d ) ;
      grade = g ;
   }
   void print() {
      Person::print() ;       // 基底クラスPersonで名前と年齢を表示
      printf( "- %s %d\n" , dep , grade ) ;
   }
} ;
int main() {
   Person saitoh( "t-saitoh" , 55 ) ;
   Student yama( "yamada" , 21 , "ES" , 1 ) ;
   Student nomu( "nomura" , 22 , "PS" , 2 ) ; 
   saitoh.print() ; // 表示 t-saitoh 55
   yama.print() ;   // 表示 yamada 21
                    //      - ES 1
   nomu.print() ;   // 表示 nomura 22
   return 0 ;       //      - PS 2
}

このような処理でのデータ構造は、次のようなイメージで表される。

派生クラスでの問題提起

基底クラスのオブジェクトと、派生クラスのオブジェクトを混在してプログラムを記述したらどうなるであろうか?
上記の例では、Person オブジェクトと、Student オブジェクトがあったが、それをひとまとめで扱いたいこともある。

以下の処理では、Person型の saitoh と、Student 型の yama, nomu を、一つの table[] にまとめている。

int main() {
   Person saitoh( "t-saitoh" , 55 ) ;
   Student yama( "yamada" , 21 , "ES" , 1 ) ;
   Student nomu( "nomura" , 22 , "PS" , 2 ) ;

   Person* table[3] = {
      &saitoh , &yama , &nomu ,
   } ;
   for( int i = 0 ; i < 3 ; i++ ) {
      table[ i ]->print() ;
   }
   return 0 ;
}

C++では、Personへのポインタの配列に代入する時、Student型ポインタは、その基底クラスへのポインタとしても扱える。ただし、このように記述すると、table[] には、Person クラスのデータして扱われる。

このため、このプログラムを動かすと、以下のように、名前と年齢だけが3人分表示される。

t-saitoh 55
yamada   21
nomura   22

派生した型に応じた処理

上記のプログラムでは、 Person* table[] に、Person*型,Student*型を混在して保存をした。しかし、Person*として呼び出されると、yama のデータを表示しても、所属・学年は表示されない。上記のプログラムで、所属と名前を表示することはできないのだろうか?

// 混在したPersonを表示
for( int i = 0 ; i < 3 ; i++ )
   table[i]->print() ;
// Student は、所属と名前を表示して欲しい
t-saitoh 55
yamada 21
- ES 1
nomura 22
- PS 2

上記のプログラムでは、Person型では、後でStudent型と区別ができないと困るので、Person型に、Person型(=0)なのか、Student型(=1)なのか区別するための type という要素を追加し、type=1ならば、Student型として扱うようにしてみた。

// 基底クラス
class Person {
private:
   int  type ; // 型識別情報
   char name[ 20 ] ;
   int  age ;
public:
   Person( int tp , const char s[] , int x )
     : type( tp ) , age( x ) {
      strcpy( name , s ) ;
   }
   int type_person() { return type ; }
   void print() {
      printf( "%s %d\n" , name , age ) ;
   }
} ;
// 派生クラス(Student は Person から派生)
class Student : public Person {
private:
   char dep[ 20 ] ;
   int  grade ;
public:
   Student( int tp , const char s[] , int x ,
            const char d[] , int g )
            : Person( tp , s , x ) // 基底クラスのコンストラクタ
   {  // 追加された処理
      strcpy( dep , d ) ;
      grade = g ;
   }
   void print() {
      Person::print() ;       // 基底クラスPersonで名前と年齢を表示
      printf( "- %s %d\n" , dep , grade ) ;
   }
} ;
int main() {
   // type=0 は Person 型、type=1は Student 型
   Person saitoh( 0 , "t-saitoh" , 55 ) ;
   Student yama( 1 , "yamada" , 21 , "ES" , 1 ) ;
   Student nomu( 1 , "nomura" , 22 , "PS" , 2 ) ;

   Person* table[3] = {
      &saitoh , &yama , &nomu ,
   } ;
   for( int i = 0 ; i < 3 ; i++ ) {
      switch( table[i]->type_person() ) {
      case 0 :
         table[i]->print() ;
         break ;
      case 1 :
         // 強制的にStudent*型として print() を呼び出す。
         //   最近のC++なら、(static_cast<Student*>(table[i]))->>print() ;
         ((Student*)table[i])->print() ;
         break ;
      }
   }
   return 0 ;
}

しかし、このプログラムでは、プログラマーがこのデータは、Personなので type=0 で初期化とか、Studentなので type=1 で初期化といったことを記述する必要がある。また、型情報(type)に応じて、その型にふさわしい処理を呼び出すための switch 文が必要になる。

もし、派生したクラスの種類がいくつもあるのなら、型情報の代入は注意深く書かないとバグの元になるし、型に応じた分岐は巨大なものになるだろう。実際、オブジェクト指向プログラミングが普及する前の初期の GUI プログラミングでは、巨大な switch 文が問題となっていた。

仮想関数

上記の、型情報の埋め込みと巨大なswitch文の問題の解決策として、C++では仮想関数(Virtual Function)が使える。

型に応じて異なる処理をしたい関数があったら、その関数の前に virtual と書くだけで良い。このような関数を、仮想関数と呼ぶ。

// 基底クラス
class Person {
private:
   char name[ 20 ] ;
   int  age ;
public:
   Person( const char s[] , int x )
     : age( x ) {
      strcpy( name , s ) ;
   }
   virtual void print() {
      printf( "%s %d\n" , name , age ) ;
   }
} ;
// 派生クラス(Student は Person から派生)
class Student : public Person {
private:
   char dep[ 20 ] ;
   int  grade ;
public:
   Student( const char s[] , int x ,
            const char d[] , int g )
            : Person( s , x ) // 基底クラスのコンストラクタ
   {  // 追加された処理
      strcpy( dep , d ) ;
      grade = g ;
   }
   virtual void print() {
      Person::print() ;       // 基底クラスPersonで名前と年齢を表示
      printf( "- %s %d\n" , dep , grade ) ;
   }
} ;
int main() {
   // type=0 は Person 型、type=1は Student 型
   Person saitoh( "t-saitoh" , 55 ) ;
   Student yama( "yamada" , 21 , "ES" , 1 ) ;
   Student nomu( "nomura" , 22 , "PS" , 2 ) ;

   Person* table[3] = {
      &saitoh , &yama , &nomu ,
   } ;
   for( int i = 0 ; i < 3 ; i++ ) {
      table[i]->print() ;
   }
   return 0 ;
}

クラスの中に仮想関数が使われると、C++ では、プログラム上で見えないが、何らかの型情報をオブジェクトの中に保存してくれる。

また、仮想関数が呼び出されると、その型情報を元に、ふさわしい関数を自動的に呼び出してくれる。このため、プログラムも table[i]->print() といった極めて簡単に記述できるようになる。

関数ポインタ

仮想関数の仕組みを実現するためには、関数ポインタが使われる。

以下の例では、返り値=int,引数(int,int)の関数( int(*)(int,int) )へのポインタfpに、最初はaddが代入され、(*fp)(3,4) により、7が求まる。

int add( int a , int b ) {
   return a + b ;
}
int mul( int a , int b ) {
   return a * b ;
}
int main() {
   int (*fp)( int , int ) ;
   fp = add ;
   printf( "%d\n" , (*fp)( 3 , 4 ) ) ; // 3+4=7
   fp = mul ;
   printf( "%d\n" , (*fp)( 3 , 4 ) ) ; // 3*4=12

   int (*ftable[2])( int , int ) = {
      add , mul ,
   } ;
   for( int i = 0 ; i < 2 ; i++ )
      printf( "%d\n" , (*ftable[i])( 3 , 4 ) ) ;
   return 0 ;
}

仮想関数を使うクラスが宣言されると、一般的にそのコンストラクタでは、各クラス毎の仮想関数へのポインタのテーブルが型情報として保存されるのが一般的。仮想関数の呼び出しでは、仮想関数へのポインタを使って処理を呼び出す。このため効率よく仮想関数を動かすことができる。

ソートのアルゴリズムの分岐点

ソートアルゴリズムの処理時間で、選択法よりクイックソートが有利になる分岐点を求める問題で、

の結果が、N=53.1 になる…との説明が判らないとの質問。# 53.017 が正しいとの結果だったが。

あてずっぽうな計算方法

Excelで実際に計算すればいい。B列2行目に =A2/LOG(A2) を入れて、A列に2,3,…. と数字を入れて変化させて、30.74 に近い場所を探すと53となった。# 手抜きだな!

ニュートン法で解く

方程式で解けない場合は、ニュートン法 f(x)=0 の式でxの値を求める…のが定番の方法。

この場合は f(x) = x/log(x) – 1000/25/log(20) として、ニュートン法の漸化式を解けばいい。
ちょいとプログラムを書いてみた。

((( nlogn.cxx )))
#include <stdio.h>
#include <math.h>

// N/logN = 1000/25/log20 をニュートン法で解く                                                                          
//                                                                                                                      
//      f(x) = x/log(x) - 1000/25/log20                                                                                 
//      f'(x) = log(10) (log(x) - 1) / log(x)^2                                                                         
//                                                                                                                      
//      x <= x + f(x) / f'(x)                                                                                           

//      要注意 C言語のlog関数                                                                                           
//              log(x) = 自然対数を底とするlog                                                                          
//              log10(x) = 10を底とするlog                                                                              

double f( double x ) {
  return x / log10(x) - 1000.0/25.0/log10(20.0) ;
}

double df( double x ) {
  return log(10.0) * ( log(x) - 1.0 ) / pow( log(x) , 2.0 ) ;
}

int main() {
  double x = 5.0 ;

  for( int i = 0 ; i < 5 ; i++ ) {
    double f1 = f(x) ;
    double f2 = df(x) ;
    x = x - f1 / f2 ;
    printf( "%d %lf\n" , i , x ) ;
  }
  return 0 ;
}

((( 実行結果 )))
$ g++ nlogn.cxx
$ ./a.out
0 48.547041
1 52.983539
2 53.016894
3 53.016896
4 53.016896

うーむ、Webの資料では、53 に近い値だし、あてずっぽうで 53.1 となる…って書いたけど、本当は 53.017 だな。m(_ _)m

数年前までの電子情報の学生さんなら、TI の関数電卓持ってたから「授業で解いて…」というと、結果をすぐに答えてくれる優秀な人がクラスに1人くらい必ずいたけど、最近は BYOD でパソコンを使うので TI の関数電卓持ってないから、解いてくれるヤツいないのよね。

SolidWorks使い方入門

SolidWorks

3Dプリンタ AFINIA H800

ソート処理の見積もりとポインタ処理

前回の授業では、再帰処理やソートアルゴリズムの処理時間の見積もりについて説明を行った。

ソート処理の見積もり

この際の練習問題の1つめは、「再帰方程式の理解度確認の回答」にて解説を示す。

最後の練習問題はここで説明を行う。

選択法とクイックソートの処理時間の比較

例として、データ数N=20件で、選択法なら10msec、クイックソートで20msecかかったとする。

これより、選択法の処理時間を、クイックソートの処理時間を、とすると、





よって、

処理時間が逆転するときのデータ件数は、2つのグラフの交点を求めることになるので、

より、以下の式を解いた答えとなる。これは通常の方程式では解けないが電卓で求めると、N=53.1 ほどかな。(2020/05/26) 真面目に解いたら N=53.017 だった。

実際にも、クイックソートを再帰呼び出しだけでプログラムを書くと、データ件数が少ない場合は選択法などのアルゴリズムの方が処理時間が早い。このため、C言語などの組み込み関数の qsort() などは、データ件数が20とか30とか一定数を下回ったら、再帰を行わずに選択法でソートを行うのが一般的である。

ポインタ処理

ここからは、次のメモリの消費を考慮したプログラムの説明を行うが、ポインタの処理に慣れない人が多いので、ポインタを使ったプログラミングについて説明を行う。

値渡し(call by value)

// 値渡しのプログラム
void foo( int x ) {  // x は局所変数(仮引数は呼出時に
                     // 対応する実引数で初期化される。
   x++ ;
   printf( "%d¥n" , x ) ;
}
void main() {
   int a = 123 ;
   foo( a ) ;  // 124
               // 処理後も main::a は 123 のまま。
   foo( a ) ;  // 124
}

このプログラムでは、aの値は変化せずに、124,124 が表示される。

言い方を変えるなら、呼び出し側main() では、関数の foo() の処理の影響を受けない。このように、関数には仮引数の値を渡すことを、値渡し(call by value)と言う。

でも、プログラムによっては、124,125 と変化して欲しい場合もある。
どのように記述すべきだろうか?

// 大域変数を使う場合
int x ;
void foo() {
   x++ ;
   printf( "%d¥n" , x ) ;
}
void main() {
   x = 123 ;
   foo() ;  // 124
   foo() ;  // 125
}

しかし、このプログラムは大域変数を使うために、間違いを引き起こしやすい。

// 大域変数が原因で予想外の挙動をしめす簡単な例
int i ;
void foo() {
   for( i = 0 ; i < 2 ; i++ )
      printf( "A" ) ;
}
void main() {
   for( i = 0 ; i < 3 ; i++ )  // このプログラムでは、AA AA AA と
      foo() ;                   // 表示されない。
}

ポインタ渡し(call by pointer)

C言語で引数を通して、呼び出し側の値を変化して欲しい場合は、(引数を経由して関数の副作用を受け取るには)、変更して欲しい変数のアドレスを渡し、関数側では、ポインタ変数を使って受け取った変数のアドレスの示す場所の値を操作する。このような値の受け渡し方法は、ポインタ渡し(call by pointer)と呼ぶ。

// ポインタ渡しのプログラム
void foo( int* p ) {  // p はポインタ
   (*p)++ ;
   printf( "%d¥n" , *p ) ;
}
void main() {
   int a = 123 ;
   foo( &a ) ;  // 124
                // 処理後 main::a は 124 に増えている。
   foo( &a ) ;  // 124
}               // さらに125と増える。

C言語では、関数から結果をもらうには、通常は関数の返り値を使う。しかし、返り値は1つの値しか受け取ることができないので、上記のようにポインタを使って、呼び出し側は:結果を入れてもらう場所を伝え、関数側は:指定されたアドレスに結果を書む。

ポインタの加算と配列アドレス

ポインタに整数値を加えることは、アクセスする場所が、指定された分だけ後ろにずれることを意味する。

// ポインタ加算の例
int a[ 5 ] = { 11 , 22 , 33 , 44 , 55 } ;

void main() {
   int* p ;
                               //            p∇
   p = &a[2] ;                 // a[] : 11,22,33,44,55
                               //       -2    +0 +1
   printf( "%d¥n" , *p ) ;     // 33  p[0]
   printf( "%d¥n" , *(p+1) ) ; // 44  p[1]
   printf( "%d¥n" , *(p-2) ) ; // 11  p[-2]

   p = a ;                  //      p∇
   printf( "%d¥n" , *p ) ;  // a[] : 11,22,33,44,55
   p++ ;                    //       → p∇
   printf( "%d¥n" , *p ) ;  // a[] : 11,22,33,44,55
   p += 2 ;                 //           → → p∇
   printf( "%d¥n" , *p ) ;  // a[] : 11,22,33,44,55
}

ここで、注意すべき点は、ポインタの加算した場所の参照と、配列の参照は同じ意味となる。

*(p + 整数式)p[ 整数式 ] は同じ意味

特に配列 a[] の a だけを記述すると、配列の先頭を意味することに注意。

構造体とポインタ

構造体を関数に渡して処理を行う例を示す。

struct Person {
   char name[ 10 ] ;
   int  age ;
} ;
struct Person table[3] = {
   { "t-saitoh" , 55 } ,
   { "tomoko" ,   44 } ,
   { "mitsuki" ,  19 } ,
} ;

void print_Person( struct Person* p ) {
   printf( "%s %d\n" ,
           (*p).name , // * と . では . の方が優先順位が高い
                       // p->name と簡単に書ける。
           p->age ) ;  // (*p).age の簡単な書き方
}

void main() {
   for( int i = 0 ; i < 3 ; i++ ) {
      print_Person( &(table[i]) ) ;
   // print_Person( table + i ) ; でも良い
   }
}

構造体へのポインタの中の要素を参照する時には、アロー演算子 -> を使う。

練習問題(2018年度中間試験問題より)

派生と継承

隠ぺい化の次のステップとして、派生・継承を説明する。オブジェクト指向プログラミングでは、一番基本となるデータ構造を宣言し、その基本構造に様々な機能を追加した派生クラスを記述することでプログラムを作成する。今回は、その派生を理解するためにC言語で発生する問題点を考える。

派生を使わずに書くと…

元となるデータ構造(例えばPersonが名前と年齢)でプログラムを作っていて、 途中でその特殊パターンとして、所属と学年を加えた学生(Student)という データ構造を作るとする。

// 元となる構造体(Person) / 基底クラス
struct Person {
   char name[ 20 ] ; // 名前
   int  age ;        // 年齢
} ;
// 初期化関数
void set_Person( struct Person* p ,
                 char s[] , int x ) {
   strcpy( p->name , s ) ;
   p->age = x ;
}
// 表示関数
void print_Person( struct Person* p ) {
   printf( "%s %d\n" , p->name , p->age ) ;
}
int main() {
   struct Person saitoh ;
   set_Person( &saitoh , "t-saitoh" , 50 ) ;
   print_Person( &saitoh ) ;
   return 0 ;
}

パターン1(そのまんま…)

上記のPersonに、所属と学年を加えるのであれば、以下の方法がある。 しかし以下パターン1は、要素名がname,ageという共通な部分があるようにみえるが、 プログラム上は、PersonとPersonStudent1は、まるっきり関係のない別の型にすぎない。

このため、元データと共通部分があっても、同じ処理を改めて書き直しになる。

// 元のデータに追加要素(パターン1)
struct PersonStudent1 {
   // Personと同じ部分
   char name[ 20 ] ; // 名前
   int  age ;        // 年齢

   // 追加部分
   char dep[ 20 ] ;  // 所属
   int  grade ;      // 学年
} ;
void set_PersonStudent1( struct PersonStudent1* p ,
                         char s[] , int x ,
                         char d[] , int g ) {
   // set_Personと同じ処理を書いている。
   strcpy( p->name , s ) ;
   p->age = x ;

   // 追加された処理
   strcpy( p->dep , d ) ;
   p->grade = g ;
}

// 名前と年齢 / 所属と学年を表示
void print_PersonStudent1( struct PersonStudent1* p ) {
   // print_Personと同じ処理を書いている。
   printf( "%s %d\n" , p->name , p->age ) ;
   printf( "- %s %d¥n" , p->dep , p->grade ) ;
}

int main() {
   struct PersonStudent1 yama1 ;
   set_PersonStudent1( &yama1 ,
                       "yama" , 22 , "PS" , 2 ) ;
   print_PersonStudent1( &yama1 ) ;
   return 0 ;
}

パターン2(元データの処理を少し使って…)

パターン1では、機能が追加された新しいデータ構造のために、同じような処理を改めて書くことになりプログラムの記述量を減らせない。面倒なので、 元データ用の関数をうまく使うように書いてみる。

// 元のデータに追加要素(パターン2)
struct PersonStudent2 {
   // 元のデータPerson
   struct Person person ;

   // 追加部分
   char          dep[ 20 ] ;
   int           grade ;
} ;

void set_PersonStudent2( struct PersonStudent2* p ,
                         char s[] , int x ,
                         char d[] , int g ) {
   // Personの関数を部分的に使う
   set_Person( &(p->person) , s , x ) ;

   // 追加分はしかたない
   strcpy( p->dep , d ) ;
   p->grade = g ;
}

void print_PersonStudent2( struct PersonStudent2* p ) {
   // Personの関数を使う。
   print_Person( &p->person ) ;
   printf( "- %s %d¥n" , p->dep , p->grade ) ; 
}

int main() {
   struct PersonStudent2 yama2 ;
   set_PersonStudent2( &yama2 ,
                       "yama" , 22 , "PS" , 2 ) ;
   print_PersonStudent2( &yama2 ) ;
   return 0 ;
}

このパターン2であれば、元データ Person の処理をうまく使っているので、 プログラムの記述量を減らすことはできるようになった。

しかし、print_PersonStudent2() のような処理は、元データ構造が同じなのに、 いちいちプログラムを記述するのは面倒ではないか?

そこで、元データの処理を拡張し、処理の流用ができないであろうか?

基底クラスから派生クラスを作る

オブジェクト指向では、元データ(基底クラス)に新たな要素を加えたクラス(派生クラス)を 作ることを「派生」と呼ぶ。派生クラスを定義するときは、クラス名の後ろに、 「:」「public/protected/private」基底クラス名を書く。

// 基底クラス
class Person {
private:
   char name[ 20 ] ;
   int  age ;
public:
   Person( const char s[] , int x )
     : age( x ) {
      strcpy( name , s ) ;
   }
   void print() {
      printf( "%s %d\n" , name , age ) ;
   }
} ;
// 派生クラス(Student は Person から派生)
class Student : public Person {
private:
   // 追加部分
   char dep[ 20 ] ;
   int  grade ;
public:
   Student( const char s[] , int x ,
            const char d[] , int g )
            : Person( s , x ) // 基底クラスのコンストラクタ
   {  // 追加された処理
      strcpy( dep , d ) ;
      grade = g ;
   }
} ;

int main() {
   Person saitoh( "t-saitoh" , 50 ) ;
   saitoh.print() ;
   Student yama( "yama" , 22 , "PS" , 2 ) ;
   yama.print() ;
   return 0 ;
}

ここで注目すべき点は、main()の中で、Studentクラス”yama”に対し、yama.print() を呼び出しているが、パターン2であれば、print_PersonStudent2()に相当するプログラムを 記述していない。 しかし、この派生を使うと Person の print() が自動的に流用することができる。 これは、基底クラスのメソッドを「継承」しているから、 このように書け、名前と年齢「yama 22」が表示される。

さらに、Student の中に、以下のような Student 専用の新しい print()を記述してもよい。

class Student ...略... {
   ...略...

   // Student クラス専用の print() 
   void print() {
      // 親クラス Person の print() を呼び出す
      Person::print() ;
      // Student クラス用の処理
      printf( "%s %d\n" , dep , grade ) ;
   }
} ;
void main() {
   ...略...
   Student yama( "yama" , 22 , "PS" , 2 ) ;
   yama.print() ;
}

この場合は、継承ではなく機能が上書き(オーバーライト)されるので、 「yama 22 / PS 2」が表示される。

派生クラスを作る際の後ろに記述した、public は、他にも protected , private を 記述できる。

public    だれもがアクセス可能。
protected であれば、派生クラスからアクセスが可能。
          派生クラスであれば、通常は protected で使うのが一般的。
private   派生クラスでもアクセス不可。

仮想関数への伏線

上記のような派生したプログラムを記述した場合、以下のようなプログラムでは何が起こるであろうか?

class Student ... {
   :
   void print() {
      Person::print() ;                    // 名前と年齢を表示
      printf( " %s %d¥n" , dep , grade ) ; // 所属と学年を表示
   }
} ;
int main() {
   Person saitoh( "t-saitoh" , 55 ) ;
   saitoh.print() ;                // t-saitoh 55 名前と年齢を表示

   Student mitsu( "mitsuki" , 20 , "KIT" ,  3 ) ;
   Student ayuka( "ayuka" ,   18 , "EI" ,   4 ) ;
   mitsu.print() ;                 // mitsuki 20 / KIT 3  名前,年齢,所属,学年を表示
   ayuka.print() ;                 // ayuka 18   / EI  4  名前,年齢,所属,学年を表示

   Person* family[] = {
      &saitoh , &mitsu , &ayuka ,  // 配列の中に、Personへのポインタと
   } ;                             // Studentへのポインタが混在している
                                   // 派生クラスのポインタは、
                                   // 基底クラスのポインタとしても扱える
   for( int i = 0 ; i < 3 ; i++ )
      family[ i ]->print() ;       // t-saitoh 55/mitsuki 20/ayuka 18
   return 0 ;                      // が表示される。 
}                                  // # "mitsuki 20/KIT 3" とか "ayuka 18/EI 4"
                                   // # が表示されてほしい?

Remote-sshでcould not to establish…

PHPとデータベースの実験で、クラウドサーバ上のファイルを触るために、VSCode に Remote-ssh をインストールして、サーバのファイルを編集だけど、何人かの学生がログインできない様子。

“Could not establish connection to ホスト名”といったエラーが表示される。同様のエラーの情報を探していたら、以下のようにremotePlatformを指定すれば良いとの情報。

Remote-sshの設定⚙の、「拡張機能の設定」を選び、Remote-sshのRemote Platformを編集するためのsettings.jsonの編集画面となる。

この中で、”remote.SSH.remotePlatform”に、接続相手の名前とOS種別(今回はLinux)を設定する。

再帰方程式の理解度確認の解答

再帰呼び出しと再起方程式の資料の中の「理解度確認」の解答

解答

pyraをループで

// pyra() をループで書いたら。
int pyra( int x ) {
   int ans = 0 ;
   for( int i = 1 ; i <= x ; i++ )
      ans += i * i ;
   return ans ;
}

2分探索法を再帰で

// 2分探索を再帰で書いたら
int find( int array[] , int L , int R , int key ) {
   if ( R == L ) {
      return 0 ; // みつからない
   } else {
      int M = (L + R) / 2 ;
      if ( array[ M ] == key )     // みつかった
         return 1 ;           
      else if ( array[ M ] > key ) // 左側にある - 末尾再帰
         return find( array , L , M , key ) ;
      else                         // 右側にある
         return find( array , M+1 , R , key ) ;
   }
}

このプログラムでは、検索する範囲がLとRで与えられ、この範囲の幅が再帰が呼び出される毎に半分になっていく。

であれば、処理時間は対象となるデータ件数 N = R – L によって、処理時間は変化するので、T(N) で考えると、(ただし途中でデータが見つからない最悪の場合で式を示す)

データ件数0件では、if,R==L,return 0の処理を行う。このため、以下の様な再帰方程式の細小時での処理時間は、

  … ①

となる。(find(件数=1)の次は必ずfind(件数=0)が実行されるのででも良い。)

Nは整数という条件をつけないと、N=1/2, 1/4, 1/8…で止まらない…と勘違いする人もいるし、データ件数が1件になったら再帰がとまると考えた方が分かりやすいので、の方が都合がいいかな…

データが1以上の時で、対象データが L..Mの範囲にあるのなら、再帰の時に対象データ件数は半分になるから、if,R==L,else,if,array[M]==key,else-if,array[M]>keyの処理時間(Tb) と find(…,L,M,…) の処理時間を要するので、次の式となる。

  … ②

一方、(M+1)..Rの範囲にあるのなら同様に、if,R==L,else,if,array[M]==key,else-if,array[M]>key,elseの処理時間(Tc)と、find(…,M+1,R,…) の処理時間なので、

  … ③

となる。ただ、処理時間の見積もりでは厳密な分析が求められないので、(N-1)/2 だと一般式が複雑になるので、N/2 で考える。さらにTbTcの処理時間は少しの時間の違いだし、確率1/2でTbTcのどちらかを実行するので、その平均時間をTbで表すことにすれば、③は②とほぼ同じと見なすことができるので、再帰方程式は①と②で良い。

fib()の再帰方程式

再帰のフィボナッチ数 fib() の処理時間に相応しい再帰方程式は、xの値(Nとする)が2以下の時は、return を実行するだけ。3以上の時は、if,else,return,+ の処理時間と、fib(x-2)の処理時間、fib(x-1)の処理時間を要する。

よって、再帰方程式は、以下の式となる。

これを代入法で一般式を求めると、T(N)=fib(N-2)×Tb+fib(N+1)×Tかな?よって、再帰のfib()の処理時間は、フィボナッチ数に比例する。ちなみに、フィボナッチ数ピネの公式で一般項が示されているので、処理時間のオーダーは、次式となる。

コンピュータとN進数

3年の情報制御基礎の授業の一回目。この授業では、情報系以外の学生も受講することから、基礎的な共通的な話題を中心に説明を行う。

情報制御基礎のシラバス

情報制御基礎では、ここに上げたシラバスに沿って授業を行う。

基本的に、センサーから読み取ったデータを使って動くシステムを作る場合の、知識ということでアナログ量・デジタル量の話から、移動平均やデータ差分といった数値処理や、そこで求まった値を制御に用いるための基礎的な話を行う。

コンピュータと組み込み系

最近では、コンピュータといっても様々な所で使われている。(1)科学技術計算用の大型コンピュータやインターネットの処理を行うサーバ群、(2)デスクトップパソコン、(3)タブレットPCやスマートフォンのような端末、(4)電化製品の中に収まるようなワンチップコンピュータなどがある。


(1) サーバ:ブレードサーバ

(4) ワンチップコンピュータ:PIC

身近で使われている情報制御という点では、(4)のような小型のコンピュータも多く、こういったものは組み込み型コンピュータとも呼ばれる。しかし、こういったコンピュータは、小さく機能も限られているので、

  • 組み込み系では、扱える数値が8bit や 16bit といった精度しかなかったり、
  • 複雑な計算をするには、処理時間がかかったりする

ため、注意が必要である。

この情報制御基礎の授業では、組み込み系のコンピュータでも数値を正しく扱うための知識や、こういった小さいコンピュータで制御を行うことを踏まえた知識を中心に説明を行う。

2進数と10進数

コンピュータの中では、電圧が高い/低いといった状態で0と1の2通りの状態を表し、その0/1を組み合わせて、大きな数字を表す(2進数)。

練習として、2進数を10進数で表したり、10進数を2進数に直してみよう。

N進数を10進数に変換

N進数で “abcde” があったとする。(2進数で”10101″とか、10進数で”12345″とか)

この値は、以下のような式で表せる。

(例1)

(例2)

10進数をN進数に変換

N進数のは、前式を変形すると、以下のような式で表せることから、

値をNで割った余りを求めると、N進数の最下位桁eを取り出せる。

このため、10進数で与えられた35を2進数に変換するのであれば、35を2で次々と割った余りを、下の桁から書きならべれば2進数100011)2が得られる。

実数の場合

途中に小数点を含むN進数のab.cde)Nであれば、以下の値を意味する。

ここで、小数点以下だけを取り出した、0.cde)Nを考えると、

の値に、Nをかけると、次のように変形できる。

この式を見ると、小数部にNをかけると、整数部分に小数点以下1桁目が取り出せる

このため、10進数で与えられた、0.625を2進数に変換するのであれば、0.625に次々と2をかけて、その整数部を上の桁から書きならべれば、2進数0.101)2が得られる。

ただし、10進数で0.1という値で、上記の計算を行うと、延々と繰り返しが発生する。つまり、無限小数になるので注意せよ。

2の補数と負の数

コンピュータの中で引き算を行う場合には、2の補数がよく使われる。2の補数とは、2進数の0と1を入替えた結果(1の補数)に、1を加えた数である。

元の数に2の補数を加えると(2進数が8bitの数であれば)、どのような数でも1,0000,0000という値になる。この先頭の9bit目が必ずはみ出し、この値を覚えないのであれば、元の数+2の補数=0とみなすことができる。このことから、2の補数= (-元の数) であり、負の数を扱うのに都合が良い。

練習問題

(1) 自分の誕生日で、整数部を誕生日の日、小数点以下を誕生日の月とした値について、2進数に変換せよ。(例えば、2月7日の場合は、”7.02″という小数点を含む10進数とする。)

変換の際には、上の説明の中にあるような計算手順を示すこと。また、その2進数を10進数に直し、元の値と同じか確認すること。(ただし、結果の2進数が無限小数になる場合最大7桁まで求めれば良い。同じ値か確認する際には無限少数の場合は近い値になるか確認すること)

(2) 自分の誕生日の日と、自分の学籍番号の下2桁の値を加えた値について、8bitの2進数で表わせ。(2月7日生まれの出席番号13番なら7+13=21)

その後、8bitの2進数として、2の補数を求めよ。また、元の数と2の補数を加えた値についても検証すること。

VSCodeとnode.js

サーバを使ったセキュリティの実験で、VSCode の Remote-ssh を使って作業を行う方式をとっているが、実験中に何気なくpsコマンドを実行したら、入れた記憶のない node が動いている。

確認したら、$HOME/.vscode-server ディレクトリの中に、色々とファイルが置かれ、node.js などを使っている見たい。ls -R .vscode-server しても大量のファイルがある。

emacs の tramp みたいに、その都度 ssh でファイルをコピーしているのかと思ったけど、なんかややこしそうなテクニックを使っているみたいだな。

システム

最新の投稿(電子情報)

アーカイブ

カテゴリー