ホーム » 2023 (ページ 8)

年別アーカイブ: 2023

2025年5月
 123
45678910
11121314151617
18192021222324
25262728293031

検索・リンク

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

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

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

malloc()とfree()

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

malloc()とfree()

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

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

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

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

文字列を保存する場合

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

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

文字列を保存する場合には、上記の names[i] への代入のような malloc() と strcpy() を組み合わせて使うことが多い。しかし、この一連の処理の関数として、strdup() がある。基本的には、以下のような機能である。

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

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

char  buff[ 1000 ] ;
char* name[10] ;
for( int i = 0 ; i < 10 ; i++ ) {
   if ( fgets( buff , sizeof(buff) , stdin ) != NULL )
      name = buff ;
      // ここは、name = strdup( buff ) ; と書くべき。
}

配列に保存する場合

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

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

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

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

構造体の配列

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

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

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

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

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

   // 処理する件数を入力
   scanf( "%d" , &size ) ;
   // 配列を確保して、データの入力&出力
   if ( (array = (struct Complex*)malloc(
                    sizeof(struct Complex) * size )) != NULL ) {
      int i ;
      for( i = 0 ; i < size ; i++ )
         if ( !input_Complex( &array[i] ) )
            break ;
      for( i = 0 ; i < size ; i++ )
         print_Complex( &array[i] ) ;
         // or printf( "%lf + j%lf\n" ,
         //            array[ i ].re , array[ i ].im ) ;

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

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

(おまけ)C++の場合

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

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

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

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

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

また、C++ではデストラクタの呼び出しが必要となることから、配列を開放する場合には 「delete[] ポインタ ;」のように、配列を開放することを明記する必要がある。

フローチャートと整数型

学際科目の情報制御基礎において、プログラムの基本としてフローチャートと基本的な処理を説明し、数値型の注意点を説明。

フローチャートの基本

プログラムの処理の順序を理解するには、初心者であればフローチャート(流れ図)を使う。

処理の1つ1つを箱で表し、流れを箱の間の矢印で示すことでアルゴリズム(プログラムの考え方)や処理順序を表現する。処理単位の箱は、命令の種類によって箱の書き方が決まっている。

上図右側のフローチャートの例では、以下の説明のように実行され、0,1,2,…,9 が表示され、最終的に変数 i が10以上になり処理を停止する。

(1) 変数 i に 0 を保存
(2) 変数 i は10未満なら(3)、10以上なら終了
(3) 変数 i を表示
(4) i = i + 1 右辺の計算結果を、左辺に代入iが0から1に変化
(5) 処理(2)から繰り返し。

上記のようなプログラムは、C言語であれば以下のようになる。

#include <stdio.h>                 | 入出力関数を使うための準備

int main() {                       | 最初に main() という関数が呼び出される。
   int i ;                         | 変数 i の入れ物を準備
   for( i = 0 ; i < 10 ; i++ ) {   | 最初に i = 0 を行い、i < 10 の条件を満たす間繰り返し、
                                   |       繰り返しの度に i を1つ増やす
      printf( "%d\n" , i ) ;       |    i の値を表示
   }
   return 0 ;                      | 正しく終わったら0を返す。
}

練習問題1

以下のフローチャートの処理A,処理B,処理C,処理Dの実行結果を答えよ。

  • 電気電子工学科,電子情報工学科の学生は、出席番号が偶数は処理C,奇数は処理Dについて回答せよ。
  • それ以外の学科の学生は、出席番号が偶数は処理A,奇数は処理Bの結果について回答せよ。
  • このプログラムではどういった意味の値を求めようとしているのか答えよ。

情報量の単位

データを覚える最小単位は、0と1の2通りで表される1bit (ビット)と呼ぶ。単位として書く場合には b で表す。さらに、その1bitを8個組み合わせると、256通りの情報を保存できる。256通りあれば一般的な英数字などの記号を1文字保存する入れ物として便利であり、この単位を 1byte (バイト) と呼ぶ。単位として書く場合には B で表す。

通信関係の人は8bit=1byteを1オクテットと呼ぶことも多い。日本語を表現するには、かなや漢字を使うため16bit = 2byte = 1word(ワード) で表現することが多い。(ただしワードは32bitを意味することもあるので要注意, double word=32bit, quad word=64bit という呼び方もある。)

物理では単位が大きくなると、103=kキロ,106=Mメガ,109=Gギガ,1012=Tテラ を使うが、コンピュータの世界では、103≒210=1024 なので、1kB(キロバイト)というと1024Bを意味することが多い。明確に区別する時は、1024B(バイト)=1KiB(キビバイト), 10242B=1MiB(メビバイト), 10243B=1GiB(ギビバイト) などと記載する。

2進数,8進数,16進数

プログラムの中で整数値を覚える場合は、2進数の複数桁で記憶する。例えば、2進数3桁(3bit)であれば、000, 001, 010, 011, 100, 101, 110, 111 で、10進数であれば 0~7 の8通りの値が扱える。(8進数)

2進数4桁(4bit)であれば、0000, 0001, 0010, 0011, 0100, 0101, 0110, 0111, 1000, 1001, 1010, 1011, 1100, 1101, 1110, 1111 の16通りを表現できる(16進数)。これを1桁で表現するために、0,1,2,3,4,5,6,7,8,9,A,B,C,D,E,F を使って表現する。

例  8進数    16進数
    0123    0x123     ※C言語では、
  +  026   + 0xEA      8進数を表す場合、先頭に0をつけて表す。
 -------- --------     16進数を表す場合、先頭に0xをつけて表す。
    0151    0x20D      0x3+0xA = 0xD
                       0x2+0xE = 2+14 = 16 = 0x0 + 桁上がり
                       0x1+桁上がり = 0x2

整数型と扱える値の範囲

コンピュータの開発が進むにつれ計算の単位となるデータ幅は、8bit, 16bit, 32bit, 64bit と増えていった。整数型データには、正の値しか覚えられない符号無し整数と、2の補数で負の数を覚える符号付き整数に分けられる。

プログラムを作るためのC言語では、それぞれ 8bitの文字型(char)、16bitの short int型、32bitの int 型、64bitの long int 型(C言語では long int で宣言すると32bitの場合も多いので要注意)があり、それぞれに符号なし整数(unsigned), 符号あり整数(signed: C言語の宣言では書かない)がある。

精度 符号あり 符号なし
8bit(1byte) char (int8_t) unsigned char (uint8_t)
16bit(2byte) short int (int16_t) unsigned short int (uint16_t)
32bit(4byte) int (int32_t) unsigned int (uint32_t)
64bit(8byte) long int (int64_t) unsigned long int (uint64_t)

符号付きのデータは、負の数は2の補数によって保存され、2進数の最上位bit(符号ビット)負の数であれば1、正の数であれば0となる。

整数型で扱える数

(例) 符号なしの1byte(8bit)であれば、いくつの数を扱えるであろうか?

符号なしの N bit の整数であれば2N通りの値を表現でき、0(2N-1) までの値が扱える。

bit数 符号なし(unsigned)
8 unsigned char 0~28-1 0~255
16 unsigned short int 0~216-1 0~65535
32 unsigned int 0~232-1 0~4294967295

符号付きの N bit の整数であれば、2の補数表現では最上位ビットが符号を表すために使われる。

100)10 64)16 0110,0100)2
-100)10 9C)16 1001,1100)2

