ホーム » スタッフ » 斉藤徹 » 講義録

講義録」カテゴリーアーカイブ

2025年6月
1234567
891011121314
15161718192021
22232425262728
2930  

検索・リンク

Webプログラミングとセキュリティ

ここまでの授業では、Webを使った情報公開で使われる、HTML , JavaScirpt , PHP , SQL などの解説を行ってきたが、これらを組み合わせたシステムを構築する場合には、セキュリティについても配慮が必要である。

今回は、初心者向けの情報セキュリティの講習で使われるCTFという競技の練習問題をつかって、ここまで説明してきた Web の仕組みを使ったセキュリティの問題について解説を行う。

派生や集約と多重継承

派生や継承について、一通りの説明が終わったので、データ構造(クラスの構造)の定義の方法にも様々な考え方があり、どのように実装すべきかの問題点を考えるための説明を行う。その中で特殊な継承の問題についても解説する。

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

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

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

// 動物クラス
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++ には、複数のクラスから、派生する多重継承という機能がある。であれば、鳥類と哺乳類から進化したのだから、以下のように書きたい。

// 多重継承 鳥(Bird)と哺乳類(Mammal) から SeaBeam を作る
class SeaBream : public Bird , public 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 , public Wind {
public:
  Bird( const char s[] ) : Animal( s ) {}
  virtual void move() {
    printf( "%s %s.\n" , get_name() , move_method() ) ;
  }
} ;
class Mammal : public Animal , public Leg {
public:
  Mammal( const char s[] ) : Animal( s ) {}
  virtual void move() {
    printf( "%s %s.\n" , get_name() , move_method() ) ;
  }
} ;

継承を使うべきか、部品として持つべきか

ただし、ここで述べた方式は、UML による設計の際に改めて説明を行うが、is-a , has-a の関係でいうなら、

  • Bird is a Animal. – 鳥は動物である。
    • “Bird has a Animal” はおかしい。
    • 鳥は、動物から派生させるのが正しい。
  • Bird has a Wing. – 鳥は羽をもつ。
    • “Bird is a Wing” はおかしい。
    • 鳥は、羽を部品として持つべき。

であることから、Wing は 継承で実装するのではなく、集約もしくはコンポジションのような部品として実装すべきである。

このカモノハシ問題をどうしても多重継承で実装したいのなら、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 という機能が使えるようになっている。

多重継承を使える CLOS や Python では、適用するメソッドやインスタンス変数の曖昧さについては親クラスへの優先度を明確にできる機能がある。曖昧さの問題を避けるのであればクラス限定子”::”を使うべきである。

Javaでリスト構造

テスト前のリスト導入の復習

前回のリスト構造の導入では、配列のデータに次のデータの入っている番号を添えることで途中にデータを挿入できるデータ構造の説明をした。

また、それをクラスを用いたプログラムを示した。

ヒープメモリとは

Javaでは、すべてのオブジェクトはヒープメモリに保存する。

ヒープメモリとは、一時的なデータの保管場所であり、new 演算子でデータを保存する場所を確保する。

Javaでは、分かり難いのでC言語で説明を行う。malloc() は、指定されたバイト数のメモリをヒープ領域に確保する命令。malloc() に失敗すると、NULL が返ってくる。また、使い終わったら malloc() の領域は free() 命令で返却が必要となる。

((( 配列をヒープメモリで確保 )))
#include <stdio.h>
#include <stdlib.h>

int main() {
   int a[ 5 ] = { 1 , 2 , 3 , 4 , 5 } ;
   int* b ;
   if ( (b = (int*)malloc( sizeof( int ) * 5 )) != NULL ) {
      for( int i = 0 ; i < 5 ; i++ )
         b[ i ] = i + 1 ;
      free( b ) ;    // malloc() で確保したメモリ領域は返却が必要
   }
   return 0 ;
}

((( オブジェクトをヒープメモリに確保 )))
struct Complex {
   double re ;
   double im ;
} ;
int main() {
   struct Complex* c ;
   if ( (c = (struct Complex*)malloc( sizeof( struct Complex ) )) != NULL ) {
      c->re = 1.23 ;
      c->im = 2.34 ;
      free( c ) ;
      return 0 ;
   } else {
      printf( "No heap memory\n" ) ;
      return 1 ;
   }
}

((( 上記C言語をJavaで書くと )))
class Complex {
   double re ;
   double im ;
   Complex( double r , double i ) {
      this.re = r ;
      this.im = i ;
   }
}

public class Main {
   public static void main( String[] args ) {
      try {
         Complex c = new Complex( 1.23 , 2.34 ) ;
                    // Javaではヒープメモリが確保に失敗したら、
                    // OutOfMemoryErrorの例外が発生する。

         c = null ; // free()はJavaでは不要
                    // nullを代入してもいい。
      } catch( OutOfMemoryError e ) {
         System.out.println( "No heap memory" ) ;
         System.exit( 1 ) ;
      }
   }
}

リスト構造 ListNode

前述の data と next で次々とデータを続けて保存する方法を、next の部分を次のデータへの参照を用いるように、リスト構造(連結リスト)を定義する。

import java.util.*;

class ListNode {
    int      data ;   // データ部分
    ListNode next ;   // 次のデータへの参照
    // コンストラクタ
    ListNode( int d , ListNode nx ) {
        this.data = d ;
        this.next = nx ;
    }
} ;

public class Main {
    public static void main(String[] args) throws Exception {
        ListNode top = new ListNode( 11 , new ListNode( 22 , new ListNode( 33 , null ) ) ) ;

        for( ListNode p = top ; p != null ; p = p.next )
            System.out.println( p.data ) ;
        // 途中にデータを入れる
        top.next = new ListNode( 15 , top.next ) ;

        for( ListNode p = top ; p != null ; p = p.next )
            System.out.println( p.data ) ;
    }
}

リスト操作

リスト構造に慣れるために簡単な練習をしてみよう。リスト構造のデータに対するメソッドをいくつか作ってみよう。print() や sum() を参考に、データ数を求める count() , 最大値を求める max() , データを検索する find() を完成させてみよう。

class ListNode {
    (略)
} ;

public class Main {
    static void print( ListNode p ) {   // リストを表示
        for( ; p != null ; p = p.next )
            System.out.print( p.data + " " ) ;
        System.out.println() ;
    }

    static int sum( ListNode p ) {      // リストの合計を求める
        int s = 0 ;
        for( ; p != null ; p = p.next )
            s += p.data ;
        return s ;
    }

    static int count( ListNode p ) {    // データ件数を数える

    }

    static int max( ListNode p ) {      // データの最大値を求める

    }

    static boolean find( ListNode p , int key ) { // データ列の中から特定のデータを探す
                                                  //   見つかったら true , 見つからなければ false
    }
    public static void main(String[] args) throws Exception {
        ListNode top = new ListNode( 11 , new ListNode( 22 , new ListNode( 33 , null ) ) ) ;
        print( top ) ;
        System.out.println( "合計:" + sum( top ) ) ;
        System.out.println( "件数:" + count( top ) ) ;
        System.out.println( "最大:" + max( top ) ) ;
        System.out.println( "検索:" + (find( top , 22 )
                                       ? "みつかった" : "みつからない" ) ) ;
    }
}

オブジェクト指向っぽく書いてみる

前述のプログラムでは、print( top ) のように使う static な関数としてプログラムを書いていた。しかし、オブジェクト指向であれば、オブジェクトに対するメソッドだと top.print() のように書きたい。この場合だと、以下のように書くかもしれない。

import java.util.*;

class ListNode {
    int data ;
    ListNode next ;
    ListNode( int d , ListNode n ) {
        this.data = d ;
        this.next = n ;
    }
    void print() {   // リストの全データを表示
        for( ListNode p = this ; p != null ; p = p.next )
            System.out.print( p.data + " " ) ;
        System.out.println() ;
    }
    int sum() {    // リストの合計を求める
        int  s = 0 ;
        for( ListNode p = this ; p != null ; p = p.next )
            s += p.data ;
        return s ;
    }
} ;

