ホーム » 2021

年別アーカイブ: 2021

2021年6月
 12345
6789101112
13141516171819
20212223242526
27282930  

最新の投稿(電子情報)

アーカイブ

カテゴリー

多重継承の問題

派生や継承について、一通りの説明が終わったので、最後に特殊な継承の問題を説明し、2回目のレポート課題を示す。

動物・鳥類・哺乳類クラス

派生や継承を使うと、親子関係のあるデータ構造をうまく表現できることを、ここまでの授業で示してきた。

しかしながら、以下に述べるような例では、問題が発生する。

// 動物クラス
class Animal {
private:
  char name[ 10 ] ;
public:
  Animal( const char s[] ) {
    strcpy( name , s ) ;
  }
  const char* get_name() const { return name ; }
  virtual void move() = 0 ;
  virtual void birth() = 0 ;
} ;

// 鳥類クラス
class Bird : public Animal {
public:
  Bird( const char s[] ) : Animal( s ) {}
  virtual void move() {
    printf( "%s fry.\n" , get_name() ) ;
  }
  virtual void birth() {
    printf( "%s lay egg.\n" , get_name() ) ;
  }
} ;

// 哺乳類クラス
class Mammal : public Animal {
public:
  Mammal( const char s[] ) : Animal( s ) {}
  virtual void move() {
    printf( "%s walk.\n" , get_name() ) ;
  }
  virtual void birth() {
    printf( "%s lay baby.\n" , get_name() ) ;
  }
} ;

int main() {
  Bird chiken( "piyo" ) ;
  chiken.move() ;
  chiken.birth() ;
  Mammal cat( "tama" ) ;
  cat.move() ;
  cat.birth() ;
  return 0 ;
}

ここで、カモノハシを作るのであれば、どうすれば良いだろうか?

鳥類・哺乳類とは別にカモノハシを作る

class SeaBream : public Animal {
public:
  Mammal( const char s[] ) : Animal( s ) {}
  virtual void move() {
    printf( "%s walk.\n" , get_name() ) ;
  }
  virtual void birth() {
    printf( "%s lay egg.\n" , get_name() ) ;
  }
} ;

この例では、簡単な処理だが、move() の中身が複雑であれば、改めて move() を宣言するのではなく、継承するだけの書き方ができないだろうか?

多重継承

C++ には、複数のクラスから、派生する多重継承という機能がある。であれば、鳥類と哺乳類から進化したのだから、以下のように書きたい。

class SeaBream : public Bird , Mammal {
} ;

しかし、カモノハシに move() を呼び出すと、鳥類の move() と哺乳類の move() のどちらを動かすか曖昧になる。また、派生クラスは親クラスのデータ領域と、派生クラスのデータ領域を持つため、鳥類の name[] と、哺乳類の name[] を二つ持つことになる。

足と羽のクラスを作る場合

class Animal {
private:
  char name[ 10 ] ;
public:
  Animal( const char s[] ) {
    strcpy( name , s ) ;
  }
  const char* get_name() const { return name ; }
  virtual void move() = 0 ;
} ;
// 羽
class Wing {
public:
   const char* move_method() { return "fly" ; }
} ;
// 
class Leg {
public:
   const char* move_method() { return "walk" ; }
} ;
class Bird : public Animal , Wind {
public:
  Bird( const char s[] ) : Animal( s ) {}
  virtual void move() {
    printf( "%s %s.\n" , get_name() , move_method() ) ;
  }
} ;
class Mammal : public Animal , Leg {
public:
  Mammal( const char s[] ) : Animal( s ) {}
  virtual void move() {
    printf( "%s %s.\n" , get_name() , move_method() ) ;
  }
} ;

C++では、以下のような方法で、ダイヤモンド型の継承問題を解決できる。

class Animal {
private:
   char name[ 10 ] ;
public:
   Animal( const char s[] ) {
      strcpy( name , s ) ;
   }
   const char* get_name() const { return name ; }
   virtual void move() = 0 ;
   virtual void birth() = 0 ;
} ;

// 鳥類クラス
class Bird : public virtual Animal {
public:
   Bird( const char s[] ) : Animal( s ) {}
   virtual void move() {
      printf( "%s fry.\n" , get_name() ) ;
   }
   virtual void birth() {
      printf( "%s lay egg.\n" , get_name() ) ;
   }
} ;

