ホーム » スタッフ » 斉藤徹 (ページ 11)

斉藤徹」カテゴリーアーカイブ

2025年6月
1234567
891011121314
15161718192021
22232425262728
2930  

検索・リンク

派生や集約と多重継承

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

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

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

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

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
// 動物クラス
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 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 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 ;
}

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

鳥類・哺乳類とは別にカモノハシを作る(いちばん無難な方法)

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
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() ) ;
}
} ;
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() ) ; } } ;
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++ には、複数のクラスから、派生する多重継承という機能がある。であれば、鳥類と哺乳類から進化したのだから、以下のように書きたい。

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
// 多重継承 鳥(Bird)と哺乳類(Mammal) から SeaBeam を作る
class SeaBream : public Bird , public Mammal {
//
} ;
// 多重継承 鳥(Bird)と哺乳類(Mammal) から SeaBeam を作る class SeaBream : public Bird , public Mammal { // } ;
// 多重継承 鳥(Bird)と哺乳類(Mammal) から SeaBeam を作る
class SeaBream : public Bird , public Mammal {
   //
} ;

しかし、カモノハシに move() を呼び出すと、鳥類の move() と哺乳類の move() のどちらを動かすか曖昧になる。

また「派生」は、基底クラスと派生クラスの両方のデータを持つデータ構造を作る。このため、単純に多重継承を行うと、カモノハシのクラスでは、派生クラスは親クラスのデータ領域と、派生クラスのデータ領域を持つため、鳥類の name[] と、哺乳類の name[] を二つ持つことになる。多重継承による”ダイヤモンド型継承”の問題

足と羽のクラスを作る場合(本来は多重継承で実装すべきではない)

以下に、足と羽のクラスを作ったうえで、多重継承を行うプログラム例を示す。

しかし、この例では、相変わらずカモノハシのクラスを多重継承で実装すると、ダイヤモンド型継承の問題が残る。

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
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() ) ;
}
} ;
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() ) ; } } ;
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++では、以下のような方法で、ダイヤモンド型の継承問題を解決できる。

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
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() ;
}
} ;
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() ; } } ;
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 では、適用するメソッドやインスタンス変数の曖昧さについては親クラスへの優先度を明確にできる機能がある。曖昧さの問題を避けるのであればクラス限定子”::”を使うべきである。

Unix演習で ./a.out の理由

今日の演習の後で “コマンドを実行する時に ./a.out とか先頭に ./ をつけるのはなぜ?” との質問があった。

環境変数 PATH とは

通常、フォルダ一覧を表示するために “ls” とか入力しているけど、ls という命令はどこにあるのだろうか?

実は、unix や Windows では「よく使うコマンドの保存場所」を 環境変数 PATH にて管理している。環境変数は “echo $PATH” といった命令で確認ができる。unix では PATH は “:” 区切りで「よく使うコマンドの場所(ディレクトリ)」が列記してある。通常は /usr/local/bin:/usr/bin:/bin といった値になっているはず。

コマンドを実行する時にディレクトリが明記されていない場合は、PATH のディレクトリから探して実行することになっている。
(Windows の PATH は “;” 区切りなので要注意。cmd.exe を起動し echo %PATH% を実行すれば PATHが確認できる)

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
$ ls
helloworld.c
$ which ls
/usr/bin/ls
$ echo $PATH
/usr/local/bin:/usr/bin/:/bin
$ ls helloworld.c $ which ls /usr/bin/ls $ echo $PATH /usr/local/bin:/usr/bin/:/bin
$ ls
helloworld.c
$ which ls
/usr/bin/ls
$ echo $PATH
/usr/local/bin:/usr/bin/:/bin

このため、a.out のプログラムを実行する時には、”a.out” とだけ入力しても PATH に記載がないため「どこにある a.out を実行するの?」という状態になる。このため、カレントディレクトリにある a.out を実行する時には、”./” をつけて ./a.out と明示が必要となっている。

カレントディレクトリを PATH に加えればいいじゃん

コマンド実行で、いちいち“./”をつけるのはめんどくさい…と思う人もいるだろう。であれば、PATH を変更すればいい。

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
$ echo $PATH
/usr/local/bin:/usr/bin/:/bin
$ a.out
a.out: コマンドが見つかりません。
$ PATH=.:/usr/local/bin:/usr/bin/:/bin
$ a.out
HelloWorld
$ echo $PATH /usr/local/bin:/usr/bin/:/bin $ a.out a.out: コマンドが見つかりません。 $ PATH=.:/usr/local/bin:/usr/bin/:/bin $ a.out HelloWorld
$ echo $PATH
/usr/local/bin:/usr/bin/:/bin
$ a.out
a.out: コマンドが見つかりません。
$ PATH=.:/usr/local/bin:/usr/bin/:/bin
$ a.out
HelloWorld

しかし、この設定はセキュリティ的にも危険なのでやってはいけない設定の代表例です。
# Windows は、カレントディレクトリのプログラム実行で PATH 指定が不要なので要注意。

PATH=.:/usr/bin:/bin が危険な理由

もし、誰にでも書き込みができるフォルダがあって、そのフォルダに “ls” という名前のファイルを置き逃げした人がいたとしよう。

別の人はそのフォルダに入って、どんなファイルがあるのかな?ということで “ls” とタイプするかもしれない。そうすると何が起こるだろうか?

どういったことが発生するか、体験するためのフォルダが作ってあるので何が起こるか試してみよう。

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
$ cat /home0/Challenge/1-CTF.d/Task5/Bomb/ls
#!/bin/bash
killall -KILL bash 2> /dev/null # ← bash プロセスを殺すshell script(結果として強制ログアウトされる)
$ PATH=.:/usr/bin:/bin # 危険なPATHの指定
$ cd /home0/Challenge/1-CTF.d/Task5/Bomb
$ ls # ← どんなファイルがあるかな? ls
Connection to nitfcei.mydns.jp closed.
$ cat /home0/Challenge/1-CTF.d/Task5/Bomb/ls #!/bin/bash killall -KILL bash 2> /dev/null # ← bash プロセスを殺すshell script(結果として強制ログアウトされる) $ PATH=.:/usr/bin:/bin # 危険なPATHの指定 $ cd /home0/Challenge/1-CTF.d/Task5/Bomb $ ls # ← どんなファイルがあるかな? ls Connection to nitfcei.mydns.jp closed.
$ cat /home0/Challenge/1-CTF.d/Task5/Bomb/ls
#!/bin/bash
killall -KILL bash 2> /dev/null           # ← bash プロセスを殺すshell script(結果として強制ログアウトされる)
 
$ PATH=.:/usr/bin:/bin                    # 危険なPATHの指定