public class Main {
    public static void main(String[] args) throws Exception {
        ListNode top = new ListNode( 11 , new ListNode( 22 , new ListNode( 33 , null ) ) ) ;
        top.print() ;
        System.out.println( "合計: " + top.sum() ) ;

        ListNode list_empty = null ;
        list_empty.print() ;  // 実行時エラー java.lang.NullPointerException ぬるぽ!
    }
}

しかし、データ件数 0件 に対してメソッドを呼び出せない。

ListNode と List というクラスで書いてみる

ひとつの方法として、リストの先頭だけのデータ構造を宣言する方法もある。

class ListNode {
    int   data ;
    ListNode next ;
    ListNode( int d , ListNode n ) {
        this.data = d ;
        this.next = n ;
    }
} ;

class List {
    ListNode top ;
    List( ListNode p ) {
        this.top = p ;
    }
    void print() {
        for( ListNode p = top ; p != null ; p = p.next )
            System.out.print( p.data + " " ) ;
        System.out.println() ;
    }
} ;

public class Main {
    public static void main(String[] args) throws Exception {
        List list = new List( new ListNode( 11 , new ListNode( 22 , new ListNode( 33 , null ) ) ) ) ;
        list.print() ;
        
        List list_empty = new List( null ) ;
        list_empty.print() ;
    }
}

しかし、List と ListNode の2つのデータの型でプログラムを書くのは面倒くさい。

授業ではシンプルに説明したいので、今後はこの方法は極力避けていく。

先頭にダミーデータを入れる

複数のクラス宣言するぐらいなら、リストデータの先頭は必ずダミーにしておく方法もあるだろう。

import java.util.*;

class ListNode {
    int data ;
    ListNode next ;
    ListNode( int d , ListNode n ) {
        this.data = d ;
        this.next = n ;
    }
    void print() {   // リストの全データを表示
        for( ListNode p = this.next ; p != null ; p = p.next )
            System.out.print( p.data + " " ) ;
        System.out.println() ;
    }
} ;

public class Main {
    public static void main(String[] args) throws Exception {
        ListNode list = new ListNode( -1 , null ) ;
        list.next = new ListNode( 11 , new ListNode( 22 , new ListNode( 33 , null ) ) ) ;
        top.print() ;
        System.out.println( "合計: " + top.sum() ) ;

        ListNode list_empty = new ListNode( -1 , null ) ;
        list_empty.print() ;
    }
}

以降、必要に応じて、先頭にダミーを入れる手法も取り混ぜながらプログラムを書くこととする。

入力データをリストに追加

入力しながらデータをリストに格納する処理を考えてみる。

リストでデータを追加保存するのであれば、一番簡単なプログラムは、以下のように先頭にデータを入れていく方法だろう。

class ListNode {
    (略)
    void print() {
        for( ListNode p = this ; p != null ; p = p.next )
            System.out.print( p.data ) ;
        System.out.println() ;
    }
} ;
    
public class Main {
    public static void main(String[] args) throws Exception {
        int[] inputs = { 11 , 22 , 33 } ;
        ListNode top = null ;

        for( int datum : inputs )
            top = new ListNode( datum , top ) ;
        top.print() ;
    }
}

でもこの方法だと、先頭にデータを入れていくため、保存されたデータは逆順になってしまう。

末尾にデータを入れる

逆順になるのを避けるのであれば、データを末尾に追加する方法があるだろう。ただし初期状態でデータが0件だと処理が書きづらいので、先頭にダミーを入れておく方法で書いてみる。

public class Main {
    public static void main(String[] args) throws Exception {
        int[] test_data = { 11 , 22 , 33 } ;

        ListNode top = new ListNode( -1 , null ) ; // ダミー
        ListNode tail = top ;
        for( int x : test_data ) {
            tail.next = new ListNode( x , null ) ;
            tail = tail.next ;
        }
        top.print() ; // -1   11  22  33
    }                 // ダミー
}

バックエンドと所有権の設定

前回の講義でファイルのパーミッション(読み書き権限)について確認したが、バックエンドプログラミングで必要となるファイルの所有権の設定を通して、演習を行う。これに合わせ、サーバ上のファイルの編集作業なども体験する。

サーバ上のファイルの編集

以前のバックエンドのプログラムの演習ではサーバの設定などの体験もできていないため、フロントエンドの処理でサーバ上に送られたデータは、最終的な書き込み処理は行っていなかった。今回は、サーバ上でデータをサーバ上のバックエンドプログラムの PHP ファイルを修正し、データが書き込めるようにプログラムの修正を行う。

サーバ上のファイルを編集するには、色々な方法がある。

サーバ上のエディタで直接編集
unix のシステムで直接ファイルを編集するのであれば、vimemacs を利用するのが一般的であろう。これらのエディタはリモートサーバにsshなどでログインしている時は、端末ソフトの文字表示機能だけで動作し、GUI 機能を使わない。vim や emacs は、古くから使われ、Windows で動く vim emacs もある。
システム管理者権限で編集する必要があるファイルの場合は、以下に紹介するような方法は煩雑であり、サーバ上で直接編集も知っておくべき。
プログラムをローカルPCで編集しアップロード(今回はコレ)
前回の演習では、リモートサーバに接続する際には ssh コマンドを用いたが、ssh にはファイル転送のための scp コマンドも用意されている。
scp コマンドは、通常の cp 命令 ( cp コピー元 コピー先 ) を ssh のプロトコルでリモートする機能を拡張したものであり、リモートのコンピュータをコピー元やコピー先として指定する場合は、 ユーザ名@リモートホスト:ファイル場所 と記載する。
# remotehostのファイル helloworld.c をローカルホストのカレントディレクトリ.にダウンロード
C:\Users\tsaitoh> scp tsaitoh@remotehost:helloworld.c .  
# ローカルホストの foobar.php を remotehostの/home/tsaitoh/public_html/ フォルダにアップロード
C:\Users\tsaitoh> scp foobar.php tsaitoh@remotehost:/home/tsaitoh/public_html/
VSCode でリモートファイルを編集
最近のエディタでは、前述のローカルPCで編集しアップロードといった作業を、自動的に行う機能が利用できる。emacs の tramp-mode や、VS Code の Remote ssh プラグインなどがこれにあたる。利用する演習用のサーバが高機能であれば、vscode + remote-ssh が一番便利と思われるが、remote-ssh はサーバで大きな node.js を動かすため、サーバ負担が大きいので今回はこの方式は使わない。

Webアプリと所有権の問題

PHPで書かれたバックエンドでのプログラムにおいて、Webサーバは www-data(uid),www-data(groupid) というユーザ権限で動作している。そして、webサーバと連動して動く PHP のプログラムも www-data の権限で動作する。一方で、通常ユーザが開発しているプログラムが置かれる $HOME/public_html フォルダは、何もしなければそのユーザのものである。このため、PHP のプログラムがユーザのフォルダ内にアクセスする際には、www-data に対してのアクセス権限が必要となる。

Windows ユーザが Web プログラミングの体験をする際には、XAMPP などのパッケージを利用することも多いだろう。しかし XAMPP などは、中身のWebサーバ(apache), DBサーバ(MySQL)などすべてがインストールしたユーザ権限で動いたりするため、所有権の設定の知識が無くても簡単に利用することができる(あるいはユーザ自身が管理者権限を持っているため設定が無くてもアクセス権問題が発生しない)。このため Linux 環境での Web プログラミングに移行する際に、ユーザ権限の設定を忘れ、プログラムが動かず戸惑うことも多い。

今回の演習では、管理者権限で動いている自分のパソコンの中のXAMPPを使わず、複数のユーザで動いている演習用サーバを用いる。

データベースサーバの場合