// 哺乳類クラス
class Mammal : public virtual Animal {
public:
   Mammal( const char s[] ) : Animal( s ) {}
   virtual void move() {
      printf( "%s walk.\n" , get_name() ) ;
   }
   virtual void birth() {
      printf( "%s lay baby.\n" , get_name() ) ;
   }
} ;

class SeaBream : public virtual Bird , virtual Mammal {
public:
   SeaBream( const char s[] ) : Animal( s ) {}
   void move() {
      Mammal::move() ;
   }
   void birth() {
      Bird::birth() ;
   }
} ;

ただし、多重継承は親クラスの情報と、メソッドを継承する。この場合、通常だと name[] を二つ持つことになるので、問題が発生する。そこで、親クラスの継承に virtual を指定することで、ダイヤモンド型継承の 2つの属性をうまく処理してくれるようになる。

しかし、多重継承は処理の曖昧さや効率の悪さもあることから、採用されていないオブジェクト指向言語も多い。特に Java は、多重継承を使えない。その代わりに interface という機能が使えるようになっている。

様々な2次元配列

#include <stdio.h>
#include <stdlib.h>
#include <string.h>

int main() {
        int i3x4[ 3 ][ 4 ] = {
                { 11 , 12 , 13 , 14 } ,
                { 21 , 22 , 23 , 24 } ,
                { 31 , 32 , 33 , 34 } ,
        } ;
        int i12[ 12 ] = {
                11 , 12 , 13 , 14 ,
                21 , 22 , 23 , 24 ,
                31 , 32 , 33 , 34 ,
        } ;

        int *pi[ 3 ] ;
        pi[ 0 ] = (int*)malloc( sizeof( int ) * 4 ) ;
        memcpy( pi[ 0 ] , i3x4[ 0 ] , sizeof( int ) * 4 ) ;
        pi[ 1 ] = (int*)malloc( sizeof( int ) * 4 ) ;
        memcpy( pi[ 1 ] , i3x4[ 1 ] , sizeof( int ) * 4 ) ;
        pi[ 2 ] = (int*)malloc( sizeof( int ) * 4 ) ;
        memcpy( pi[ 2 ] , i3x4[ 2 ] , sizeof( int ) * 4 ) ;

        for( int y = 0 ; y < 3 ; y++ ) {
                for( int x = 0 ; x < 4 ; x++ )
                        printf( "%d " , i3x4[ y ][ x ] ) ;
                printf( "\n" ) ;
        }
        for( int y = 0 ; y < 3 ; y++ ) {
                for( int x = 0 ; x < 4 ; x++ )
                        printf( "%d " , i12[ y*4 + x ] ) ;
                printf( "\n" ) ;
        }
        for( int y = 0 ; y < 3 ; y++ ) {
                for( int x = 0 ; x < 4 ; x++ )
                        printf( "%d " , pi[ y ][ x ] ) ;
                printf( "\n" ) ;
        }
        return 0 ;
}

様々なデータの覚え方のレポート課題

前回の malloc() + free() の資料で、補足で説明したC++のnew, delete を含め、様々なデータ構造の覚え方の例やメモリイメージを説明し、前期中間のレポート課題を示す。

malloc+freeの振り返り

// 文字列(可変長)の保存
char  str[] = "ABCDE" ;
char* pc ;
pc = (char*)malloc( strlen( str ) + 1 ) ;
if ( pc != NULL ) { // ↑正確に書くと sizeof( char ) * (strlen(str)+1)
   strcpy( pc , str ) ;
   ////////////////////
   // pcを使った処理
   ////////////////////
   free( pc ) ;
}
//
// 可変長の配列の保存
int  data[] = { 11 , 22 , 33 } ;
int* pi ;
pi = (int*)malloc( sizeof( int ) * 3 ) ;
if ( pi != NULL ) {
   for( int i = 0 ; i < 3 ; i++ )
      pi[ i ] = data[ i ] ;
   ////////////////////
   // piを使った処理
   ////////////////////
   free( pi ) ;
}
//
// 1件の構造体の保存
struct Person {
   char name[ 10 ] ;
   int  age ;
} ;
struct Person* pPsn ;
pPsn = (struct Person*)malloc( sizeof( struct Person ) ) ;
if ( pPsn != NULL ) {
   strcpy( pPsn->name , "t-saitoh" ) ;
   pPsn->age = 55 ;
   ////////////////////
   // pPsnを使った処理
   ////////////////////
   free( pPsn ) ;
}