$ cd /home0/Challenge/1-CTF.d/Task5/Bomb
$ ls                                      # ← どんなファイルがあるかな? ls
Connection to nitfcei.mydns.jp closed.

この例では、強制ログアウトする命令となっているが、ls の処理として「login: … password: …」といった入力を行うようなプログラムが置いてあったら、「ls ってタイプしたらなぜかログイン画面にもどっちゃった。よくわからんけど再ログインするか…」と、ID とパスワードを入力する人もいるかもしれない。でも、この ID と パスワードを特定の人にメールするようにしてあれば、アカウント乗っ取りが可能となる。

リダイレクトとパイプ

Linuxを使う上で、キーボードでコマンドを入力しているが、こういうコマンドの管理を行うプログラムshell と呼ぶ。shell には、色々なものがある(sh, csh, bash, zsh)が、広く使われている bash( born-again shell )について説明する。最初に、コマンドの入出力を組み合わせるために重要となるリダイレクトとパイプについて説明し、次にコマンドなどの処理単位となるジョブやプロセスの考え方について説明を行う。

標準入出力とリダイレクト

出力リダイレクト

C言語のプログラミングで、プログラムの実行結果をレポートに張り付ける時はどのように行っているだろうか?多くの人は、実行画面を PrintScreen でキャプチャした画像を張り付けているかもしれない。しかし、数十行にわたる結果であれば何度もキャプチャが必要となる。
そこで、今日の最初はリダイレクト機能について説明する。

“gcc ファイル.c” は、C言語のプログラムをコンパイルし、a.out という実行ファイルを生成する。”./a.out” にてプログラムを実行する。実行する命令に、“> ファイル名” と書くと、通常の出力画面(標準出力) をファイル名に記録してくれる。これを出力リダイレクトと呼ぶ。また、“>> ファイル名” と書くと、既存ファイルの後ろに追記してくれる。

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
guest00@nitfcei:~$ cat helloworld.c
#include <stdio.h>
int main() {
printf( "Hello World\n" ) ;
return 0 ;
}
guest00@nitfcei:~$ gcc helloworld.c
guest00@nitfcei:~$ ./a.out
Hello World
guest00@nitfcei:~$ ./a.out > helloworld.txt
guest00@nitfcei:~$ cat helloworld.txt
Hello World
guest00@nitfcei:~$ ./a.out >> helloworld.txt
guest00@nitfcei:~$ cat helloworld.txt
Hello World
Hello World
guest00@nitfcei:~$ cat helloworld.c #include <stdio.h> int main() { printf( "Hello World\n" ) ; return 0 ; } guest00@nitfcei:~$ gcc helloworld.c guest00@nitfcei:~$ ./a.out Hello World guest00@nitfcei:~$ ./a.out > helloworld.txt guest00@nitfcei:~$ cat helloworld.txt Hello World guest00@nitfcei:~$ ./a.out >> helloworld.txt guest00@nitfcei:~$ cat helloworld.txt Hello World Hello World
guest00@nitfcei:~$ cat helloworld.c
#include <stdio.h>
int main() {
    printf( "Hello World\n" ) ;
    return 0 ;
}

guest00@nitfcei:~$ gcc helloworld.c
guest00@nitfcei:~$ ./a.out
Hello World

guest00@nitfcei:~$ ./a.out > helloworld.txt

guest00@nitfcei:~$ cat helloworld.txt
Hello World

guest00@nitfcei:~$ ./a.out >> helloworld.txt

guest00@nitfcei:~$ cat helloworld.txt 
Hello World
Hello World 

入力リダイレクト

次に、1行に名前と3教科の点数が書いてある複数行に渡るデータの各人の平均点を求めるプログラムを考える。

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
guest00@nitfcei:~$ cp /home0/Challenge/2.1-RedirectPipe.d/avg-each-low.c .
guest00@nitfcei:~$ cat avg-each-low.c
#include <stdio.h>
// ((input)) ((output))
// saitoh 43 54 82 saitoh 59.67
// tomoko 89 100 32 tomoko 73.67
// mitsuki 79 68 93 mitsuki 80.00
int main() {
char name[ 100 ] ;
int point[ 3 ] ;
while( scanf( "%s%d%d%d" ,
name , &point[0] , &point[1] , &point[2] ) == 4 ) {
double sum = 0.0 ;
for( int i = 0 ; i < 3 ; i++ )
sum += point[i] ;
printf( "%s %6.2f\n" , name , sum / 3.0 ) ;
}
return 0 ;
}
guest00@nitfcei:~$ gcc avg-each-low.c
guest00@nitfcei:~$ ./a.out
saitoh 43 54 82 入力
saitoh 59.67 出力
tomoko 89 100 32 入力
tomoko 73.67 出力
^D ← Ctrl-D を押すとファイル入力を終了
guest00@nitfcei:~$ cp /home0/Challenge/2.1-RedirectPipe.d/avg-each-low.c . guest00@nitfcei:~$ cat avg-each-low.c #include <stdio.h> // ((input)) ((output)) // saitoh 43 54 82 saitoh 59.67 // tomoko 89 100 32 tomoko 73.67 // mitsuki 79 68 93 mitsuki 80.00 int main() { char name[ 100 ] ; int point[ 3 ] ; while( scanf( "%s%d%d%d" , name , &point[0] , &point[1] , &point[2] ) == 4 ) { double sum = 0.0 ; for( int i = 0 ; i < 3 ; i++ ) sum += point[i] ; printf( "%s %6.2f\n" , name , sum / 3.0 ) ; } return 0 ; } guest00@nitfcei:~$ gcc avg-each-low.c guest00@nitfcei:~$ ./a.out saitoh 43 54 82 入力 saitoh 59.67 出力 tomoko 89 100 32 入力 tomoko 73.67 出力 ^D ← Ctrl-D を押すとファイル入力を終了
guest00@nitfcei:~$ cp /home0/Challenge/2.1-RedirectPipe.d/avg-each-low.c .
guest00@nitfcei:~$ cat avg-each-low.c
#include <stdio.h>
// ((input))           ((output))
// saitoh  43  54 82   saitoh 59.67
// tomoko  89 100 32   tomoko 73.67
// mitsuki 79  68 93   mitsuki 80.00
int main() {
   char name[ 100 ] ;
   int point[ 3 ] ;
   while( scanf( "%s%d%d%d" ,
                 name , &point[0] , &point[1] , &point[2] ) == 4 ) {
      double sum = 0.0 ;
      for( int i = 0 ; i < 3 ; i++ )
         sum += point[i] ;
      printf( "%s %6.2f\n" , name , sum / 3.0 ) ;
   }
   return 0 ;
}

