PHPとデータベースによるバックエンドプログラミング
前回の講義では、Webページの作り方として、JavaScriptを用いたブラウザで動くプログラミングについて説明を行った。今回の授業では、データを管理しているサーバ側(バックエンド)で使われるプログラミング言語 PHP についての紹介と、データを管理するためのプログラム言語 SQL について説明し、簡単な演習をレポート課題とする。
前回授業の未解説部分
- JavaScriptを用いたフロントエンド
- Webの公開からWebサービスの公開へ
- 2025/情報メディア工学(4/30) Formsによる小テスト
PHPとデータベースによるバックエンドプログラミング
- PHPとデータベースによるバックエンドプログラミング
- 以下のサンプル(sampleD.php~) PHP のファイルなので、ダウンロードしたファイルを開いてもこのままでは動きません。動作確認のページにて実行結果を確認してください。
- PHPによるHelloWorld
- PHPによるデータの受け取り
- データベースとは
- sampleG-itemlist.sql
- sampleG-userlist.sql
- sampleG-buylist.sql
- Paiza.io の itemlist,userlist,buylist の動作確認ページ – このページにてSQLの練習問題を答えてください
- PHPの中でSQLを使う
- 05/12 SQL練習問題のレポート提出先はこちら
派生と継承
隠ぺい化の次のステップとして、派生・継承を説明する。オブジェクト指向プログラミングでは、一番基本となるデータ構造を宣言し、その基本構造に様々な機能を追加した派生クラスを記述することでプログラムを作成する。今回は、その派生を理解するために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" // # が表示されてほしい?
Javaのオブジェクト指向の基礎
前期中間前レポート課題(選択2)
4年の情報構造論で、リスト構造などの内容を進める前に、3年プログラミング応用でクラスなどに自信がない人向けの簡単レクチャ。
クラスは、データ構造と手続き
例えば、名前と年齢のデータをクラスで扱うのであれば、以下のようなコードが基本となるだろう。
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
- オブジェクト指向の基礎(Paiza.io)
クラスとは、データ構造(オブジェクト)とそのデータ構造を扱うための関数(メソッド)をまとめて扱う。
クラス NameAge の中で宣言されている、NameAge() の関数は、オブジェクトを初期化するための関数(メソッド)であり、特にコンストラクタと呼ばれる。
実際にデータを保存するための tsaitoh や tomoko とよばれる変数に NameAge オブジェクトの実体(インスタンス)を作る時には 「new クラス名」 とやることで、初期化ができる。
イメージでは、下図のようなデータ構造ができあがる。
でも、年齢の覚え方は、将来的に誕生日を覚えるように変化するかもしれない。この際に、Main 関数の中で age を使うと後で混乱の元になるかもしれない。こういう時は、NameAge クラス以外では中身を勝手に使わせないために、インスタンス変数などに public / private といったアクセス制限を加える。
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つしか書けないというルールがあるので要注意。
練習問題
科目(Subject)と学生(Student)の情報があり、科目を受講した成績(Result)で成績を管理している。
このデータを管理するためのクラスを宣言し、下に示すようなデータSubject,Result,Studentの配列を作り、下に示したような出力が得られるプログラムを作成せよ。
科目: 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 = { 58563 10020 83 // new Result( 16213 , 10020 , 83 ) , 58564 10010 95 // new Result( ... 58573 10030 64 // } ; 58563 10010 89 学生: Student s_id name age // Student[] student_table = { 58563 斉藤太郎 18 // new Student( 16213 , "斉藤太郎" , 18 ) , 58564 山田次郎 19 // new Student( ... 58573 渡辺花子 18 // } ; 以下のようなデータが出力されること 斉藤太郎 電気磁気学 83 渡辺花子 情報構造論 95 山田次郎 電気回路 64 斉藤太郎 情報構造論 89
- 課題のひな形(Paiza.io)
処理速度を計測
前期中間前レポート課題(選択1)
例年であれば、プログラム作成中心のレポート課題をやってもらっているけど、前期中間試験は今回早めに行われるので、プログラム作成か、処理速度のオーダを実際に計測実験のいずれかとする。
現在時間を ミリ秒の精度で求める System.currentTimeMills() を使って、様々なプログラムの処理時間を計測してみよう。ただし、OS によって 100ミリ秒未満はあまり正確に測れない。このため、処理時間は繰り返し処理などを入れることで、10秒程度になるようにすること。また、動作例で示した Paiza.io では、長い処理時間のプログラムは、途中で強制終了させられるので、自分のパソコンにインストールしてある環境で動作させること。また、並列処理しているプログラムの影響を受ける可能性もあることから、他の処理の影響がでないように工夫すること。
様々なプログラムの実行時間を計測してみよう
import java.util.*; public class Main { // 乱数を配列にセット public static void array_set_random( int[] array ) { Random rnd = new Random() ; for( int i = 0 ; i < array.length ; i++ ) array[ i ] = rnd.nextInt( array.length ) ; } // 配列を選択法でソート public static void array_sort( int[] array ) { for( int i = 0 ; i < array.length - 1 ; i++ ) { int min = i , j ; for( j = i + 1 ; j < array.length ; j++ ) { if ( array[ min ] > array[ j ] ) min = j ; } int tmp = array[ i ] ; array[ i ] = array[ min ] ; array[ min ] = tmp ; } } public static void main(String[] args) throws Exception { // 変化させるデータ件数 int[] data_n = { 2500 , 5000 , 7500 , 10000 } ; for( int i = 0 ; i < data_n.length ; i++ ) { int[] array = new int[ data_n[ i ] ] ; // 配列を乱数で埋める時間は測りたくない array_set_random( array ) ; long start = System.currentTimeMillis() ; array_sort( array ) ; // ソート結果の表示時間は測りたくない //for( int x = 0 ; x < array.length ; x++ ) // System.out.print( array[ x ] + " " ) ; //System.out.println() ; long end = System.currentTimeMillis() ; System.out.println( "Time = " + (end - start) ) ; } } }
- データ件数と処理時間の計測(Paiza.io)
プログラムによっては、処理時間が短すぎる場合があるので、下記のように 1000 回ループさせるなどで、一定時間以上の処理となるように工夫すること。
public static int fact( int x ) { if ( x == 1 ) return 1 ; else return x * fact( x - 1 ) ; } public static void main(String[] args) throws Exception { int[] data_n = { 2 , 4 , 6 , 8 , 10 } ; for( int i = 0 ; i < data_n.length ; i++ ) { long start = System.currentTimeMillis() ; for( int j = 0 ; j < 1000 ; j++ ) { int ans = fact( data_n[ i ] ) ; } long end = System.currentTimeMillis() ; System.out.println( "Time = " + ( end - start ) ) ; } }
レポート内容
データ件数や 引数に与える数によって、処理時間が変化するプログラムを記述し、そのプログラムを N を変化させながら上記のプログラムなどを参考に処理時間を計測する。時間計測には誤差が大きく含まれる可能性があることから、複数回実行して平均をとるなどの工夫もすること。
授業中に示したプログラムなどの計測を行う場合は、ループのプログラムの変化と、別プログラムの再帰の場合の変化の2つについて結果を示すこと。創造工学演習の再帰問題の課題の整数比の直角三角形探索、辺の組み合わせ問題、N Queen、ハノイの塔など、自分で興味のあるテーマを選んだ場合は1つでも良い。
この上で、(1) プログラムリスト, (2) 時間計測にあたり工夫したこと, (3) 実際の実行結果のグラフ, (4) その結果の考察した結果をレポートにまとめて提出せよ。