また、データの保存でデータベースを利用する場合、Oracle や MySQL といった、ネットワーク型のデータベースでは、Webサーバとは別にデータベースのサーバプログラムが動作している。ネットワーク型のデータベースでは、様々なユーザ・アプリケーションがデータの読み書きを行うため、SQL の create user 命令でユーザを割り当てgrant 命令でユーザのデータへのアクセス権限を指定する。

Webアプリは、データベースのサーバに接続する際には、ユーザ名とパスワードが必要となる。

簡易データベースSQLiteの場合

簡単なデータベースシステムの SQLite は、PHP の SQLite プラグインを経由してディレクトリ内のファイルにアクセスする。このため、データベースファイルやデータベースのファイルが置かれているフォルダへのアクセス権限が必要となる。今回の演習用サーバでは、ゲストアカウントは www-data グループに所属しているので、データベースファイルやフォルダに対し、www-data グループへの書き込み権限を与える。

chown , chgrp , chmod コマンド

ファイル所有者やグループを変更する場合には、chown (change owner) 命令や chgrp (change group) 命令を使用する。ただし、chown はシステム管理者権限(root)でなければ使えない。chgrp も自分が所有してかつ変更後のグループに加入していなければ使えない。

chown ユーザID ファイル
 例: $ chown tsaitoh helloworld.c
chgrp グループID ファイル
 例: $ chgrp www-data public_html

ファイルに対するパーミッション(利用権限)を変更するには、chmod (change mode) 命令を用いる。
chmod 命令では、読み書きの権限は2進数3桁の組み合わせで扱う。読書可 “rw-“ = 6, 読出可 = “r–“ = 4 , ディレクトリの読み書き可 “rwx” = 7 など。ファイルには、所有者,グループ,その他の3つに分けて、読み書きの権限を割り当てる。2進数3桁=8進数1桁で表現できることから、一般的なファイルの “rw-,r–,r–“ は、8進数3桁 で 644 , ディレクトリなら “rwx,r-x,r-x”755 といった値で表現する。

chmod 権限 ファイル
 例: $ chmod 664 helloworld.c
       $ ls -al
       -rw-rw-r-- tsaitoh ei        123 5月20 12:34 helloworld.c
       $ chmod 775 public_html
       drwxrwxr-x tsaitoh www-data 4096 5月20 12:34 public_html
  8進数表現を使わない場合
       $ chmod u+w,g+w helloworld.c
               ユーザ(u)への書き込み権限,グループ(g)への書き込み権限の追加(+)
       $ chmod g-w,o-rw helloworld.c
               グループ(g)書き込み権限を消す、その他(o)の読み書き権限を消す(-)
       $ chmod u=rw,g=r,o=r helloworld.c
               ユーザ(u)への読み書き,グループ(g),その他(o)への読み出し権限を設定(=)

演習内容

前回の演習と同じ方法でサーバにログインし、サーバ上で直接ファイル編集をしてみよう。

C:\Users\tsaitoh> ssh -P 443 guest00@nitfcei.mydns.jp
$ ls -al
-rw-r--r-- 1 guest00 root 76 Mar 8 12:06 helloworld.c
$ vi helloworld.c
もしくは
$ emacs helloworld.c
vim の使い方
挿入 iテキストESC
削除 x
ファイルの保存 :w
エディタの修了 ZZ
emacs の使い方
ファイルの保存 Ctrl-X Ctrl-S
エディタの修了 Ctrl-X Ctrl-C

GitHubから演習ファイルを複製

GitHub は、複数の開発者が共同でプログラムを開発するための環境で、プログラムの情報共有などに広く使われている。ファイルは、git コマンドで複製や更新ができる。

(( public_html の中に演習用ファイルを github からダウンロード ))
$ cd ~/public_html
public_html$ git clone https://github.com/tohrusaitoh/recp.git
public_html/recp$ cd recp/
public_html/recp$ ls -al
-rw-r--r-- 1 t-saitoh home 870 11月 10 2021 Makefile
-rw-r--r-- 1 t-saitoh home 1152 10月 8 2021 README.md
 :

サーバ上のファイルをパソコンにコピーして編集

(( サーバ上のファイル sampleI.php (sample-アイ.php) をダウンロード ))
C:\Users\tsaitoh> scp -P 443 guest00@nitfcei.mydns.jp:public_html/recp/sampleI.php .

VSCode などのエディタで編集

(( 編集した sampleI.php をサーバにアップロード ))
C:\Users\tsaitoh> scp -P 443 sampleI.php guest00@nitfcei.mydns.jp:public_html/recp/

Webサーバで書き込みができるように設定

(( public_html のデータベースファイル shopping.db を書き込み可能にする ))
$ chgrp www-data ~guest00/public_html/recp/shopping.db
$ chmod g+w ~guest00/public_html/recp/shopping.db

(( public_html/recp フォルダを書き込み可能にする ))
$ chgrp www-data ~guest00/public_html/recp
$ chmod g+w ~guest00/public_html/recp

バックエンドプログラムを実行してみる

パソコンのブラウザで、http://nitfcei.mydns.jp/~guest00/recp/sampleI.php を開く。

 

書き込み結果を確認してみる

(( データベースファイル shopping.db の書込み結果を確認 ))
$ cd ~guest00/public_html/recp
public_html/recp$ sqlite3 shopping.db
SQLite version 3.31.1 2020-01-27 19:55:54
Enter ".help" for usage hints.
sqlite> select * from BUYLIST ;
1010|10001|2021-11-05|1
1020|10001|2021-11-05|2
1022|10001|2021-11-05|3
  :
sqlite> [Ctrl-D] コントロールDで sqlite3 を抜ける
public_html/recp$

テスト返却と追加説明

前期中間試験の返却に伴い、補足説明。

C言語,Javaにおける文の定義

テストの時に、下記のようなプログラムで piyo() は、for の範囲?みたいな質問があったので、補足説明

for( ... ; ... ; ... )
   if ( ... )
      foo = bar ;
   else {
      baz() ;
      hoge() ;
   }
piyo() ;

C言語やJavaにおける文の定義は、以下の通り

文 ::= 式 ;    // 単文
     | ;       // 空文
     | { 文 文 ... }        // 複文
     | for( .. ; .. ; .. ) 文   // 制御構文
     | do 文 while( 式 ) ;
     | if ( 式 ) 文 [ else 文 ]
     ;

こういった文の範囲の誤解を避けるためのものが、インデント。

正しいインデントをつける習慣が重要。

Javaにおけるクラスとデータ構造のイメージ

この後のリスト構造の説明でもオブジェクトがどのように保存されるかを理解するのは重要なので、オブジェクトの説明。

Java では、class 宣言されたデータ構造は、ヒープメモリと呼ばれるデータ領域に保存されている。

class A {
   int data ;
   A( int value ) {
      this.data = value ;
   }
} ;

public class Main {
   public static void main( String[] args ) {
      A a = new A( 123 ) ;
   }
}

ここで、new 演算子は、指定されたオブジェクトをヒープメモリ上に確保する。そして、そのデータの入っている領域アドレスを、インスタンスの変数 a に代入している。

テストで出題した例であれば、

class IdNameAge {
   int    id ;
   String name ;
   Age    age ;
   IdNameAge( int i , String n , int a ) {
      this.id   = i ;
      this.name = n ;
      this.age  = a ;
   }
} ;

IdNameAge[] = people = {
   new IdNameAge( 1001 , "saitoh" ,  60 ) ,
   new IdNameAge( 1002 , "tomoko" ,  49 ) ,
   new IdNameAge( 1003 , "mitsuki" , 26 ) ,
} ;

Integer 型も、整数データを記憶するけど、オブジェクト型である。

ただし、Integer 型を常にヒープメモリに保存するのは、効率が悪いので、-128~127までの値ではIntegerオブジェクトは内部でキャッシュされる。Integer y = 123 ; 書いてはいるけど本来であれば、Integer y = new Integer( 123 ) ;  と書く必要があった。しかしJava5以降に、オートボクシングと呼ばれる機能があるため Integer y = 123 のように書けるし、こう書く方が推奨されている。