guest00@nitfcei:~$ gcc avg-each-low.c
guest00@nitfcei:~$ ./a.out
saitoh 43  54 82    入力
saitoh 59.67        出力
tomoko 89 100 32    入力
tomoko 73.67        出力
^D             ← Ctrl-D を押すとファイル入力を終了

しかし、プログラムの書き方を間違えてプログラムを修正していると、動作確認のたびに何度も同じデータを入力するかもしれないが、面倒ではないだろうか?

プログラムを実行する時に、“< ファイル名” をつけると、通常はキーボードから入力する所を、ファイルからの入力に切り替えて実行することができる。このようなscanf()を使う時のようなプログラムの入力を標準入力といい、それをファイルに切り替えることを入力リダイレクトと呼ぶ。

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
guest00@nitfcei:~$ cp /home0/Challenge/2.1-RedirectPipe.d/name-point3.txt .
guest00@nitfcei:~$ cat name-point3.txt
saitoh 43 54 82
tomoko 89 100 32
mitsuki 79 68 93
guest00@nitfcei:~$ ./a.out < name-point3.txt
saitoh 59.67
tomoko 73.67
mitsuki 80.00
guest00@nitfcei:~$ cp /home0/Challenge/2.1-RedirectPipe.d/name-point3.txt . guest00@nitfcei:~$ cat name-point3.txt saitoh 43 54 82 tomoko 89 100 32 mitsuki 79 68 93 guest00@nitfcei:~$ ./a.out < name-point3.txt saitoh 59.67 tomoko 73.67 mitsuki 80.00
guest00@nitfcei:~$ cp /home0/Challenge/2.1-RedirectPipe.d/name-point3.txt .

guest00@nitfcei:~$ cat name-point3.txt
saitoh  43  54 82
tomoko  89 100 32
mitsuki 79  68 93 

guest00@nitfcei:~$ ./a.out < name-point3.txt
saitoh  59.67
tomoko  73.67
mitsuki 80.00

この入力リダイレクトと出力リダイレクトを合わせて使うこともできる。

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
guest00@nitfcei:~$ ./a.out < name-point3.txt > name-avg.txt
guest00@nitfcei:~$ cat name-avg.txt
saitoh 59.67
tomoko 73.67
mitsuki 80.00
guest00@nitfcei:~$ ./a.out < name-point3.txt > name-avg.txt guest00@nitfcei:~$ cat name-avg.txt saitoh 59.67 tomoko 73.67 mitsuki 80.00
guest00@nitfcei:~$ ./a.out < name-point3.txt > name-avg.txt

guest00@nitfcei:~$ cat name-avg.txt
saitoh  59.67
tomoko  73.67
mitsuki 80.00

パイプ

先の名前と3教科のプログラムの結果から、全員の平均点をも計算したい場合、どのようなプログラムを作るだろうか?C言語だけの知識なら、各人の行のデータを計算するループの中に、全員の合計と人数を求めるプログラムを書いて、最後に平均点を出力するだろう。

一方で、複数人の名前と平均点のデータから平均点を求めるプログラムを書いて、前述のプログラムの実行結果を使う人もいるだろう。

以下の例では、“gcc -o avg-each-row avg-each-row.c” で、avg-each-row という実行ファイル、“gcc -o avg-all avg-all.c” で、avg-all という実行ファイルを生成し、avg-each-row で入力リダイレクト・出力リダイレクトを使って、name-avg.txt を作り、avg-all を入力リダイレクトで、最終結果を表示している。

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
guest00@nitfcei:~$ cp /home0/Challenge/2.1-RedirectPipe.d/avg-all.c .
guest00@nitfcei:~$ cat avg-all.c
#include <stdio.h>
// ((input)) ((output))
// saitoh 59.67 73.11
// tomoko 73.67
// mitsuki 80.00
int main() {
char name[ 100 ] ;
double point ;
double sum = 0 ;
int count = 0 ;
while( scanf( "%s%lf" , name , &point ) == 2 ) {
sum += point ;
count++ ;
}
printf( "%6.2f\n" , sum / (double)count ) ;
return 0 ;
}
guest00@nitfcei:~$ gcc -o avg-each-low avg-each-low.c
guest00@nitfcei:~$ gcc -o avg-all avg-all.c
guest00@nitfcei:~$ ./avg-each-low < name-point3.txt > name-avg.txt
guest00@nitfcei:~$ ./avg-all < name-avg.txt
71.11
guest00@nitfcei:~$ cp /home0/Challenge/2.1-RedirectPipe.d/avg-all.c . guest00@nitfcei:~$ cat avg-all.c #include <stdio.h> // ((input)) ((output)) // saitoh 59.67 73.11 // tomoko 73.67 // mitsuki 80.00 int main() { char name[ 100 ] ; double point ; double sum = 0 ; int count = 0 ; while( scanf( "%s%lf" , name , &point ) == 2 ) { sum += point ; count++ ; } printf( "%6.2f\n" , sum / (double)count ) ; return 0 ; } guest00@nitfcei:~$ gcc -o avg-each-low avg-each-low.c guest00@nitfcei:~$ gcc -o avg-all avg-all.c guest00@nitfcei:~$ ./avg-each-low < name-point3.txt > name-avg.txt guest00@nitfcei:~$ ./avg-all < name-avg.txt 71.11
guest00@nitfcei:~$ cp /home0/Challenge/2.1-RedirectPipe.d/avg-all.c .
guest00@nitfcei:~$ cat avg-all.c
#include <stdio.h>
// ((input))      ((output))
// saitoh  59.67  73.11
// tomoko  73.67
// mitsuki 80.00
int main() {
   char name[ 100 ] ;
   double point ;
   double sum = 0 ;
   int count = 0 ;
   while( scanf( "%s%lf" , name , &point ) == 2 ) {
      sum += point ;
      count++ ;
   }
   printf( "%6.2f\n" , sum / (double)count ) ;
   return 0 ;
}

guest00@nitfcei:~$ gcc -o avg-each-low avg-each-low.c
guest00@nitfcei:~$ gcc -o avg-all avg-all.c

guest00@nitfcei:~$ ./avg-each-low < name-point3.txt > name-avg.txt

guest00@nitfcei:~$ ./avg-all < name-avg.txt
71.11

しかし、いちいち入出力の結果を name-avg.txt を作るのは面倒である。であれば、以下の様なイメージで処理をすれば答えが求まる。

name-point3.txt(avg-each-row)name-avg.txt(avg-all)結果