(おまけ)C++の場合

malloc+freeでのプログラミングは、mallocの結果を型キャストしたりするので、間違ったコーディングの可能性がある。このため、C++ では、new 演算子, delete 演算子というものが導入されている。

// 同じ処理をC++で書いたら
// 文字列の保存
char  str[] = "ABCDE" ;
char* pc = new char[ strlen( str ) + 1 ] ;
strcpy( pc , str ) ;
// pcを使った処理
delete[] pc ;  // new型[]を使ったらdelete[]

// int配列の保存
int  data[] = { 11 , 22 , 33 } ;
int* pi ;
pi = new int[ 3 ] ;
for( int i = 0 ; i < 3 ; i++ )
   pi[ i ] = data[ i ] ;
// piを使った処理
delete[] pi ;

// 構造体の保存
struct Person {
   char name[ 10 ] ;
   int  age ;
} ;
Person* pPsn ;
pPsn = new Person ;
strcpy( pPsn->name , "t-saitoh" ) ;
pPsn->age = 55 ;
// pPsnを使った処理
delete pPsn ; // new型ならdelete

注意すべき点は、malloc+freeとの違いは、mallocがメモリ確保に失敗した時の処理の書き方。返り値のNULLをチェックする方法は、呼び出し側ですべてでNULLの場合を想定した書き方が必要になり、処理が煩雑となる。C++の new 演算子は、メモリ確保に失敗すると、例外 bad_alloc を投げてくるので、try-catch 文で処理を書く。(上記例はtry-catchは省略)

安全な1行1件のデータ入力

C言語では、scanf などの関数は、バッファオーバーフローなどの危険性があるため、以下のような処理を使うことが多い。fgets は、指定されたファイルから1行分のデータを読み込む。sscanf は、文字列のなかから、scanf() と同じようなフォーマット指定でデータを読み込む。

fgets は、これ以上の入力データが無い場合には、NULL を返す。
(Windowsであれば、キー入力でCtrl+Z を入力、macOSやLinuxであれば、Ctrl+Dを入力)

sscanf() は、読み込めたデータ件数を返す。

int main() {
   char buff[ 1024 ] ;
   for( int i = 0 ; i < 3 ; i++ ) {
      if ( fgets( buff , sizeof( buff ) , stdin ) != NULL ) {
         char name[ 1024 ] ;
         int  age ;
         if ( sscanf( buff , "%s%d" , name , &age ) == 2 ) {
            // 名前と年齢の2つのデータが正しく読み込めたとき
            ...
         }
      }
   }
   return 0 ;
}

様々なデータの覚え方

配列サイズ固定・名前が固定長

例えば、このデータ構造であれば、table1[] の場合、長い名前にある程度対応できるように nameの配列を100byteにしたりすると、データ件数が少ない場合には、メモリの無駄も多い。

そこで、実際に入力された存在するデータだけをポインタで覚える方法 table2[] という保存方法も考えられる。

// 固定長データのプログラム
#define SIZE 50

// 名前(固定長)と年齢の構造体
struct NameAge {
   char name[ 32 ] ;
   int  age ;
} ;
struct NameAge table1[ SIZE ] ;
int    size1 = 0 ;

void entry1( char s[] , int a ) {
   strcpy( table1[ size1 ].name , s ) ;
   table1[ size1 ].age = a ;
   size1++ ; 
}
// ポインタで覚える場合
struct NameAge* table2[ SIZE ] ;
int    size2 = 0 ;

void entry2( char s[] , int a ) {
   table2[size2] = (struct NameAge*)malloc( sizeof( struct NameAge ) ) ;
   if ( table2[size2] != NULL ) {  // なぜ != NULL のチェックを行うのか、説明せよ
      strcpy( table2[size2]->name , s ) ;
      table2[size2]->age = a ;
      size2++ ;
   }
}
// データ出力
void print_NA( struct NameAge* p ) {
   printf( "%s %d¥n" , p->name , p->age ) ;
}
int main() {
   // table1に保存
   entry1( "t-saitoh" , 55 ) ;
   entry1( "tomoko" ,   44 ) ;
   print_NA( &table1[0] ) ;
   print_NA( &table1[1] ) ;
   // table2に保存
   entry2( "t-saitoh" , 55 ) ;
   entry2( "tomoko" , 44 ) ;
   print_NA( _________________ ) ;  // table2の中身を表示せよ
   print_NA( _________________ ) ;
   return 0 ;
}