正の数なら残りの(N-1)bitで扱うため 0〜2N-1-1を表現できる。負の数は2N-1通りを表現できるので、N bit の符号つき整数は、-2N-1 〜0〜 2N-1-1の範囲の値を覚えられる。

bit数 符号あり(signed)
8 char -27~0~27-1 -128~127
16 short int -215~0~215-1 -32768~32767
32 int -231~0~231-1 -2147483648~2147483647

2の べき乗 の概算

プログラムを作る場合、2のべき乗がだいたいどの位の値なのか知りたいことが多い。この場合の計算方法として、2つの方法を紹介する。

  • 232 = 22 × (210)3 = 4 × 10243 ≒ 4,000,000,000
  • 232をN桁10進数で表すとすれば なので、両辺のlog10を求める。

      (つまり、bit数に0.3をかければ10進数の桁数が求まる。)

数値の範囲の問題で動かないプログラム

この話だけだと、扱える数値の上限について実感がわかないかもしれないので、以下のプログラムをみてみよう。(C言語の詳細は説明していないので、問題点がイメージできるだけでいい。)

組み込み系のコンピュータでは、int 型で宣言される変数でも、16bitの場合もある。以下のプログラムは期待した値が計算できない例である。以下の例では、16bit int型として short int で示す。

// ✳️コード1
#include <stdio.h>
#include <math.h>

int main() { // 原点から座標(x,y)までの距離を求める
   short int x  = 200 ;
   short int y  = 200 ;
   short int r2 = x*x + y*y ;  // (x,y)までの距離の2乗
   short int r  = sqrt( r2 ) ; // sqrt() 平方根
   printf( "%d\n" , r ) ;      // 何が求まるか?
   return 0 ;                  // (例) 282ではなく、120が表示された。
}

コンピュータで一定時間かかる処理を考えてみる。