これは、パイプ機能を使って以下の様に動かすことができる。

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
guest00@nitfcei:~$ ./avg-each-low < name-point3.txt | ./avg-all
71.11
guest00@nitfcei:~$ cat name-point3.txt | ./avg-each-low | ./avg-all
71.11
guest00@nitfcei:~$ ./avg-each-low < name-point3.txt | ./avg-all 71.11 guest00@nitfcei:~$ cat name-point3.txt | ./avg-each-low | ./avg-all 71.11
guest00@nitfcei:~$ ./avg-each-low < name-point3.txt | ./avg-all
71.11

guest00@nitfcei:~$ cat name-point3.txt | ./avg-each-low | ./avg-all
71.11

プログラムを実行する時に、“A | B” ように書くと、プログラムA の標準出力結果を、プログラムB の標準入力に接続させて、2つのプログラムを実行できる。このような機能を、パイプと呼ぶ。上記例の2つめ “cat… | ./avg-each-low | ./avg-all” では3つのプログラムをパイプでつないでいる。


リダイレクトのまとめ

 

入力リダイレクト(標準入力) 実行コマンド < 入力ファイル
出力リダイレクト(標準出力) 実行コマンド > 出力ファイル
 出力リダイレクト(標準出力の追記) 実行コマンド >> 出力ファイル
 標準エラー出力のリダイレクト 実行コマンド 2> 出力ファイル
パイプ
コマンドAの標準出力をコマンドBの標準入力に接続
コマンドA | コマンドB

標準エラー出力,/dev/null, /dev/zero デバイス

パイプやリダイレクトを使っていると、出力をファイルに保存する場合、途中で異常を伝える目的で出力メッセージを出力するものが見逃すかもしれない。こういった際に、計算結果などを出力する標準出力(stdout)とは別に標準エラー出力(stderr)がある。ファイルを使う際には、デバイスを区別するためのデバイス番号(ファイルディスクリプタ)を使う。この際に、標準入力(stdin) = 0, 標準出力(stdout) = 1, 標準エラー出力(stderr) = 2 という番号を使うことになっている。

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
#include <stdio.h>
int main() {
int x , y ;
scanf( "%d%d" , &x , &y ) ;
if ( y == 0 )
fprintf( stderr , "zero divide\n" ) ;
else
printf( "%f\n" , (double)x / (double)y ) ;
}
#include <stdio.h> int main() { int x , y ; scanf( "%d%d" , &x , &y ) ; if ( y == 0 ) fprintf( stderr , "zero divide\n" ) ; else printf( "%f\n" , (double)x / (double)y ) ; }
#include <stdio.h>
int main() {
   int x , y ;
   scanf( "%d%d" , &x , &y ) ;
   if ( y == 0 )
      fprintf( stderr , "zero divide\n" ) ;
   else
      printf( "%f\n" , (double)x / (double)y ) ;
}

Unix のシステムでは、特殊なデバイスとして、/dev/null, /dev/zero というのがある。

/dev/null は、入力リダイレクトに使うと ファイルサイズ 0 byte のデータが得られるし、出力リダイレクトに使うと 書き込んだデータは捨てられる。 /dev/zero は、入力リダイレクトに使うと、’\0′ のデータを延々と取り出すことができ、ファイルをゼロで埋めるなどの用途で使う。

/dev/null の使い方の例としては、例えば標準エラー出力が不要なときは、コマンドラインで以下のようにすれば、標準エラー出力(デバイス番号2番)の内容を捨てることができる。

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
$ command 2> /dev/null
$ command 2> /dev/null
$ command 2> /dev/null
Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter

C言語のコンパイルまとめ

 

C言語のコンパイル(実行ファイルはa.out) gcc ソースファイル
 実行ファイル名を指定してコンパイル gcc -o 実行ファイル ソースファイル

 

理解度確認

Windows における PRN デバイス, AUX デバイス

/dev/null といった特殊デバイスの一例として、昔の Windows では PRN デバイス、AUX デバイスというのがあった。

C:< command > PRN

昔の文字印刷しかできないプリンタが普通だったころは、PRN という特殊デバイスがあって、このデバイスにリダイレクトをすれば、出力をプリンタに出力することができた。同じように AUX というデバイスは、通信ポートにつながっていて ” command > AUX ” と入力すれば、通信先にデータを送ることができた。最近のプリンタや通信デバイスはもっと複雑になっているためにこういった機能は使えなくなっているが、このデバイス名の名残りで、Windows では PRN とか AUX という名前のファイルを作ることはできない。

Javaのオブジェクト指向入門

今日は、テスト返しの残り時間で、4年の情報構造論で、リスト構造などの内容を進める前に、3年プログラミング応用でクラスなどに自信がない人向けの簡単レクチャ。

クラスは、データ構造と手続き