配列サイズ固定・名前が可変長

しかしながら、前回の授業で説明したように、際限なく長い名前があるのであれば、以下の様に名前は、ポインタで保存し、データを保存する時に strdup(…) を使って保存する方法もあるだろう。

// 名前が可変長のプログラム

// 名前(固定長)と年齢の構造体
struct NamePAge {
   char* name ;  // ポインタで保存
   int   age ;
} ;
struct NamePAge table3[ SIZE ] ;
int    size3 = 0 ;

void entry3( char s[] , int a ) {
   table3[ size3 ].name = strdup( s ) ;  // ★★★★
   table3[ size3 ].age = a ;
   size3++ ; 
}
// ポインタで覚える場合
struct NamePAge* table4[ SIZE ] ;
int    size4 = 0 ;

void entry4( char s[] , int a ) {
   table4[size4] = (struct NamePAge*)malloc( ____________________ ) ;
   if ( table4[size4] != NULL ) {            // ↑適切に穴埋めせよ
      table4[size4]->name = strdup( s ) ; // ★★★★
      _________________________________ ; // ←適切に穴埋めせよ
      size4++ ;
   }
}
// データ出力
void print_NPA( struct NameAge* p ) {
   printf( "%s %d¥n" , ____________ , ____________ ) ;
}                      // ↑適切に穴埋めせよ
int main() {
   // table3に保存
   entry3( "t-saitoh" , 55 ) ;
   entry3( "jyugemu jyugemu ..." ,   44 ) ;
   print_NPA( _________________ ) ;  // table3[] の中身を表示せよ。
   print_NPA( _________________ ) ; 
   // table4に保存
   entry4( "t-saitoh" , 55 ) ;
   entry4( "jyugemu jyugemu ..." , 44 ) ;
   print_NPA( table4[0] ) ;
   print_NPA( table4[1] ) ; 
   return 0 ;
}

データ件数が可変長ならば

前述のプログラムでは、データ件数全体は、SIZE という固定サイズを想定していた。しかしながら、データ件数自体も数十件かもしれないし、数万件かもしれないのなら、配列のサイズを可変長にする必要がある。

struct NamePAge* table5 ;
int    size5 = 0 ;

void entry5( char s[] , int a ) {
   strcpy( table5[ size5 ].name , s ) ;
   table5[ size5 ].age = a ;
   size5++ ; 
}

int main() {
   // table5に保存
   table5 = (struct NameAge*)malloc( sizeof( struct NameAge ) * 2 ) ;
   if ( table5 != NULL ) {
      entry5( "t-saitoh" , 55 ) ;
      entry5( "tomoko" ,   44 ) ;
   }
   return 0 ;
}

メモリの管理に十分気を付ける必要があるが、名前の長さも配列全体のサイズも可変長であれば、以下のようなイメージ図のデータを作る必要があるだろう。(JavaScriptやJavaといった言語ではデータのほとんどがこういったポインタで管理されている)

レポート課題

授業での malloc , free を使ったプログラミングを踏まえ、以下のレポートを作成せよ。

以下のデータのどれか1つについて、データを入力し、何らかの処理を行うこと。
課題は、原則として、(自分の出席番号%3)+1 についてチャレンジすること。

  1. 名前と電話番号
  2. 名前と生年月日
  3. 名前と身長・体重

このプログラムを作成するにあたり、以下のことを考慮しmallocを適切に使うこと。

名前は、長い名前の人が混ざっているかもしれない。
保存するデータ件数は、10件かもしれない1000件かもしれない。(データ件数は、処理の最初に入力すること。)

ただし、mallocの理解に自信がない場合は、名前もしくはデータ件数のどちらか一方は固定値でも良い。

レポートには、(a)プログラムリスト, (b)プログラムの説明, (c)正しく動いたことがわかる実行例, (d)考察 を記載すること。

考察には、自分のプログラムが正しく動かない事例はどういう状況でなぜ動かないのか…などを検討したり、プログラムで良くなった点はどういう所かを説明すること。

実数の取り扱い

実数型(float / double)

実数型は、単精度実数(float型)と、倍精度実数(double型)があり、それぞれ32bit,64bitでデータを扱う。