// コード2.1
// 1 [msec] かかる処理が以下のように書いてあったとする。
short int i ;
for( i = 0 ; i < 1000 ; i++ )
   NOP() ; // NOP() = 約1μsecかかる処理とする。

// ✳️コード2.2
// 0.5 [sec]かかる処理を以下のようにかいた。
short int i ;
for( i = 0 ; i < 500000 ; i++ )
   NOP() ;
// でもこの処理は16bitコンピュータでは、1μsecもかからずに終了する。なぜか?

上記の例は、性能の低い16bit コンピュータの問題で、最近は32bit 整数型のコンピュータが普通だし、特に問題ないと思うかもしれない。でも、32bit でも扱える数の範囲で動かなくなるプログラムを示す。

OS(unix) では、1970年1月1日からの経過秒数で時間(unix時間)を扱う。ここで、以下のプログラムは、正しい値が計算できなかった有名な例である。(2004年1月11日にATMが動かなくなるトラブルの原因だった)

// ✳️コード3.1
int t1 = 1554735600 ; // 2019年4月09日,00:00
int t2 = 1555340400 ; // 2019年4月16日,00:00

// この2日の真ん中の日を求める。
//  t1       |        t2
//  |--------+--------|
//  |      t_mid      |
//  以下のプログラムは、正しい 2019年4月12日12:00 が求まらない。なぜか?
int t_mid = (t1 + t2) / 2;  // (例) 1951年03月25日 08:45 になった。

// コード3.2
//  以下のプログラムは正しく動く。 time_t 型(時間処理用の64bit整数)を使えば問題ない。
time_t t1 = 1554735600 ; // 2019年4月09日,00:00
time_t t2 = 1555340400 ; // 2019年4月16日,00:00

// time_t型が32bitであったとしても桁溢れない式
time_t t_mid = t1 + (t2 - t1) / 2 ;

練習問題2

以下の整数の範囲を具体的な値で答えよ。
出席番号・自分の誕生日(の日にち)に合わせて該当する2問について答えること。

  1. 7bitの符号なし整数で扱える数値の範囲 (出席番号が偶数)
  2. 12bitの符号あり整数で扱える数値の範囲 (出席番号が奇数)
  3. 20bitの符号なし整数で扱える数値の範囲 (誕生日の日づけが偶数)
  4. 24bitの符号あり整数で扱える数値の範囲 (誕生日の日づけが奇数)

練習問題3

先に示した数値の範囲が原因で動かないプログラム(コード1,コード2.2,コード3.1)の中から1つを選んで、計算結果が正しく求まらない原因を、具体的な値を示しながら説明せよ。

練習問題1,練習問題2,練習問題3について、レポートとして提出せよ。

Teamsのこちらの共有フォルダに、回答記入用のひな型がおいてあるので、この書式を参考に各自レポートにまとめ、同フォルダに提出してください。

派生と継承

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

派生を使わずに書くと…

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

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

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

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

このため、元データと共通部分があっても、同じ処理を改めて書き直しになる。(プログラマーの手間が減らせない)

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

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

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

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

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

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

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

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

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

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

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

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

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

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

しかし、print_PersonStudent2() のような処理は、名前と年齢だけ表示すればいいという場合、元データ構造が同じなのに、 PersonStudent2 用のプログラムをいちいち記述するのは面倒ではないか?

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

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

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

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

int main() {
   Person saitoh( "t-saitoh" , 50 ) ;
   saitoh.print() ;
   Student yama( "yama" , 22 , "PS" , 2 ) ;
   yama.print() ;  // "yama 22"が表示される
   return 0 ;
}

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

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

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

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

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

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

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

C言語で無理やりオブジェクト指向の”派生”を使う方法

オブジェクト指向の機能の無いC言語で、このような派生と継承を実装する場合には、共用体を使う以下のようなテクニックが使われていた。
unix の GUI である X11 でも共用体を用いて派生を実装していた。

// 基底クラス
struct PersonBase {     // 基底クラス
   char name[ 20 ] ;
   int  age ;
} ;

struct PersonStudent {  // 派生クラス
   struct PersonBase base ;
   char dep[ 20 ] ;
   int  grade ;
} ;
                                   //(base) //(student)
union Person {                     // name  //[name]
   struct PersonBase    base ;     // age   //[age ]
   struct PersonStudent student ;           // dep
} ;                                         // grade

void person_Print( struct Person* p ) {
   printf( "%s %d\n" , p->base.name , p->base.age ) ;   
}