例えば、名前と年齢のデータをクラスで扱うのであれば、以下のようなコードが基本となるだろう。

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
import java.util.*;
class NameAge {
String name ; // インスタンス変数
int age ; // インスタンス変数
static int count = 0 ; // クラス変数
// コンストラクタ
NameAge( String s , int a ) {
this.name = s ;
this.age = a ;
count++ ;
}
// メソッド
void print() {
System.out.println( this.name + "," + this.age ) ;
System.out.println( "member = " + count ) ;
}
} ;
public class Main {
public static void main(String[] args) throws Exception {
NameAge tsaitoh = new NameAge( "tsaitoh" , 59 ) ;
tsaitoh.print() ;
System.out.println( "age = " + tsaitoh.age ) ;
NameAge tomoko = new NameAge( "tomoko" , 48 ) ;
tomoko.print() ;
}
}
実行結果
tsaitoh,59
member = 1
age = 59
tomoko,48
member = 2
import java.util.*; class NameAge { String name ; // インスタンス変数 int age ; // インスタンス変数 static int count = 0 ; // クラス変数 // コンストラクタ NameAge( String s , int a ) { this.name = s ; this.age = a ; count++ ; } // メソッド void print() { System.out.println( this.name + "," + this.age ) ; System.out.println( "member = " + count ) ; } } ; public class Main { public static void main(String[] args) throws Exception { NameAge tsaitoh = new NameAge( "tsaitoh" , 59 ) ; tsaitoh.print() ; System.out.println( "age = " + tsaitoh.age ) ; NameAge tomoko = new NameAge( "tomoko" , 48 ) ; tomoko.print() ; } } 実行結果 tsaitoh,59 member = 1 age = 59 tomoko,48 member = 2
import java.util.*;

class NameAge {
    String name ;          // インスタンス変数
    int    age ;           // インスタンス変数
    static int count = 0 ; // クラス変数
    // コンストラクタ
    NameAge( String s , int a ) {
        this.name = s ;
        this.age  = a ;
        count++ ;
    }
    // メソッド
    void print() {
        System.out.println( this.name + "," + this.age ) ;
        System.out.println( "member = " + count ) ;
    }
} ;

public class Main {
    public static void main(String[] args) throws Exception {
        NameAge tsaitoh = new NameAge( "tsaitoh" , 59 ) ;
        tsaitoh.print() ;
        System.out.println( "age = " + tsaitoh.age ) ;
        NameAge tomoko  = new NameAge( "tomoko" ,  48 ) ;
        tomoko.print() ;
    }
}

実行結果
tsaitoh,59
member = 1
age = 59
tomoko,48
member = 2

クラスとは、データ構造(オブジェクト)とそのデータ構造を扱うための関数(メソッド)をまとめて扱う。

クラス NameAge の中で宣言されている、NameAge() の関数は、オブジェクトを初期化するための関数(メソッド)であり、特にコンストラクタと呼ばれる。

実際にデータを保存するための tsaitoh や tomoko とよばれる変数に NameAge オブジェクトの実体を作る時には 「new クラス名」 とやることで、初期化ができる。

イメージでは、下図のようなデータ構造ができあがる。

でも、年齢の覚え方は、将来的に誕生日を覚えるように変化するかもしれない。この際に、Main 関数の中で age を使うと後で混乱の元になるかもしれない。こういう時は、NameAge クラス以外では中身を勝手に使わせないために、インスタンス変数などに public / private といったアクセス制限を加える。

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
import java.util.*;
class NameAge {
private String name ; // インスタンス変数
private int age ; // インスタンス変数
public static int count = 0 ; // クラス変数
// コンストラクタ
public NameAge( String s , int a ) {
this.name = s ;
this.age = a ;
count++ ;
}
// メソッド
public void print() {
System.out.println( this.name + "," + this.age ) ;
System.out.println( "member = " + count ) ;
}
} ;
public class Main {
public static void main(String[] args) throws Exception {
NameAge tsaitoh = new NameAge( "tsaitoh" , 59 ) ;
tsaitoh.print() ;
System.out.println( "age = " + tsaitoh.age ) ;
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ ここがエラーになる。NameAge::age は private
NameAge tomoko = new NameAge( "tomoko" , 48 ) ;
tomoko.print() ;
}
}
import java.util.*; class NameAge { private String name ; // インスタンス変数 private int age ; // インスタンス変数 public static int count = 0 ; // クラス変数 // コンストラクタ public NameAge( String s , int a ) { this.name = s ; this.age = a ; count++ ; } // メソッド public void print() { System.out.println( this.name + "," + this.age ) ; System.out.println( "member = " + count ) ; } } ; public class Main { public static void main(String[] args) throws Exception { NameAge tsaitoh = new NameAge( "tsaitoh" , 59 ) ; tsaitoh.print() ; System.out.println( "age = " + tsaitoh.age ) ; ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ ここがエラーになる。NameAge::age は private NameAge tomoko = new NameAge( "tomoko" , 48 ) ; tomoko.print() ; } }
import java.util.*;
class NameAge {
    private String name ;          // インスタンス変数
    private int    age ;           // インスタンス変数
    public  static int count = 0 ; // クラス変数
    // コンストラクタ
    public  NameAge( String s , int a ) {
        this.name = s ;
        this.age  = a ;
        count++ ;
    }
    // メソッド
    public void print() {
        System.out.println( this.name + "," + this.age ) ;
        System.out.println( "member = " + count ) ;
    }
} ;

public class Main {
    public static void main(String[] args) throws Exception {
        NameAge tsaitoh = new NameAge( "tsaitoh" , 59 ) ;
        tsaitoh.print() ;
        System.out.println( "age = " + tsaitoh.age ) ;
        ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ ここがエラーになる。NameAge::age は private 
        NameAge tomoko  = new NameAge( "tomoko" ,  48 ) ;
        tomoko.print() ;
    }
}

クラス自体も、public class NameAge … のように宣言することもあるが、public なクラスは 1つ の *.java ファイルの中に1つしか書けないというルールがあるので要注意。

中間試験問題の回答

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
import java.util.*;
import java.util.* ;
class Item {
int id ;
String name ;
int price ;
Item( int i , String n , int p ) {
this.id = i ;
this.name = n ;
this.price = p ;
}
} ;
class Buy {
int id ;
int count ;
Buy( int i , int n ) {
this.id = i ;
this.count = n ;
}
} ;
public class Main {
static Item[] item_list = {
new Item( 1010 , "orange" , 50 ) ,
new Item( 1020 , "apple" , 100 ) ,
new Item( 1022 , "pineapple" , 1000 ) ,
} ;
static Buy[] buy_list = {
new Buy( 1010 , 5 ) ,
new Buy( 1020 , 3 ) ,
new Buy( 1022 , 1 ) ,
} ;
public static void main( String[] args ) {
System.out.println( total_price( item_list , buy_list ) ) ;
}
static int total_price( Item[] i_list , Buy[] b_list ) {
int sum = 0 ;
for( Item item : i_list )
for( Buy buy : b_list )
if ( item.id == buy.id )
sum += item.price * buy.count ;
return sum ;
}
// static int total_price( Item[] i_list , Buy[] b_list ) {
// int sum = 0 ;
// for( int i = 0 ; i < i_list.length ; i++ )
// for( int j = 0 ; j < b_list.length ; j++ )
// if ( i_list[ i ].id == b_list[ j ].id )
// sum += i_list[ i ].price * b_list[ j ].count ;
// return sum ;
// }
}
import java.util.*; import java.util.* ; class Item { int id ; String name ; int price ; Item( int i , String n , int p ) { this.id = i ; this.name = n ; this.price = p ; } } ; class Buy { int id ; int count ; Buy( int i , int n ) { this.id = i ; this.count = n ; } } ; public class Main { static Item[] item_list = { new Item( 1010 , "orange" , 50 ) , new Item( 1020 , "apple" , 100 ) , new Item( 1022 , "pineapple" , 1000 ) , } ; static Buy[] buy_list = { new Buy( 1010 , 5 ) , new Buy( 1020 , 3 ) , new Buy( 1022 , 1 ) , } ; public static void main( String[] args ) { System.out.println( total_price( item_list , buy_list ) ) ; } static int total_price( Item[] i_list , Buy[] b_list ) { int sum = 0 ; for( Item item : i_list ) for( Buy buy : b_list ) if ( item.id == buy.id ) sum += item.price * buy.count ; return sum ; } // static int total_price( Item[] i_list , Buy[] b_list ) { // int sum = 0 ; // for( int i = 0 ; i < i_list.length ; i++ ) // for( int j = 0 ; j < b_list.length ; j++ ) // if ( i_list[ i ].id == b_list[ j ].id ) // sum += i_list[ i ].price * b_list[ j ].count ; // return sum ; // } }
import java.util.*;
import java.util.* ;

class Item {
    int    id ;
    String name ;
    int    price ;
    Item( int i , String n , int p ) {
        this.id    = i ;
        this.name  = n ;
        this.price = p ;
    }
} ;

class Buy {
    int    id ;
    int    count ;
    Buy( int i , int n ) {
        this.id    = i ;
        this.count = n ;
    }
} ;

public class Main {
    static Item[] item_list = {
        new Item( 1010 , "orange" ,      50 ) ,
        new Item( 1020 , "apple" ,      100 ) ,
        new Item( 1022 , "pineapple" , 1000 ) ,
    } ;
    static Buy[] buy_list = {
        new Buy( 1010 , 5 ) ,
        new Buy( 1020 , 3 ) ,
        new Buy( 1022 , 1 ) ,
    } ;
    public static void main( String[] args ) {
        System.out.println( total_price( item_list , buy_list ) ) ;
    }
    static int total_price( Item[] i_list , Buy[] b_list ) {
        int sum = 0 ;
        for( Item item : i_list )
            for( Buy buy : b_list )
                if ( item.id == buy.id )
                    sum += item.price * buy.count ;
        return sum ;
    }
    // static int total_price( Item[] i_list , Buy[] b_list ) {
    //     int sum = 0 ;
    //     for( int i = 0 ; i < i_list.length ; i++ )
    //         for( int j = 0 ; j < b_list.length ; j++ )
    //             if ( i_list[ i ].id == b_list[ j ].id )
    //                 sum += i_list[ i ].price * b_list[ j ].count ;
    //     return sum ;
    // }
}

練習問題

科目(Subject)と学生(Student)の情報があり、科目を受講した成績(Result)で成績を管理している。
このデータを管理するためのクラスを宣言し、下に示すような出力が得られるプログラムを作成せよ。

今回の中間テストで成績が悪かった人は、テスト前に示したレポート課題ではなく下記の課題で提出してよいこととする。

科目: Subject
id    name       teacher   // Subject[] subject_table = {
10010 情報構造論 t-saitoh   //    new Subject( 10010 , "情報構造論" , "t-saitoh" ) ,
10020 電気磁気学 takaku     //    new Subject( ....
10030 電気回路   komatsu    // } ;

成績: Result
s_id  id         point      // Result[] result_table = {
16213 10020      83         //    new Result( 16213 , 10020 , 83 ) ,
18348 10010      95         //    new Result( ...
17101 10030      64         // } ;
16213 10010      89

学生: Student
s_id  name       age        // Student[] student_table = {
16213 斉藤太郎   18          //    new Student( 16213 , "斉藤太郎" , 18 ) ,
17101 山田次郎   19          //    new Student( ...
18348 渡辺花子   18          // } ;

以下のようなデータが出力されること
斉藤太郎 電気磁気学 83
渡辺花子 情報構造論 95
山田次郎 電気回路   64
斉藤太郎 情報構造論 89

Microsoft365のデータ容量上限設定

今までは、OneDrive,Teams,Outlookは、Microsoft との協定で高専では、かなり余裕をもって使えていたけど、利用の拡大で色々と制限がかかるようになった。そりゃしかたがないよね。

システム名    現行の容量        制限後の容量(対象)
・OneDrive         1TB     50GB(全教職員・学生)
・Teams・SharePoint   1TB     100GB(Teams(チーム)・SharePointサイト)
・Outlook             50GB       10GB(全教職員・学生)

ということで、職場個人の OneDrive と Outlook を大掃除。

  • OneDrive 55GB → 20GB
  • Outlook 11GB→ 6.5GB

Outlook は、数年前までは用途別にフォルダ分けをしていたけど、この3,4年はフォルダ分けを断念。受信トレイに18000件,3.8GB のメッセージが溜まってる。(でもほとんど改めては見ることのないメールばかりだろう)

ネットワークとセキュリティ

前回の授業では、バックエンドのプログラムの問題によるセキュリティ問題を説明した。今回は、ネットワークの物理的な接続方法などの話を中心にセキュリティについて説明を行う。

ネットワークからの攻撃とFireWall

脆弱性とバッファオーバーフロー

プログラムには、何らかのバグが潜んでいる可能性があり、悪用すると悪意のプログラムの実行や、情報の漏えい、システム異常を発生させサービスができなくするなどの脆弱性があって、悪意のある利用者から攻撃をうける可能性がある。

例えば、下記のようなC言語のプログラムは、配列をはみ出るようなデータを与えることで、関数の戻り番地を破壊させ、はみ出た部分に送り込んだ悪意のプログラムを実行させることが可能となる。このような入力用のデータ領域(バッファ)をはみ出させる攻撃はバッファオーバーフローと呼ばれる。

なお、最近のC言語のライブラリでは、上記のようなバッファオーバーフロー攻撃が一般的であることから、ASLR(Address Space Layout Randomization)により、スタック領域などの位置をランダム化することで、バッファオーバーフローによる攻撃が失敗するようになっている。

ルータとFireWall

外部にサービスを提供するようなシステムで、何らかの脆弱性のあるプログラムが使われていると、外部からのネットワーク接続で悪意のあるプログラム(マルウェア)を実行させられてしまうかもしれない。

このため、コンピュータでは不必要なプログラム(ネットワークサービス)は、起動されないようにする必要がある。もしくは、そのサービスは外部から利用できないように、途中のルータで FireWall(防火壁) を設置する。

FireWall では、(1)攻撃の可能性のあるIPアドレスからの接続を拒否、(2)外部に公開していないネットワークサービスのポート番号の接続を拒否といった方法をとる(拒否リスト方式)。もっと厳しい対策であれば、(3)特定のIPアドレスの機器からのみ接続を許可、(4)許可されているネットワークサービスのポート番号だけからだけ許可する方式をとる(許可リスト方式)

外部に公開する必要のないサービスがFireWallなどで正しく保護されていないと、攻撃をうける可能性がある。

ネットワーク接続のための装置

ルータやFireWallなどの仕組みをもう少し理解するために、組織内でネットワークを接続するための機器とその機能について改めて確認する。

ルータとは

元々の有線LANでは、1本のケーブルを時分割多重で共有しながら通信を行う。このため、瞬間的にはとある機器がネットワークを使用している時は、他の機器はデータ通信ができなくなる。この1本の線を大量の機器で使おうとすると、機器が使えるタイミングは減ってしまう。そこで、1本の線に直接接続する機器を分割したサブネットに分けて、必要な時だけ隣接するサブネットにパケットを中継するルータ or ブリッジが重要となる。

ルータは、隣接するサブネットのネットワーク番号(IPアドレスとサブネットマスク)を確認し、パケットを流す先を決定する。このネットワーク番号(IPアドレスとサブネットマスクの論理積)と中継を依頼するゲートウェイ(転送先)の一覧をルーティングテーブルと呼ぶ。

組織内のルータであれば、ネットワークの構造に合わせてあらかじめルーティングテーブルを定義しておく(静的ルーティング)。組織と組織を接続するようなルータは、自分に送ってほしいネットワーク番号の情報を相互に交換している(動的ルーティング)

ブリッジとHUB

ネットワークを接続するための機器には、ブリッジHUBが使われていた。

スイッチングHUB

機器を接続するための古いHUB(ダムHUB)では、通信中は他の機器の通信ができず効率が悪い。最近のHUBでは、通信する相手に応じて、内部のネットワークケーブルをスイッチのように接続・分離することができるスイッチングHUBを用いる。通信相手の識別には、一般的にMACアドレスが用いられる。(レイヤ2でのスイッチングHUB)

家庭用のスイッチングHUBは、特に細かい設定などは不要で管理機能が無いものは、アン マネージド スイッチングHUBと呼ばれる。

L2スイッチとL3スイッチ

サブネットに分割し、それぞれに異なるネットワーク番号を割り振り、中継するルータで FireWall を機能させることで、セキュリティを高めることが行われる。しかし、性能の高いスイッチングHUBは高価でもあり、1つのHUBに異なるネットワークを接続する必要がでてくることもある。この場合、IPアドレスを異なるネットワークの番号を偽装されると、データが盗み見られるかもしれない。

こういった相互に分離すべきネットワークであっても、柔軟なネットワーク構成とするためには、VLAN機能を持った L2スイッチ(レイヤ2スイッチングHUB) が使われる。タグVLAN機能付きのL2スイッチでは、特定のポートにVLANのタグ番号を割り当て、ポートに入る時にパケットに VLAN のタグ情報を付加し、そのパケットは同じ VLAN のタグをもつポートからしかデータを取り出せない。

L2スイッチ(レイヤ2スイッチ)は、機器のMACアドレスやパケットに付けられたVLANのタグ番号の情報(レイヤ2=データリンク層)でパケットの流れを制御している(下記OSI参照モデルの表を参照)。最近では、許可されていない機器がネットワークに侵入する不正侵入を防ぐために、登録されていないMACアドレスのパケットを通さないといった機能がある。

OSI参照モデルとレイヤ
第7層 アプリケーション層 アプリケーションの種類の規定
第6層 プレゼンテーション層 データフォーマットの交換
第5層 セッション層 コネクションの確立や切断などの管理
第4層 トランスポート層 パケットの分割合成や再送といった管理(TCP)
第3層 ネットワーク層 隣接するネットワーク間の通信(IPアドレス)
第2層 データリンク層 直接接続された機器間の通信(MACアドレス)
第1層 物理層 物理的な接続方法(コネクタや電圧など)

スイッチングHUBの中には、レイヤ3(IPアドレス)の情報でパケットの流れを制御するものもある。こういったスイッチは、L3スイッチ(レイヤ3スイッチ)と呼ばれるが、機能的にはルータと同じである。

一般的には、LANとWANを接続するための機器はルータ、LAN内部のネットワークを分離するためのものはL3スイッチと呼ぶ。

インターネットと接続するルータの機能

ネットワーク通信のIPアドレスとポート番号

クライアントの機器と通信相手との通信では、通信相手のIPアドレスとポート番号を指定してパケットを送出するが、処理結果を送信元に送り返すために、送信元のIPアドレスとポート番号が付加されている。送信元ではポート番号は、通信でよく使われる0~1023までのポート番号(ウェルノウンポート)以外で、1024~65535のポート番号(エフェメラルポート)の中から使われていないものをランダムに選んで使う。

送信相手に届いたパケットの返信データには、送信元と送信相手のIPアドレスとポート番号を入れ替えたものを割り当てることで、送信元にパケットが戻ってくる。

  • DIP = 送信先IPアドレス、DP = 送信先ポート番号
  • SIP = 送信元IPアドレス、SP = 送信元ポート番号

NAT(Network Address Translation)

現在広く使われているIPv4アドレス(32bit)では、40億台の機器間の通信しかできない。このため、組織内だけで使われるIPアドレス(プライベートIPアドレス)を使い、インターネットではグローバルIPアドレスを使う。

プライベートIPアドレス
クラスA/8 10.0.0.0~10.255.255.255 大規模組織向け
クラスB/12 172.16.0.0~172.31.255.255 中規模組織向け
クラスC/16 192.168.0.0~192.168.255.255 家庭用ルータ向け

組織内のLANからインターネット(WAN)に接続する際には、プライベートアドレスをグローバルアドレスに変換するNAT(Network Address Translation)の機能が使われる。

NATの問題点

しかし、インターネットの内側で異なる機器で同じポート番号が割り振られると、戻ってきたパケットをどちらの機器に送ればいいのか区別できなくなる。

NAPT(Netowrk Address and Port Translation)

そこで、最近のNATでは、IPアドレスの変換だけでなくポート番号の付け替えも行われる。この機能は正式には NAPT(Network Address and Port Translation) と呼ぶが、単に NAT と呼ぶことも多い。Linuxでは、NAPTの実装をIPマスカレードと呼ぶ。

FireWall と DMZ

組織内で外部に公開しているサーバがある場合は、以下の図のような構成にするかもしれない。しかし、このようなネットワーク構成では、FireWallの内側の公開サーバが攻撃されて、踏み台となった場合、組織内のPCが簡単に攻撃をうけてしまう。

そこで、外部からの接続を行う DMZ(De-Militarized Zone 非武装地帯) を設け、外部公開用の公開サーバは DMZ 内に設置する。外部とつながる FireWall では、外部からのパケットは DMZ に流れるように構成する。DMZ 内のサーバが踏み台になった場合を想定し、組織内のルータでは DMZ のサーバと組織内PCは通信できないように FireWall を2重に設置する。

配列に要素を追加

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

配列にデータを追加

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

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
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 ) ;
}
}
}
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 ) ; } } }
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 のようなデータ構造の仕組みを考えるために、最も単純な配列でプログラムを作ってみる。

