多重継承の問題
派生や継承について、一通りの説明が終わったので、最後に特殊な継承の問題を説明し、2回目のレポート課題を示す。
動物・鳥類・哺乳類クラス
派生や継承を使うと、親子関係のあるデータ構造をうまく表現できることを、ここまでの授業で示してきた。
しかしながら、以下に述べるような例では、問題が発生する。
// 動物クラス class Animal { private: char name[ 10 ] ; public: Animal( const char s[] ) { strcpy( name , s ) ; } const char* get_name() const { return name ; } virtual void move() = 0 ; virtual void birth() = 0 ; } ; // 鳥類クラス class Bird : public Animal { public: Bird( const char s[] ) : Animal( s ) {} virtual void move() { printf( "%s fry.\n" , get_name() ) ; } virtual void birth() { printf( "%s lay egg.\n" , get_name() ) ; } } ; // 哺乳類クラス class Mammal : public Animal { public: Mammal( const char s[] ) : Animal( s ) {} virtual void move() { printf( "%s walk.\n" , get_name() ) ; } virtual void birth() { printf( "%s lay baby.\n" , get_name() ) ; } } ; int main() { Bird chiken( "piyo" ) ; chiken.move() ; chiken.birth() ; Mammal cat( "tama" ) ; cat.move() ; cat.birth() ; return 0 ; }
ここで、カモノハシを作るのであれば、どうすれば良いだろうか?
鳥類・哺乳類とは別にカモノハシを作る
class SeaBream : public Animal { public: Mammal( const char s[] ) : Animal( s ) {} virtual void move() { printf( "%s walk.\n" , get_name() ) ; } virtual void birth() { printf( "%s lay egg.\n" , get_name() ) ; } } ;
この例では、簡単な処理だが、move() の中身が複雑であれば、改めて move() を宣言するのではなく、継承するだけの書き方ができないだろうか?
多重継承
C++ には、複数のクラスから、派生する多重継承という機能がある。であれば、鳥類と哺乳類から進化したのだから、以下のように書きたい。
class SeaBream : public Bird , Mammal { } ;
しかし、カモノハシに move() を呼び出すと、鳥類の move() と哺乳類の move() のどちらを動かすか曖昧になる。また、派生クラスは親クラスのデータ領域と、派生クラスのデータ領域を持つため、鳥類の name[] と、哺乳類の name[] を二つ持つことになる。
足と羽のクラスを作る場合
class Animal { private: char name[ 10 ] ; public: Animal( const char s[] ) { strcpy( name , s ) ; } const char* get_name() const { return name ; } virtual void move() = 0 ; } ; // 羽 class Wing { public: const char* move_method() { return "fly" ; } } ; // class Leg { public: const char* move_method() { return "walk" ; } } ; class Bird : public Animal , Wind { public: Bird( const char s[] ) : Animal( s ) {} virtual void move() { printf( "%s %s.\n" , get_name() , move_method() ) ; } } ; class Mammal : public Animal , Leg { public: Mammal( const char s[] ) : Animal( s ) {} virtual void move() { printf( "%s %s.\n" , get_name() , move_method() ) ; } } ;
C++では、以下のような方法で、ダイヤモンド型の継承問題を解決できる。
class Animal { private: char name[ 10 ] ; public: Animal( const char s[] ) { strcpy( name , s ) ; } const char* get_name() const { return name ; } virtual void move() = 0 ; virtual void birth() = 0 ; } ; // 鳥類クラス class Bird : public virtual Animal { public: Bird( const char s[] ) : Animal( s ) {} virtual void move() { printf( "%s fry.\n" , get_name() ) ; } virtual void birth() { printf( "%s lay egg.\n" , get_name() ) ; } } ; // 哺乳類クラス class Mammal : public virtual Animal { public: Mammal( const char s[] ) : Animal( s ) {} virtual void move() { printf( "%s walk.\n" , get_name() ) ; } virtual void birth() { printf( "%s lay baby.\n" , get_name() ) ; } } ; class SeaBream : public virtual Bird , virtual Mammal { public: SeaBream( const char s[] ) : Animal( s ) {} void move() { Mammal::move() ; } void birth() { Bird::birth() ; } } ;
ただし、多重継承は親クラスの情報と、メソッドを継承する。この場合、通常だと name[] を二つ持つことになるので、問題が発生する。そこで、親クラスの継承に virtual を指定することで、ダイヤモンド型継承の 2つの属性をうまく処理してくれるようになる。
しかし、多重継承は処理の曖昧さや効率の悪さもあることから、採用されていないオブジェクト指向言語も多い。特に Java は、多重継承を使えない。その代わりに interface という機能が使えるようになっている。
レポート課題2回目
前回の仮想関数による、汎用のデータソートのプログラムについて、自分で作った新しいデータ構造について、ソート処理を行うプログラムに改良せよ。
データ構造は、名前と誕生日とか、名前と身長体重といった複数要素のデータ構造とし、そのデータ構造を並び替えるための大小判断のルールをプログラムの説明の中に明記すること。
プログラム言語(C言語)の基礎
学際科目の情報制御基礎において、学科間でプログラミングの初歩の理解差があるので、簡単なC言語プログラミングの基礎の説明。
Hello World
“Hello World”と表示するだけのC言語プログラムは以下のようになる。
// コメントの書き方1 // "//"で始まる行は、プログラムの説明(コメント) /* コメントの書き方2 */ // "/*"から"*/"で囲まれる範囲もコメント #include <stdio.h> // #で始まる行はプリプロセッサ行 // stdio.h には、入出力関数の説明が書いてある int main() { // 一連の処理の塊を関数と呼ぶ。 // C言語では main() 関数を最初に実行する。 printf( "Hello World\n" ) ; // printf() は、以下の物を表示する関数。 // "\n"は、文字を出力して改行するための特殊文字 return 0 ; // main() 関数が、正常終了したことを意味する } // 0 を返り値として返す。
“#include <…>”のプリプロセッサ行は、最初のうちは解りにくいので、「これを書かないとダメ…」と思っていればいい。
#include <stdio.h> は、別ファイル(ヘッダファイル) stdio.h に記載されているプログラムリストを読み込む機能。
stdio.h には、printf() や scanf() などの基本的な関数や定数などの情報が記載されている。
C言語の基本的な命令(文)は、”;”で終わる。(単文)
複数の処理をまとめる場合には、”{“から”}”の中に、複数の文を書き並べる。(複文)
関数とは、複数の処理をひとまとめにした、処理の「かたまり」と思えばいい。
関数の型 関数名( 仮引数 ... ) { 処理1 ... ; 処理2 ... ; }printf() の 文字列中の”\n”(あるいは”¥n”)は、改行を意味する。
「\:バックスラッシュ」は、日本語環境では「¥:円記号」で入力・表示することが多い。
変数と代入
#include <stdio.h> #include <math.h> // 数学関数を使う 平方根 sqrt() を使っている int main() { // 変数の宣言 int i ; // 符号付き32bit変数 i の宣言 int a = 123 , j ; // a を 123 で初期化 , j も整数型 float x ; // 単精度実数の x を宣言 double y = 1.234 , z ; // 倍精度実数の y を宣言し 1.234 で初期化, // z も倍精度実数 // 変数への代入 i = 1 ; // i に 1 を代入 i = 12 + 2 * a ; // 12+2*a を代入 a は123なので、 // iには、258 が入る。 x = sqrt( 2.0 ) ; // x に 2.0 の平方根(1.4142)を代入 z = y * 2.0 + x * 3.0 ; // y*2+x*3をzに代入 // 変数の内容の表示 printf( "%d\n" , i ) ; // 整数型(%d)で、 i の値を表示 printf( "%f\n" , x ) ; // 単精度実数(%f) で、x の値を表示 printf( "%lf\n" , z ) ; // 倍精度実数(%lf)で、z の値を表示 printf( "iの値は%d,xの値は%lfです。\n" , i , x ) ; return 0 ; // 正常終了 0 を返す }
変数(計算結果を格納する入れ物)を使う場合は、変数を宣言する。
変数名には、何が入っているのか理解しやすいように、名前をつければいい。(英字で始まり、英数字が続くもの,_が入ってもいい)
変数に値を記憶する時は、”変数名=式 ;”の様に書くと、代入演算子”=” の右辺を計算し、その計算結果が左辺の変数に保存される。
変数の内容を表示する時には、printf() の文字列の中に、%d,%f,%lf などの表示したい式の型に応じたものを書いておく。
式の値が、その %.. の部分に書き込まれて、出力される。
繰り返しの制御命令
最も基礎的な繰り返し命令として、for() 文を説明。
#include <stdio.h> int main() { int i ; for( i = 1 ; i <= 10 ; i++ ) { // iを1から10まで変化させる。 printf( "%d %d\n" , i , i*i ) ; // i と iの二乗を表示 } return 0 ; }
for文の意味を説明するために、対応するフローチャートを示す。
先のプログラムをフローチャートで示し、その命令の実行順序と、その変数の変化を下図に示す。
練習問題1
簡単なプログラミングの練習として、前回講義の練習問題をC言語で書いてみよう。
- 電気電子工学科,電子情報工学科の学生は、出席番号が奇数は処理C,偶数は処理Dについて回答せよ。
- それ以外の学科の学生は、出席番号が奇数は処理A,偶数は処理Bの結果について回答せよ。
制御構文とフローチャート
構文の入れ子
文と複文
C言語の文法で、{,} は複数の処理をまとめる複文とよばれる。
これに対して、a = 123 ; といったセミコロンで終わる「処理 ;」は単文という。
制御構文は、「if ( 条件) 文」で文となる。このため、文が単文であれば、{,} は不要である。
if ( 条件 ) { a = 123 ; } if ( 条件 ) a = 123 ; // 単文なら中括弧は不要
同じように、「while(条件) 文」、「for(A,B,C) 文」、「do 文 while(条件) ;」も、それぞれ文を構成する。
{,} の複文は、{ 文 文 文… } のように、一連の文を実行し、それを1つの文として扱うための機能である。
練習問題2
プログラムの制御構造の確認として、以下の3つ(No.1,No.2,No.3)の問題から、
M科,C科,B科の学生は((自分の出席番号+1) % 2)+1 の問題、E科,EI科の学生は、((自分の出席番号+1) % 3)+1について、プログラムのフローチャートを描き、その処理がどのように進むのか答えよ。
レポートには、以下の点を記載すること。
- フローチャート
- 実行順序
- 変数の変化がわかる内容
- (できれば、実際にプログラムを動かし、正しいことを検証)
// No.1 #include <stdio.h> int main() { int i , j ; for( i = 1 ; i <= 4 ; i++ ) { if ( i % 2 == 0 ) { // i%2 は2で割った余り,i%2==0ならば偶数のとき for( j = 1 ; j <= 2 ; j++ ) printf( "%d %d\n" , i , j ) ; } } return 0 ; } // No.2 #include <stdio.h> int main() { int x = 10 , y = 7 , s = 0 ; while( x > 0 ) { if ( x % 2 != 0 ) s = s + y ; y = y * 2 ; x = x / 2 ; // 注意: xは整数型 } printf( "%d\n" , s ) ; return 0 ; } // No.3 #include <stdio.h> int a[ 6 ] = { 2 , 3 , 5 , 8 , 13 , 21 } ; int main() { int left = 0 , right = 6 , mid ; int key = 13 ; while( right - left > 0 ) { mid = (left + right) / 2 ; // 整数型で計算 printf( "%d\n" , a[ mid ] ) ; if ( a[ mid ] == key ) break ; else if ( a[ mid ] > key ) right = mid ; else left = mid + 1 ; } return 0 ; }
遠隔授業6回目の反省
遠隔授業も6回目。Web掲載の講義資料に、Microsoft Edgeのペン書き機能で、図などを交えた説明のスタイルでやってきた。私としては、講義資料のWeb掲載は数年前からやっていたので、黒板がWeb画面になっただけ…の感覚。
んで、今日は出席確認もかねてForms機能でアンケートで意見聞いたら、「書き込みのメモをとるのが追いつかない」とのご意見。
一応、講義の録画機能は使ってるし、書き込みすぎて逆にみづらくなった書き込みを消したりしてるけど、真面目にノートをとる人にしてみれば、ペースが早いみたい。
これまでも、できるだけ雑談を交えながら、ペースを落とすようにしているけど、雑談ネタでも書き込みしながら説明するもんだから、相変わらず追いつかないか… 反省!!
様々なデータの覚え方
前回の malloc() + free() の説明では、実例が少なくイメージがわかりにくいので、名前と年齢のデータを覚える場合の様々な方法を議論する。最後に前期中間のプログラム課題を示す。
malloc+freeの振り返り
// 文字列(可変長)の保存 char str[] = "ABCDE" ; char* pc ; pc = (char*)malloc( strlen( str ) + 1 ) ; if ( pc != NULL ) { // ↑正確に書くと sizeof( char ) * (strlen(str)+1) strcpy( pc , str ) ; //////////////////// // pcを使った処理 //////////////////// free( pc ) ; } // // 可変長の配列の保存 int data[] = { 11 , 22 , 33 } ; int* pi ; pi = (int*)malloc( sizeof( int ) * 3 ) ; if ( pi != NULL ) { for( int i = 0 ; i < 3 ; i++ ) pi[ i ] = data[ i ] ; //////////////////// // piを使った処理 //////////////////// free( pi ) ; } // // 1件の構造体の保存 struct Person { char name[ 10 ] ; int age ; } ; struct Person* pPsn ; pPsn = (struct Person*)malloc( sizeof( struct Person ) ) ; if ( pPsn != NULL ) { strcpy( pPsn->name , "t-saitoh" ) ; pPsn->age = 55 ; //////////////////// // pPsnを使った処理 //////////////////// free( pPsn ) ; }
(おまけ)C++の場合
malloc+freeでのプログラミングは、mallocの結果を型キャストしたりするので、間違ったコーディングの可能性がある。このため、C++ では、new 演算子, delete 演算子というものが導入されている。
// 同じ処理をC++で書いたら // 文字列の保存 char str[] = "ABCDE" ; char* pc = new char[ strlen( str ) + 1 ] ; strcpy( pc , str ) ; // pcを使った処理 delete[] pc ; // new型[]を使ったらdelete[] // int配列の保存 int data[] = { 11 , 22 , 33 } ; int* pi ; pi = new int[ 3 ] ; for( int i = 0 ; i < 3 ; i++ ) pi[ i ] = data[ i ] ; // piを使った処理 delete[] pi ; // 構造体の保存 struct Person { char name[ 10 ] ; int age ; } ; Person* pPsn ; pPsn = new Person ; strcpy( pPsn->name , "t-saitoh" ) ; pPsn->age = 55 ; // pPsnを使った処理 delete pPsn ; // new型ならdelete注意すべき点は、malloc+freeとの違いは、mallocがメモリ確保に失敗した時の処理の書き方。返り値のNULLをチェックする方法は、呼び出し側ですべてでNULLの場合を想定した書き方が必要になり、処理が煩雑となる。C++の new 演算子は、メモリ確保に失敗すると、例外 bad_alloc を投げてくるので、try-catch 文で処理を書く。(上記例はtry-catchは省略)
安全な1行1件のデータ入力
C言語では、scanf などの関数は、バッファオーバーフローなどの危険性があるため、以下のような処理を使うことが多い。fgets は、指定されたファイルから1行分のデータを読み込む。sscanf は、文字列のなかから、scanf() と同じようなフォーマット指定でデータを読み込む。
fgets は、これ以上の入力データが無い場合には、NULL を返す。
(Windowsであれば、キー入力でCtrl+Z を入力、macOSやLinuxであれば、Ctrl+Dを入力)
sscanf() は、読み込めたデータ件数を返す。
int main() { char buff[ 1024 ] ; for( int i = 0 ; i < 3 ; i++ ) { if ( fgets( buff , sizeof( buff ) , stdin ) != NULL ) { char name[ 1024 ] ; int age ; if ( sscanf( buff , "%s%d" , name , &age ) == 2 ) { // 名前と年齢の2つのデータが正しく読み込めたとき ... } } } return 0 ; }
様々なデータの覚え方
配列サイズ固定・名前が固定長
例えば、このデータ構造であれば、table1[] の場合、長い名前にある程度対応できるように nameの配列を100byteにしたりすると、データ件数が少ない場合には、メモリの無駄も多い。
そこで、実際に入力された存在するデータだけをポインタで覚える方法 table2[] という保存方法も考えられる。
// 固定長データのプログラム #define SIZE 50 // 名前(固定長)と年齢の構造体 struct NameAge { char name[ 32 ] ; int age ; } ; struct NameAge table1[ SIZE ] ; int size1 = 0 ; void entry1( char s[] , int a ) { strcpy( table1[ size1 ].name , s ) ; table1[ size1 ].age = a ; size1++ ; } // ポインタで覚える場合 struct NameAge* table2[ SIZE ] ; int size2 = 0 ; void entry2( char s[] , int a ) { table2[size2] = (struct NameAge*)malloc( sizeof( struct NameAge ) ) ; if ( table2[size2] != NULL ) { // なぜ != NULL のチェックを行うのか、説明せよ strcpy( table2[size2]->name , s ) ; table2[size2]->age = a ; size2++ ; } } // データ出力 void print_NA( struct NameAge* p ) { printf( "%s %d¥n" , p->name , p->age ) ; } int main() { // table1に保存 entry1( "t-saitoh" , 55 ) ; entry1( "tomoko" , 44 ) ; print_NA( &table1[0] ) ; print_NA( &table1[1] ) ; // table2に保存 entry2( "t-saitoh" , 55 ) ; entry2( "tomoko" , 44 ) ; print_NA( _________________ ) ; // table2の中身を表示せよ print_NA( _________________ ) ; return 0 ; }
配列サイズ固定・名前が可変長
しかしながら、前回の授業で説明したように、際限なく長い名前があるのであれば、以下の様に名前は、ポインタで保存し、データを保存する時に strdup(…) を使って保存する方法もあるだろう。
// 名前が可変長のプログラム // 名前(固定長)と年齢の構造体 struct NamePAge { char* name ; // ポインタで保存 int age ; } ; struct NamePAge table3[ SIZE ] ; int size3 = 0 ; void entry3( char s[] , int a ) { table3[ size3 ].name = strdup( s ) ; // ★★★★ table3[ size3 ].age = a ; size3++ ; } // ポインタで覚える場合 struct NamePAge* table4[ SIZE ] ; int size4 = 0 ; void entry4( char s[] , int a ) { table4[size4] = (struct NamePAge*)malloc( ____________________ ) ; if ( table4[size4] != NULL ) { // ↑適切に穴埋めせよ table4[size4]->name = strdup( s ) ; // ★★★★ _________________________________ ; // ←適切に穴埋めせよ size4++ ; } } // データ出力 void print_NPA( struct NameAge* p ) { printf( "%s %d¥n" , ____________ , ____________ ) ; } // ↑適切に穴埋めせよ int main() { // table3に保存 entry3( "t-saitoh" , 55 ) ; entry3( "jyugemu jyugemu ..." , 44 ) ; print_NPA( _________________ ) ; // table3[] の中身を表示せよ。 print_NPA( _________________ ) ; // table4に保存 entry4( "t-saitoh" , 55 ) ; entry4( "jyugemu jyugemu ..." , 44 ) ; print_NPA( table4[0] ) ; print_NPA( table4[1] ) ; return 0 ; }
データ件数が可変長ならば
前述のプログラムでは、データ件数全体は、SIZE という固定サイズを想定していた。しかしながら、データ件数自体も数十件かもしれないし、数万件かもしれないのなら、配列のサイズを可変長にする必要がある。
struct NamePAge* table5 ; int size5 = 0 ; void entry5( char s[] , int a ) { strcpy( table5[ size5 ].name , s ) ; table5[ size5 ].age = a ; size5++ ; } int main() { // table5に保存 table5 = (struct NameAge*)malloc( sizeof( struct NameAge ) * 2 ) ; if ( table5 != NULL ) { entry5( "t-saitoh" , 55 ) ; entry5( "tomoko" , 44 ) ; } return 0 ; }
メモリの管理に十分気を付ける必要があるが、名前の長さも配列全体のサイズも可変長であれば、以下のようなイメージ図のデータを作る必要があるだろう。(JavaScriptやJavaといった言語ではデータのほとんどがこういったポインタで管理されている)
レポート課題
授業での malloc , free を使ったプログラミングを踏まえ、以下のレポートを作成せよ。
以下のデータのどれか1つについて、データを入力し、何らかの処理を行うこと。
課題は、原則として、(自分の出席番号%3)+1 についてチャレンジすること。
- 名前と電話番号
- 名前と生年月日
- 名前と身長・体重
このプログラムを作成するにあたり、以下のことを考慮しmallocを適切に使うこと。
名前は、長い名前の人が混ざっているかもしれない。
保存するデータ件数は、10件かもしれない1000件かもしれない。(データ件数は、処理の最初に入力すること。)
ただし、mallocの理解に自信がない場合は、名前もしくはデータ件数のどちらか一方は固定値でも良い。
レポートには、(a)プログラムリスト, (b)プログラムの説明, (c)正しく動いたことがわかる実行例, (d)考察 を記載すること。
考察には、自分のプログラムが正しく動かない事例はどういう状況でなぜ動かないのか…などを検討したり、プログラムで良くなった点はどういう所かを説明すること。
純粋仮想基底クラス
前回説明した仮想関数では、基底クラスから派生させたクラスを作り、そのデータが混在してもクラスに応じた関数(仮想関数)を呼び出すことができる。
この仮想関数の機能を逆手にとったプログラムの記述方法として、純粋仮想基底クラスがある。その使い方を説明する。
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++では仮想関数が使えるようになり、型の管理をプログラマーが行う必要のある「面倒で危険な」共用体を使う必要はなくなった。
// 純粋仮想基底クラス class Object { public: virtual void print() const = 0 ; // 中身の無い純粋基底クラスで、 // 仮想関数を記述しない時の書き方。 } ; // 整数データの派生クラス class IntObject : public Object { private: int data ; public: IntObject( int x ) { data = x ; } virtual void print() const { printf( "%d\n" , data ) ; } } ; // 文字列の派生クラス class StringObject : public Object { private: char data[ 100 ] ; public: StringObject( const char* s ) { strcpy( data , s ) ; } virtual void print() const { printf( "%s\n" , data ) ; } } ; // 実数の派生クラス class DoubleObject : public Object { private: double data ; public: DoubleObject( double x ) { data = x ; } virtual void print() const { printf( "%lf\n" , data ) ; } } ; // 動作確認 int main() { Object* data[3] = { new IntObject( 123 ) , new StringObject( "abc" ) , new DoubleObject( 1.23 ) , } ; for( int i = 0 ; i < 3 ; i++ ) { // 123 data[i]->print() ; // abc } // 1.23 と表示 return 0 ; } ;
このプログラムでは、純粋仮想基底クラスObjectから、整数IntObject, 文字列StringObject, 実数DoubleObject を派生させ、そのデータを new により生成し、Objectの配列に保存している。
仮想関数を使うと、Object型の中に自動的に型情報が保存されるようになる。一般的な実装では、各派生クラス用の仮想関数のポインタテーブル(vtable)へのポインタが使われる。
Javaなどのオブジェクト指向言語では、全てのクラス階層のスーパークラスとして、Object を持つように作られている。
様々な型に適用できるプログラム
次に、純粋仮想基底クラスの特徴を応用したプログラムの作り方を説明する。
例えば、以下のような最大選択法で配列を並び替えるプログラムがあったとする。
int a[5] = { 11, 55, 22, 44, 33 } ; void my_sort( int array[] , int size ) { for( int i = 0 ; i < size - 1 ; i++ ) { int max = i ; for( int j = i + 1 ; j < size ; j++ ) { if ( array[j] > array[max] ) max = j ; } int tmp = array[i] ; array[i] = array[max] ; array[max] = tmp ; } } int main() { my_sort( a , 5 ) ; }
しかし、この整数を並び替えるプログラムがあっても、文字列の並び替えや、実数の並び替えがしたい場合には、改めて文字列用並び替えの関数を作らなければいけない。しかも、ほとんどが同じような処理で、改めて指定された型のためのプログラムを作るのは面倒である。
C言語のデータの並び替えを行う、qsort() では、関数ポインタを用いることで、様々なデータの並び替えができる。しかし、1件あたりのデータサイズや、データ実体へのポインタを正しく理解する必要がある。
#include <stdio.h> #include <stdlib.h> int a[ 4 ] = { 11, 33, 22, 44 } ; double b[ 3 ] = { 1.23 , 5.55 , 0.11 } ; // 並び替えを行いたいデータ専用の比較関数を作る。 // a>bなら+1, a=bなら0, a<bなら-1を返す関数 int cmp_int( int* pa , int* pb ) { // int型用コールバック関数 return *pa - *pb ; } int cmp_double( double* pa , double* pb ) { // double型用コールバック関数 if ( *pa == *pb ) return 0 ; else if ( *pa > *pb ) return 1 ; else return -1 ; } int main() { // C言語の怖さ qsort( a , 4 , sizeof( int ) , // このあたりの引数を書き間違えると (int(*)(void*,void*)) cmp_int ) ; // とんでもない目にあう。 qsort( b , 3 , sizeof( double ) , (int(*)(void*,void*)) cmp_double ) ; }このように、自分が作っておいた関数のポインタを、関数に渡して呼び出してもらう方法は、コールバックと呼ぶ。
JavaScript などの言語では、こういったコールバックを使ったコーディングがよく使われる。// コールバック関数 f を呼び出す関数 function exec_callback( var f ) { f() ; } // コールバックされる関数 foo() function foo() { console.log( "foo()" ) ; } // foo() を実行する。 exec_callback( foo ) ; // 無名関数を実行する。 exec_callback( function() { console.log( "anonymous" ) ; } ) ;
任意のデータを並び替え
class Object { public: virtual void print() const = 0 ; virtual int cmp( Object* ) = 0 ; } ; // 整数データの派生クラス class IntObject : public Object { private: int data ; public: IntObject( int x ) { data = x ; } virtual void print() const { printf( "%d\n" , data ) ; } virtual int cmp( Object* p ) { int pdata = ((IntObject*)p)->data ; // 本当はこのキャストが危険 return data - pdata ; // ↓安全な実装したいなら↓ } // IntObject* pi = dynamic_cast<IntObject*>(p) ; } ; // return pi != NULL ? data - pi->data : 0 ; // 文字列の派生クラス class StringObject : public Object { private: char data[ 100 ] ; public: StringObject( const char* s ) { strcpy( data , s ) ; } virtual void print() const { printf( "%s\n" , data ) ; } virtual int cmp( Object* p ) { char* pdata = ((StringObject*)p)->data ; return strcmp( data , pdata ) ; // 文字列比較関数 } } ; // 実数の派生クラス class DoubleObject : public Object { private: double data ; public: DoubleObject( double x ) { data = x ; } virtual void print() const { printf( "%lf\n" , data ) ; } virtual int cmp( Object* p ) { double pdata = ((DoubleObject*)p)->data ; if ( data == pdata ) return 0 ; else if ( data > pdata ) return 1 ; else return -1 ; } } ; // Objectからの派生クラスでcmp()メソッドを // 持ってさえいれば、どんな型でもソートができる。 void my_sort( Object* array[] , int size ) { for( int i = 0 ; i < size - 1 ; i++ ) { int max = i ; for( int j = i + 1 ; j < size ; j++ ) { if ( array[j]->cmp( array[max] ) > 0 ) max = j ; } Object* tmp = array[i] ; array[i] = array[max] ; array[max] = tmp ; } } // 動作確認 int main() { Object* idata[3] = { new IntObject( 11 ) , new IntObject( 33 ) , new IntObject( 22 ) , } ; Object* sdata[3] = { new StringObject( "abc" ) , new StringObject( "defghi" ) , new StringObject( "c" ) , } ; my_sort( idata , 3 ) ; // 整数のソート for( int i = 0 ; i < 3 ; i++ ) idata[i]->print() ; my_sort( sdata , 3 ) ; // 文字列のソート for( int i = 0 ; i < 3 ; i++ ) sdata[i]->print() ; return 0 ; } ;
このような方式でプログラムを作っておけば、新しいデータ構造がでてきてもソートのプログラムを作らなくても、比較専用の関数 cmp() を書くだけで良い。
ただし、この並び替えの例では、Object* を IntObject* に強制的に型変換している。
また、このプログラムでは、データを保管するために new でポインタを保管し、データの比較をするために仮想関数の呼び出しを行うことから、メモリの使用効率も処理効率でもあまりよくない。
こういう場合、最近の C++ ではテンプレート機能が使われる。
template <typename T> void my_sort( T a[] , int size ) { for( int i = 0 ; i < size - 1 ; i++ ) { int max = i ; for( int j = i + 1 ; j < size ; j++ ) { if ( a[j] > a[max] ) max = j ; } T tmp = a[i] ; a[i] = a[max] ; a[max] = tmp ; } } int main() { int idata[ 5 ] = { 3, 4, 5 , 1 , 2 } ; double fdata[ 4 ] = { 1.23 , 0.1 , 3.4 , 5.6 } ; // typename T = int で int::mysort() が作られる my_sort( idata , 5 ) ; for( int i = 0 ; i < 5 ; i++ ) printf( "%d " , idata[i] ) ; printf( "\n" ) ; // typename T = double で double::mysort() が作られる my_sort( 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() が作られる。
テンプレート機能では、各型用のコードが自動的に複数生成されるという意味では、出来上がったコードがコンパクトという訳ではない。
愚痴
ここにあるサンプルコードを試していたけど、以下のような間違いしたけど、エラ〜メッセージが「抽象クラスのオブジェクトとは作れない(error: allocating an object of abstract class type ‘BA’)」というのは、間違い探すのに一苦労だった。C++の文法が変更されたかと思ったぜ。
class A { public: virtual void print() = 0 ; // const のつけ忘れ } ; // print() const = 0 ; class BA : public A { private: int b ; public: virtual void print() const { printf( "%d" , b ) ; } } ; int main() { BA ba ; }
フローチャートと数値型
学際科目の情報制御基礎において、プログラムの基本としてフローチャートと基本的な処理を説明し、数値型の注意点を説明。
フローチャートの基本
プログラムの処理の順序を理解するには、初心者であればフローチャート(流れ図)を使う。
処理の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)から繰り返し。
練習問題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
整数型と扱える値の範囲
コンピュータの開発が進むにつれ計算の単位となるデータ幅は、8bit, 16bit, 32bit, 64bit と増えていった。整数型データには、正の値しか覚えられない符号無し整数と、2の補数で負の数を覚える符号付き整数に分けられる。
プログラムを作るためのC言語では、それぞれ 8bitの文字型(char)、16bitの short int型、32bitの int 型、64bitの long int 型(※C言語では long int で宣言すると32bitの場合も多いので要注意)がある。
精度 | 符号あり | 符号なし |
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 の整数であれば、最上位ビットが符号に使われるので、正の数なら残りの(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 = (210)3 × 22 = 10243 × 4 ≒ 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問について答えること。
- 7bitの符号なし整数で扱える数値の範囲 (出席番号が偶数)
- 12bitの符号あり整数で扱える数値の範囲 (出席番号が奇数)
- 20bitの符号なし整数で扱える数値の範囲 (誕生日の日づけが偶数)
- 24bitの符号あり整数で扱える数値の範囲 (誕生日の日づけが奇数)
練習問題3
先に示した数値の範囲が原因で動かないプログラム(コード1,コード2.2,コード3.1)の中から1つを選んで、計算結果が正しく求まらない原因を、具体的な値を示しながら説明せよ。
練習問題1,練習問題2,練習問題3について、レポートとして提出せよ。
効率のよいメモリ使用と動的メモリ/スタック/malloc+free
次にメモリの利用効率の話について解説する。
配列宣言でサイズは定数
C言語では、配列宣言を行う時は、本来ならば、配列サイズに変数を使うことはできない。
最近のC(C99)では、実は下記のようなものは、裏で後述のalloca()を使って動いたりするので普通に書けるけど…。(^_^;
void foo( int size ) { int array[ size ] ; // エラー for( int i = 0 ; i < size ; i++ ) array[ i ] = i*i ; } int main() { foo( 3 ) ; foo( 4 ) ; return 0 ; }
メモリ利用の効率
配列サイズには、定数式しか使えないので、1クラスの名前のデータを覚えるなら、以下のような宣言が一般的であろう。
#define MEMBER_SIZE 50 #define NAME_LENGTH 20 char name[ MEMBER_SIZE ][ NAME_LENGTH ] ;
しかしながら、クラスに寿限無とか銀魂の「ビチグソ丸」のような名前の人がいたら、20文字では足りない。(“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(2020年5月リンク切れてる…)
大きな配列を少しづつ貸し出す処理
// 巨大な配列 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 j u g e m u - .... ¥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 ; // p = (int*)alloca( sizeof( int ) * size ) ; for( int i = 0 ; i < size ; i++ ) p[ i ] = i*i ; } int main() { foo( 3 ) ; foo( 4 ) ; return 0 ; }
alloca() は、指定された byte 数のデータ領域の先頭ポインタを返すが、その領域を 文字を保存するために使うか、int を保存するために使うかは alloca() では解らない。alloca() の返り値は、使う用途に応じて型キャストが必要である。文字を保存するなら、(char*)alloca(…) 、 intを保存するなら (int*)alloca(…) のように使う。
ただし、関数内で alloca で確保したメモリは、その関数が終了すると、その領域は使えなくなる。このため、最後に alloca で確保したメモリが、最初に不要となる…ような使い方でしか使えない。
malloc()とfree()
malloc() は、動的(ヒープ領域)にメモリを確保する命令で、データを保存したい時に malloc() を実行し、不要になった時に free() を実行する。
malloc() では、alloca() と同じように、格納したいデータの byte 数を指定する。また、malloc() は、確保したメモリ領域の先頭を返すが、ヒープメモリが残っていない場合 NULL ポインタを返す。処理が終わってデータ領域をもう使わなくなったら、free() で解放する必要がある。
基本的には、確保したメモリ領域を使い終わった後 free() を実行しないと、再利用できないメモリ領域が残ってしまう。こういう処理を繰り返すと、次第にメモリを食いつぶし、仮想メモリ機能によりハードディスクの読み書きで性能が低下したり、最終的にOSが正しく動けなくなる可能性もある。こういった free() 忘れはメモリーリークと呼ばれ、malloc(),free()に慣れない初心者プログラマーによく見られる。
ヒープメモリは、プロセスの起動と共に確保され、プログラムの終了と同時に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 ] ) ; } int main() { // 文字列の入力&出力 inputs() ; prints() ; // 使い終わったら、free() で解放 for( int i = 0 ; i < 10 ; i++ ) free( names[ i ] ) ; return 0 ; }
文字列を保存する場合には、上記の 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 ; }
配列に保存する場合
任意サイズの配列を作りたい場合には、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 ) ; } }
構造体の配列
同じように、任意サイズの構造体の配列を作りたいのであれば、配列サイズに「sizeof( struct Complex ) * データ件数」を指定すればいい。
#include <stdlib.h> struct Complex { double re , im ; } ; // 指定した場所にComplexを読み込む。 int input_Complex( struct Complex* p ) { return scanf( "%f %f" , &(p->re) , &(p->re) ) == 2 ; } // 指定したComplexを出力 void print_Complex( struct Complex* p ) { printf( "%f+j%f¥n" , p->re , p->im ) ; } void main() { int size ; struct Complex* array ; // 処理する件数を入力 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] ) ; // mallocしたら必ずfree free( array ) ; } }
派生と継承と仮想関数
前回の派生と継承のイメージを改めて記載する。
// 基底クラス class Person { private: char name[ 20 ] ; int age ; public: Person( const char s[] , int x ) : age( x ) { strcpy( name , s ) ; } void print() { printf( "%s %d\n" , name , age ) ; } } ; // 派生クラス(Student は Person から派生) class Student : public Person { private: char dep[ 20 ] ; int grade ; public: Student( const char s[] , int x , const char d[] , int g ) : Person( s , x ) // 基底クラスのコンストラクタ { // 追加された処理 strcpy( dep , d ) ; grade = g ; } void print() { Person::print() ; // 基底クラスPersonで名前と年齢を表示 printf( "- %s %d\n" , dep , grade ) ; } } ; int main() { Person saitoh( "t-saitoh" , 55 ) ; Student yama( "yamada" , 21 , "ES" , 1 ) ; Student nomu( "nomura" , 22 , "PS" , 2 ) ; saitoh.print() ; // 表示 t-saitoh 55 yama.print() ; // 表示 yamada 21 // - ES 1 nomu.print() ; // 表示 nomura 22 return 0 ; // - PS 2 }
このような処理でのデータ構造は、次のようなイメージで表される。
派生クラスでの問題提起
基底クラスのオブジェクトと、派生クラスのオブジェクトを混在してプログラムを記述したらどうなるであろうか?
上記の例では、Person オブジェクトと、Student オブジェクトがあったが、それをひとまとめで扱いたいこともある。
以下の処理では、Person型の saitoh と、Student 型の yama, nomu を、一つの table[] にまとめている。
int main() { Person saitoh( "t-saitoh" , 55 ) ; Student yama( "yamada" , 21 , "ES" , 1 ) ; Student nomu( "nomura" , 22 , "PS" , 2 ) ; Person* table[3] = { &saitoh , &yama , &nomu , } ; for( int i = 0 ; i < 3 ; i++ ) { table[ i ]->print() ; } return 0 ; }
C++では、Personへのポインタの配列に代入する時、Student型ポインタは、その基底クラスへのポインタとしても扱える。ただし、このように記述すると、table[] には、Person クラスのデータして扱われる。
このため、このプログラムを動かすと、以下のように、名前と年齢だけが3人分表示される。
t-saitoh 55 yamada 21 nomura 22
派生した型に応じた処理
上記のプログラムでは、 Person* table[] に、Person*型,Student*型を混在して保存をした。しかし、Person*として呼び出されると、yama のデータを表示しても、所属・学年は表示されない。上記のプログラムで、所属と名前を表示することはできないのだろうか?
// 混在したPersonを表示 for( int i = 0 ; i < 3 ; i++ ) table[i]->print() ; // Student は、所属と名前を表示して欲しい t-saitoh 55 yamada 21 - ES 1 nomura 22 - PS 2
上記のプログラムでは、Person型では、後でStudent型と区別ができないと困るので、Person型に、Person型(=0)なのか、Student型(=1)なのか区別するための type という要素を追加し、type=1ならば、Student型として扱うようにしてみた。
// 基底クラス class Person { private: int type ; // 型識別情報 char name[ 20 ] ; int age ; public: Person( int tp , const char s[] , int x ) : type( tp ) , age( x ) { strcpy( name , s ) ; } int type_person() { return type ; } void print() { printf( "%s %d\n" , name , age ) ; } } ; // 派生クラス(Student は Person から派生) class Student : public Person { private: char dep[ 20 ] ; int grade ; public: Student( int tp , const char s[] , int x , const char d[] , int g ) : Person( tp , s , x ) // 基底クラスのコンストラクタ { // 追加された処理 strcpy( dep , d ) ; grade = g ; } void print() { Person::print() ; // 基底クラスPersonで名前と年齢を表示 printf( "- %s %d\n" , dep , grade ) ; } } ; int main() { // type=0 は Person 型、type=1は Student 型 Person saitoh( 0 , "t-saitoh" , 55 ) ; Student yama( 1 , "yamada" , 21 , "ES" , 1 ) ; Student nomu( 1 , "nomura" , 22 , "PS" , 2 ) ; Person* table[3] = { &saitoh , &yama , &nomu , } ; for( int i = 0 ; i < 3 ; i++ ) { switch( table[i]->type_person() ) { case 0 : table[i]->print() ; break ; case 1 : // 強制的にStudent*型として print() を呼び出す。 // 最近のC++なら、(static_cast<Student*>(table[i]))->>print() ; ((Student*)table[i])->print() ; break ; } } return 0 ; }
しかし、このプログラムでは、プログラマーがこのデータは、Personなので type=0 で初期化とか、Studentなので type=1 で初期化といったことを記述する必要がある。また、型情報(type)に応じて、その型にふさわしい処理を呼び出すための switch 文が必要になる。
もし、派生したクラスの種類がいくつもあるのなら、型情報の代入は注意深く書かないとバグの元になるし、型に応じた分岐は巨大なものになるだろう。実際、オブジェクト指向プログラミングが普及する前の初期の GUI プログラミングでは、巨大な switch 文が問題となっていた。
仮想関数
上記の、型情報の埋め込みと巨大なswitch文の問題の解決策として、C++では仮想関数(Virtual Function)が使える。
型に応じて異なる処理をしたい関数があったら、その関数の前に virtual と書くだけで良い。このような関数を、仮想関数と呼ぶ。
// 基底クラス class Person { private: char name[ 20 ] ; int age ; public: Person( const char s[] , int x ) : age( x ) { strcpy( name , s ) ; } virtual void print() { printf( "%s %d\n" , name , age ) ; } } ; // 派生クラス(Student は Person から派生) class Student : public Person { private: char dep[ 20 ] ; int grade ; public: Student( const char s[] , int x , const char d[] , int g ) : Person( s , x ) // 基底クラスのコンストラクタ { // 追加された処理 strcpy( dep , d ) ; grade = g ; } virtual void print() { Person::print() ; // 基底クラスPersonで名前と年齢を表示 printf( "- %s %d\n" , dep , grade ) ; } } ; int main() { // type=0 は Person 型、type=1は Student 型 Person saitoh( "t-saitoh" , 55 ) ; Student yama( "yamada" , 21 , "ES" , 1 ) ; Student nomu( "nomura" , 22 , "PS" , 2 ) ; Person* table[3] = { &saitoh , &yama , &nomu , } ; for( int i = 0 ; i < 3 ; i++ ) { table[i]->print() ; } return 0 ; }
クラスの中に仮想関数が使われると、C++ では、プログラム上で見えないが、何らかの型情報をオブジェクトの中に保存してくれる。
また、仮想関数が呼び出されると、その型情報を元に、ふさわしい関数を自動的に呼び出してくれる。このため、プログラムも table[i]->print() といった極めて簡単に記述できるようになる。
関数ポインタ
仮想関数の仕組みを実現するためには、関数ポインタが使われる。
以下の例では、返り値=int,引数(int,int)の関数( int(*)(int,int) )へのポインタfpに、最初はaddが代入され、(*fp)(3,4) により、7が求まる。
int add( int a , int b ) { return a + b ; } int mul( int a , int b ) { return a * b ; } int main() { int (*fp)( int , int ) ; fp = add ; printf( "%d\n" , (*fp)( 3 , 4 ) ) ; // 3+4=7 fp = mul ; printf( "%d\n" , (*fp)( 3 , 4 ) ) ; // 3*4=12 int (*ftable[2])( int , int ) = { add , mul , } ; for( int i = 0 ; i < 2 ; i++ ) printf( "%d\n" , (*ftable[i])( 3 , 4 ) ) ; return 0 ; }仮想関数を使うクラスが宣言されると、一般的にそのコンストラクタでは、各クラス毎の仮想関数へのポインタのテーブルが型情報として保存されるのが一般的。仮想関数の呼び出しでは、仮想関数へのポインタを使って処理を呼び出す。このため効率よく仮想関数を動かすことができる。
ソートのアルゴリズムの分岐点
ソートアルゴリズムの処理時間で、選択法よりクイックソートが有利になる分岐点を求める問題で、
の結果が、N=53.1 になる…との説明が判らないとの質問。# 53.017 が正しいとの結果だったが。
あてずっぽうな計算方法
Excelで実際に計算すればいい。B列2行目に =A2/LOG(A2) を入れて、A列に2,3,…. と数字を入れて変化させて、30.74 に近い場所を探すと53となった。# 手抜きだな!
ニュートン法で解く
方程式で解けない場合は、ニュートン法 f(x)=0 の式でxの値を求める…のが定番の方法。
この場合は f(x) = x/log(x) – 1000/25/log(20) として、ニュートン法の漸化式を解けばいい。
ちょいとプログラムを書いてみた。
((( nlogn.cxx ))) #include <stdio.h> #include <math.h> // N/logN = 1000/25/log20 をニュートン法で解く // // f(x) = x/log(x) - 1000/25/log20 // f'(x) = log(10) (log(x) - 1) / log(x)^2 // // x <= x + f(x) / f'(x) // 要注意 C言語のlog関数 // log(x) = 自然対数を底とするlog // log10(x) = 10を底とするlog double f( double x ) { return x / log10(x) - 1000.0/25.0/log10(20.0) ; } double df( double x ) { return log(10.0) * ( log(x) - 1.0 ) / pow( log(x) , 2.0 ) ; } int main() { double x = 5.0 ; for( int i = 0 ; i < 5 ; i++ ) { double f1 = f(x) ; double f2 = df(x) ; x = x - f1 / f2 ; printf( "%d %lf\n" , i , x ) ; } return 0 ; } ((( 実行結果 ))) $ g++ nlogn.cxx $ ./a.out 0 48.547041 1 52.983539 2 53.016894 3 53.016896 4 53.016896
- n/log(n)の微分の計算が面倒だったので、Wolfram Alpha を使ったのは内緒だ! 学生さんは、こーゆー手抜きをしないこと。
- log(x)の計算で、loge(x) と log10(x) を使い分けるのがキーポイント。
- Excel では、loge(x) は、LN(x) 、log10(x) は、LOG10(x)
- C言語では、loge(x) は、log(x) 、log10(x) は、log10(x)
うーむ、Webの資料では、53 に近い値だし、あてずっぽうで 53.1 となる…って書いたけど、本当は 53.017 だな。m(_ _)m
数年前までの電子情報の学生さんなら、TI の関数電卓持ってたから「授業で解いて…」というと、結果をすぐに答えてくれる優秀な人がクラスに1人くらい必ずいたけど、最近は BYOD でパソコンを使うので TI の関数電卓持ってないから、解いてくれるヤツいないのよね。