int main() {
   struct PersonBase    tsaitoh = { "tsaitoh" , 55 } ;
   struct PersonStudent mitsuki = { { "mitsuki" , 21 } , "KIT" , 4 } ;
   print_Person( (struct Person*)&tsaitoh ) ;
   print_Person( (struct Person*)&mitsuki ) ;  // 無理やり print_Person を呼び出す
   return 0 ;
}

仮想関数への伏線

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

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

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

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

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

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

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

以前のバックエンドのプログラムの演習ではサーバの設定などの体験もできていないため、フロントエンドの処理でサーバ上に送られたデータは、最終的な書き込み処理は行っていなかった。今回は、サーバ上でデータをサーバ上のバックエンドプログラムの 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 命令でユーザのデータへのアクセス権限を指定する。

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

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

chown , chgrp , chmod コマンド

ファイル所有者やグループを変更する場合には、chown (change owner) 命令や chgrp (change group) 命令を使用する。

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言語では、ポインタを動かしながら処理を行う場合に以下のようなプログラムもよくでてくる。

// string copy 配列のイメージで記載
void strcpy( char d[] , char s[] ) {
   int i ;
   for( i = 0 ; s[ i ] != '
// string copy 配列のイメージで記載
void strcpy( char d[] , char s[] ) {
   int i ;
   for( i = 0 ; s[ i ] != '\0' ; i++ )
      d[ i ] = s[ i ] ;
   d[ i ] = '\0' ;
}

int main() {
   char a[] = "abcde" ;
   char b[ 10 ] ;
   strcpy( b , a ) ;
   printf( "%s\n" , b ) ;
   return 0 ;
}
' ; i++ ) d[ i ] = s[ i ] ; d[ i ] = '
// string copy 配列のイメージで記載
void strcpy( char d[] , char s[] ) {
   int i ;
   for( i = 0 ; s[ i ] != '\0' ; i++ )
      d[ i ] = s[ i ] ;
   d[ i ] = '\0' ;
}

int main() {
   char a[] = "abcde" ;
   char b[ 10 ] ;
   strcpy( b , a ) ;
   printf( "%s\n" , b ) ;
   return 0 ;
}
' ; } int main() { char a[] = "abcde" ; char b[ 10 ] ; strcpy( b , a ) ; printf( "%s\n" , b ) ; return 0 ; }

しかし、この strcpy は、ポインタを使って書くと以下のように書ける。

// string copy ポインタのイメージで記載
void strcpy( char* p , char* q ) {
   while( *q != '
// string copy ポインタのイメージで記載
void strcpy( char* p , char* q ) {
   while( *q != '\0' ) {
      *p = *q ;
      p++ ;
      q++ ;
   }
   *p = '\0' ;
}
// ポインタ加算と代入を一度に書く
void strcpy( char* p , char* q ) {
   while( *q != '\0' )
      *p++ = *q++ ;    // *(p++) = *(q++)
   *p = '\0' ;
}
// ポインタ加算と代入と'¥0'判定を一度に書く
void strcpy( char* p , char* q ) {
   while( (*p++ = *q++) != '\0' )   // while( *p++ = *q++ ) ; でも良い
      ;
}
' ) { *p = *q ; p++ ; q++ ; } *p = '
// string copy ポインタのイメージで記載
void strcpy( char* p , char* q ) {
   while( *q != '\0' ) {
      *p = *q ;
      p++ ;
      q++ ;
   }
   *p = '\0' ;
}
// ポインタ加算と代入を一度に書く
void strcpy( char* p , char* q ) {
   while( *q != '\0' )
      *p++ = *q++ ;    // *(p++) = *(q++)
   *p = '\0' ;
}
// ポインタ加算と代入と'¥0'判定を一度に書く
void strcpy( char* p , char* q ) {
   while( (*p++ = *q++) != '\0' )   // while( *p++ = *q++ ) ; でも良い
      ;
}
' ; } // ポインタ加算と代入を一度に書く void strcpy( char* p , char* q ) { while( *q != '
// string copy ポインタのイメージで記載
void strcpy( char* p , char* q ) {
   while( *q != '\0' ) {
      *p = *q ;
      p++ ;
      q++ ;
   }
   *p = '\0' ;
}
// ポインタ加算と代入を一度に書く
void strcpy( char* p , char* q ) {
   while( *q != '\0' )
      *p++ = *q++ ;    // *(p++) = *(q++)
   *p = '\0' ;
}
// ポインタ加算と代入と'¥0'判定を一度に書く
void strcpy( char* p , char* q ) {
   while( (*p++ = *q++) != '\0' )   // while( *p++ = *q++ ) ; でも良い
      ;
}
' ) *p++ = *q++ ; // *(p++) = *(q++) *p = '
// string copy ポインタのイメージで記載
void strcpy( char* p , char* q ) {
   while( *q != '\0' ) {
      *p = *q ;
      p++ ;
      q++ ;
   }
   *p = '\0' ;
}
// ポインタ加算と代入を一度に書く
void strcpy( char* p , char* q ) {
   while( *q != '\0' )
      *p++ = *q++ ;    // *(p++) = *(q++)
   *p = '\0' ;
}
// ポインタ加算と代入と'¥0'判定を一度に書く
void strcpy( char* p , char* q ) {
   while( (*p++ = *q++) != '\0' )   // while( *p++ = *q++ ) ; でも良い
      ;
}
' ; } // ポインタ加算と代入と'¥0'判定を一度に書く void strcpy( char* p , char* q ) { while( (*p++ = *q++) != '
// string copy ポインタのイメージで記載
void strcpy( char* p , char* q ) {
   while( *q != '\0' ) {
      *p = *q ;
      p++ ;
      q++ ;
   }
   *p = '\0' ;
}
// ポインタ加算と代入を一度に書く
void strcpy( char* p , char* q ) {
   while( *q != '\0' )
      *p++ = *q++ ;    // *(p++) = *(q++)
   *p = '\0' ;
}
// ポインタ加算と代入と'¥0'判定を一度に書く
void strcpy( char* p , char* q ) {
   while( (*p++ = *q++) != '\0' )   // while( *p++ = *q++ ) ; でも良い
      ;
}
' ) // while( *p++ = *q++ ) ; でも良い ; }

インクリメント演算子

C言語での++ や — といった演算子は、変数の前に書く場合と後ろに書く場合で挙動が異なる。

前置記法の “++i” は、i の値を使う前に +1 加算が行われ、”i++” は、iの値を使った後に、 +1 が行われる。

int main() {
    int i = 11 ;
    printf( "%d\n" , ++i ) ; // i = i + 1 ;
                             // printf( "%d\n" , i ) ; と同じ
                             // 12 が表示される
    printf( "%d\n" , i++ ) ; // printf( "%d\n" , i ) ;
                             // i = i + 1 ; と同じ
                             // 12 が表示され、i の値は13
    return 0 ;
}

構造体とポインタ

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

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

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

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

配列宣言でサイズは定数

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

こういった話をすると「C# とか C++ とか JavaScript とか使えば、配列のサイズなんて考える必要ないから、そんなこと考えなくてもいいじゃん…」って思うかもしれないけど、こういった新しい言語では、この後の授業で説明するリスト構造やらハッシュといった機能を使っていて、それをどう扱っているのか知らずに、処理速度が遅い原因を見逃してしまうかもしれない。

void foo( int size ) {
   int array[ size ] ;         // エラー
   for( int i = 0 ; i < size ; i++ )
      array[ i ] = i*i ;
}
void main() {
   foo( 3 ) ;   // 最近のC(C99)では、こういったプログラムも
   foo( 4 ) ;   // 裏で後述のalloca()を使って動いたりする。(^_^;
}

メモリ利用の効率

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

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

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

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

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

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

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


さる御方、ビチグソ丸(文字数:全角210文字)

引用Wikipedia

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

上に示したデータの覚え方を、データが出現する度に保存するのであれば、以下のようなコードになるだろう。

// 巨大な配列
char str[ 10000 ] ;
// 使用領域の末尾(初期値は巨大配列の先頭)
char* sp = str ;

// 文字列を保存する関数
char* entry( char* s ) {
   char* ret = sp ;
   strcpy( sp , s ) ;
   sp += strlen( s ) + 1 ;
   return ret ;
}
int main() {
   char* names[ 10 ] ;
   names[ 0 ] = entry( "saitoh" ) ;
   names[ 1 ] = entry( "jugemu-jugemu-gokono-surikire..." ) ;
   return 0 ;
}
// str[] s a i t o h ¥0 t o m o k o ¥0
//       ↑             ↑
//     names[0]        names[1]

このプログラムでは、貸し出す度に、sp のポインタを後ろに移動していく。しかし、名前を覚えておく必要がなくなった場合、その場所はどうすればいいだろうか?

スタック

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

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


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

alloca() 関数

局所変数と同じスタック上に、一時的にデータを保存する配列を作り、関数が終わると不要になる場合には、alloca() 関数が便利である。alloca の引数には、必要なメモリの byte 数を指定する。

100個の整数データを保存するのであれば、int が 32bit の 4byte であれば 400byte を指定する。ただし、int 型は16bitコンピュータなら2byteかもしれないし、64bitコンピュータなら、8byte かもしれないので、確保するバイト数を計算する際には sizeof() 演算子を使い、100 * sizeof( int ) と書くべきである。

#include <alloca.h>
void foo( int size ) {
   // 本当なら int p[ size ] ; と宣言したい。
   int* p ;
   // size件のint型配列を作る
   p = (int*)alloca( sizeof( int ) * size ) ;
   // 確保した配列に値を保存
   for( int i = 0 ; i < size ; i++ )
      p[ i ] = i*i ;
   // 確保した値を使う
   for( int i = 0 ; i < size ; i++ )
      printf( "%d\n" , p[ i ] ) ;
}
void main() {
   foo( 3 ) ;
   foo( 4 ) ;
}

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

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

必要に応じてメモリを確保して、その領域が不要となる順序が「最後に確保したものから不要になる」という順序でなかったら、メモリはどのように管理すればいいだろうか?こういった場合のために、C 言語では malloc() 関数と free() 関数によるヒープメモリ領域がある。次の講義ではこの malloc, free について解説を行う。

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 が見ると、以下のようなファイルであった。

$ 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 コマンドで使用状況が確認できる。一般的に、主記憶メモリの数倍を割り当てる。

ポインタ処理

ここからは、次のメモリの消費を考慮したプログラムの説明を行うが、データ保存場所としてのポインタを多用するので、ポインタの処理に慣れない人のために説明。

値渡しとポインタ渡し

大きなプログラムを作成する場合、変数名の使い方には注意が必要となる。大域変数は、どこでも利用できるが、間違った使い方をすると値が予想外の変化があったりするため危険である。一方で、局所変数を使うと、関数呼び出しでデータの受け渡しに注意が必要となる。

値渡し(call by value)

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

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

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

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

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

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

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

ポインタ渡し(call by pointer)

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

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

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

変数の寿命とスコープ

変数の管理では、変数の寿命スコープの理解が重要。

静的変数:変数は、プログラムの起動時に初期化、プログラムの終了時に廃棄。
動的変数:変数は、関数に入るときに初期化、関数を抜けるときに廃棄。
もしくは、ブロックに入るときに初期化、ブロックを抜けるときに廃棄。

大域変数:大域変数は、プログラム全体で参照できる。
局所変数:関数の中 or そのブロックの中でのみ参照できる。

ブロックの中で変数が宣言されると、そのブロックの外の変数とは別の入れ物となる。そのブロックの中では、新たに宣言された変数が使われる。

int i = 111 ;    // 静的大域変数

void foo() {
   int i = 222 ; // 動的局所変数
   i++ ;
   printf( "%d\n" , i ) ;
}
void bar() {
   static int i = 333 ; // 静的局所変数(プログラム起動時に初期化)
   i++ ;
   printf( "%d\n" , i ) ;
}
void hoge( int x ) {    // x: 動的局所変数(値渡し)
   x++ ;
   printf( "%d\n" , x ) ;
}
void fuga( int* p ) {   // p: 動的局所変数(ポインタ渡し)
   (*p)++ ;
   printf( "%d\n" , (*p) ) ;
}
int main() {
   int i = 444 , j = 555 ;
   foo() ;      // 223 (副作用ナシ)
   bar() ;      // 334
   hoge( i ) ;  // 445 (副作用ナシ)
   fuga( &j ) ; // 556
   printf( "%d\n" , i ) ;
   foo() ;      // 223 (副作用ナシ)
   bar() ;      // 335
   hoge( i ) ;  // 445 (副作用ナシ)
   fuga( &j ) ; // 557

   printf( "%d\n" , i ) ;                     // 444
   for( int i = 0 ; i < 2 ; i++ ) { // (a)    // A:0
      printf( "A:%d\n" , i ) ;                // B:0
      for( int i = 0 ; i < 2 ; i++ ) { // (b) // B:1
         printf( "B:%d\n" , i ) ;             // A:1
      }                                       // B:0
   }                                          // B:1

   printf( "%d\n" , i ) ;  // 333 ← 要注意C言語のバージョンによっては
                           //       2 になる場合あり。(a)の変数iの値
   return 0 ;
}

JavaScriptのvarとletのスコープ

C言語でのスコープと寿命と、JavaScriptのvar宣言では考え方が違うので要注意。
C言語では、変数はその宣言された場所がブロックの中であれば、その中でのみ使用できる局所変数となる。JavaScript の let 宣言も基本は同じ考え方。ただし、JavaScript の var 宣言は、その変数が使われる関数ブロックの中(もしくはグローバルスコープ)に関連付けられる。この変数の宣言部分がもっとも近い関数の先頭に移動しているように見える動作のことを変数の巻き上げ(hoisting)と呼ばれる。

C言語の局所変数のスコープ。

JavaScript の let 宣言は、C言語のスコープと同じ考え方。

JavaScript の var 宣言は、関数スコープであり、ブロック{} 内で新しく宣言があっても、関数スコープまで巻き上げられる。

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

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

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

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

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

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

*(p + 整数式)p[ 整数式 ] は同じ意味 (参照”悪趣味なプログラム”)

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

ポインタインクリメントと式

C言語では、ポインタを動かしながら処理を行う場合に以下のようなプログラムもよくでてくる。

// string copy 配列のイメージで記載
void strcpy( char d[] , char s[] ) {
   int i ;
   for( i = 0 ; s[ i ] != '¥0' ; i++ )
      d[ i ] = s[ i ] ;
   d[ i ] = '¥0' ;
}

int main() {
   char a[] = "abcde" ;
   char b[ 10 ] ;
   strcpy( b , a ) ;
   printf( "%s¥n" , b ) ;
   return 0 ;
}

しかし、この strcpy は、ポインタを使って書くと以下のように書ける。

// string copy ポインタのイメージで記載
void strcpy( char* p , char* q ) {
   while( *q != '¥0' ) {
      *p = *q ;
      p++ ;
      q++ ;
   }
   *p = '¥0' ;
}
// ポインタ加算と代入を一度に書く
void strcpy( char* p , char* q ) {
   while( *q != '¥0' )
      *p++ = *q++ ;    // *(p++) = *(q++)
}
// ポインタ加算と代入と'¥0'判定を一度に書く
void strcpy( char* p , char* q ) {
   while( (*p++ = *q++) != '¥0' )   // while( *p++ = *q++ ) ; でも良い
      ;
}

構造体とポインタ

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

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

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

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

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

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

printf() に慣れていない人もいるので…ヒント:%d 引数を10進数で表示、%s 引数の文字列として表示(文字列の先頭アドレスから’\0’までの文字を表示)、%c 引数を文字(char型)として表示。

コンピュータとN進数

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

情報制御基礎のシラバス

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

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

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

最近では、コンピュータといっても様々な所で使われている。

  1. 科学技術計算用の大型コンピュータ(最近なら富岳が有名)や
  2. インターネットの処理を行うサーバ群
    (必要に応じてサービスとして提供されるものはクラウドコンピューティングと呼ぶ)、
  3. デスクトップパソコン・ノートパソコン
  4. タブレットPCやスマートフォンのような端末、
  5. 電化製品の中に収まるようなワンチップコンピュータ

などがある。(3)のパソコンでもグラフィックス表示機能のために GPU(Graphics Processing Unit) を搭載したものは(ゲームでの3次元表示用)、高速計算に使われることも多い。


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

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

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

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

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

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


2進数と10進数

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

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

N進数を10進数に変換

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

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

(例1)

(例2)

10進数をN進数に変換

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

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

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

実数の場合

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

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

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

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

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

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

2の補数と負の数

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

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

練習問題

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

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

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

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

レポートの提出先は、こちらのリンクを参照(この中にレポート書式のひな型を置いてあります)

複素数クラスによる演習

複素数クラスの例

隠蔽化と基本的なオブジェクト指向の練習課題として、前回の授業では、直交座標系による複素数クラスを示した。今回の授業では、演習を行うとともに直交座標系を極座標系にクラス内部を変更したことにより、隠蔽化の効果について考えてもらい、第1回レポートとする。

直交座標系

前回の授業で示した直交座標系のクラス。比較対象とするために再掲。

#include <stdio.h>
#include <math.h>

// 直交座標系の複素数クラス
class Complex {
private:
   double re ; // 実部
   double im ; // 虚部
public:
   void print() {
      printf( "%lf + j%lf¥n" , re , im ) ;
   }
   Complex( double r , double i ) // 実部虚部のコンストラクタ
     : re( r ) , im( i ) {}
   Complex()                      // デフォルトコンストラクタ
     : re( 0.0 ) , im( 0.0 ) {} 
   void add( Complex z ) {
      // 加算は、直交座標系だと極めてシンプル
      re = re + z.re ;
      im = im + z.im ;
   }
   void mul( Complex z ) {
      // 乗算は、直交座標系だと、ちょっと煩雑
      double r = re * z.re - im * z.im ;
      double i = re * z.im + im * z.re ;
      re = r ;
      im = i ;
   }
   double get_re() {
      return re ;
   }
   double get_im() {
      return im ;
   }
   double get_abs() { // 絶対値
      return sqrt( re*re + im*im ) ;
   }
   double get_arg() { // 偏角
      return atan2( im , re ) ;
   }
} ; // ←何度も繰り返すけど、ここのセミコロン忘れないでね
int main() {
   // 複素数を作る
   Complex a( 1.0 , 2.0 ) ;
   Complex b( 2.0 , 3.0 ) ;

   // 複素数の計算
   a.print() ;
   a.add( b ) ;
   a.print() ;
   a.mul( b ) ;
   a.print() ;

   return 0 ;
}

極座標系

上記の直交座標系の Complex クラスは、加減算の関数は単純だけど、乗除算の関数を書く時には面倒になってくる。この場合、極座標系でプログラムを書いたほうが判りやすいかもしれない。

// 局座標系の複素数クラス
class Complex {
private:
   double r ;  // 絶対値 r
   double th ; // 偏角   θ
public:
   void print() {
      printf( "%lf ∠ %lf¥n" , r , th / 3.14159265 * 180.0 ) ;
   }
   Complex() // デフォルトコンストラクタ
     : r( 0.0 ) , th( 0.0 ) {}

   // 表面的には、同じ使い方ができるように
   //  直交座標系でのコンストラクタ
   Complex( double x , double y ) {
      r  = sqrt( x*x + y*y ) ;
      th = atan2( y , x ) ;    // 象限を考慮したatan()
   }
   // 極座標系だと、わかりやすい処理
   void mul( Complex z ) {
      // 極座標系での乗算は
      r = r * z.r ;    // 絶対値の積
      th = th + z.th ; // 偏角の和
   } 
   // 反対に、加算は面倒な処理になってしまう。
   void add( Complex z ) {
      ; // 自分で考えて
   }
   // ゲッターメソッド
   double get_abs() {
      return r ;
   }
   double get_arg() {
      return th ;
   }
   double get_re() {  // 直交座標系との互換性のためのゲッターメソッド
      return r * cos( th ) ;
   }
   double get_im() {
      return r * sin( th ) ;
   }
} ; // ←しつこく繰り返すけど、セミコロン忘れないでね(^_^;

このように、プログラムを開発していると、当初は直交座標系でプログラムを記述していたが、途中で極座標系の方がプログラムが書きやすいという局面となるかもしれない。しかし、オブジェクト指向による隠蔽化を正しく行っていれば、利用者に影響なく「データ構造」や「その手続き(メソッド)」を書換えることも可能となる。

このように、プログラムをさらに良いものとなるべく書換えることは、オブジェクト指向ではリファクタリングと呼ぶ。
正しくクラスを作っていれば、クラス利用者への影響が最小にしながらリファクタリングが可能となる。

const 指定 (経験者向け解説)

C++ では、間違って値を書き換えるような処理を書けないようにするための、const 指定の機能がある。

void bar( char* s ) {       // void bar( const char* s ) {...}
   printf( "%s\n" , s ) ;   //  で宣言すべき。
}

void foo( const int x ) {
   //     ~~~~~~~~~~~
   x++ ; // 定数を書き換えることはできない。
   printf( "%d\n" , x ) ;
}
int main() {
   const double pi = 3.141592 ;
   // C言語で #define PI 3.141592 と同等

   bar( "This is a pen" ) ; // Warning: string constant to 'char*' の警告
   int a = 123 ;
   foo( a ) ;

   return 0 ;
}

前述の、getter メソッドの例では要素を参照するだけで、オブジェクトの中身が変化しない。逆に言えば、getter のメソッド内にはオブジェクトに副作用のある処理を書いてはいけない。こういった用途に、オブジェクトを変化させないメソッド宣言がある。先の、get_re() は、

class ... {
   :
   inline double get_re() const {
               //         ~~~~~
      re = 0 ; // 文法エラー
      return re ;
   }
} ;

クラスオブジェクトを引数にする場合

前述の add() メソッドでは、”void add( Complex z ) { … }” にて宣言をしていた。しかし、引数となる変数 z の実体が巨大な場合、この書き方では値渡しになるため、データの複製の処理時間が問題となる場合がある。この場合は、(書き方1)のように、z の参照渡しにすることで、データ複製の時間を軽減する。また、この例では、引数 z の中身を間違って add() の中で変化させる処理を書いてしまうかもしれない。そこで、この事例では(書き方2)のように const 指定もすべきである。

// (書き方1)
class Complex {
   :
   void add( Complex& z ) {
      re += z.re ;
      im += z.im ;
   }
} ;
// (書き方2)
class Complex {
   :
   void add( const Complex& z )
   {  //     ~~~~~~~~~~~~~~~~
      re += z.re ;
      im += z.im ;
   }
} ;

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

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

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

システム

最新の投稿(電子情報)

アーカイブ

カテゴリー