指数表現は、大きい値や小さい値を表現する場合に使われ、物理などで1.2345×10-4といった、仮数×基数指数で表現する方法。数学や物理では基数に10を用いるが、コンピュータの世界では基数を2とすることが多い。

単精度型(float)では、符号1bit,指数部8bit,仮数部23bitで値を覚え、数値としては、以下の値を意味する。(精度が低いので普通のコンピュータではあまり使われることはない)

符号✕ 1.仮数部 ✕ 2(指数数部-127)

符号部は、正の値なら0, 負の値なら1 を用いる。

仮数部が23bitなので、有効桁(正しい桁の幅)は約7桁となる。

例えば、float型で扱える最大数は、以下のようになる。

0,1111,1111,111,1111,1111,1111,1111,1111 = 1.1111…×2128 2129 1038

倍精度型(double)では、符号1bit,指数部11bit,仮数部52bitで値を覚え、数値としては、以下の意味を持つ。

符号✕ 1.仮数部 ✕ 2(指数部-1023)

これらの実数で計算を行うときには、0.00000001011×210といった値の時に、仮数部に0が並んだ状態を覚えると、計算の精度が低くなるので、1.01100000000×22のように指数部の値を調整して小数点の位置を補正しながら行われる。

double型の場合、52bit=10進数16桁相当の有効桁、最大数で、1.1111…×2102410308

倍精度型を使えば、正しく計算できるようになるかもしれないが、実数型はただの加算でも仮数部の小数点の位置を合わせたりする処理が必要で、浮動小数点専用の計算機能を持っていないような、ワンチップコンピュータでは整数型にくらべると10倍以上遅い場合もある。

実数の注意点

C言語でプログラムを作成していて、簡単な数値計算のプログラムでも動かないと悩んだことはないだろうか?解らなくて友達のプログラムを真似したら動いたけど、なぜ自分のプログラムは動かなかったのか深く考えたことはあるだろうか?

単純な合計と平均

整数を入力し、最後に合計と平均を出力するプログラムを以下に示す。
しかし、C言語でこのプログラムを動かすと、10,10,20,-1 と入力すると、合計(sum)40,件数(cnt)3で、平均は13と表示され、13.33333 とはならない。

小数点以下も正しく表示するには、どうすればいいだろうか?
ただし、変数の型宣言を “double data,sum,cnt ;” に変更しないものとする。

// 入力値の合計と平均を求める。
#include <stdio.h>

int main() {
   int data ;
   int sum = 0 ;
   int cnt = 0 ;
   for(;;) {
      printf( "数字を入力せよ。-1で終了¥n" ) ;
      scanf( "%d" , &data ) ;
      if ( data < 0 )
         break ;
      cnt = cnt + 1 ;
      sum = sum + data ;
   }
   printf( "合計 %d¥n" , sum ) ;
   printf( "平均 %d¥n" , sum / cnt ) ;
}

C言語では、int型のsum / int型のcnt の計算は、int 型で計算を行う(小数点以下は切り捨てられる)。このため、割り算だけ実数で行いたい場合は、以下のように書かないといけない。

   printf( "平均 %lf¥n" , (double)sum / (double)cnt ) ;
   // (double)式 は、sum を一時的に実数型にするための型キャスト

まずは動く例

以下のプログラムは、見れば判るけど、th を 0度〜360度まで5度刻みで変化させながら、y = sin(th) の値を表示するプログラム。

// sin の値を出力
#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 ;
}

動かないプログラム

では、以下のプログラムはどうだろうか?

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

どちらも、何気なく読んでいると、動かない理由が判らないと思う。そして、元のプログラムと見比べながら、case-1 では、「!=」を「<=」に書き換えたり、case-2 では、「int th ;」を「double th ;」に書き換えたら動き出す。

では何が悪かったのか…
回答編


数値と誤差

コンピュータで計算すると、計算結果はすべて正しいと勘違いをしている人も多い。ここで、改めて誤差について考える。特に、計器で測定した値であれば、測定値自体に誤差が含まれている。

こういった誤差が含まれる数字を扱う場合注意が必要である。例えば、12.3 と 12.300 では意味が異なる。測定値であやふやな桁を丸めたのであれば、前者は 12.2500〜12.3499 の間の値であり有効数字3桁である。後者は、12.2995〜12.300499 の間の値であり、有効数字5桁である。このため、誤差が含まれる数字の加算・減算・乗算・除算では注意が必要である。

加減乗除算の場合