末尾に追加

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
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] ) ;
}
}
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] ) ; } }
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言語で書いてみる。

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
#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 ;
}
#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 ; }
#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分探索法を使うのが一般的であろう。では、前述のプログラムを昇順で保存するにはどうすべきか?

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

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
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++ ;
}
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++ ; }
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++ ;
}
Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
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++ ;
}
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++ ; }
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[] に次の値の保存されている場所が入っている。

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
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 ] ) ;
}
}
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 ] ) ; } }
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 ] ) ;
    }
}
Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
#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 ;
}
#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 ; }
#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)で途中にデータを挿入できる。

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

リスト構造 ListNode

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

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
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 ) ;
}
}
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 ) ; } }
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 ) ;
    }
}
Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
#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 ;
}
#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 ; }
#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++のテンプレート)を使って書いてみた。ジェネリクスは、クラスやメソッドにおいて、特定の型を指定することなく動作するコードを記述することができる機能。これにより、型安全性を保ちながら、コードの再利用性と柔軟性を向上させることがでる。

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
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 ) ;
}
}
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 ) ; } }
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のジェネリッククラスで記述

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
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.*; 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.*;

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

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

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
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() ;
}
}
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() ; } }こ
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() ;
    }
}こ

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

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

前回説明した仮想関数では、基底クラスから派生させたクラスを作り、そのデータが混在してもクラスに応じた関数(仮想関数)を呼び出すことができる。