public class Main {
    public static void main(String[] args) {
        // Your code here!
        Integer sx = 123 ;
        Integer sy = 123 ;
        System.out.println( sx == sy ) ; // true
        Integer fx = 12345 ;
        Integer fy = 12345 ;
        System.out.println( fx == fy ) ; // false
    }
}

プログラム言語(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”)は、改行を意味する。
「\:バックスラッシュ」は、日本語環境では「¥:円記号」で入力・表示することが多い。

Paiza.io で動かしてみよう

C言語を本格的に使いたいなら、Microsoft Visual Studio などをインストールして使う方が便利だが、情報制御基礎で説明する程度のプログラムなら、Paiza.io が便利。ブラウザの画面で簡単にプログラムの動作を確認することができる。https://paiza.io/jaにアクセスして、上述の Hello World を動かしてみよう。

変数と代入

#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 などの表示したい式の型に応じたものを書いておく。%d=int型 , %f=float型 , %lf=double型
式の値が、その %.. の部分に書き込まれて、出力される。

繰り返しの制御命令

最も基礎的な繰り返し命令として、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の結果について回答せよ。
  • 自分が考えたプログラムは、前述の Paiza.io や、自分のパソコンのC言語環境で入力し、動作結果も確認せよ。

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

構文の入れ子

文と複文

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における派生の使い方

先週の講義では、派生を使ったプログラミングは、GUI で使われていることを紹介したが、例として Java のプログラミングスタイルを少し紹介する。

例えば、Java で アプレット(ブラウザの中で動かすJavaプログラム)を作る場合の、最小のサンプルプログラムは、以下のようになる。

import java.applet.Applet; // C言語でいうところの、Applet 関連の処理を include
import java.awt.Graphics;

public class App1 extends Applet {  // Applet クラスから、App1 クラスを派生
    public void paint(Graphics g) { // 画面にApp1を表示する必要がある時に呼び出される。
        g.drawString("Hello World." , 100 , 100);
    }
}

この例では、ブラウザのGUIを管理する Applet クラスから、App1 というクラスを派生(extendsキーワード)し、App1 固有の処理として、paint() メソッドを定義している。GUI のプログラミングでは、本来ならマウスが押された場合の処理などを記述する必要があるが、このプログラムでは paint() 以外何も書かれていない。これはマウス処理などは、基底クラスのAppletのマウス処理が継承されるので、省略してもうまく動くようになっている。paint() メソッドは、このApp1クラスの中で、継承されたメソッドでなく自分で新たに定義している。

このように、派生クラスの継承機能を使うことで、雑多な処理を基底クラスですべて書くようにすれば、同じようなデータ型が出てくる場合プログラムを書く手間を減らすことができる。

抽象クラス(純粋仮想基底クラス)

抽象クラス(純粋仮想基底クラス)とは、見かけ上はデータを何も持たないクラスであり、本来なら意味がないデータ構造となってしまう。しかし、派生クラスで要素となるデータと仮想関数で機能を与えることで、基底クラスという共通部分から便利な活用ができる。(実際には、型を区別するための型情報を持っている)

例えば、C言語であれば一つの配列に、整数、文字列、実数といった異なる型のデータを記憶させることは本来ならできない。しかし、以下のような処理を記載すれば、可能となる。

C言語では、1つの記憶域を共有するために共用体(union)を使うが、C++では仮想関数が使えるようになり、型の管理をプログラマーが行う必要のある「面倒で危険な」共用体を使う必要はなくなった。

// 純粋仮想基底クラス
class Object {
public:
   virtual void print() const = 0 ;
   // 中身の無い純粋基底クラスで、
   // 仮想関数を記述しない時の書き方。
} ;

// 整数データの派生クラス
class IntObject : public Object {
private:
   int data ;
public:
   IntObject( int x ) {
      data = x ;
   }
   virtual void print() const {
      printf( "%d\n" , data ) ;
   }
} ;

// 文字列の派生クラス
class StringObject : public Object {
private:
   char data[ 100 ] ;
public:
   StringObject( const char* s ) {
      strcpy( data , s ) ;
   }
   virtual void print() const {
      printf( "%s\n" , data ) ;
   }
} ;

// 実数の派生クラス
class DoubleObject : public Object {
private:
   double data ;
public:
   DoubleObject( double x ) {
      data = x ;
   }
   virtual void print() const {
      printf( "%lf\n" , data ) ;
   }
} ;

// 動作確認
int main() {
   Object* data[3] = {
      new IntObject( 123 ) ,
      new StringObject( "abc" ) ,
      new DoubleObject( 1.23 ) ,
   } ;
   for( int i = 0 ; i < 3 ; i++ ) { // 123
      data[i]->print() ;            // abc
   }                                // 1.23 と表示
   return 0 ;
} ;

このプログラムでは、純粋仮想基底クラスObjectから、整数IntObject, 文字列StringObject, 実数DoubleObject を派生させ、そのデータを new により生成し、Objectの配列に保存している。

仮想関数を使うと、Object型の中に自動的に型情報が保存されるようになる。一般的な実装では、各派生クラス用の仮想関数のポインタテーブル(vtable)へのポインタが使われる。

Javaなどのオブジェクト指向言語では、全てのクラス階層のスーパークラスとして、Object を持つように作られている。

様々な型に適用できるプログラム

次に、抽象クラス(純粋仮想基底クラス)の特徴を応用したプログラムの作り方を説明する。

例えば、以下のような最大選択法で配列を並び替えるプログラムがあったとする。

int a[5] = { 11, 55, 22, 44, 33 } ;

void my_sort( int array[] , int size ) {
   for( int i = 0 ; i < size - 1 ; i++ ) {
      int max = i ;
      for( int j = i + 1 ; j < size ; j++ ) {
         if ( array[j] > array[max] )
            max = j ;
      }
      int tmp = array[i] ;
      array[i] = array[max] ;
      array[max] = tmp ;
   }
}
int main() {
   my_sort( a , 5 ) ;
}

しかし、この整数を並び替えるプログラムがあっても、文字列の並び替えや、実数の並び替えがしたい場合には、改めて文字列用並び替えの関数を作らなければいけない。しかも、ほとんどが同じような処理で、改めて指定された型のためのプログラムを作るのは面倒である。

C言語のデータの並び替えを行う、qsort() では、関数ポインタを用いることで、様々なデータの並び替えができる。しかし、1件あたりのデータサイズや、データ実体へのポインタを正しく理解する必要がある。これを間違えると、メモリエラーなどが簡単に発生することから、オブジェクト指向の抽象クラスを用いた方が、型として安全なプログラムが記述できる。

#include <stdio.h>
#include <stdlib.h>
int a[ 4 ] = { 11, 33, 22, 44 } ;
double b[ 3 ] = { 1.23 , 5.55 , 0.11 } ;
// 並び替えを行いたいデータ専用の比較関数を作る。
// a>bなら+1, a=bなら0, a<bなら-1を返す関数
int cmp_int( int* pa , int* pb ) { // int型用コールバック関数
   return *pa - *pb ;
}
int cmp_double( double* pa , double* pb ) { // double型用コールバック関数
   if ( *pa == *pb )
      return 0 ;
   else if ( *pa > *pb )
      return 1 ;
   else
      return -1 ;
}
int main() {                                   // C言語の怖さ
   qsort( a , 4 , sizeof( int ) ,              //   このあたりの引数を書き間違えると
          (int(*)(void*,void*)) cmp_int ) ;    //   とんでもない目にあう。
   qsort( b , 3 , sizeof( double ) ,           // "void*は、型を明記しないポインタ型"
          (int(*)(void*,void*)) cmp_double ) ; //   の意味。
} 

このように、自分が作っておいた関数のポインタを、関数に渡して呼び出してもらう方法は、コールバックと呼ぶ。
JavaScript などの言語では、こういったコールバックを使ったコーディングがよく使われる。

