C/C++ 勉強会 2007 年 5 月 14 日 目次 式と文(後) 2 1.1 反復処理 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 2 1.2 再帰 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 9 1 ユーザ定義型 15 2.1 列挙型 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 15 2.2 構造体 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 17 2.3 型の別名 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 20 Windows プログラムの基本 22 3.1 Windows=Window(s) . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 22 3.2 ウィンドウの生成 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 26 3.3 ウィンドウ関数 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 27 2 3 1 1 式と文(後) 1.1 反復処理 分岐と並んで重要なプログラムの制御構造に、反復処理(ループ)がある。プログラミン グ言語が達成すべき目標の 1 つに、同じ処理や似た処理を何度も繰り返し書かずに済むよう な方法を提供する、というものがある。同じ処理をプログラム中で何度も記述すると、その 処理にバグや不具合があった場合にその全てを修正しなければならなくなるし、そうでなく てもコードの字数行数が増えれば管理の手間は増大する。反復処理はこうした問題を解決 するための直接的なアプローチとなる。 1.1.1 while 文 while 文の書き方を示す。 while.cpp 1 #i n c l u d e <i o s t r e a m > 2 3 u s i n g namespace s t d ; 4 5 i n t main ( v o i d ) 6 { 7 8 9 10 11 12 int i = 0; w h i l e ( i < 10 0 ) { c o u t << i << e n d l ; i ++; } 13 } 上のプログラムは 0 から 99 までの数字を 1 つずつ改行しながらコンソールへ出力する。 このように、 while (条件部) 本文 と書くことで条件部の式が真の間次の文を実行し続けるプログラムが作れる。if 文同様、 本文には中括弧を使うことで複数の文が使える。 「++」はインクリメントと呼ばれる演算子 2 であり、被演算子を 1 だけ進める意味を持つ。つまり「i++」は「i = i + 1」と同じ意味 である。なお逆の計算、被演算子を 1 だけ減らすのはデクリメントと呼ばれ、 「--」で表す。 1.1.2 for 文 反復にはもう 1 つよく使われる書き方があり、これは for 文を使う。 for.cpp 1 #i n c l u d e <i o s t r e a m > 2 3 u s i n g namespace s t d ; 4 5 i n t main ( v o i d ) 6 { f o r ( i n t i = 0 ; i < 1 0 0 ; i ++) { c o u t << i << e n d l ; } 7 8 9 10 } これは先の while.cpp と同じ処理を行う。for 文の本文を実行する前に「int i = 0」で ループで使われるカウンタを定義・初期化し、続いて条件部の式「i < 100」が真であるか を評価し、本文を実行する。本文の実行が 1 度終わるごとに更新部の「i++」が実行され、 条件部の式が真である限り本文と更新部の実行を繰り返す。 for (初期化部; 条件部; 更新部) 本文 for 文はカウンタ変数の初期化と更新、条件式の記述を一箇所で行えるため、ループの性 質が一目で分かるという利点がある。for 文の初期化部、条件部、更新部はそれぞれ省略し て「for(;;)」のように書くことも出来る。 3 1.1.3 do-while 文 do-while 文は少し変わった反復処理の書き方である。 do.cpp 1 #i n c l u d e <i o s t r e a m > 2 3 u s i n g namespace s t d ; 4 5 i n t main ( v o i d ) 6 { 7 int i = 0; 8 9 10 do { c o u t << i << e n d l ; 11 12 13 } i ++; } while ( i < 100); 先の 2 つのプログラムと同様、0 から 99 までの数字を順に出力する。注意しなければ ならないのは、do-while 文では条件式の評価が 1 度本文を実行した後に行われることであ る。よって先の while 文や for 文を使ったプログラムでは条件式の不等号を逆にした場合 ( 「i > 100」などとした場合)ループの本文は一度も実行されないが、do-while の場合は最 初の 1 度だけ実行される。 do 本文 while(条件部); 4 1.1.4 continue continue 文はループの末尾までジャンプするという意味を持つ。以下のプログラムを見 ると分かり易い。 continue.cpp 1 /∗ 2 0 か ら100 ま で の 奇 数 を 出 力 す る プ ロ グ ラ ム 3 ∗/ 4 #i n c l u d e <i o s t r e a m > 5 6 u s i n g namespace s t d ; 7 8 i n t main ( v o i d ) 9 { 10 11 12 13 14 15 } f o r ( i n t i = 0 ; i < 1 0 0 ; i ++) { i f ( i % 2 == 0 ) // 2 で 割 り 切 れ る = 偶 数 continue ; c o u t << i << e n d l ; } i が偶数の場合は continue でループの末尾までジャンプするため、cout の行が実行され ない。その結果 i が奇数の場合だけ数字が出力されるようになる。for 文だけでなく、while 文、do-while 文の中でも使える。 5 1.1.5 無限ループと break 文 while 文や do-while 文、for 文の条件部へ常に真になるような式を書いた場合、また for 文の条件部を省略した場合、本文が際限なく実行される無限ループになる。終わらないので 困る。 endless.cpp 1 #i n c l u d e <i o s t r e a m > 2 3 u s i n g namespace s t d ; 4 5 i n t main ( v o i d ) 6 { 7 8 9 int i = 0; for ( ; ; ) { 10 11 12 13 14 15 16 /∗ 17 18 19 20 21 22 23 24 i n t tmp ; c o u t << ” 整数クレクレ ” << e n d l ; c i n >> tmp ; c o u t << ( i += tmp ) << e n d l ; } 以下のように書いても同じ w h i l e ( 1 ) { // 1 は 0 じ ゃ な い 値 = 真 i n t tmp ; c o u t << ” 整 数 ク レ ク レ ” << e n d l ; c i n >> tmp ; c o u t << ( i += tmp ) << e n d l ; } 25 ∗/ 26 } 6 上のプログラムは入力された値を全て加算して出力する。「+=」は「加算して代入」とい う意味の演算子であり、 「-=」 、 「*=」、「/=」はそれぞれ減算、乗算、除算のバージョンであ る。プログラムが無限ループしてしまった場合、コンソールの(CUI の)プログラムなら Ctrl+C で無理矢理終わらせることができるかもしれない。 条件部が偽になること以外にもループを終わらせる方法があり、1 つは break 文を使うこ とだ。 break.cpp 1 #i n c l u d e <i o s t r e a m > 2 3 u s i n g namespace s t d ; 4 5 i n t main ( v o i d ) 6 { 7 8 9 10 int i = 0; for ( ; ; ) { i n t tmp ; 11 12 13 c o u t << ” 整数クレクレ ” << e n d l ; c i n >> tmp ; 14 15 16 17 i f ( tmp >= 5 0 0 0 ) break ; c o u t << ( i += tmp ) << e n d l ; } 18 } 上のプログラムは 5000 以上の値が入力された時、break 文によってループを抜けて終了 する。 1.1.6 goto 文 C/C++ には悪名高い goto 文がある。プログラムの処理をあちらこちらへぶっ飛ばす ので、乱用するとコードの可読性が著しく損なわれる。goto に出来る事の殆どはループや switch で代用できるため、特別の理由がなければ極力使用を避けるべきである。 7 goto.cpp 1 /∗ 2 break . c p p と 同 じ 処 理 3 ∗/ 4 5 #i n c l u d e <i o s t r e a m > 6 7 u s i n g namespace s t d ; 8 9 i n t main ( v o i d ) 10 { 11 int i = 0; 12 13 14 15 for ( ; ; ) { i n t tmp ; 16 17 18 19 c o u t << ” 整数クレクレ ” << e n d l ; c i n >> tmp ; i f ( tmp >= 5 0 0 0 ) g o t o end ; // ラ ベ ル e n d ま で ジ ャ ン プ 20 c o u t << ( i += tmp ) << e n d l ; 21 } 22 end : // ラ ベ ル 23 24 } return 0; ラベル名は変数名や関数名と同様の規則で、自由に付けることができる。 goto を使ってもよい稀なケースは、入れ子になったループや switch からの脱出、エラー 処理、パーサジェネレータでコードを自動生成する場合などである。コードの上から下への ジャンプは、下から上へのものより比較的罪が薄いとされている。 goto ラベル; ラベル: 8 1.2 再帰 C/C++ は関数定義の中でその関数自身を呼び出すこと、再帰呼び出し (recursive-call) をサポートしている。再帰は C/C++ の構文の 1 つで反復と同じことができるが、効率の 観点から普段あまり使われない。以下は再帰関数の使用例。 recursion.cpp 1 #i n c l u d e <i o s t r e a m > 2 3 u s i n g namespace s t d ; 4 5 /∗ 再 帰 関 数 の 定 義 ∗/ 6 int recursion ( int i ) 7 { 8 c o u t << i << e n d l ; 9 10 11 12 } i f ( ! i ) // 「 i ==0 」 と 等 価 ( 「 i が 偽 の 場 合 」 っ て 意 味 ) return 0; r e t u r n r e c u r s i o n ( i −1); // 自 分 自 身 を 呼 び 出 し て い る 13 14 i n t main ( v o i d ) 15 { 16 recursion (100); 17 } recursion() の定義が終わらない内から、recursion() の定義内で recursion() を呼び出すこ とができる(11 行目)。処理の流れは以下のようになる。 • システムから main 関数が呼ばれる • main 関数内で recursion() を引数 100 で呼び出す • recursion()8 行目で 100 を表示する • 100 は非 0 で真なので、if 文内 10 行目は実行されず 11 行目へ • recursion を 100 - 1、つまり引数 99 で呼び出す • recursion()8 行目で 99 を表示する •(中略) 9 • • • • • • recursion を 1 - 1、つまり引数 0 で呼び出す 0 は偽なので、10 行目の return 0 が実行される i=1 の時の return が実行される i=2 の時の return が実行される i=3 の時の return が (ry i=100 の時の return が実行され、main 関数へ戻っておしまい 再帰で書くことのできる処理は反復でも書くことができ、反復で書ける処理は再帰でも書 けるということが計算論の分野で証明されている。しかし C/C++ では以下に挙げる幾つ かの理由より、反復で書ける(反復での書き方の分かる)処理は必ず反復で書くべきである。 1.2.1 メモリ使用量の増大 ローカル変数は関数が呼び出される度に生成される。つまり recursion() は呼び出される 度に引数 i を格納するためのメモリ領域を確保する。recursion() から次の recursion() が呼 び出された時も、前の recursion() の実行はまだ終わっていないため(return 文の途中で 関数呼び出して処理が返るのを待ってる)、i のためのメモリ領域は解放できない。よって recursion() が呼びだされる度に、少しずつプログラムのメモリ使用量が増えていくことと なる。更にプログラムは関数を呼び出す際、後で return した時にコードの実行をどこから 再開するかを憶えていなくてはならないため、関数を「深く」呼び出していく再帰ではメモ リ使用量は更に増える*1 。 1.2.2 計算量の増大 またこの例では分からないが、再帰は単純に書くと計算量の深刻な増大を招く(指数関数 的に増える)場合がある。以下はフィボナッチ数列の n 番目の値を再帰で求めるプログラ ムである。 fibonacci.cpp 1 #i n c l u d e <i o s t r e a m > 2 3 u s i n g namespace s t d ; 4 5 unsigned i n t f i b o n a c c i ( unsigned i n t n) 6 { 7 i f ( n <= 1 ) 8 return n ; *1 Scheme など末尾再帰(tail-recursion)の除去が言語仕様で規定されている言語では、反復と完全に等価な再帰を書くこともで きる 10 9 10 } 11 r e t u r n f i b o n a c c i (n − 1) + f i b o n a c c i (n − 2 ) ; 12 i n t main ( v o i d ) 13 { 14 unsigned i n t n ; 15 16 17 18 c o u t << ” 何番目の数を求める? ” << e n d l ; c i n >> n ; c o u t << f i b o n a c c i ( n ) << e n d l ; 19 } また、以下は同じものを反復で書いたものである。 fibonacci2.cpp 1 #i n c l u d e <i o s t r e a m > 2 3 u s i n g namespace s t d ; 4 5 unsigned i n t f i b o n a c c i ( unsigned i n t n) 6 { 7 8 9 10 11 12 13 14 15 16 17 18 } 19 u n s i g n e d i n t v a l 1 = 0 , v a l 2 = 1 , tmp = 0 ; i f ( n <= 1 ) return n ; f o r ( u n s i g n e d i n t i = 1 ; i < n ; i ++) { tmp = v a l 1 + v a l 2 ; val1 = val2 ; v a l 2 = tmp ; } r e t u r n tmp ; 11 20 i n t main ( v o i d ) 21 { 22 unsigned i n t n ; 23 24 25 26 c o u t << ” 何番目の数を求める? ” << e n d l ; c i n >> n ; c o u t << f i b o n a c c i ( n ) << e n d l ; 27 } 2 つ の 実 行 結 果 は 同 じ だ が 、大 き く 異 な る の は そ の 処 理 時 間 だ 。再 帰 で 書 い た fibonacci.cpp は 50 番目のフィボナッチ数を求める程度の計算でも驚くほどの時間がか かるが (Athlon XP-M 2400+ 1.79GHz)、反復で書いた fibonacci2.cpp ならほぼ一瞬で終 わる。fibonacci() が再帰で呼ばれる回数とループが実行される回数を考えて比較すると分 かるが、反復で書いた方の計算量はデータに対し線形に(単純に比例して)増加するのに対 し、再帰の方は fibonacci(n) 自身と同じ速さで(指数的に)増加する。また先に述べた理由 から、計算に必要なメモリ量も再帰版ではより増大している。 再帰でも工夫すればデータに対して線形の計算量の処理が書けるが(反復的手続き)、 C/C++ には反復処理を書くための構文が用意されているため、わざわざそこまでして再帰 を書く必要がない。 1.2.3 使い道 では再帰には全く使い道がないのかというと、そういう訳でもない。先に述べたように、 その処理の反復での書き方が分かる場合は反復で書くべきだが、複雑な問題に対しては反復 による処理の記述法が直観的に分からない場合、思いつかない場合がある。再帰は問題の処 理を比較的単純に書き下すことができるため、複雑な問題であってもある程度楽に書くこと ができる。先の再帰版フィボナッチ関数がフィボナッチ数の定義をほぼそのままコードに したような形になっていることを思い出して欲しい。再帰で書くと楽な問題の中で、特に有 名なものに「ハノイの塔」がある。 1.2.4 ハノイの塔 • • • • • • インドのどっかに 3 本の柱と、中央に穴が開いた 64 枚の円盤がある 円盤は全て大きさが異なり、大きいものほど上になるよう柱に通してある 最初は全ての円盤が一本の柱に通っている 円盤を 1 度に 1 枚ずつどれかの柱に移動させられる 大きい円盤を小さい円盤の上へ重ねてはいけない 全ての円盤を初めの柱以外の柱へ移したら完成 12 • これが完成すると世界が終わるらしい • 以上全部フランス野郎の作り話 以上が「ハノイの塔」のルールである。結論から言うと、1 枚の円盤を 1 秒で動かした場 合で、世界が終わるまでに 5800 億年かかる。そんな計算を俺の貧弱な BIBLO 様にやらせ るのは忍びないので、今回は円盤の数を 8 枚に減らしてこのパズルを行う。 hanoi.cpp 1 #i n c l u d e <i o s t r e a m > 2 3 u s i n g namespace s t d ; 4 5 int counter = 0; 6 7 /∗ 8 3 本 の 柱 を そ れ ぞ れ A、B、Cとする。 9 8枚 の 円 盤 そ れ ぞ れ へ 上 か ら 順 に 番 号 を 付 け る 。 10 11 12 13 最 初 の8 枚 は A に 刺 さ っ て る 。 14 15 16 1. ま ず 刺 さ っ て る 元 の 柱 か ら ど う に か1 枚 残 し 全 て 作 業 場 へ 。 2. 次 い で 残 っ て る 一 枚 を 目 的 の 柱 へ 。 3. そ し て 作 業 場 の 柱 を ど う に か 目 的 の 柱 へ 。 最終的には Bに全ての円盤を移すとする。 円盤の大きさの問題があるので、 Bに円盤を移動するには Cを経由して少しずつ動かしていく必要がある。 17 1 か ら 3 の 工 程 を 再 帰 的 に 繰 り 返 す と 結 果 が 得 ら れ る 。 18 ∗/ 19 v o i d h a n o i ( i n t n , c h a r s r c , c h a r d s t , c h a r work ) 20 { 21 22 23 i f ( n == 0 ) return ; h a n o i ( n − 1 , s r c , work , d s t ) ; 24 25 26 27 c o u n t e r ++; c o u t << c o u n t e r << ” : ”” D i s k ( ” << n << ” ) : ” << s r c << ” t o ” << d s t << e n d l ; h a n o i ( n − 1 , work , d s t , s r c ) ; 13 28 } 29 30 i n t main ( v o i d ) 31 { 32 33 34 } h a n o i ( 8 , ’A ’ , ’B ’ , ’C ’ ) ; c o u t << c o u n t e r << ” 回 ” << e n d l ; パズルについては文章ではどう説明していいのか分からないし、コードについてはもっ とどう書けばいいか分からないので、口頭で頑張って説明する。が、これはパズルであって C/C++ の基礎的な内容ではないので、理解する必要はあまりない。数学好きな人はこの手 のロジックが好きかもしれず。 なお、5 行目で定義した counter は関数の外で定義されている。このように関数の外で定 義される変数をグローバル変数、大域変数と呼ぶ。ローカル変数とは違い実体がたった 1 つ で、関数を横断して参照することが出来る。これには良い面と悪い面があり、goto と同様 悪い面を強調して語られる場合が多い。詳しくは次回に説明する。 14 2 ユーザ定義型 整数型や文字型といった組込み型の他にプログラマが自由に意味を定義できる型がある。 これをユーザ定義型と呼ぶ。 2.1 列挙型 以下に列挙型 enum の定義方法をしめす。 enum.cpp 1 #i n c l u d e <i o s t r e a m > 2 #i n c l u d e <s t r i n g > 3 4 u s i n g namespace s t d ; 5 6 /∗ 列 挙 の 定 義 ∗/ 7 enum S i g n a l { 8 RED, // 9 GREEN, // 10 BLUE, // 11 12 13 } ; // 単 に enum { . . . } と し て 無 名 で 定 義 し て も よ い 0 1 2 MIN=0, // 0 MAX=2 // 2 14 15 i n t main ( v o i d ) 16 { 17 string s ; 18 19 20 21 f o r ( i n t i=MIN ; i<=MAX; i ++) { // e n u m は i n t と 互 換 性 が あ る switch ( i ) { c a s e RED: s = ”赤” ; break ; 22 23 24 25 c a s e GREEN: s = ” 緑 ” ; break ; c a s e BLUE: s = ” 青 ” ; break ; default : s = ” 不明 ” ; b r e a k ; } 15 26 27 28 c o u t << i << ” は ” << s << e n d l ; } return 0; 29 } このように、列挙型を使うことで値に意味のある名前を付けることができる。enum は int に昇格でき、switch 文の case ラベルとして使われることが多い。Signal は定義された ことによりプログラムの中へ新たな型として導入されたため、「Signal sig;」のような形で Signal 型の変数を作ることもできる。Signal 型の変数には RED や GREEN など、Signal 型の定数や変数を代入することができる。列挙型が「enum{(省略)};」のように無名で定 義された場合、 「enum{(省略)}変数名;」のように型の定義と同時に変数を定義するので ない限り、その型の変数を定義することはできない。 なお enum.cpp は const 定数を使って次のように書いても同じ意味になる。 const.cpp 1 #i n c l u d e <i o s t r e a m > 2 #i n c l u d e <s t r i n g > 3 4 u s i n g namespace s t d ; 5 6 c o n s t i n t RED = 0; // 0 7 c o n s t i n t GREEN = 1 ; 8 c o n s t i n t BLUE = 2 ; 9 c o n s t i n t MIN = 0; // 1 // 2 // 0 10 11 12 13 const const const const i n t MAX = 2; // 2 s t r i n g RED S = ”赤” ; s t r i n g GREEN S = ” 緑 ” ; s t r i n g BLUE S = ” 青 ” ; 14 c o n s t s t r i n g UNKNOWN = ” 不明 ” ; 15 16 i n t main ( v o i d ) 17 { 18 19 string s ; 16 f o r ( i n t i=MIN ; i<=MAX; i ++) { switch ( i ) { c a s e RED: s = RED S ; 20 21 22 23 24 25 26 break ; c a s e GREEN: s = GREEN S ; b r e a k ; c a s e BLUE: s = BLUE S ; b r e a k ; default : s = UNKNOWN; b r e a k ; } 27 28 29 c o u t << i << ” は ” << s << e n d l ; } return 0; 30 } 変数定義の際に const を付けると、名前つきの定数(代入できない変数)を定義でき る。文字や数値の定数を即値でなく名前で管理することは、コードの可読性とメンテナ ンス性の向上につながる。例えばコード中で 9.8488578017961047217 という数字が唐突に 現れた場合、後になってコードを読む者は(それが他人であれ書いた当人であれ)おそら く面食らうことになるだろう。これは 97 の平方根の近似値であるが、事情を知らない者 にとっては不可解な数字(俗にマジックナンバーと呼ぶ)がコード中に埋め込まれている 訳で、それを目にした瞬間コードを読むのが嫌になってくる。「const double ROOT97 = 9.8488578017961047217」のような定数を定義し、コードの他の場所では常に ROOT97 と してアクセスするようにすると幾分マシになる。また、そのような書き方を心がけコード中 に即値をばら撒かないようにすると、より高い精度の近似値を利用するようになっても最初 の定数定義だけを修正すればよいなど、仕様の変更に強くなる。 const int と enum の大きな違いは、enum のメンバは定義順に 0 からの値を自動的に割 り振られるため、必要がなければプログラマが特定の値を指定しなくともよいことだ。また 必要があれば MIN と MAX のように、 「=」を使って直接値を指定することもできる。一方 const は const.cpp で示したように文字列など数値以外の定数も定義できるが、enum では 数値しか定義できない。 2.2 構造体 int や char などの既存の型を組み合わせた型を構造体と呼ぶ。これを使うことで数値や 文字といった単純なもの以上の、より高度な概念をプログラムに表現出来るようになる。 struct.cpp 17 1 #i n c l u d e <i o s t r e a m > 2 3 u s i n g namespace s t d ; 4 5 enum C o l o r { 6 Black , White , Brown 7 }; 8 s t r u c t Neko { // 構 造 体 N e k o の 定 義 9 s t r i n g name ; // 名 前 10 i n t ag e ; // 年 齢 11 12 } ; 13 14 int color ; // 色 15 v o i d s h o w p r o f i l e ( Neko neko ) 16 { 17 string s ; 18 19 20 21 c o u t << ” 名前 : ” << neko . name << e n d l ; c o u t << ” 年齢 : ” << neko . ag e << e n d l ; s w i t c h ( neko . c o l o r ) { 22 23 24 25 c a s e B l a c k : s = ”黒” ; b r e a k ; c a s e White : s = ” 白 ” ; b r e a k ; c a s e Brown : s = ” 茶 ” ; b r e a k ; } 26 c o u t << ” 毛の色 : ” << s << e n d l ; 27 } 28 29 i n t main ( v o i d ) 30 { 31 32 33 34 35 s t r u c t Neko neko1 = {” タマ ” , 3 , Brown } ; s t r u c t Neko neko2 ; s t r u c t Neko neko3 ; 18 36 37 38 neko2 . name = ” ジロー ” ; neko2 . a g e = 5 ; neko2 . c o l o r = B l a c k ; 39 40 41 42 c o u t << ” 猫の名前は? ” << e n d l ; c i n >> neko3 . name ; c o u t << neko3 . name << ” の歳は? ” << e n d l ; 43 44 45 c i n >> neko3 . a g e ; c o u t << neko3 . name << ” の色は? ( 0 : 黒 1 : 白 2 : 茶 ) ” << e n d l ; c i n >> neko3 . c o l o r ; 46 47 48 49 s h o w p r o f i l e ( neko1 ) ; s h o w p r o f i l e ( neko2 ) ; s h o w p r o f i l e ( neko3 ) ; 50 } このプログラムは猫を表現する型、Neko を定義している。 構造体は以下の書式で定義する。 struct 型名 { メンバの型 メンバの名前; メンバの型 メンバの名前; ... }; 型名 変数名; 一度定義された構造体型は組み込み型と同じようにその型の変数を作ることができる。 構造体は構造体を含む任意の型のメンバを持つ事が出来、演算子「.」を使うことでそのメ ンバにアクセス出来る。 変数名. メンバ名 初期化の際は各メンバに対応する値を中括弧でまとめ、コンマで区切って指定する (struct.cpp の 32 行目)。順番は構造体定義内でメンバが並ぶ順。 構造体はポインタや配列と組み合わせることで真の力を発揮する。ポインタや配列につ いては次回に触れる。 19 2.3 型の別名 ユーザ定義型とは少し違うのだが、型には都合に応じ任意の別名を付けることが出来る。 typedef.cpp 1 #i n c l u d e <i o s t r e a m > 2 #i n c l u d e <s t r i n g > 3 4 u s i n g namespace s t d ; 5 6 typedef int seisuu ; // i n t の 別 名 を 定 義 7 t y p e d e f s t r i n g m o j i r e t s u ; // s t r i n g の 別 名 を 定 義 8 9 i n t main ( v o i d ) 10 { 11 seisuu i ; 12 mojiretsu s ; 13 14 15 c o u t << ” 整数クレクレ ” << e n d l ; c i n >> i ; 16 17 18 19 c o u t << ” 文字列クレクレ ” << e n d l ; c i n >> s ; c o u t << i << ” と ” << s << e n d l ; 20 } typedef を使うことで型の別名を作ることが出来る。 typedef 型名 型の別名; 変数とよく似た形で定義されているのが分かる。こうして作られた別名は単に元の型の 別名なので、元の型の値と相互に代入出来るし、機能的な違いも全くない。例のように int や string のような型に seisuu、mojiretsu などとという別名を付けるのは、コードを分かり づらくするだけで愚の骨頂というべきだが、もっと有意義な使い方も沢山ある。たとえば int や char のサイズがマシンによって異なるということは前回に述べたが、typedef で int 20 や char などの別名として int32 や int8 といった型を定義し、それぞれの元となる型をマシ ン毎に切り替えるようにすることでデータのビットワイズを明確にしながらコードを書く ことが出来る。またそれぞれの unsigned 版として uint32 や uint8 を定義すれば、タイピン グの手間がいくらか省ける*2 。このように必要に応じて型の別名を定義することで、抽象的 な型を具体化したり、具体的な型を抽象化したりして扱うことが出来る。 *2 このような「便利な typedef 整数型」は、99 年版の C 言語では標準ライブラリに stdint として組み込まれている 21 3 Windows プログラムの基本 C/C++ を使って Windows のプログラムを作るために必要な概念を幾つか紹介して いく。 3.1 Windows=Window(s) Windows は Window の複数形である。Window とは窓のことだが、要するに Windows 使ってると沢山出てくる、アプリケーションの、あの四角かったり四角くなかったりする領 域のこと。ボタンとか文字が出る箱とか、あれのこと。 Windows のプログラムを作るということはウィンドウを作るということであり、作った ウィンドウがどういう形を持つか、マウスやキーボードからのイベントに応じどういう処理 を行うかを記述するということだ。 図1 ウィンドウが沢山並んでる 説明のため、第一回目の勉強会で使った GUI 版 hello world をもう一度使う。まだ完全 な理解は難しいだろうが、一番最初に見た時よりはずっと分かる部分が増えているはずだ。 hellow.cpp 1 /∗ G U I 版 h e l l o w o r l d ∗/ 2 3 // Windows の API ( A p p l i c a t i o n Programming I n t e r f a c e ) を 叩 く 場 合 4 // 最 初 に windows . h を i n c l u d e す る 必 要 が あ る 5 #i n c l u d e <windows . h> 6 22 7 8 // ウ ィ ン ド ウ 関 数 の 宣 言 9 LRESULT CALLBACK WndProc (HWND, UINT , WPARAM, LPARAM) ; 10 11 i n t WINAPI WinMain (HINSTANCE h I n s t a n c e , 12 HINSTANCE h P r e v I n s t a n c e , 13 PSTR szCmdLine , 14 15 { 16 i n t iCmdShow ) // TEXT( 文 字 列 ) で U n i c o d e が 使 え る 17 18 19 20 // T C H A R は U n i c o d e が 入 れ ら れ る 文 字 型 s t a t i c TCHAR szAppName [ ] = TEXT( ” h e l l o w o r l d ” ) ; HWND hwnd ; // HWND型の hwnd MSG msg ; // MSG 型の msg 21 22 23 24 WNDCLASS w n d c l a s s ; // WNDCLASS 型の wndclass 25 26 27 w n d c l a s s . s t y l e = CS HREDRAW | CS VREDRAW; // ウ ィ ン ド ウ 関 数 の 登 録 w n d c l a s s . lpfnWndProc = WndProc ; 28 29 30 31 // メ モ リ 領 域 の 追 加 設 定 ( 普 通 0 で い い ) wndclass . cbClsExtra = 0; w n d c l a s s . cbWndExtra = 0 ; // イ ン ス タ ン ス ハ ン ド ル 32 33 34 35 wndclass . hInstance = hInstance ; // ア イ コ ン の 設 定 w n d c l a s s . h I c o n = LoadIcon (NULL, IDI APPLICATION ) ; // カ ー ソ ル の 設 定 36 37 38 w n d c l a s s . hCursor = LoadCursor (NULL, IDC ARROW ) ; // 背 景 の 設 定 w n d c l a s s . h b rB a c k g r o u n d = (HBRUSH) G e t S t o c k O b j e c t (WHITE BRUSH ) ; 39 40 41 // M e n u 名 の 設 定 w n d c l a s s . lpszMenuName = NULL; // ク ラ ス 名 の 設 定 /∗ ウ ィ ン ド ウ ク ラ ス の 設 定 ∗/ // ク ラ ス ス タ イ ル の 設 定 23 42 43 44 w n d c l a s s . l p s z C l a s s N a m e = szAppName ; i f ( ! R e g i s t e r C l a s s (& w n d c l a s s ) ) { // O S へ w n d c l a s s の 登 録 45 46 47 48 // エ ラ ー が 出 た ら M e s s a g e B o x 出 し て 通 知 MessageBox (NULL, TEXT( ” e r r o r : R e g i s t e r C l a s s ” ) , szAppName , MB ICONERROR ) ; return 0; 49 50 51 } 52 53 54 55 hwnd = CreateWindow ( szAppName , // ク ラ ス 名 TEXT( ” h e l l o w o r l d ” ) , // Window 名 WS OVERLAPPEDWINDOW, // S t y l e CW USEDEFAULT, // 水 平 位 置 // ウ ィ ン ド ウ の 生 成 56 57 58 59 CW USEDEFAULT, // 垂 直 位 置 CW USEDEFAULT, // 幅 CW USEDEFAULT, // 高 さ NULL, // 親 ウ ィ ン ド ウ の ハ ン ド ル 60 61 62 NULL, // メ ニ ュ ー の ハ ン ド ル h I n s t a n c e , // モ ジ ュ ー ル の ハ ン ド ル NULL ) ; // よ く 分 か ん ね 63 64 65 66 ShowWindow ( hwnd , iCmdShow ) ; // 表 示 状 態 を 指 定 UpdateWindow ( hwnd ) ; // ウ ィ ン ド ウ に 描 画 を 要 求 67 68 69 70 // メ ッ セ ー ジ ル ー プ w h i l e ( GetMessage (&msg , NULL, 0 , 0 ) ) { T r a n s l a t e M e s s a g e (&msg ) ; D i s p a t c h M e s s a g e (&msg ) ; 71 72 73 } r e t u r n msg . wParam ; 74 } 75 76 24 77 // ウ ィ ン ド ウ 関 数 の 定 義 78 LRESULT CALLBACK WndProc (HWND hwnd , 79 UINT message , 80 81 82 { 83 WPARAM wParam , LPARAM lParam ) HDC hdc ; // HDC 型の hdc 84 85 86 PAINTSTRUCT p s ; // PAINTSTRUCT型のps RECT r e c t ; // RECT 型の rect 87 88 89 90 // m e s s a g e の 値 ( イ ベ ン ト の 種 類 ) に よ っ て 分 岐 s w i t c h ( message ) { c a s e WM CREATE: // ウ ィ ン ド ウ が 作 成 時 に 送 ら れ る return 0; 91 92 93 94 c a s e WM PAINT: // 再 描 画 が 必 要 な 時 に 送 ら れ る hdc = B e g i n P a i n t ( hwnd , &ps ) ; // 描 画 の 開 始 // ク ラ イ ア ン ト 領 域 の 矩 形 を 取 得 95 96 97 G e t C l i e n t R e c t ( hwnd , &r e c t ) ; // 「 h e l l o , w o r l d 」 と い う 文 字 列 の 描 画 DrawText ( hdc , TEXT( ” h e l l o , w o r l d ” ) , −1, &r e c t , DT SINGLELINE | DT CENTER | DT VCENTER ) ; 98 99 100 101 102 103 104 105 106 107 108 } EndPaint ( hwnd , &ps ) ; // 描 画 の 終 了 return 0; c a s e WM DESTROY: // × ボ タ ン が 押 さ れ た 時 に 送 ら れ る PostQuitMessage ( 0 ) ; return 0; } // 特 に 処 理 し な い メ ッ セ ー ジ は DefWindowProc ( ) に 渡 す r e t u r n DefWindowProc ( hwnd , message , wParam , lParam ) ; 25 3.2 ウィンドウの生成 おおまかに言って、ウィンドウクラスとウィンドウ関数を用意して CreateWindow() を 呼ぶとウィンドウが作れる。 3.2.1 ウィンドウクラス ウィンドウは「ウィンドウクラス」に基づいて作成される。ウィンドウクラスはウィン ドウ関数やアイコン、カーソル、背景色といったウィンドウの基本的な情報を持つ、ウィ ンドウの雛形である。1 つのウィンドウクラスから複数のウィンドウを作ることも出来る。 hellow.cpp の 25 行目からの wndclass へ値を設定しているのがその部分。 「wndclass.style = CS_HREDRAW | CS_VREDRAW」でウィンドウの水平方向のリサイズ と垂直方向のリサイズ両方でウィンドウを再描画するという意味。 「wndclass.lpfnWndProc = WndProc」で WndProc 関数をウィンドウ関数に登録するとい う意味。ウィンドウ関数についてはすぐ後で説明。 「wndclass.cbClsExtra」と「wndclass.cbWndExtra」は両方 0 でいい。 「wndclass.hInstance」は lpfnWndProc に指定したウィンドウ関数があるプロセスのハンド ル。とりあえず WinMain() の引数で受け取る hInstance を指定。 「wndclass.hIcon」はデフォルトのアイコンを使うなら 「LoadIcon(NULL, IDI APPLICATION)」で。 「wndclass.hCursor」も同様に、デフォルトのカーソルを使うなら 「LoadCursor(NULL, IDC ARROW)」で。 「wndclass.hbrBackground」は背景色を指定している。ここでは 「(HBRUSH)GetStockObject(WHITE BRUSH)」で白を指定している。 「wndclass.lpszMenuName」はメニューを付ける場合に指定するが、今は付けないので NULL(無効な値)を指定しておけばいい。 「wndclass.lpszClassName」にはこのウィンドウクラスの名前を文字列で指定する。 「RegisterClass(&wndclass)」の呼び出しで wndclass を Windows へ登録する。登録されて いないウィンドウクラスは使用することが出来ない。 3.2.2 CreateWindow() 直接的には CreateWindow() や CreateWindowEx() を使ってウィンドウを作ること になる。hellow.cpp では 52 行目で CreateWindow() を呼んでいる。CreateWindow() は HWND 型の値(ウィンドウハンドル)を返し、以降のこのウィンドウへの操作はこの HWND 型の値を通して行うことになる。 各パラメータの意味はコメントにある通り。 クラス名には RegisterClass() で登録済みのクラス名を選ぶ。 Window 名はタイトルバーに表示される名前。 26 Style にはウィンドウの形式を指定する。「WS OVERLAPPEDWINDOW」で「最小化ボ タンとか色々付いてる普通のウィンドウ」という程度の意味。 水平・垂直位置や幅・高さは数値で指定してもいいが、「CW USEDEFAULT」でデフォル トの値、つまり「とにかくどっかに作れ」とアバウトに命令出来るので、面倒だからこれを 使う。 3.3 ウィンドウ関数 Windows はウィンドウ上でマウスが動いた場合やマウスのクリックがあった場合、キー ボードからの入力があった場合など、様々なイベントが起きる度にウィンドウメッセージを 各ウィンドウへ送り、ウィンドウはウィンドウ関数を起動してそのイベントを処理する。 hellow.cpp のウィンドウ関数である WndProc() は 78 行目から定義されている。 Windows はプログラムの「メッセージキュー」へメッセージを溜めていく。プログラムは GetMessage() でメッセージキューから先頭のメッセージを取り出し、DispatchMessage() でそれをウィンドウ関数を起動して取り出したメッセージを渡す、という動作を繰り返す (68 行目の while ループ) 。 つまりウィンドウ関数はプログラマではなく DispatchMessage() の中から、OS の中から 呼び出される。そのためにウィンドウクラスへウィンドウ関数の名前を渡して登録してい る。このような形で関数を OS やライブラリから呼び出される関数をコールバック関数と呼 ぶ。WndProc() の定義にある「CALLBACK」というのは「これから定義するのはコール バック関数だ」という意味のものなのだが、何故これが必要かはアセンブラを学んだ後に説 明する。 WndProc() 内の処理は switch 文によりイベントの種類毎に分岐している。Windows は ウィンドウの作成時には WM CREATE を送るし、再描画が必要なら WM PAINT を、× ボタンが押されれば WM DESTROY を送る。試しに下のコードをこの switch 文へ書き加 えてみれば、ウィンドウメッセージの扱い方は半分くらい分かる。 case WM_LBUTTONUP: MessageBox(NULL, TEXT("lbuttonup"), "lbutton", 0); return 0; 27
© Copyright 2024 Paperzz