この仮想関数の機能を逆手にとったプログラムの記述方法として、抽象クラス(純粋仮想基底クラス)がある。その使い方を説明する。

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のマウス処理が継承されるので、省略してもうまく動くようになっている。

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

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

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

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

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

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
// 純粋仮想基底クラス
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 ;
} ;
// 純粋仮想基底クラス 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 ; } ;
// 純粋仮想基底クラス
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 を持つように作られている。

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

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

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

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
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 ) ;
}
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 ) ; }
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件あたりのデータサイズや、データ実体へのポインタを正しく理解する必要がある。

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
#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 ) ,
(int(*)(void*,void*)) cmp_double ) ;
}
#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 ) , (int(*)(void*,void*)) cmp_double ) ; }
#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 ) ,
          (int(*)(void*,void*)) cmp_double ) ;
} 

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

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
// コールバック関数 f を呼び出す関数
function exec_callback( var f ) {
f() ;
}
// コールバックされる関数 foo()
function foo() {
console.log( "foo()" ) ;
}
// foo() を実行する。
exec_callback( foo ) ;
// 無名関数を実行する。
exec_callback( function() {
console.log( "anonymous" ) ;
} ) ;
// コールバック関数 f を呼び出す関数 function exec_callback( var f ) { f() ; } // コールバックされる関数 foo() function foo() { console.log( "foo()" ) ; } // foo() を実行する。 exec_callback( foo ) ; // 無名関数を実行する。 exec_callback( function() { console.log( "anonymous" ) ; } ) ;
// コールバック関数 f を呼び出す関数
function exec_callback( var f ) {
   f() ;
}
// コールバックされる関数 foo()
function foo() {
   console.log( "foo()" ) ;
}
// foo() を実行する。
exec_callback( foo ) ;
// 無名関数を実行する。
exec_callback( function() {
                  console.log( "anonymous" ) ;
               } ) ;