// コールバック関数 f を呼び出す関数
function exec_callback( var f ) {
   f() ;
}
// コールバックされる関数 foo()
function foo() {
   console.log( "foo()" ) ;
}
// foo() を実行する。
exec_callback( foo ) ;
// 無名関数を実行する。
exec_callback( function() {
                  console.log( "anonymous" ) ;
               } ) ;

任意のデータを並び替え

class Object {
public:
   virtual void print() const = 0 ;
   virtual int  cmp( Object* ) = 0 ;
} ;

// 整数データの派生クラス
class IntObject : public Object {
private:
   int data ;
public:
   IntObject( int x ) {
      data = x ;
   }
   virtual void print() const {
      printf( "%d\n" , data ) ;
   }
   virtual int cmp( Object* p ) {
      int pdata = ((IntObject*)p)->data ;  // 本当はこのキャストが危険
      return data - pdata ;                //  ↓安全な実装したいなら↓
   }                                       // IntObject* pi = dynamic_cast<IntObject*>(p) ;
} ;                                        // return pi != NULL ? data - pi->data : 0 ;

// 文字列の派生クラス
class StringObject : public Object {
private:
   char data[ 100 ] ;
public:
   StringObject( const char* s ) {
      strcpy( data , s ) ;
   }
   virtual void print() const {
      printf( "%s\n" , data ) ;
   }
   virtual int cmp( Object* p ) {
      char* pdata = ((StringObject*)p)->data ;
      return strcmp( data , pdata ) ; // 文字列比較関数
   }
} ;

// 実数の派生クラス
class DoubleObject : public Object {
private:
   double data ;
public:
   DoubleObject( double x ) {
      data = x ;
   }
   virtual void print() const {
      printf( "%lf\n" , data ) ;
   }
   virtual int cmp( Object* p ) {
      double pdata = ((DoubleObject*)p)->data ;
      if ( data == pdata )
         return 0 ;
      else if ( data > pdata )
         return 1 ;
      else
         return -1 ;
   }
} ;

// Objectからの派生クラスでcmp()メソッドを
//   持ってさえいれば、どんな型でもソートができる。
void my_sort( Object* array[] , int size ) {
   for( int i = 0 ; i < size - 1 ; i++ ) {
      int max = i ;
      for( int j = i + 1 ; j < size ; j++ ) {
         if ( array[j]->cmp( array[max] ) > 0 ) // 仮想関数により適切な
            max = j ;                           //  比較メソッドが呼び出される。
      }
      Object* tmp = array[i] ;
      array[i] = array[max] ;
      array[max] = tmp ;
   }
}
// 動作確認
int main() {
   Object* idata[3] = {
      new IntObject( 11 ) ,
      new IntObject( 33 ) ,
      new IntObject( 22 ) ,
   } ;
   Object* sdata[3] = {
      new StringObject( "abc" ) ,
      new StringObject( "defghi" ) ,
      new StringObject( "c" ) ,
   } ;
   my_sort( idata , 3 ) ; // 整数のソート
   for( int i = 0 ; i < 3 ; i++ )
      idata[i]->print() ;
   my_sort( sdata , 3 ) ; // 文字列のソート
   for( int i = 0 ; i < 3 ; i++ )
      sdata[i]->print() ;
   return 0 ;
} ;

このような方式でプログラムを作っておけば、新しいデータ構造がでてきてもソートのプログラムを作らなくても、比較専用の関数 cmp() を書くだけで良い。このような抽象クラスをデータを入れる入れ物として使う手法は「コンテナクラス」(あるいはコレクション)と呼ぶ。JavaにおけるList(ArrayList, LinkedList), Set, Map, Deque など(コレクションフレームワーク)は、Java.lang.Object から派生した物として扱うことで、面倒なデータ構造のプログラミングを簡潔に記述することができるようになっている。

ただし、この並び替えの例では、Object* を IntObject* に強制的に型変換している。

また、このプログラムでは、データを保管するために new でポインタを保管し、データの比較をするために仮想関数の呼び出しを行うことから、メモリの使用効率も処理効率でもあまりよくない

こういう場合、最近の C++ ではテンプレート機能が使われる。

template <typename T>
void my_sort( T a[] , int size ) {
  for( int i = 0 ; i < size - 1 ; i++ ) {
    int max = i ;
    for( int j = i + 1 ; j < size ; j++ ) { if ( a[j] > a[max] )
        max = j ;
    }
    T  tmp = a[i] ;
    a[i] = a[max] ;
    a[max] = tmp ;
  }
}

int main() {
  int idata[ 5 ] = { 3, 4, 5 , 1 , 2 } ;
  double fdata[ 4 ] = { 1.23 , 0.1 , 3.4 , 5.6 } ;

  // typename T = int で int::mysort() が作られる
  my_sort<int>( idata , 5 ) ;
  for( int i = 0 ; i < 5 ; i++ )
    printf( "%d " , idata[i] ) ;
  printf( "\n" ) ;

  // typename T = double で double::mysort() が作られる
  my_sort<double>( fdata , 4 ) ;
  for( int i = 0 ; i < 4 ; i++ )
    printf( "%lf " , fdata[i] ) ;
  printf( "\n" ) ;
  return 0 ;
}

C++のテンプレート機能は、my_sort( int[] , int ) で呼び出されると、typename T = int で、整数型用の my_sort() の処理が自動的に作られる。同じく、my_sort( double[] , int ) で呼び出されると、typename = double で 実数型用の my_sort() が作られる。

テンプレート機能では、各型用のコードが自動的に複数生成されるという意味では、出来上がったコードがコンパクトという訳ではない。C++では、STL(標準テンプレートライブラリ:Standard Template Library)による、コンテナ, イテレータ(反復子), アルゴリズム, 関数オブジェクトにより、様々なデータ構造を簡単に扱うことができる。

#include <vector>
#include <stdio.h>

int main(void) {
    std::vector<int> vec{ 1, 2, 3 } ;  // STLによる整数のvector

    for( int i = 0 ; i < vec.size() ; i++ )
        cout << vec[i] ;
    cout << endl ;

    // 反復子による繰り返し処理 auto は型推論による反復子の宣言
    for( auto it = vec.begin() ; it != vec.end() ; it++ )
        cout << *it ;      // it は std::vector<int>::iterator 型
    cout << endl ;

    // もっとシンプルな書き方 (C++11で導入された範囲ベース for ループ)
    for( auto &num : vec ) // auto は型推論による変数宣言 vec が int なので num は int& 型になる 
        cout << num ;      // イテレータ反復子による繰り返しの処理
    cout << endl ;
    return 0;
}

仮想関数レポート課題

ここで示したプログラムを参考に、独自のデータ(例えば、複素数のデータや名前と誕生日といったデータ)について、my_sort() などで並び替えるプログラムを作成せよ。並び替える時の順序も、各自て定義すればいい。(複素数なら絶対値順とか、名前と誕生日なら、誕生日順とか)

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

unixにおけるファイルとユーザ管理

Unix演習サーバへの接続

Unix(Linux)は、インターネットでのサーバとして広く活用されている。Linuxを試すには、Windows ならば WSL や Cygwin であったり、Mac でも使える仮想OSの VMware, VirrtualBox を使うこともできる。今回の演習では、全員が同じ環境で使うために、クラウド環境にサーバを準備し利用する。

ネットワークの向こう側にあるサーバを利用する場合、以下のような方法が使われる。

  • telnet (port 23)
    • キー入力を相手に送って、送られてくるデータを画面に表示する。
    • 通信データが暗号化されないので盗聴される心配があり、一般的には使用しない。
  • rsh (remote shell – port 514)
    • ネットワークを越えてコマンドを実行したりファイル転送ができる。
    • telnet 同様に暗号化されていないので、次に示す ssh を使うのが一般的。
  • ssh (secure shell – port 22)
    • rsh の処理を暗号化しながら実行。
    • ネットワークを越えた処理を行う際の基本だが、ssh を経由した攻撃が多いことから、通常のポート番号22以外を使ったり、アクセス制限を厳しく設定する必要がある。
  • remote Desktop
    • ネットワークの先のPCの画面をネットワーク越しに触れるようにしたもの。