加減算であれば小数点の位置を揃え、誤差が含まれる桁は有効桁に含めてはいけない。

上記の計算では、0.4567の0.0567の部分は意味がないデータとなる。(情報落ち)

乗除算であれば、有効桁の少ない値と有効桁の多い値の計算では、有効桁の少ない方の誤差が計算結果に出てくるため、通常は、有効桁5桁と2桁の計算であれば、乗除算結果は少ない2桁で書くべきである。

桁落ち

有効桁が大きい結果でも、減算が含まれる場合は注意が必要である。

例えば、以下のような計算では、有効桁7桁どうしでも、計算結果の有効桁は3桁となる。

このような現象は、桁落ちと呼ばれる。

演習問題(4回目)

こちらのフォルダに示す、Excel の表で、有効桁を考えてもらうための演習問題(ランダムに値が作られます)を有効数字を考えながら計算し、答えをレポートにまとめてください。例を以下に示す。

抽象クラスの演習(part2)

前回の講義に引き続き、抽象クラスの演習

Windows Terminal 1.8 公開

Windows Terminal 1.8 が正式公開となった。これで、

  • [Shift]+クリックで別ウィンドウで新しいターミナルを開くことができる。
  • [Alt]+クリックでは、タブ分割で新しいターミナルを開くことができる。

malloc()とfree()

前回の授業で説明した、alloca() は、スタック領域にデーターを覚えるので、allocaを実行した関数の終了ともに配列領域が消えてしまう。しかし、関数が終わってもそのデータを使いたいといった場合には、malloc()+free()を使う必要がある。

malloc()とfree()

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

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

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

ただし、ヒープメモリ全体は、プロセスの起動と共に確保され(不足すればOSから追加でメモリを分けてもらうこともできる)、プログラムの終了と同時に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 ] ) ;
}
void main() {
   // 文字列の入力&出力
   inputs() ;
   prints() ;
   // 使い終わったら、free() で解放
   for( int i = 0 ; i < 10 ; i++ )
      free( names[ i ] ) ;
}

文字列を保存する場合には、上記の 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 ;
      // ここは、name = strdup( 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 ) ;
   }
}

構造体の配列

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

後半の array2[] では、ポインタの配列を使った例を示す。この例では、1つの構造体毎に1つのmallocでメモリを確保している。

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

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

// 指定したComplexを出力
void print_Complex( struct Complex* p ) {
   printf( "%lf+j%lf¥n" , p->re , p->im ) ;
}
void main() {
   int size ;
   struct Complex* array ;
   struct Complex** array2 ;

   // 処理する件数を入力
   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] ) ;
         // or printf( "%lf + j%lf\n" ,
         //            array[ i ].re , array[ i ].im ) ;

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

   // ポインタの配列で保存
   if ( (array2 = (struct Complex**)malloc(
                     sizeof(struct Complex*) * size)) != NULL ) {
      int i ;
      for( i = 0 ; i < size ; i++ ) {
         // 各データごとにmalloc()
         array2[ i ] = (struct Complex*)malloc( sizeof( struct Complex ) ) ;
         if ( array2[ i ] != NULL ) {
            array2[ i ]->re = (double)i ;
            array2[ i ]->im = (double)i ;
         }
      }
      // 保存した構造体をすべて表示
      for( i = 0 ; i < size ; i++ )
         print_Complex( array[ i ] ) ;
      // 各データごとに free
      for( i = 0 ; i < size ; i++ )
         free( array[ i ] ) ;
      // ポインタの配列を free
      free( array2 ) ;
   }
}

プログラム言語(C言語)の基礎

学際科目の情報制御基礎において、学科間でプログラミングの初歩の理解差があるので、簡単なC言語プログラミングの基礎の説明。

Hello World

“Hello World”と表示するだけのC言語プログラムは以下のようになる。

// コメントの書き方1              // "//"で始まる行は、プログラムの説明(コメント)
/* コメントの書き方2 */           // "/*"から"*/"で囲まれる範囲もコメント
#include <stdio.h>             // #で始まる行はプリプロセッサ行
                               // stdio.h には、入出力関数の説明が書いてある
int main() {                   // 一連の処理の塊を関数と呼ぶ。
                               // C言語では main() 関数を最初に実行する。
   printf( "Hello World\n" ) ; // printf() は、以下の物を表示する関数。
                               // "\n"は、文字を出力して改行するための特殊文字
   return 0 ;                  // main() 関数が、正常終了したことを意味する
}                              // 0 を返り値として返す。