任意のデータを並び替え

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
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 ;
} ;
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 ; } ;
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() を書くだけで良い。

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

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

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

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
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 ;
}
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 ; }
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() が作られる。

テンプレート機能では、各型用のコードが自動的に複数生成されるという意味では、出来上がったコードがコンパクトという訳ではない。

仮想関数レポート課題

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

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

実数の取り扱いと誤差

実数型(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

float 型は、計算精度が低いので 通常の数値計算のプログラミングではあまり使われることはない。一方で、ゲームなどの3次元座標計算などでは、精度は必要もないことから、GPU(グラフィックス専用のプロセッサ)では float 型を使うことも多い。また、最近の機械学習のプログラミングでは、神経の動きをまねた計算(ニューラルネットワークプログラミング)が行われるが、これも精度はあまり高くなくてもいいので float 型を使うことも多く、グラフィックス用の GPU で float 型で機械学習の計算を行うことも多い。

倍精度型(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 ;” に変更しないものとする。

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
// 入力値の合計と平均を求める。
#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 ) ;
}
// 入力値の合計と平均を求める。 #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 ) ; }
// 入力値の合計と平均を求める。
#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) の値を表示するプログラム。

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
// 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 ;
}
// 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 ; }
// 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 ;
}

動かないプログラム

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

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
// 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-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-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 ;
}
Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
// 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-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-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 の表で、有効桁を考えてもらうための演習問題(ランダムに値が作られます)を有効数字を考えながら計算し、答えをレポートにまとめてください。例を以下に示す。

レポートは、こちらのひな型をベースに作成し(手書きノートをキャプチャした資料でもOKです)、同じフォルダに提出してください。

 

Surface GO に Ubuntu 24.04 をインストール

仕事で使っていた Surface Go だけど、最近は処理速度も「もっさり」で使う機会もほぼなく、Ubuntu 24.04 をインストールを試してみる。ブートメディアで Type-C USB を購入し Rufus で イメージファイルを書き込む。

BitLockerの解除から

ひとまず、Try モードで起動もできたし、本気のインストールを試そうとしたら、BitLocker で暗号化されているとの表示。暗号化解除をしようと再起動したら、BitLocker キーの催促画面。はて、なんだっけ。

学校のアカウントでデバイスを探したら普通に キーが見つかったので、解除。といっても時間かかるなぁ…

{CAPTION}Surface Go では「vol-長押ししながら電源」でブートメニューを出すとあるが、上手くいかなかった。仕方がないのでWindowsで「回復」機能でブートメニューを起動させた。
{CAPTION}
{CAPTION}

OneNoteの起動用アイコン

snap を使っていろいろな最新アプリも使うことができるが、Microsoft OneNote が起動できなかった。手作業で起動しようとすると、「うまく起動できない場合は –no-sabdbox のオプションをつけるといいかも」みたいな説明が出る。ということで、デスクトップに下記のファイルを置いて対応。

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
#!/usr/bin/env xdg-open
[Desktop Entry]
Terminal=false
Type=Application
Name=Microsoft OneNote
Icon=/snap/onenote-desktop/current/meta/gui/icon.png
Exec=/snap/onenote-desktop/current/onenote-desktop --no-sandbox
#!/usr/bin/env xdg-open [Desktop Entry] Terminal=false Type=Application Name=Microsoft OneNote Icon=/snap/onenote-desktop/current/meta/gui/icon.png Exec=/snap/onenote-desktop/current/onenote-desktop --no-sandbox
#!/usr/bin/env xdg-open
[Desktop Entry]
Terminal=false
Type=Application
Name=Microsoft OneNote
Icon=/snap/onenote-desktop/current/meta/gui/icon.png
Exec=/snap/onenote-desktop/current/onenote-desktop --no-sandbox

ひとまず設定完了

システム

最新の投稿(電子情報)

アーカイブ

カテゴリー