教室のWiFi環境(fnct-student)では、HTTP(80) , HTTPS(443) の通信しか使えないことから、ssh(22) が通常利用できない。電子情報のWiFiアクセスポイント(nitfc-ei-student等)であれば、ssh などが使用できる。

今回授業の演習では、さくらインターネットのサーバ上のクラウドサーバを利用する。

ただし、さくらインターネットのクラウドサーバでは、ssh(port=22)が使用できるが、ssh 接続の際にログインパスワードの間違いなどが多発すると、ssh 経由の攻撃の可能性があると判断され、ssh(port=22)接続が一定時間使えなくなる対策がとられている。今回は、ゲストアカウントでパスワード入力ミスが多発することが想定されるので、port=22のsshは使用しない。

リモート接続を行う

Windows 10 or Windows 11 ならば、cmd.exe , macOS ならば、ターミナルソフトを起動し、以下の操作を行う。

$ ssh -p 443 ゲストID@演習サーバ
  • 443ポートは通常は https 用だが、今回はサーバで ssh プロトコルを 443 ポートで受け付けるように設定してある。かなり特殊な使い方なので要注意。
  • 演習サーバの接続方法(学内のみ) – サーバへの攻撃を極力へらすために非公開。
  • パスワード入力時は、打つたびに●●●といった文字は表示されません。
  • パスワード入力時にタイプミスした時は、Ctrl-U で最初から入力のやり直しができます。

ファイル操作の基本

まずは基本操作をしてみよう。ls コマンド(list) は、ディレクトリ内にあるファイルの一覧を表示する。cat コマンド(catalog)は、指定されたファイルの内容を表示する。

s53599xx@nitfcei:~$ ls
helloworld.c  Maildir  public_data  public_html

s53599xx@nitfcei:~$ ls -l
total 8
-rw-r--r-- 1 s53599xx students   76 Dec 21 14:30 helloworld.c
drwx------ 5 s53599xx students 4096 Dec 21 14:30 Maildir
(略)

s53599xx@nitfcei:~$ cat helloworld.c
#include <stdio.h>

int main() {
    printf( "Hello World\n" ) ;
    return 0 ;
}
s53599xx@nitfcei:~$

ファイルをコピーするには cp コマンド(copy)、不要なファイルを消すには rm コマンド(remove)を使う。

s53599xx@nitfcei:~$ cp helloworld.c test.c
s53599xx@nitfcei:~$ ls -l
total 8
-rw-r--r-- 1 s53599xx students   76 Dec 21 14:30 helloworld.c
drwx------ 5 s53599xx students 4096 Dec 21 14:30 Maildir
-rw-r--r-- 1 s53599xx students   76 Dec 21 14:40 test.c
(略)
s53599xx@nitfcei:~$ rm test.c
s53599xx@nitfcei:~$ ls -l
total 8
-rw-r--r-- 1 s53599xx students   76 Dec 21 14:30 helloworld.c
drwx------ 5 s53599xx students 4096 Dec 21 14:30 Maildir
s53599xx@nitfcei:~$

ファイル詳細表示の説明

ls -l で表示される詳細の内容は以下の通り。

属性 リンク数 所有者 グループ サイズ 日付 ファイル名
rw- r– r– 1 s53599xx students 76 Dec 21 14:30 helloworld.c
d rwx 5 s53599xx students 4096 Dec 21 14:30 Maildir
d -: 通常ファイル, d:ディレクトリ
rw- r,w,x 所有者が r:読み出し, w:書き込み, -: 権限なし
ファイルなら、x:実行可能
ディレクトリなら、x:ディレクトリに入れる
r – – – – – グループの rwx の属性 r– は 読み込みだけ許可
r – – – – – その他の rwx の属性  — は、読み書き禁止

基本的なファイル操作コマンド一覧

操作 Linux Windows
ディレクトリ一覧(list)
ディレクトリ詳細
ls 場所  ※
ls -l 場所
dir /w 場所  ※
dir 場所
※ 省略時はカレントディレクトリ
ファイル表示(catalog) cat 場所 type 場所
ファイルコピー(copy) cp コピー元 コピー先
cp コピー元 コピー先ディレクトリ
copy コピー元 コピー先
ファイル削除(remove) rm 場所 del 場所
ディレクトリ作成(make dir) mkdir 場所 md 場所
ディレクトリ削除(remove dir) rmdir 場所 rmdir 場所
カレントディレクトリ移動
(change directory)
cd 場所 cd 場所
ドライブの場合は
ドライブ名:
所有者を変更(change owner) chown 所有者 場所
グループを変更(change group) chgrp グループ 場所
属性を変更(change mode) chmod 属性 場所 ←属性の書き方

ワイルドカード文字

ls などのコマンドで、複数のファイルを対象とするとき、ワイルドカード文字が使える。

任意の1文字
?
(例)
$ ls          # 全部のファイル
aaa.c  ab.c    abc.c   bcd.c   defgh.c  hij.cxx
$ ls a?.c   # aで始まる2文字のC言語ファイル
ab.c
$ ls ???.c  # 3文字のC言語のファイル
aaa.c   abc.c   bcd.c
任意の文字
*
(例)
$ ls a*.c   # aで始まるC言語ファイル
aaa.c ab.c abc.c
$ ls *.cxx  # 拡張子が.cxxのファイル(C++)
hij.cxx

相対PATHと絶対PATH

ファイルの場所を指定するには、2つの方法がある。

絶対PATHは、木構造の根(ルートディレクトリ / で表す) からの経路のディレクトリ名を”/”で区切って書き連ねる。ルートディレクトリからの場所であることを示すために、先頭を / で始める。住所を /福井県/越前市/宮谷町/斉藤家 と書くようなもの。

相対PATHは、現在注目しているディレクトリ(カレントディレクトリと呼ぶ)からの経路を書く。住所でいうと、/福井県/越前市 に注目している状態で、宮谷町/斉藤家 と書くようなもの。

ただし、/福井県/福井市 に注目している状態で、片町/山本家 は1つのファイルでも、/福井県/福井市/片町/山本家 とは別に /石川県/金沢市/片町/山本家 があるかもしれない。

上記の絵であれば、/home/tsaitoh/helloworld.c を、相対PATHで書く場合、s53599xx の一つ上にさかのぼって場所を指定することもできる。一つ上のディレクトリ(親ディレクトリ).. (ピリオド2つ)

この場合、” $ cat ../tsaitoh/helloworld.c ” の様な相対PATHでもアクセスできる。

カレントディレクトリ自身を表す場合は、. (ピリオド1つ)を使う。

/home/s53599xx/helloworld.c の場所は、” $ cat ./helloworld.c ” と書くこともできる。

ユーザとグループ

unixでは、ユーザとグループでアクセス制限をすることができる。ユーザ情報は、/etc/passwd ファイルで確認できる。グループ情報は、/etc/group ファイルで確認できる。

$ more /etc/passwd
root:x:0:0:root:/root:/bin/bash
daemon:x:1:1:daemon:/usr/sbin:/usr/sbin/nologin
bin:x:2:2:bin:/bin:/usr/sbin/nologin
(略)
guest00:x:1200:1200:guest00,,,:/home0/guests/guest00:/bin/bash

$ more /etc/group
root:x:0:
daemon:x:1:
bin:x:2:
(略)
guests:x:1200:guest00,guest01,guest02,...
/etc/passwd /etc/group
guest00 — ユーザID
x — 昔は暗号化されたパスワード
1200 — ユーザID番号
1200 — グループID番号(/etc/groupを参照)
guest00,,, — ユーザの正式名や電話番号など
/home0/guests/guest00 — ホームディレクトリ
/bin/bash — 使用する shell
guests — グループID
x — 昔は暗号化されたグループパスワード
1200 — グループID番号
guest00,guest01,guest02 — 所属するユーザ一覧