“#include <…>”のプリプロセッサ行は、最初のうちは解りにくいので、「これを書かないとダメ…」と思っていればいい。

#include <stdio.h> は、別ファイル(ヘッダファイル) stdio.h に記載されているプログラムリストを読み込む機能。
stdio.h には、printf() や scanf() などの基本的な関数や定数などの情報が記載されている。

C言語の基本的な命令(文)は、”;”で終わる。(単文)
複数の処理をまとめる場合には、”{“から”}”の中に、複数の文を書き並べる。(複文)

関数とは、複数の処理をひとまとめにした、処理の「かたまり」と思えばいい。

関数の型 関数名( 仮引数 ... ) {
   処理1 ... ;
   処理2 ... ;
}

printf() の 文字列中の”\n”(あるいは”¥n”)は、改行を意味する。
「\:バックスラッシュ」は、日本語環境では「¥:円記号」で入力・表示することが多い。

変数と代入

#include <stdio.h>
#include <math.h>            // 数学関数を使う 平方根 sqrt() を使っている
int main() {
   // 変数の宣言
   int    i ;                // 符号付き32bit変数 i の宣言
   int    a = 123 , j ;      // a を 123 で初期化 , j も整数型
   float  x ;                // 単精度実数の x を宣言
   double y = 1.234 , z ;    // 倍精度実数の y を宣言し 1.234 で初期化,
                             // z も倍精度実数
   // 変数への代入
   i = 1 ;                   // i に 1 を代入
   i = 12 + 2 * a ;          // 12+2*a を代入 a は123なので、
                             //   iには、258 が入る。
   x = sqrt( 2.0 ) ;         // x に 2.0 の平方根(1.4142)を代入
   z = y * 2.0 + x * 3.0 ;   // y*2+x*3をzに代入

   // 変数の内容の表示
   printf( "%d\n" , i ) ;    // 整数型(%d)で、    i の値を表示
   printf( "%f\n" , x ) ;    // 単精度実数(%f) で、x の値を表示
   printf( "%lf\n" , z ) ;   // 倍精度実数(%lf)で、z の値を表示

   printf( "iの値は%d,xの値は%lfです。\n" , i , x ) ;

   return 0 ;                // 正常終了 0 を返す
}

変数(計算結果を格納する入れ物)を使う場合は、変数を宣言する。
変数名には、何が入っているのか理解しやすいように、名前をつければいい。(英字で始まり、英数字が続くもの,_が入ってもいい)

変数に値を記憶する時は、”変数名=式 ;”の様に書くと、代入演算子”=” の右辺を計算し、その計算結果が左辺の変数に保存される。

変数の内容を表示する時には、printf() の文字列の中に、%d,%f,%lf などの表示したい式の型に応じたものを書いておく。
式の値が、その %.. の部分に書き込まれて、出力される。

繰り返しの制御命令

最も基礎的な繰り返し命令として、for() 文を説明。

#include <stdio.h>
int main() {
   int i ;
   for( i = 1 ; i <= 10 ; i++ ) {     // iを1から10まで変化させる。
      printf( "%d %d\n" , i , i*i ) ; // i と iの二乗を表示
   }
   return 0 ;
}

for文の意味を説明するために、対応するフローチャートを示す。

先のプログラムをフローチャートで示し、その命令の実行順序と、その変数の変化を下図に示す。

練習問題1

簡単なプログラミングの練習として、前回講義の練習問題をC言語で書いてみよう。

  • 電気電子工学科,電子情報工学科の学生は、出席番号が奇数は処理C,偶数は処理Dについて回答せよ。
  • それ以外の学科の学生は、出席番号が奇数は処理A,偶数は処理Bの結果について回答せよ。

制御構文とフローチャート

構文の入れ子

文と複文

C言語の文法で、{,} は複数の処理を連続して実行し、複文とよばれる。複数ので文を構成する。

これに対して、a = 123 ; といったセミコロンで終わる「処理 ;」は単文といい、1つの式で文となる。

制御構文のif文は、「if ( 条件 ) 文」で文となる。このため条件が満たされたときに実行する文が単文であれば、{,} は不要である。条件が満たされない場合の処理も記述するときには、「if ( 条件 ) 文 else 文」を使う。