アクセス制限の実験

/home0/Challenge/AccesControl に、いくつかのファイルが保存してあり、t-saitoh が見ると、以下のようなファイルであった。tree コマンドでは、いくつかのディレクトリとその中のファイルが確認できる。しかし、ls -al にてファイルのアクセス権限が確認できる。tree コマンドで確認できるファイルにアクセスすると何が起こるか確認すること。

$ cd /home0/Challenge/AccessControl
$ id        # 自分のID,グループを確認
uid=1200(guest00) gid=1200(guests) groups=1200(guests)
$ tree      # ディレクトリ構造を表示
$ ls -al    # 権限情報を表示

Windows とアクセスコントロール

Unix のシステムでは、ファイル毎に、ユーザID,グループIDを割り当て、ユーザ, グループ, その他に対して、Read, Write などの制限をかける。Windows では、さらに細かくアクセス制限を加えることができる。Windows では、1つのファイルに対して、ユーザやグループのRead/Writeなどの制限をいくつでも設定できる。Access Control List と呼ばれる。

主要なディレクトリとファイルシステム

unix では、すべてのデバイスを / (ルートディレクトリ) 配下に木構造につなげて管理している。CD-ROM や USB ディスクなどは、指定したディレクトリに mount (マウント) して使用する。

ext4 は、Linux で採用されているファイルシステムで、システムの保存に使われる。

tmpfs は、主記憶(D-RAM) の一部を、ディスクと同じように扱えるようにしたファイルシステム。通称 ram disk(ラムディスク)。保存はメモリへのアクセスなので、保存やアクセスは極めて高速だが、保存領域は少ない。高速に扱えて、システムが再起動された時に消えても問題のない情報を保存するために使われる。

proc は、実行中のプロセス情報を、ハードディスクに保存されたファイルの様に参照できる。

vfat , exfat は、USBメモリ, SDカード のデータ保存で使われるファイルシステムで、Windows(MS-DOS) で使われている保存形式。ファイルにファイル所有者などの概念がない

ntfs は、Windows で使われているファイル形式。

swap は、仮想メモリのためのデータが保存される。主記憶メモリが不足した際に、使用頻度の少ないメモリ領域をハードディスクに保存するための領域。以下のような free コマンドで使用状況が確認できる。一般的に、主記憶メモリの数倍を割り当てる。

派生と継承と仮想関数

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

// 基底クラス
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 文が必要になる。

もし、派生したクラスの種類がいくつもあるのなら、(1)型情報の代入は注意深く書かないとバグの元になるし、(2)型に応じた分岐処理は巨大なものになるだろう。実際、オブジェクト指向プログラミングが普及する前の初期の GUI プログラミング(初期のX11)では、巨大な switch 文が問題となっていた。巨大な switch 文は、選択肢だけの if else-if else-if が並ぶと処理効率も悪い。

仮想関数

上記の、型情報の埋め込みと巨大な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 ;
}

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

仮想関数の実装方法

仮想関数の一般的な実装方法としては、仮想関数を持つオブジェクトには型情報として仮想関数へのポインタテーブルへのポインタを保存する。この場合、仮想関数の呼び出しは、object->table[n]( arg… ) のような処理が行われる。

配列に要素を追加

データが登録済みかどうかを判定する処理を作るために、登録された値を配列に次々と値を追加保存する場合、どのようにプログラムを記述するだろうか?

配列にデータを追加

次々と与えられた値を保存していくのであれば、Java であれば下記のようなコードが一般的であろう。
でも、ArrayList とはどのようにデータを覚えているのだろうか? なぜ 宣言は ArrayList<Integer> array であって ArrayList<int> array で宣言するとエラーが出るのであろうか?

import java.util.*;

public class Main {
    public static void main(String[] args) throws Exception {
        // ArrayList は連続アドレス空間に保存してくれる可変長配列
        //   ランダムアクセスをする場合に向いている
        ArrayList<Integer> array = new ArrayList<Integer>() ;
        array.add( 11 ) ;
        array.add( 2 ) ;
        array.add( 333 ) ;
        
        for( Integer i : array ) {
            System.out.println( i ) ;
        }
    }
}

このような ArrayList のようなデータ構造の仕組みを考えるために、最も単純な配列でプログラムを作ってみる。

末尾に追加

import java.util.*;

public class Main {
    static int[] array = new int[ 10 ] ;
    static int   size  = 0 ;

    public static void add( int x ) {
        array[ size ] = x ;
        size++ ;
    }
    public static void main(String[] args) throws Exception {
        add( 11 ) ;
        add( 2 ) ;
        add( 333 ) ;
        
        for( int i = 0 ; i < size ; i++ )
            System.out.println( array[i] ) ;
    }
}

同じ処理をC言語で書いてみる。

#include <stdio.h>

int array[ 10 ] ;
int size = 0 ;

void add( int x ) {          // if ( size < array.length ) ... の判定が必要かも
    array[ size ] = x ;
    size++ ;
}

int main() {
    add( 11 ) ;
    add( 2 ) ;
    add( 333 ) ;

    for( int i = 0 ; i < size ; i++ )
        printf( "%d\n" , array[ i ] ) ;
    return 0 ;
}

しかし、このプログラムでは、最初に宣言した要素数10個を越えてデータを保存できないし、配列溢れさせないためには要素数の上限チェックも必要となるだろう。

昇順に並べながら途中に要素を追加

前述のプログラムでは、配列の末尾の場所を size で覚えておき、末尾にデータを追加していた。でも、配列に保存されている値の中から目的の値が含まれているか検索したいのであれば、配列に要素を昇順に保存しておいて2分探索法を使うのが一般的であろう。では、前述のプログラムを昇順で保存するにはどうすべきか?

最も簡単な方法で書くのであれば、下記のようなコードになるかもしれない。

public static void add( int x ) {
    int i ;
    for( i = 0 ; i < size ; i++ ) { // ここは2分探索で書けば O( log N ) にできるかも
        if ( array[ i ] > x )
            break ;
    }
    // for( int j = i ; j < size ; j++ )   // 途中に挿入は、コレじゃダメ?
    //     array[ j + 1 ] = array[ j ] ;
    for( int j = size - 1 ; j >= i ; j-- ) // 途中にデータを入れるために要素を1つ後ろに移動
        array[ j + 1 ] = array[ j ] ;
    array[ i ] = x ;
    size++ ;
}
void add( int x ) {
    int i ;
    for( i = 0 ; i < size ; i++ ) {
        if ( array[ i ] > x )
            break ;
    }
    // for( int j = i ; j < size ; j++ )
    //     array[ j + 1 ] = array[ j ] ;
    for( int j = size - 1 ; j >= i ; j-- )
        array[ j + 1 ] = array[ j ] ;
    array[ i ] = x ;
    size++ ;
}

このプログラムでは、for( i … ) の処理でデータを挿入すべき場所を見つけ、for( int j … ) の繰り返しでデータを1つ後ろにずらしてから要素を加えている。

for( i … ) の処理は、このプログラムでは O( N ) となっているが、2分探索法を用いれば O( log N ) に改善ができるかもしれない。しかし、for( int j… ) の処理は、データを1つ後ろにずらす必要があるため O( N ) の処理が必要となる。

ここで、途中にデータを追加する処理の効率を改善することを考える。

リスト構造の導入

以下のデータ構造では、配列にデータと次のデータの場所を覚えることで、一見デタラメな順序に保存されているようにみえるが、next[] に次の値の保存されている場所が入っている。

import java.util.*;

public class Main {           //    0    1    2    3    4    5
    static int[] data = new int[] { 11 , 55 , 22 , 44 , 33 , 0 , 0 , 0 , 0 , 0 } ;
    static int[] next = new int[] { 2  , -1 , 4  , 1  , 3  , 0 , 0 , 0 , 0 , 0 } ;
    static int   size = 5 ;
    static int   top = 0 ;

    static void insert( int n , int x ) {
        data[ size ] = x ;
        next[ size ] = next[ n ] ;
        next[ n ] = size ;
        size++ ;
    }
    
    public static void main(String[] args) throws Exception {
        for( int idx = top ; idx >= 0 ; idx = next[ idx ] )
            System.out.println( data[ idx ] ) ;
        insert( 2 , 25 ) ;
        for( int idx = top ; idx >= 0 ; idx = next[ idx ] )
            System.out.println( data[ idx ] ) ;
    }
}
#include <stdio.h>

int  data[ 10 ] = { 11 , 55 , 22 , 44 , 33 , 0 , 0 , 0 , 0 , 0 } ;
int  next[ 10 ] = { 2  , -1 , 4  , 1  , 3  , 0 , 0 , 0 , 0 , 0 } ;
int  size = 5 ;
int  top  = 0 ;

void insert( int n , int x ) {
    data[ size ] = x ;
    next[ size ] = next[ n ] ;
    next[ n ] = size ;
    size++ ;
}

int main() {
    for( int idx = top ; idx >= 0 ; idx = next[ idx ] )
        printf( "%d\n" , data[ idx ] ) ;
    insert( 2 , 25 ) ;
    for( int idx = top ; idx >= 0 ; idx = next[ idx ] )
        printf( "%d\n" , data[ idx ] ) ;
    return 0 ;
}

このようなデータ構造であれば、データ自体は末尾に保存しているが、次の値が入っている場所を修正することで途中にデータを挿入することができる。この方法であれば、途中にデータを入れる場合でもデータを後ろにずらすような処理が不要であり、O(1)で途中にデータを挿入できる。

同じプログラムを、データと次のデータの配列番号のオブジェクトで記述してみた。

import java.util.*;

class DataNext {
    public int data ;
    public int next ;
    DataNext( int d , int n ) {
        this.data = d ;
        this.next = n ;
    }
}

public class Main {
    public static DataNext[] table = {
        new DataNext( 11 , 2 ) ,
        new DataNext( 55 , -1 ) ,
        new DataNext( 22 , 4 ) ,
        new DataNext( 44 , 1 ) ,
        new DataNext( 33 , 3 ) ,
        null , 
        null , 
        null ,
        null ,
        null ,
    } ;
    public static int size = 5 ;
    
    public static void insert( int n , int x ) {
        table[ size ] = new DataNext( x , table[ n ].next ) ;
        table[ n ].next = size ;
        size++ ;
    }
    public static void main(String[] args) throws Exception {
        for( int idx = 0 ; idx >= 0 ; idx = table[ idx ].next )
            System.out.println( table[ idx ].data ) ;
            
        insert( 2 , 25 ) ;
        
        for( int idx = 0 ; idx >= 0 ; idx = table[ idx ].next )
            System.out.println( table[ idx ].data ) ;
    }
}

このプログラムでは、配列の当初の長さを超えてデータを格納することはできない。

リスト構造 ListNode

前述の data と next で次々とデータを続けて保存するために、リスト構造(連結リスト)を定義する。

import java.util.*;

class ListNode {
    int      data ;
    ListNode next ;
    ListNode( int d , ListNode nx ) {
        this.data = d ;
        this.next = nx ;
    }
} ;

public class Main {
    public static void main(String[] args) throws Exception {
        ListNode top = new ListNode( 11 , new ListNode( 22 , new ListNode( 33 , null ) ) ) ;

        for( ListNode p = top ; p != null ; p = p.next )
            System.out.println( p.data ) ;

        top.next = new ListNode( 15 , top.next ) ;

        for( ListNode p = top ; p != null ; p = p.next )
            System.out.println( p.data ) ;
    }
}
#include <stdio.h>
#include <stdlib.h>

struct ListNode {
    int       data ;
    ListNode* next ;
} ;

ListNode* newListNode( int d , ListNode* nx ) {
    ListNode* _this = new ListNode() ;
    if ( _this != NULL ) {
        _this->data = d ;
        _this->next = nx ;
    }
    return _this ;
}

int main() {
    ListNode* top = newListNode( 11 , newListNode( 22 , newListNode( 33 , NULL ) ) ) ;
    for( ListNode* p = top ; p != NULL ; p = p->next )
        printf( "%d\n" , p->data ) ;
    top->next = newListNode( 15 , top->next ) ;
    for( ListNode* p = top ; p != NULL ; p = p->next )
        printf( "%d\n" , p->data ) ;
    return 0 ;
}

Javaのジェネリクス

Javaのジェネリクス(C++のテンプレート)を使って書いてみた。ジェネリクスは、クラスやメソッドにおいて、特定の型を指定することなく動作するコードを記述することができる機能。これにより、型安全性を保ちながら、コードの再利用性と柔軟性を向上させることがでる。

import java.util.*;

class ListNode<T> {
    T           data ;
    ListNode<T> next ;
    
    ListNode( T d , ListNode<T> n ) {
        this.data = d ;
        this.next = n ;
    }
} ;

public class Main {
    public static void main(String[] args) throws Exception {
        // var 宣言は型推論で、右辺のデータ型を自動的に選択してくれる。
        // itop は整数型のリスト
        var itop = new ListNode<Integer>( 11 , new ListNode<Integer>( 22 , new ListNode<Integer>( 33 , null ) ) ) ;
        // new List<int>( 11 , ... ) と書くと、<>の中は reference しか使えないと言われる。
        for( var p = itop ; p != null ; p = p.next )
            System.out.println( p.data ) ;

        // stop は文字列型のリスト
        var stop = new ListNode<String>( "aa" , new ListNode<String>( "bb" , new ListNode<String>( "cc" , null ) ) ) ; 
        for( var p = stop ; p != null ; p = p.next )
            System.out.println( p.data ) ;
    }
}

前述のプログラムをJavaのジェネリッククラスで記述

import java.util.*;

public class Main {
    public static void main(String[] args) throws Exception {
        // LinkedList は上記のリスト構造で保存される。
        //    途中に要素の追加削除を行ったり、シーケンシャルアクセスに向いたデータ構造
        var top = new LinkedList<Integer>() ;
        top.add( 11 ) ;
        top.add( 22 ) ;
        top.add( 33 ) ;
        for( int i : top )            // 11 22 33
            System.out.println( i ) ;
        top.add( 1 , 15 ) ;
        for( int i : top )            // 11 15 22 33
            System.out.println( i ) ;
    }
}

クラスの宣言とコンストラクタ・メソッド

import java.util.*;

// クラス宣言
class Person {
    // データ構造
    String name ;
    int    age ;

    // コンストラクタ(データ構造を初期化する関数)
    Person( String n , int x ) {
        this.name = n ;    // this は対象となるデータそのものを指す
        this.age  = x ;    // 対象が明言されていれば、this は省略可能
    }
    // データを扱うメソッド
    void print() {                 // データを表示
        System.out.println( this.name + "," + this.age ) ;
    }
    boolean sameAge( Person x ) {  // 同じ年齢か判断するメソッド
        return this.age == x.age ;
    }
} ;

public class Main {
    public static void main(String[] args) throws Exception {

        Person tsaitoh = new Person( "Tohru Saitoh" ,  59 ) ;
        Person tomoko  = new Person( "Tomoko Saitoh" , 48 ) ;

        tsaitoh.print() ;  // Tohru Saitoh, 59
        tomoko.print() ;   // Tomoko Saitoh,48

        if ( tsaitoh.sameAge( tomoko ) ) {
            // sameAge( Person x ) では、
            // this = tsaitoh , x = tomoko となって呼び出される
            System.out.println( "同じ年齢ですよ" ) ;
        }
        Person[] family = new Person[ 2 ] ;
        family[0] = tsaitoh ;
        family[1] = tomoko ;
        for( int i = 0 ; i < 2 ; i++ )
            family[ i ].print() ;
    }
}こ

このプログラムのデータ構造は下記のような状態。

システム

最新の投稿(電子情報)

アーカイブ

カテゴリー