// if文
if ( 条件 ) {
   a = 123 ;
}
if ( 条件 )
   a = 123 ; // 単文なら中括弧は不要

// if-then-else
if ( x >= 60 ) {
   printf( "合格点\n" ) ;
} else {
   printf( "不合格点\n" ) ;
}

同じように、「while(条件) 文」、「for(A,B,C) 文」、「do 文 while(条件) ;」も、それぞれ文を構成する。
{,} の複文は、{ 文 文 文… } のように、一連の文を実行し、それを1つの文として扱うための機能である。

// while 文
i = 0 ;
while( i < 10 ) {
   printf( "%d\n" , i ) ;
   i++ ;
}

// for 文
for( i = 0 ; i < 10 ; i++ ) {
   printf( "%d\n" , i ) ;
}

// do-while 文
i = 0 ;
do {
   printf( "%d\n" , i ) ;
   i++ ;
} while( i < 10 ) ;

練習問題2

プログラムの制御構造の確認として、以下の3つ(No.1,No.2,No.3)の問題から、
M科,C科,B科の学生は((自分の出席番号+1) % 2)+1 の問題、E科,EI科の学生は、((自分の出席番号+1) % 3)+1について、プログラムのフローチャートを描き、その処理がどのように進むのか答えよ。

レポートには、以下の点を記載すること。

  • フローチャート
  • 実行順序と変数の変化がわかる内容
  • (できれば、実際にプログラムを動かし、正しいことを検証)
// No.1
#include <stdio.h>
int main() {
   int i , j ;
   for( i = 1 ; i <= 4 ; i++ ) {
      if ( i % 2 == 0 ) {  // i%2 は2で割った余り,i%2==0ならば偶数のとき
         for( j = 1 ; j <= 2 ; j++ )
            printf( "%d %d\n" , i , j ) ;
      }
   }
   return 0 ;
}
// No.2
#include <stdio.h>
int main() {
   int x = 10 , y = 7 , s = 0 ;
   while( x > 0 ) {
      if ( x % 2 != 0 )
         s = s + y ;
      y = y * 2 ;
      x = x / 2 ;  // 注意: xは整数型      
   }
   printf( "%d\n" , s ) ;
   return 0 ;
}

// No.3
#include <stdio.h>
int a[ 6 ] = { 2 , 3 , 5 , 8 , 13 , 21 } ; 
int main() {
   int left = 0 , right = 6 , mid ;
   int key = 13 ;
   while( right - left > 0 ) {
      mid = (left + right) / 2 ; // 整数型で計算
      printf( "%d\n" , a[ mid ] ) ;
      if ( a[ mid ] == key )
         break ;
      else if ( a[ mid ] > key )
         right = mid ;
      else
         left = mid + 1 ;
   }
   return 0 ;
}

抽象クラスの演習

前回までで、抽象クラス(純粋仮想基底クラス)の説明を行ったので、前回説明の甘かった点を若干説明して、後半は演習とする。

資料の再掲はしないけど、改めて以下の点を説明。

  • Java GUIによる派生の使われ方
  • コールバック関数
  • テンプレートクラス

抽象クラスを使った演習

前回の講義資料を元に、様々な異なる型のデータを並び替えるプログラムを作成せよ。

講義資料では、整数・文字列・実数型のデータの並び替えを示した。新たに、名前と年齢のデータの並び替えができることを確かめよ。
データは、年齢の昇順とし、同じ年齢の場合は、名前で昇順となること。漢字の名前は読み仮名などの問題があるので、名前は英字による名前とする。

レポートには、プログラムリスト、説明、動作検証、考察を記載すること。

レポートの提出先先は、こちら。

マイコンとは

履歴書の添削してると、「マイコンで○○作成の経験あり」のようなこと書いてある。あらためて学生に聞くとマイコン=AVR。私世代の人間は、8bitの「パソコン」を触ってきた人間なので「マイコン=(Micro+My)/2 なコンピュータ」なので「マイコン=パソコン=デスクトップPC」のイメージが抜けない。だから組込み系とかマイクロコントローラーとか書いてほしいと思う人間。でも、実験の資料でも(AVR)マイコンって書いてあるしなぁ。こういうこと言うこと時点すで「にじじぃの戯言」。
でも、8bitパソコンを触った私にとっての「マイコン」(=Z80,MC6809なパソコン)なんて、性能的としては組込み系の Arduino とか AVR に軽く負ける時代だけどな。